diff --git a/components/delete-node-dialog.tsx b/components/delete-node-dialog.tsx index a663f93..e70e111 100644 --- a/components/delete-node-dialog.tsx +++ b/components/delete-node-dialog.tsx @@ -80,15 +80,12 @@ export function DeleteNodeBody({ className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl" >

Delete node?

- {/* The node name below the title in JetBrains Mono inline-code - (owner, polish round): topic names are short technical - handles — mono suits them; the serif title keeps the - family voice ("Delete node?", like "Delete "session"?"). */} + {/* The node name below the title in JetBrains Mono (owner, + 2026-09-24): the name is the payload — mono suits it, and + NO code-block chrome (the font already says code), sized up + to text-lg so it reads. Same shape as the session dialog. */}
- + {node.name}
diff --git a/components/delete-session-dialog.tsx b/components/delete-session-dialog.tsx index cef8d6f..97c4ec1 100644 --- a/components/delete-session-dialog.tsx +++ b/components/delete-session-dialog.tsx @@ -4,45 +4,36 @@ import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import type { ChatDeleteMode, ChatListItem } from "@/src/core"; -/** The locked two-option design (owner 2026-09-22/23) — plain-language - * consequences, the graph-touching option says so. Exported pure for - * render pins: the copy is design, not implementation. */ -export const DELETE_OPTIONS: { - mode: ChatDeleteMode; - testid: string; - title: string; - body: string; -}[] = [ - { - mode: "preserve-graph", - testid: "delete-option-preserve", - title: "Delete the session only", - body: "Everything this session added to the graph stays — topics, links, and notes it produced remain untouched.", - }, - { - mode: "delete-nodes", - testid: "delete-option-nodes", - title: "Also remove its graph traces", - body: "This session’s topics, links, and events leave the graph too. Topics other sessions or imported notes also cover survive; topics only this session created are removed entirely.", - }, -]; +/** The graph-traces checkbox copy (owner, 2026-09-24 flow rework — was + * the delete-nodes option's body in the two-button design): the + * graph-touching consequence says so, shared-topic survival included. + * Exported pure for render pins: the copy is design, not implementation. */ +export const TRACES_COPY = { + testid: "delete-traces", + checkbox: "delete-traces-checkbox", + title: "Also remove its graph traces", + body: "This session’s topics, links, and events leave the graph too. Topics other sessions or imported notes also cover survive; topics only this session created are removed entirely.", +}; /** - * Session deletion (wave 6 chunk A) — THE destructive-confirmation surface, - * portaled z-50 like the setup dialog. Two options per the locked design, - * consequences in plain language; the one that touches the graph says so: + * Session deletion (wave 6 chunk A; owner flow rework 2026-09-24) — THE + * destructive-confirmation surface, portaled z-50 like the setup dialog, + * now in the node dialog's shape (the two must read as one design): * - * preserve-graph the session record, its jobs, and its extraction - * bookkeeping die; the graph keeps every contribution. - * delete-nodes the session's ledger traces go too — shared topics - * survive (other sessions / imported notes still - * reference them), topics only this session created - * are removed entirely. + * - red serif title "Delete session?", the session name below it in + * JetBrains Mono, sized up, no code-block chrome; + * - ONE unchecked-by-default checkbox "Also remove its graph traces" + * (unchecked = preserve-graph: the session record, its jobs, and its + * extraction bookkeeping die; the graph keeps every contribution. + * checked = delete-nodes: the session's ledger traces go too); + * - a bottom row Cancel + destructive-red Delete, exactly the node + * dialog's pair. * * Retryable failures render honestly with a Try-again: 409 turn_in_flight / * jobs_running ("wait for running work") and 503 extraction_winding_down * ("in a moment") both clear on their own — the server's error line is - * already written for the user; we show it verbatim. + * already written for the user; we show it verbatim. Try-again is just + * Delete pressed again — the mode re-derives from the checkbox. */ export function DeleteSessionDialog({ chat, @@ -55,15 +46,19 @@ export function DeleteSessionDialog({ /** Success path — the sidebar owns navigation + list patching. */ onDeleted: (id: string, mode: ChatDeleteMode, graphRevision: number) => void; }) { - const [busy, setBusy] = useState(null); - // The failure carries the mode it came from — Try-again re-runs THAT - // mode (busy is back to null by then, so it can't be read off busy). - const [failure, setFailure] = useState<{ code: string; error: string; mode: ChatDeleteMode } | null>(null); + const [traces, setTraces] = useState(false); + const [busy, setBusy] = useState(false); + // Retry IS pressing Delete again: the mode always derives from the + // checkbox's current state (a toggle between failure and retry is a + // change of intent, honored as such — the checkbox is disabled while + // busy, so no mid-flight switches). + const [failure, setFailure] = useState<{ code: string; error: string } | null>(null); // Fresh state per session: closing resets, opening a new chat starts clean. useEffect(() => { if (!chat) { - setBusy(null); + setTraces(false); + setBusy(false); setFailure(null); } }, [chat]); @@ -81,9 +76,10 @@ export function DeleteSessionDialog({ return () => window.removeEventListener("keydown", onKeyDown); }, [chat, onClose]); - const run = async (mode: ChatDeleteMode) => { + const run = async () => { if (!chat || busy) return; - setBusy(mode); + const mode: ChatDeleteMode = traces ? "delete-nodes" : "preserve-graph"; + setBusy(true); setFailure(null); try { const res = await fetch(`/api/chats/${chat.id}?mode=${mode}`, { method: "DELETE" }); @@ -96,18 +92,14 @@ export function DeleteSessionDialog({ if (!res.ok) { // 409 (turn/jobs running) and 503 (extraction winding down) are // both retryable — keep the dialog open, show the server's line. - setFailure({ - code: body.code ?? "error", - error: body.error ?? `Delete failed (${res.status})`, - mode, - }); - setBusy(null); + setFailure({ code: body.code ?? "error", error: body.error ?? `Delete failed (${res.status})` }); + setBusy(false); return; } onDeleted(chat.id, mode, body.graphRevision ?? 0); } catch { - setFailure({ code: "network", error: "Delete failed — network error. Try again.", mode }); - setBusy(null); + setFailure({ code: "network", error: "Delete failed — network error. Try again." }); + setBusy(false); } }; @@ -120,65 +112,120 @@ export function DeleteSessionDialog({ if (!busy) onClose(); }} > -
e.stopPropagation()} - className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl" - > -

Delete “{chat.title}”?

-

- This permanently removes the session — its messages, background jobs, and extraction records. Pick how - far it reaches: -

-
- {DELETE_OPTIONS.map((option) => ( - - ))} -
- {failure && ( -
- {failure.error} - -
- )} -
- -
-
+ setTraces(next)} + onClose={onClose} + onRun={() => void run()} + /> , document.body, ); +} + +/** The dialog's pure body — exported for SSR pins (the settings-dialog / + * node-dialog body pattern). The wrapper owns state + effects + portal. */ +export function DeleteSessionBody({ + chat, + traces, + busy, + failure, + onTraces, + onClose, + onRun, +}: { + chat: Pick; + /** The graph-traces checkbox — unchecked by default (preserve-graph). */ + traces: boolean; + busy: boolean; + failure: { code: string; error: string } | null; + onTraces: (next: boolean) => void; + onClose: () => void; + onRun: () => void; +}) { + return ( +
e.stopPropagation()} + className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl" + > +

Delete session?

+ {/* The session name below the title in JetBrains Mono (owner, + 2026-09-24): no code-block chrome — the font already says + code — sized up to text-lg. Same shape as the node dialog. */} +
+ + {chat.title} + +
+

+ This permanently removes the session — its messages, background jobs, and extraction records. Everything + it added to the graph stays unless you check below. This cannot be undone. +

+ + {failure && ( +
+ {failure.error} + +
+ )} + {/* The node dialog's exact bottom row: quiet Cancel, destructive + red Delete (border + text, fill on hover — the app's + established destructive pair). */} +
+ + +
+
+ ); } \ No newline at end of file diff --git a/tests/delete-node-dialog.test.tsx b/tests/delete-node-dialog.test.tsx index 8101bb7..3e7af8f 100644 --- a/tests/delete-node-dialog.test.tsx +++ b/tests/delete-node-dialog.test.tsx @@ -73,10 +73,14 @@ describe("DeleteNodeDialog (thin SSR)", () => { it("the body at loading: the frame + the pre-flight read pending (effects never fire in SSR — the wrapper's own state)", () => { const html = bodyAt("loading"); assert.match(html, /data-testid="delete-node-dialog"/); - // (owner, polish round) the serif title is just "Delete node?" in - // destructive red; the NAME rides below in JetBrains Mono code. + // (owner, 2026-09-24) the serif title is just "Delete node?" in + // destructive red; the NAME rides below in JetBrains Mono, sized up + // (text-lg), NO code-block chrome — same shape as the session dialog. assert.match(html, /Delete node\?/); assert.match(html, /text-destructive/); // the red title + const name = html.match(/data-testid="node-name-code"[^>]*>/)?.[0] ?? ""; + assert.match(name, /font-mono text-lg/); // bigger, mono + assert.ok(!name.includes("bg-muted"), "no code-block chrome — the font already says code") assert.match(html, /data-testid="node-name-code"[^>]*>Kepler's three laws { }); }); -describe("DeleteSessionDialog — the locked two-option copy (portals no-op in SSR; pin the pure seam)", () => { +describe("DeleteSessionDialog — the node-dialog shape (owner flow rework 2026-09-24; portals no-op in SSR; pin the pure body)", () => { it("closed (chat=null) renders nothing", () => { const html = renderToStaticMarkup( {}} onDeleted={() => {}} />, @@ -59,18 +59,68 @@ describe("DeleteSessionDialog — the locked two-option copy (portals no-op in S assert.equal(html, ""); }); - it("exactly two options, both modes covered, consequences in plain language", () => { - assert.equal(DELETE_OPTIONS.length, 2); - const modes = DELETE_OPTIONS.map((o) => o.mode).sort(); - assert.deepEqual(modes, ["delete-nodes", "preserve-graph"]); + it("red serif title 'Delete session?', the session name below in mono, sized up, no code-block chrome", () => { + const html = renderToStaticMarkup( + {}} + onClose={() => {}} + onRun={() => {}} + />, + ); + assert.match(html, /Delete session\?/); + assert.match(html, /text-destructive[^"]*">Delete session\?]*>/)?.[0] ?? ""; + assert.match(name, /font-mono text-lg/); + assert.ok(!name.includes("bg-muted"), "no code-block chrome"); + assert.match(html, /data-testid="session-name-code"[^>]*>Orbital mechanics { - const preserve = DELETE_OPTIONS.find((o) => o.mode === "preserve-graph"); - const nodes = DELETE_OPTIONS.find((o) => o.mode === "delete-nodes"); - assert.ok(preserve?.body.includes("graph stays")); - assert.ok(nodes?.body.includes("leave the graph")); - // shared-topics survival is stated on the destructive option - assert.ok(nodes?.body.includes("survive")); + it("one unchecked-by-default checkbox for the graph traces — the graph-touching consequence says so", () => { + const html = renderToStaticMarkup( + {}} + onClose={() => {}} + onRun={() => {}} + />, + ); + // unchecked by default (preserve-graph) — React SSR omits the + // attribute when false, so pin its ABSENCE on the rendered input + assert.ok(!/"checked"/.test(html), "unchecked by default"); + // plain-language consequences on the checkbox body + assert.ok(html.includes(TRACES_COPY.title)); + assert.ok(html.includes("leave the graph too")); + assert.ok(html.includes("survive")); + // the old two-button design is gone + assert.ok(!html.includes("delete-option-preserve")); + assert.ok(!html.includes("delete-option-nodes")); + }); + + it("bottom row matches the node dialog: quiet Cancel + destructive-red Delete", () => { + const html = renderToStaticMarkup( + {}} + onClose={() => {}} + onRun={() => {}} + />, + ); + assert.match(html, /data-testid="delete-cancel"[^>]*>Cancel]*>/)?.[0] ?? ""; + assert.match(del, /border-destructive\/50/); + assert.match(html, /data-testid="delete-confirm"[^>]*>Deleting…]*disabled=""/); }); }); \ No newline at end of file