Wave 6 chunk A: sidebar context menu (Rename/Delete) + the two-option delete dialog

- Right-click any session row: MenuShell context menu, exactly two actions
  (owner ruling): Rename and Delete… — MenuShell discipline (placement,
  focus, arrow cycling, lh:menu-open announcements), agent-section's
  outside-click/Esc contract with [data-lh-menu] carve-outs.
- Rename is INLINE in the row (owner ruling): select-all entry, Enter/blur
  commits through the existing plumbing — PUT /api/chats/:id then
  lh:chat-renamed. The open chat header now LISTENS for the event too, so
  renaming the active session from the sidebar updates the header in place
  (it only dispatched before).
- Delete opens the two-option dialog (locked design, DELETE_OPTIONS pure
  seam for render pins): "Delete the session only" (preserve-graph — the
  graph keeps every contribution) vs "Also remove its graph traces"
  (delete-nodes — shared topics survive, only-this-session topics die).
  409 turn_in_flight/jobs_running and 503 extraction_winding_down render
  the server's line verbatim with a same-mode Try-again.
- Success: the sidebar patches its snapshot instantly, the active session
  navigates home (chat unmounts — drafts die with it), and lh:graph-revision
  announces the returned revision — the graph view refetches the snapshot
  AND costs (deletion is the one writer the status poll can't see).
- ChatDeleteMode type re-exported through the core barrel (the shell's one
  needed seam from core/graph-deletion.ts).

Gates: tsc clean, 909/909, lint 0, build ok. Live-verified on :3001:
context menu open/Esc, inline rename (row + API + active header), both
delete modes on scratch sessions, simulated 409 + retry-then-succeed,
active-session navigation home, graph+costs refetch deltas on home.
This commit is contained in:
amosblomqvist
2026-09-23 12:45:12 +03:00
parent 683cb9fd12
commit 1c27694dbc
6 changed files with 483 additions and 13 deletions
+13
View File
@@ -240,6 +240,19 @@ export function Chat({
return () => window.removeEventListener("lh:kbd-rename-session", onRename);
}, [titleText]);
// The OTHER rename surface (wave 6: the sidebar's inline rename): its
// commit dispatches the same lh:chat-renamed event this header's rename
// does — if it renamed THIS chat, patch the header title in place (the
// event is the single source of truth; the header is one more listener).
useEffect(() => {
const onRenamed = (e: Event) => {
const { id, title } = (e as CustomEvent<{ id: string; title: string }>).detail;
if (id === chatId) setTitleText((prev) => (title !== prev ? title : prev));
};
window.addEventListener("lh:chat-renamed", onRenamed);
return () => window.removeEventListener("lh:chat-renamed", onRenamed);
}, [chatId]);
// The Esc chain's last link: abort the turn. Honest scope (the composer's
// Stop has the same rule): only a LOCAL stream has a client request to
// stop — a server-side wake turn aborts nothing (the stop would be a