"use client"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import type { ChatDeleteMode, ChatListItem } from "@/src/core"; /** 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; 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): * * - 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. Try-again is just * Delete pressed again — the mode re-derives from the checkbox. */ export function DeleteSessionDialog({ chat, onClose, onDeleted, }: { /** The session up for deletion; null = closed. */ chat: Pick | null; onClose: () => void; /** Success path — the sidebar owns navigation + list patching. */ onDeleted: (id: string, mode: ChatDeleteMode, graphRevision: number) => void; }) { 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) { setTraces(false); setBusy(false); setFailure(null); } }, [chat]); // Esc closes (claimed — a menu/dialog Esc must not also abort a turn). useEffect(() => { if (!chat) return; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { e.stopPropagation(); onClose(); } }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [chat, onClose]); const run = async () => { if (!chat || busy) return; 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" }); const body = (await res.json()) as { ok?: boolean; error?: string; code?: string; graphRevision?: number; }; 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})` }); setBusy(false); return; } onDeleted(chat.id, mode, body.graphRevision ?? 0); } catch { setFailure({ code: "network", error: "Delete failed — network error. Try again." }); setBusy(false); } }; if (!chat) return null; return createPortal(
{ if (!busy) onClose(); }} > 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). */}
); }