Session deletion dialog matches the node deletion dialog: red title, mono name below, checkbox flow
Owner polish round (2026-09-24): the two destructive-confirmation
surfaces must read as ONE design. The node dialog was the good one —
the session dialog now wears its exact look and flow.
components/delete-session-dialog.tsx (full rework):
- Red serif "Delete session?" (was neutral "Delete "title"?") — the
node dialog's destructive title, text-destructive on font-title 2xl.
- The session name below the title in JetBrains Mono at text-lg,
NO code-block chrome — the font already says code. (The node
dialog's name got the same treatment: text-xs bg-muted code block
dies, mono text-lg line lives. Owner chose below-the-title over
parens-in-the-title: long titles wrap safely below, never inside
the 25px headline.)
- TWO OPTION BUTTONS -> CHECKBOX FLOW: the locked two-button design
(preserve-graph / delete-nodes) collapses to ONE unchecked-by-default
checkbox "Also remove its graph traces" (accent-primary, the settings
panel's native-checkbox idiom) carrying the graph-touching option's
consequence copy verbatim. Unchecked = preserve-graph; checked =
delete-nodes. The unchecked promise ("everything it added to the
graph stays unless you check below") lives in the intro line now.
- Bottom row = the node dialog's exact pair: quiet Cancel + destructive
red Delete (border + text, fill on hover), "Deleting…" on the Delete
button while busy. Try-again re-derives the mode from the checkbox
(retry IS Delete pressed again; the checkbox is disabled while busy).
- DeleteSessionBody exported pure (the settings/node dialog body
pattern) for SSR pins; TRACES_COPY exported for the copy pin.
DELETE_OPTIONS dies with the two-button design.
Tests: session-context.test.tsx's two-option pins rewritten for the
node-dialog shape (red title, mono name no-chrome, unchecked checkbox
default, red Delete pair, old testids absent); delete-node-dialog
pins the chrome-less text-lg name. 905/905. Live-verified on :3001:
both dialogs render 25px red serif titles, 16px mono names, matching
red Delete buttons; Esc closes both, nothing deleted in verification.
This commit is contained in:
@@ -80,15 +80,12 @@ export function DeleteNodeBody({
|
|||||||
className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl"
|
className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 className="font-title text-2xl tracking-tight text-destructive">Delete node?</h2>
|
<h2 className="font-title text-2xl tracking-tight text-destructive">Delete node?</h2>
|
||||||
{/* The node name below the title in JetBrains Mono inline-code
|
{/* The node name below the title in JetBrains Mono (owner,
|
||||||
(owner, polish round): topic names are short technical
|
2026-09-24): the name is the payload — mono suits it, and
|
||||||
handles — mono suits them; the serif title keeps the
|
NO code-block chrome (the font already says code), sized up
|
||||||
family voice ("Delete node?", like "Delete "session"?"). */}
|
to text-lg so it reads. Same shape as the session dialog. */}
|
||||||
<div className="mt-1.5">
|
<div className="mt-1.5">
|
||||||
<code
|
<code data-testid="node-name-code" className="font-mono text-lg text-foreground">
|
||||||
data-testid="node-name-code"
|
|
||||||
className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs text-foreground"
|
|
||||||
>
|
|
||||||
{node.name}
|
{node.name}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,45 +4,36 @@ import { useEffect, useState } from "react";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import type { ChatDeleteMode, ChatListItem } from "@/src/core";
|
import type { ChatDeleteMode, ChatListItem } from "@/src/core";
|
||||||
|
|
||||||
/** The locked two-option design (owner 2026-09-22/23) — plain-language
|
/** The graph-traces checkbox copy (owner, 2026-09-24 flow rework — was
|
||||||
* consequences, the graph-touching option says so. Exported pure for
|
* the delete-nodes option's body in the two-button design): the
|
||||||
* render pins: the copy is design, not implementation. */
|
* graph-touching consequence says so, shared-topic survival included.
|
||||||
export const DELETE_OPTIONS: {
|
* Exported pure for render pins: the copy is design, not implementation. */
|
||||||
mode: ChatDeleteMode;
|
export const TRACES_COPY = {
|
||||||
testid: string;
|
testid: "delete-traces",
|
||||||
title: string;
|
checkbox: "delete-traces-checkbox",
|
||||||
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",
|
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.",
|
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,
|
* Session deletion (wave 6 chunk A; owner flow rework 2026-09-24) — THE
|
||||||
* portaled z-50 like the setup dialog. Two options per the locked design,
|
* destructive-confirmation surface, portaled z-50 like the setup dialog,
|
||||||
* consequences in plain language; the one that touches the graph says so:
|
* now in the node dialog's shape (the two must read as one design):
|
||||||
*
|
*
|
||||||
* preserve-graph the session record, its jobs, and its extraction
|
* - red serif title "Delete session?", the session name below it in
|
||||||
* bookkeeping die; the graph keeps every contribution.
|
* JetBrains Mono, sized up, no code-block chrome;
|
||||||
* delete-nodes the session's ledger traces go too — shared topics
|
* - ONE unchecked-by-default checkbox "Also remove its graph traces"
|
||||||
* survive (other sessions / imported notes still
|
* (unchecked = preserve-graph: the session record, its jobs, and its
|
||||||
* reference them), topics only this session created
|
* extraction bookkeeping die; the graph keeps every contribution.
|
||||||
* are removed entirely.
|
* 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 /
|
* Retryable failures render honestly with a Try-again: 409 turn_in_flight /
|
||||||
* jobs_running ("wait for running work") and 503 extraction_winding_down
|
* 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
|
* ("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({
|
export function DeleteSessionDialog({
|
||||||
chat,
|
chat,
|
||||||
@@ -55,15 +46,19 @@ export function DeleteSessionDialog({
|
|||||||
/** Success path — the sidebar owns navigation + list patching. */
|
/** Success path — the sidebar owns navigation + list patching. */
|
||||||
onDeleted: (id: string, mode: ChatDeleteMode, graphRevision: number) => void;
|
onDeleted: (id: string, mode: ChatDeleteMode, graphRevision: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const [busy, setBusy] = useState<ChatDeleteMode | null>(null);
|
const [traces, setTraces] = useState(false);
|
||||||
// The failure carries the mode it came from — Try-again re-runs THAT
|
const [busy, setBusy] = useState(false);
|
||||||
// mode (busy is back to null by then, so it can't be read off busy).
|
// Retry IS pressing Delete again: the mode always derives from the
|
||||||
const [failure, setFailure] = useState<{ code: string; error: string; mode: ChatDeleteMode } | null>(null);
|
// 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.
|
// Fresh state per session: closing resets, opening a new chat starts clean.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
setBusy(null);
|
setTraces(false);
|
||||||
|
setBusy(false);
|
||||||
setFailure(null);
|
setFailure(null);
|
||||||
}
|
}
|
||||||
}, [chat]);
|
}, [chat]);
|
||||||
@@ -81,9 +76,10 @@ export function DeleteSessionDialog({
|
|||||||
return () => window.removeEventListener("keydown", onKeyDown);
|
return () => window.removeEventListener("keydown", onKeyDown);
|
||||||
}, [chat, onClose]);
|
}, [chat, onClose]);
|
||||||
|
|
||||||
const run = async (mode: ChatDeleteMode) => {
|
const run = async () => {
|
||||||
if (!chat || busy) return;
|
if (!chat || busy) return;
|
||||||
setBusy(mode);
|
const mode: ChatDeleteMode = traces ? "delete-nodes" : "preserve-graph";
|
||||||
|
setBusy(true);
|
||||||
setFailure(null);
|
setFailure(null);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/chats/${chat.id}?mode=${mode}`, { method: "DELETE" });
|
const res = await fetch(`/api/chats/${chat.id}?mode=${mode}`, { method: "DELETE" });
|
||||||
@@ -96,18 +92,14 @@ export function DeleteSessionDialog({
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
// 409 (turn/jobs running) and 503 (extraction winding down) are
|
// 409 (turn/jobs running) and 503 (extraction winding down) are
|
||||||
// both retryable — keep the dialog open, show the server's line.
|
// both retryable — keep the dialog open, show the server's line.
|
||||||
setFailure({
|
setFailure({ code: body.code ?? "error", error: body.error ?? `Delete failed (${res.status})` });
|
||||||
code: body.code ?? "error",
|
setBusy(false);
|
||||||
error: body.error ?? `Delete failed (${res.status})`,
|
|
||||||
mode,
|
|
||||||
});
|
|
||||||
setBusy(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDeleted(chat.id, mode, body.graphRevision ?? 0);
|
onDeleted(chat.id, mode, body.graphRevision ?? 0);
|
||||||
} catch {
|
} catch {
|
||||||
setFailure({ code: "network", error: "Delete failed — network error. Try again.", mode });
|
setFailure({ code: "network", error: "Delete failed — network error. Try again." });
|
||||||
setBusy(null);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,6 +112,41 @@ export function DeleteSessionDialog({
|
|||||||
if (!busy) onClose();
|
if (!busy) onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<DeleteSessionBody
|
||||||
|
chat={chat}
|
||||||
|
traces={traces}
|
||||||
|
busy={busy}
|
||||||
|
failure={failure}
|
||||||
|
onTraces={(next) => setTraces(next)}
|
||||||
|
onClose={onClose}
|
||||||
|
onRun={() => void run()}
|
||||||
|
/>
|
||||||
|
</div>,
|
||||||
|
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<ChatListItem, "id" | "title">;
|
||||||
|
/** 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 (
|
||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
@@ -128,26 +155,36 @@ export function DeleteSessionDialog({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl"
|
className="w-[32rem] max-w-[calc(100vw-2rem)] rounded-xl border bg-card p-5 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 className="font-title text-2xl tracking-tight">Delete “{chat.title}”?</h2>
|
<h2 className="font-title text-2xl tracking-tight text-destructive">Delete session?</h2>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
{/* The session name below the title in JetBrains Mono (owner,
|
||||||
This permanently removes the session — its messages, background jobs, and extraction records. Pick how
|
2026-09-24): no code-block chrome — the font already says
|
||||||
far it reaches:
|
code — sized up to text-lg. Same shape as the node dialog. */}
|
||||||
</p>
|
<div className="mt-1.5">
|
||||||
<div className="mt-4 flex flex-col gap-2">
|
<code data-testid="session-name-code" className="font-mono text-lg text-foreground">
|
||||||
{DELETE_OPTIONS.map((option) => (
|
{chat.title}
|
||||||
<button
|
</code>
|
||||||
key={option.mode}
|
|
||||||
type="button"
|
|
||||||
data-testid={option.testid}
|
|
||||||
disabled={busy !== null}
|
|
||||||
onClick={() => void run(option.mode)}
|
|
||||||
className="flex flex-col items-start rounded-lg border border-border/60 px-4 py-3 text-left transition-colors hover:bg-accent disabled:opacity-40"
|
|
||||||
>
|
|
||||||
<span className="text-sm text-foreground">{option.title}</span>
|
|
||||||
<span className="mt-1 text-xs text-muted-foreground">{option.body}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<label
|
||||||
|
data-testid={TRACES_COPY.testid}
|
||||||
|
className="mt-4 flex cursor-pointer items-start gap-2.5 rounded-lg border border-border/60 px-4 py-3"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-testid={TRACES_COPY.checkbox}
|
||||||
|
checked={traces}
|
||||||
|
onChange={(e) => onTraces(e.target.checked)}
|
||||||
|
disabled={busy}
|
||||||
|
className="mt-0.5 size-4 shrink-0 accent-primary"
|
||||||
|
/>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block text-sm text-foreground">{TRACES_COPY.title}</span>
|
||||||
|
<span className="mt-1 block text-xs text-muted-foreground">{TRACES_COPY.body}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
{failure && (
|
{failure && (
|
||||||
<div
|
<div
|
||||||
role="alert"
|
role="alert"
|
||||||
@@ -158,27 +195,37 @@ export function DeleteSessionDialog({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="delete-retry"
|
data-testid="delete-retry"
|
||||||
onClick={() => failure && void run(failure.mode)}
|
onClick={onRun}
|
||||||
disabled={busy !== null}
|
disabled={busy}
|
||||||
className="ml-3 text-xs text-foreground underline-offset-2 hover:underline disabled:opacity-40"
|
className="ml-3 text-xs text-foreground underline-offset-2 hover:underline disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Try again
|
Try again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-4 flex justify-end">
|
{/* The node dialog's exact bottom row: quiet Cancel, destructive
|
||||||
|
red Delete (border + text, fill on hover — the app's
|
||||||
|
established destructive pair). */}
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="delete-cancel"
|
data-testid="delete-cancel"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={busy !== null}
|
disabled={busy}
|
||||||
className="rounded-sm px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40"
|
className="rounded-sm px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{busy ? "Deleting…" : "Cancel"}
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="delete-confirm"
|
||||||
|
onClick={onRun}
|
||||||
|
disabled={busy}
|
||||||
|
className="rounded-sm border border-destructive/50 bg-destructive/10 px-3 py-1.5 text-xs text-destructive transition-colors hover:bg-destructive/20 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
{busy ? "Deleting…" : "Delete"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
|
||||||
document.body,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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)", () => {
|
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");
|
const html = bodyAt("loading");
|
||||||
assert.match(html, /data-testid="delete-node-dialog"/);
|
assert.match(html, /data-testid="delete-node-dialog"/);
|
||||||
// (owner, polish round) the serif title is just "Delete node?" in
|
// (owner, 2026-09-24) the serif title is just "Delete node?" in
|
||||||
// destructive red; the NAME rides below in JetBrains Mono code.
|
// 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, /Delete node\?/);
|
||||||
assert.match(html, /text-destructive/); // the red title
|
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-name-code"[^>]*>Kepler's three laws</); // the name in mono
|
||||||
assert.match(html, /data-testid="node-impact-loading"/);
|
assert.match(html, /data-testid="node-impact-loading"/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { RenameRow, SessionRow } from "../components/sidebar.tsx";
|
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";
|
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", () => {
|
it("closed (chat=null) renders nothing", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<DeleteSessionDialog chat={null} onClose={() => {}} onDeleted={() => {}} />,
|
<DeleteSessionDialog chat={null} onClose={() => {}} onDeleted={() => {}} />,
|
||||||
@@ -59,18 +59,68 @@ describe("DeleteSessionDialog — the locked two-option copy (portals no-op in S
|
|||||||
assert.equal(html, "");
|
assert.equal(html, "");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exactly two options, both modes covered, consequences in plain language", () => {
|
it("red serif title 'Delete session?', the session name below in mono, sized up, no code-block chrome", () => {
|
||||||
assert.equal(DELETE_OPTIONS.length, 2);
|
const html = renderToStaticMarkup(
|
||||||
const modes = DELETE_OPTIONS.map((o) => o.mode).sort();
|
<DeleteSessionBody
|
||||||
assert.deepEqual(modes, ["delete-nodes", "preserve-graph"]);
|
chat={{ id: "c1", title: "Orbital mechanics" }}
|
||||||
|
traces={false}
|
||||||
|
busy={false}
|
||||||
|
failure={null}
|
||||||
|
onTraces={() => {}}
|
||||||
|
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", () => {
|
it("one unchecked-by-default checkbox for the graph traces — the graph-touching consequence says so", () => {
|
||||||
const preserve = DELETE_OPTIONS.find((o) => o.mode === "preserve-graph");
|
const html = renderToStaticMarkup(
|
||||||
const nodes = DELETE_OPTIONS.find((o) => o.mode === "delete-nodes");
|
<DeleteSessionBody
|
||||||
assert.ok(preserve?.body.includes("graph stays"));
|
chat={{ id: "c1", title: "Orbital mechanics" }}
|
||||||
assert.ok(nodes?.body.includes("leave the graph"));
|
traces={false}
|
||||||
// shared-topics survival is stated on the destructive option
|
busy={false}
|
||||||
assert.ok(nodes?.body.includes("survive"));
|
failure={null}
|
||||||
|
onTraces={() => {}}
|
||||||
|
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(
|
||||||
|
<DeleteSessionBody
|
||||||
|
chat={{ id: "c1", title: "Orbital mechanics" }}
|
||||||
|
traces={true}
|
||||||
|
busy={true}
|
||||||
|
failure={null}
|
||||||
|
onTraces={() => {}}
|
||||||
|
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=""/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user