"use client"; 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.", }, ]; /** * 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: * * 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. * * 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. */ 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 [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); // Fresh state per session: closing resets, opening a new chat starts clean. useEffect(() => { if (!chat) { setBusy(null); 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 (mode: ChatDeleteMode) => { if (!chat || busy) return; setBusy(mode); 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})`, mode, }); setBusy(null); return; } onDeleted(chat.id, mode, body.graphRevision ?? 0); } catch { setFailure({ code: "network", error: "Delete failed — network error. Try again.", mode }); setBusy(null); } }; if (!chat) return null; return createPortal(
{ 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}
)}
, document.body, ); }