Owner polish dispatch (2026-09-23):
- Right-click no longer pins a node or opens the detail card: the
canvas pointerdown/up handlers now gate on the primary button, so
the context menu arrives alone.
- Node context menu: label is just "Delete" and the shell hugs its
content (w-44 -> w-fit); the menu body keeps focus for arrow
cycling but never shows the browser's default focus ring
(outline-none on the MenuShell body - applies to every menu).
- Delete confirm: "Delete node?" title in CMU Serif, red
(text-destructive); the node name sits below in JetBrains Mono
inline-code; the Delete button carries the destructive treatment
(border + tint, fill deepens on hover). Rest of the dialog stays
neutral.
- Sidebar rename row: text and metadata stay at the idle row's x -
the status spacer and the input's border/padding are gone; the
underline is the only editing cue.
Platform's 50bfcba engine, surfaced. Three pieces:
1. THE RIGHT-CLICK (graph-canvas.ts): a contextmenu listener — always
preventDefault (the native menu on a full-bleed custom canvas is
never useful) — picks the node under the pointer and hands the
canvas-relative position to a new onContext callback (null on empty
stage). pickAt already respected the visibility filters: a hidden
node cannot be right-clicked, same as it cannot be clicked. The
view opens a MenuShell anchored at a zero-size point-div positioned
at the pointer — MenuShell's placement machine measures it like any
element, so the menu gets the full discipline: placement, focus on
open, arrow cycling, honest-fit flip. Delete… is its one action;
the dismissal contract is the sidebar's (wave 6): outside-click /
Esc (claimed) close, and a camera move closes it too — a menu
floating over a moved graph is anchored to nothing.
2. THE CONFIRM (delete-node-dialog.tsx, portaled z-50 like the
session-delete dialog whose tone it follows): the pre-flight impact
read (GET /api/graph/nodes/[id]) renders the plain facts — link
count, quiz-answer count, session-note count (zeros stay visible —
hiding a zero fact would understate the blast), and the referencing
sessions with display names joined CLIENT-SIDE from the snapshot
(chat titles; imported-notes file paths; unknown ids honestly
themselves). "This cannot be undone" says the permanence. The 409
extraction_running renders the dock phrasing (EXTRACTION_RETRY_LINE —
the server names the extraction, the ACTIVITY DOCK is the surface
the user actually watches drain) with a Try-again; every other
failure shows the server's line verbatim. The honest 404 (deleted
elsewhere meanwhile) states it plainly and its Close rides the same
refresh. The pure body is exported separately (the settings-dialog
pattern) — SSR pins everything the live dialog shows.
3. SUCCESS: onDeleted dispatches lh:graph-revision with the returned
graphRevision — the wave-6 chain refetches snapshot + costs. The
detail card closes if it pointed at the dying node (the refetch
would close it anyway; the same frame should not show a card for a
node the user just killed). Clusters shrink / unrender below the
minimum on the server's read — the client just renders the
refreshed snapshot.
Tests: 867/867 (+8 — the join helper, honest plurals, the 409 phrasing
pin, the dialog body at loading/gone/impact/failure states).
Live-verified on :3001 against the real engine, scratch data: impact
facts match the API, the intercepted 409 shows the dock line and
Try-again recovers into a real delete, the three-member cluster died
below MIN_CLUSTER_SIZE server-side, the labeled seedplan cluster +
its canvas label died with it (cold load draws nothing stale; the
in-session refresh refetches and the dying node's label stops
drawing), other nodes survived, and the full menu contract holds
(empty stage → no menu; Esc, outside-click, camera move all close).