/** * @file InterruptedSessionsPrompt.tsx * @description The app-wide "resume interrupted sessions?" dialog. A session * whose pane died under it — the machine shut down, tmux was killed, the CLI * crashed — is listed by `GET /api/run/interrupted`; the user ticks the ones * to resume (each in the first free tab of its workspace, with its run's * model and effort) or resumes none. Either answer dismisses the whole list, * so it is asked once; a resume that failed stays listed. Refetched on the * `runs_interrupted` broadcast, so a tmux that dies under an open dashboard * asks too. Resuming is the explicit click here, never the dialog opening. * * @author Nguyễn Ngọc Trí Vĩ */ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { api, type InterruptedRun } from "../../lib/api"; import { eventBus } from "../../lib/eventBus"; import { timeAgo } from "../../lib/format"; import { toast } from "../../lib/toast"; import { Checkbox } from "../Checkbox"; import { ConfirmModal } from "../ConfirmModal"; import { startInFreeTabDetailed } from "./startSession"; const label = (it: InterruptedRun) => it.lane_title || it.lane_cwd; export function InterruptedSessionsPrompt() { const { t } = useTranslation("run"); const navigate = useNavigate(); const [items, setItems] = useState([]); const [picked, setPicked] = useState>(new Set()); const [busy, setBusy] = useState(false); // A refetch keeps the user's ticks; a session not seen before starts ticked. const seen = useRef(new Set()); const load = useCallback(() => { api.run .interrupted() .then((r) => { const fresh = r.items.filter((i) => !seen.current.has(i.session_id)); fresh.forEach((i) => seen.current.add(i.session_id)); setItems(r.items); setPicked((p) => { const next = new Set(r.items.filter((i) => p.has(i.session_id)).map((i) => i.session_id)); fresh.forEach((i) => next.add(i.session_id)); return next; }); }) .catch(() => undefined); // a convenience: no prompt when it cannot be read }, []); useEffect(load, [load]); useEffect( () => eventBus.subscribe((msg) => { if (msg.type === "runs_interrupted") load(); }), [load] ); const dismiss = async (sessionIds: string[]) => { if (sessionIds.length) await api.run.dismissInterrupted(sessionIds).catch(toast.error); }; const resumeNone = async () => { if (busy) return; setBusy(true); await dismiss(items.map((i) => i.session_id)); setItems([]); setBusy(false); }; const resumePicked = async () => { if (busy) return; setBusy(true); const failed = new Set(); let first: { runId: string; laneId: number } | null = null; for (const it of items.filter((i) => picked.has(i.session_id))) { try { const r = await startInFreeTabDetailed(it.lane_id, { resumeSessionId: it.session_id, ...(it.model ? { model: it.model } : {}), ...(it.effort ? { effort: it.effort } : {}), // Bypass is a per-run choice, never carried over. ...(it.permission_mode && it.permission_mode !== "bypassPermissions" ? { permissionMode: it.permission_mode } : {}), }); first ??= r; } catch (err) { failed.add(it.session_id); toast.error( t("interrupted.failed", { name: label(it), message: err instanceof Error ? err.message : String(err), }) ); } } await dismiss(items.map((i) => i.session_id).filter((id) => !failed.has(id))); setItems((cur) => cur.filter((i) => failed.has(i.session_id))); setBusy(false); if (first) navigate(`/workspace?lane=${first.laneId}&run=${encodeURIComponent(first.runId)}`); }; const toggle = (id: string, on: boolean) => setPicked((p) => { const next = new Set(p); if (on) next.add(id); else next.delete(id); return next; }); const count = items.filter((i) => picked.has(i.session_id)).length; return ( 0} title={t("interrupted.title")} message={t("interrupted.message")} confirmLabel={t("interrupted.resume", { count })} cancelLabel={t("interrupted.none")} destructive={false} busy={busy} disabled={count === 0} onConfirm={() => void resumePicked()} onCancel={() => void resumeNone()} >
    {items.map((it) => (
  • toggle(it.session_id, on)} disabled={busy} className="w-full" labelClassName="min-w-0 flex-1" label={ {label(it)} · {it.tool} {it.session_name || it.prompt_preview || it.session_id} {it.ended_at ? ` · ${timeAgo(it.ended_at)}` : ""} } />
  • ))}
); }