- {/* 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();
}}
>
-
+ {/* 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); // the name in mono
assert.match(html, /data-testid="node-impact-loading"/);
});
diff --git a/tests/session-context.test.tsx b/tests/session-context.test.tsx
index fbc229e..01409ba 100644
--- a/tests/session-context.test.tsx
+++ b/tests/session-context.test.tsx
@@ -5,7 +5,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { RenameRow, SessionRow } from "../components/sidebar.tsx";
-import { DELETE_OPTIONS, DeleteSessionDialog } from "../components/delete-session-dialog.tsx";
+import { DeleteSessionBody, DeleteSessionDialog, TRACES_COPY } from "../components/delete-session-dialog.tsx";
import type { ChatListItem } from "../src/core/index.ts";
/**
@@ -51,7 +51,7 @@ describe("RenameRow — the inline rename surface", () => {
});
});
-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\?); // the red title
+ // the name: mono, text-lg (sized up), NO bg-muted code-block chrome
+ const name = html.match(/data-testid="session-name-code"[^>]*>/)?.[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);
});
- it("the graph-touching option says so; the session-only option promises the graph stays", () => {
- 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);
+ const del = html.match(/data-testid="delete-confirm"[^>]*>/)?.[0] ?? "";
+ assert.match(del, /border-destructive\/50/);
+ assert.match(html, /data-testid="delete-confirm"[^>]*>Deleting…); // busy label on Delete
+ // the checkbox survives a busy render (disabled, not unmounted)
+ assert.match(html, /data-testid="delete-traces-checkbox"[^>]*disabled=""/);
});
});
\ No newline at end of file