import { useSessionStream } from "../stream/useSessionStream"; import { useSessionStore } from "../store/sessionStore"; import { WidgetHost } from "./WidgetHost"; import { CentralStatus } from "./CentralStatus"; import { ModelActivityPanel } from "./ModelActivityPanel"; import { WorkspaceManager } from "./WorkspaceManager"; import { PiManagement } from "./PiManagement"; import { WorkAreaPanelHost } from "./WorkAreaPanel"; import { useActivityPanelResize } from "./useActivityPanelResize"; import { useSessionPanelResize } from "./useSessionPanelResize"; import { NavSessions } from "./NavSessions"; import { SessionMenu } from "./SessionMenu"; import { SessionDocumentsPanel } from "./SessionDocumentsPanel"; import { RenameDialog } from "./RenameDialog"; import { DeleteConfirmDialog } from "./DeleteConfirmDialog"; import { StopConfirmDialog } from "./StopConfirmDialog"; import { SteerInput, ComposerFooter } from "./SteerInput"; import { WorkflowBar } from "./WorkflowBar"; import { DatabaseManagementPage } from "./DatabaseManagementPage"; import { WorkspacePreprocessingControl } from "./WorkspacePreprocessingControl"; import { Pencil, ArrowLeft, ArrowRight, ChevronDown, Trash2 } from "lucide-react"; import { Accordion } from "@base-ui/react/accordion"; import { Button, buttonVariants } from "../components/ui/button"; import { Checkbox } from "../components/ui/checkbox"; import { Toaster } from "../components/ui/sonner"; import { toast } from "sonner"; import { closeSession, listSessions, resumeSession, getSession, renameSession, setSessionGroup, archiveSession, unarchiveSession, deleteSession, prewarmRuntime, } from "../api/sessions"; import { logout as logoutUser } from "../api/auth"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../components/ui/dialog"; import type { SessionScope, SessionSummary } from "../api/types"; import { useAuthGeneration, useAuthUser } from "../auth/authState"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { CSSProperties, KeyboardEvent } from "react"; import { captureAuthOperation, isAuthOperationCurrent, StaleAuthOperationError, type AuthOperationGuard } from "../auth/authOperation"; import { getSettings } from "../api/settings"; import { workspacePreferences, type WorkspacePreference } from "../workspaces/preferences"; import { getWorkspacePreprocessingStatus } from "../api/workspace-preprocessing"; interface AppShellProps { canLogout: boolean; } type ActiveSurface = "core" | "database-management"; type ActiveManagementPanel = "workspace" | "pi" | null; const SESSION_SCOPES: readonly SessionScope[] = ["mine", "all"]; function sessionScopeTabClass(selected: boolean): string { const base = "relative -mb-px flex h-8 w-full cursor-pointer items-center justify-center rounded-t-md border border-b-2 px-2 text-xs font-semibold tracking-[0.005em] outline-none focus-visible:z-10 focus-visible:ring-3 focus-visible:ring-[oklch(var(--nav-active-border)/0.28)]"; return selected ? `${base} border-[oklch(var(--nav-active-border))] bg-[oklch(var(--nav-active))] text-[oklch(var(--nav-active-foreground))] hover:bg-[oklch(var(--nav-active-hover))]` : `${base} border-border bg-card text-muted-foreground hover:border-muted-foreground/45 hover:bg-muted/55 hover:text-foreground`; } type DatabaseManagementPresentation = "legacy" | "fleet"; interface DatabaseManagementPresentationEnvironment { isDevelopment: boolean; mode: string; legacyFlag?: string | boolean; search: string; } /** * Fleet Ledger is the release-safe default. The old surface can only be * requested explicitly from a development server or a staging build, so an * accidentally retained flag can never switch the production UI back. */ export function resolveDatabaseManagementPresentation({ isDevelopment, mode, legacyFlag, search, }: DatabaseManagementPresentationEnvironment): DatabaseManagementPresentation { const legacyAllowed = isDevelopment || mode.trim().toLowerCase() === "staging"; if (!legacyAllowed) return "fleet"; const legacyRequestedByQuery = new URLSearchParams(search).get("db-ui")?.toLowerCase() === "legacy"; const legacyRequestedByEnvironment = String(legacyFlag ?? "").trim().toLowerCase() === "true"; return legacyRequestedByQuery || legacyRequestedByEnvironment ? "legacy" : "fleet"; } export function AppShell({ canLogout }: AppShellProps) { const authenticatedUser = useAuthUser(); const [panelSession, setPanelSession] = useState(null); const { containerRef, panelWidth, desktopSplit, resizing: activityResizing, separatorProps: activitySeparatorProps, } = useActivityPanelResize(); const { panelWidth: sessionPanelWidth, desktopSplit: sessionDesktopSplit, resizing: sessionResizing, separatorProps: sessionSeparatorProps, } = useSessionPanelResize(containerRef, panelSession !== null); // Publish the app area's horizontal geometry so viewport-fixed dialogs center on // the application area rather than the whole browser window. useEffect(() => { const el = containerRef.current; if (!el) return; const doc = document.documentElement; const publish = () => { const r = el.getBoundingClientRect(); doc.style.setProperty("--app-area-left", `${r.left}px`); doc.style.setProperty("--app-area-width", `${r.width}px`); }; publish(); const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(publish); observer?.observe(el); window.addEventListener("resize", publish); return () => { observer?.disconnect(); window.removeEventListener("resize", publish); doc.style.removeProperty("--app-area-left"); doc.style.removeProperty("--app-area-width"); }; }, [containerRef]); const panelWidthsStyle = { "--activity-panel-width": `${panelWidth}px`, "--session-panel-width": `${sessionPanelWidth}px`, } as CSSProperties; const [activeSessionId, setActiveSessionId] = useState(null); const activeSessionIdRef = useRef(null); const activeSessionEpochRef = useRef(0); const newSessionOperationRef = useRef<{ target: string | null; epoch: number } | null>(null); const resumeInvocationRef = useRef(0); const latestResumeIntentRef = useRef<{ token: number; id: string } | null>(null); const resumeInFlightRef = useRef(new Map; guard: AuthOperationGuard; }>()); const [streamCursorResetEpoch, setStreamCursorResetEpoch] = useState(0); const [creatingSession, setCreatingSession] = useState(false); const [awaitingQuestion, setAwaitingQuestion] = useState(false); const [sessionScope, setSessionScope] = useState("mine"); const sessionScopeTabRefs = useRef>({ mine: null, all: null, }); const principal = authenticatedUser; const permissions = authenticatedUser?.permissions ?? []; const isAdmin = authenticatedUser?.isAdmin === true; const canReadAllSessions = permissions.includes("session.read_all"); const canManageWorkspace = permissions.includes("workspace.manage"); const canManageWorkspaceSecrets = permissions.includes("workspace.secrets.manage"); const canManageDatabase = permissions.includes("database.manage"); const canManagePi = permissions.includes("pi.manage"); const authGeneration = useAuthGeneration(); const { data: installationSettings } = useQuery({ queryKey: ["settings"], queryFn: getSettings, }); const [workspacePreference, setWorkspacePreference] = useState( () => workspacePreferences.load(), ); useEffect(() => { const unsubscribe = workspacePreferences.subscribe(setWorkspacePreference); // Child effects may seed the singleton before this parent effect subscribes. setWorkspacePreference(workspacePreferences.load()); return unsubscribe; }, []); // Only an ID selected from the active Workspace Registry may address workspace-scoped APIs. // Installation settings are a legacy hint and can name a removed/non-catalog workspace. const selectedWorkspaceId = workspacePreference.workspaceId; const preprocessingQuery = useQuery({ queryKey: ["workspace-preprocessing", selectedWorkspaceId], queryFn: () => getWorkspacePreprocessingStatus(selectedWorkspaceId!), enabled: Boolean(authenticatedUser && selectedWorkspaceId), refetchInterval: (query) => query.state.data?.state === "running" ? 1_000 : 5_000, }); const preprocessingBlocksNewSession = Boolean( selectedWorkspaceId && preprocessingQuery.data && preprocessingQuery.data.state !== "ready", ); const { data: sessions = [] } = useQuery({ queryKey: ["sessions", sessionScope], queryFn: async () => { const guard = captureAuthOperation({ disposalEpoch: operationEpochRef.current }); if (!guard) throw new StaleAuthOperationError(); const result = await listSessions(sessionScope); if (!isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) throw new StaleAuthOperationError(); return result; }, refetchInterval: 10_000, }); const composerRef = useRef(null); const queryClient = useQueryClient(); const [showActivity, setShowActivity] = useState(false); const [activeSurface, setActiveSurface] = useState("core"); const databaseManagementPresentation = resolveDatabaseManagementPresentation({ isDevelopment: import.meta.env.DEV, mode: import.meta.env.MODE, legacyFlag: import.meta.env.VITE_DB_MANAGEMENT_LEGACY, search: window.location.search, }); const databaseNavigationRef = useRef({ dirty: false, busy: false }); const updateDatabaseNavigationState = useCallback((state: { dirty: boolean; busy: boolean }) => { databaseNavigationRef.current = state; }, []); const [activeManagementPanel, setActiveManagementPanel] = useState(null); const [adminNavigationValue, setAdminNavigationValue] = useState([]); const [activeOpen, setActiveOpen] = useState(true); const [archiveOpen, setArchiveOpen] = useState(false); const [renameTarget, setRenameTarget] = useState(null); const [deleteTargets, setDeleteTargets] = useState([]); const [selectedSessionIds, setSelectedSessionIds] = useState>(new Set()); const [stopConfirm, setStopConfirm] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [renameGroupTarget, setRenameGroupTarget] = useState(null); const operationEpochRef = useRef(0); useEffect(() => () => { operationEpochRef.current += 1; }, []); const groups = useMemo( () => [...new Set(sessions.map((s) => s.group).filter((g): g is string => !!g))].sort(), [sessions], ); const activeList = sessions.filter((s) => !s.archived); const ungroupedActive = activeList.filter((s) => !s.group); const archivedList = sessions.filter((s) => s.archived); const refresh = (operation?: AuthOperationGuard | null) => { if (operation && !isAuthOperationCurrent(operation, { disposalEpoch: operationEpochRef.current })) return; void queryClient.invalidateQueries({ queryKey: ["sessions"] }); }; const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null; const finalized = activeSession?.status === "finalized"; const selectedSessions = sessions.filter((session) => selectedSessionIds.has(session.id)); const allSessionsSelected = sessions.length > 0 && selectedSessions.length === sessions.length; const showingAllSessions = sessionScope === "all"; const isForeignSession = (session: SessionSummary) => { if (!showingAllSessions || !principal) return false; if (!session.author) return true; return session.author !== principal.subject; }; function selectActiveSession(id: string | null) { // Keep async Resume completions synchronized before React commits the state update. if (activeSessionIdRef.current !== id) activeSessionEpochRef.current += 1; activeSessionIdRef.current = id; setActiveSessionId(id); } function invalidateResumeIntent() { resumeInvocationRef.current += 1; latestResumeIntentRef.current = null; } // A background refresh can remove a session (for example from another browser). // Keep the local selection aligned with the authoritative list. useEffect(() => { const availableIds = new Set(sessions.map((session) => session.id)); setSelectedSessionIds((current) => { const next = new Set([...current].filter((id) => availableIds.has(id))); return next.size === current.size ? current : next; }); }, [sessions]); useEffect(() => { if (!canReadAllSessions && sessionScope === "all") setSessionScope("mine"); }, [canReadAllSessions, sessionScope]); function setSessionSelected(id: string, selected: boolean) { setSelectedSessionIds((current) => { const next = new Set(current); if (selected) next.add(id); else next.delete(id); return next; }); } function toggleAllSessions(selected: boolean) { setSelectedSessionIds(selected ? new Set(sessions.map((session) => session.id)) : new Set()); } function handleSessionScopeTabKeyDown( event: KeyboardEvent, current: SessionScope, ) { const currentIndex = SESSION_SCOPES.indexOf(current); let target: SessionScope | undefined; if (event.key === "ArrowRight") target = SESSION_SCOPES[(currentIndex + 1) % SESSION_SCOPES.length]; else if (event.key === "ArrowLeft") target = SESSION_SCOPES[(currentIndex - 1 + SESSION_SCOPES.length) % SESSION_SCOPES.length]; else if (event.key === "Home") target = SESSION_SCOPES[0]; else if (event.key === "End") target = SESSION_SCOPES.at(-1); if (!target) return; event.preventDefault(); setSessionScope(target); sessionScopeTabRefs.current[target]?.focus(); } function canLeaveDatabaseManagement(): boolean { if (activeSurface !== "database-management") return true; if (databaseNavigationRef.current.busy) { toast.info("Wait for the database operation to finish before leaving this page"); return false; } if (databaseNavigationRef.current.dirty) { return window.confirm("Discard unsaved database changes and leave database management?"); } return true; } function openPanel(id: string) { if (!canLeaveDatabaseManagement()) return; const s = sessions.find((x) => x.id === id); if (!s) return; setActiveSurface("core"); setActiveManagementPanel(null); // A session with a live Pi runtime opens straight into its live view: doResume // reconnects to the already-active runtime and replays its pending gate, so an // in-progress session never shows an empty screen that reads as "stopped". Cold or // completed sessions keep the read-only documents panel (with its explicit Resume), // so a mere click never spawns a runtime. if (s.active && s.status === "open" && !s.archived && !isForeignSession(s)) { void doResume(id, true); return; } setPanelSession(s); setShowActivity(false); } function toggleActivity() { setShowActivity((v) => { const next = !v; if (next) setPanelSession(null); return next; }); } async function doResume(id: string, databaseExitApproved = false) { if (!databaseExitApproved && !canLeaveDatabaseManagement()) return; setActiveSurface("core"); setActiveManagementPanel(null); const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current }); if (!guard) return; const token = ++resumeInvocationRef.current; latestResumeIntentRef.current = { token, id }; const inFlight = resumeInFlightRef.current.get(id); if (inFlight && isAuthOperationCurrent(inFlight.guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) { // Repeated intent for the same target shares one backend lifecycle operation and one // commit path. Updating its token still lets s1→s2→s1 make the final s1 intent authoritative. inFlight.latestToken = token; return inFlight.promise; } const operation = { latestToken: token, promise: Promise.resolve(), guard, }; operation.promise = runResume(id, operation, guard).finally(() => { if (resumeInFlightRef.current.get(id) === operation) { resumeInFlightRef.current.delete(id); } }); resumeInFlightRef.current.set(id, operation); return operation.promise; } async function runResume( id: string, operation: { latestToken: number; promise: Promise }, guard: AuthOperationGuard, ) { try { const result = await resumeSession(id); if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return; const latest = latestResumeIntentRef.current; if (latest?.token !== operation.latestToken || latest.id !== id) return; const reconnectSameSession = activeSessionIdRef.current === id; setPanelSession(null); setAwaitingQuestion(false); // A running/waiting runtime for the currently selected session is already bound to this // store and EventSource. Reopening it would replay state and can lose in-flight delivery. if (result.alreadyActive && reconnectSameSession) return; resetSession(); if (!result.alreadyActive) { recordLifecycle("Resuming session"); setAgentActive(true); } selectActiveSession(id); // A cold runtime starts a fresh SSE id sequence. Recreate a same-id source only after // Resume succeeds, and explicitly discard the old runtime's cursor. if (!result.alreadyActive && reconnectSameSession) { setStreamCursorResetEpoch((value) => value + 1); } // Single-flight covers only the backend Resume and its local binding commit. A slow // manifest read must not prevent a later explicit intent from starting a new Resume. if (resumeInFlightRef.current.get(id) === operation) { resumeInFlightRef.current.delete(id); } // Paint the persisted re-entry phase while the replacement stream starts replaying. // The manifest's `phase` is the 1-based current phase (1..8). try { const m = (await getSession(id)) as { phase?: number }; if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return; const latestAfterManifest = latestResumeIntentRef.current; if ( latestAfterManifest?.token !== operation.latestToken || latestAfterManifest.id !== id || activeSessionIdRef.current !== id ) return; if (typeof m.phase === "number" && m.phase >= 1 && m.phase <= 8) { setPhase(`F${m.phase}`); } } catch { /* non-fatal: the first gate will set the phase */ } } catch { if (isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current }) && latestResumeIntentRef.current?.token === operation.latestToken && latestResumeIntentRef.current.id === id ) { toast.error("Failed to resume session."); } } } async function move(s: SessionSummary, group: string) { const guard = captureAuthOperation({ sessionId: s.id, disposalEpoch: operationEpochRef.current }); if (!guard) return; try { await setSessionGroup(s.id, group); if (!isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) return; refresh(guard); } catch { if (isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) toast.error("Failed to move session."); } } async function newGroup(s: SessionSummary) { const name = window.prompt("New group:"); if (name && name.trim()) { const guard = captureAuthOperation({ sessionId: s.id, disposalEpoch: operationEpochRef.current }); if (!guard) return; try { await setSessionGroup(s.id, name.trim()); if (!isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) return; refresh(guard); } catch { if (isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) toast.error("Failed to update group."); } } } async function renameGroup(oldName: string, newName: string, suppliedGuard?: AuthOperationGuard | null) { const trimmed = newName.trim(); if (!trimmed || trimmed === oldName) return false; const guard = suppliedGuard ?? captureAuthOperation({ disposalEpoch: operationEpochRef.current }); if (!guard) return false; try { for (const s of sessions.filter((x) => x.group === oldName)) { await setSessionGroup(s.id, trimmed); if (!isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) return false; } refresh(guard); return true; } catch { if (isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) toast.error("Failed to rename group."); return false; } } async function renameSessionFromDialog(id: string, name: string) { const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current }); if (!guard) return; try { await renameSession(id, name); if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) return; setRenameTarget(null); refresh(guard); } catch { if (isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current })) { toast.error("Failed to rename session."); } } } async function toggleArchive(s: SessionSummary) { const guard = captureAuthOperation({ sessionId: s.id, disposalEpoch: operationEpochRef.current }); if (!guard) return; try { await (s.archived ? unarchiveSession(s.id) : archiveSession(s.id)); if (!isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) return; if (panelSession?.id === s.id) setPanelSession(null); refresh(guard); } catch { if (isAuthOperationCurrent(guard, { sessionId: s.id, disposalEpoch: operationEpochRef.current })) { toast.error(s.archived ? "Failed to restore session." : "Failed to archive session."); } } } function requestArchiveToggle(session: SessionSummary) { if (!session.archived && isForeignSession(session)) { const label = session.author ? `${session.author}'s session` : "this session"; if (!window.confirm(`Archive ${label}?`)) return; } void toggleArchive(session); } async function deleteSessions(targets: SessionSummary[]) { const guard = captureAuthOperation({ disposalEpoch: operationEpochRef.current }); if (!guard) return; try { const results = await Promise.allSettled(targets.map((session) => deleteSession(session.id))); if (!isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) return; const deletedIds = new Set( targets.filter((_, index) => results[index].status === "fulfilled").map((session) => session.id), ); const deletedActiveSession = deletedIds.has(activeSessionIdRef.current ?? ""); const deletedResumeTarget = deletedIds.has(latestResumeIntentRef.current?.id ?? ""); if (deletedResumeTarget) invalidateResumeIntent(); if (deletedIds.has(panelSession?.id ?? "")) setPanelSession(null); if (deletedActiveSession) { resetSession(); selectActiveSession(null); } setSelectedSessionIds((current) => new Set([...current].filter((id) => !deletedIds.has(id)))); refresh(guard); if (deletedIds.size !== targets.length) { toast.error(`Deleted ${deletedIds.size} of ${targets.length} sessions.`); } } catch { if (isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) toast.error("Failed to delete selected sessions."); } } function requestDelete(targets: SessionSummary[]) { if (targets.some(isForeignSession) || allSessionsSelected) { setDeleteTargets(targets); return; } void deleteSessions(targets); } function menuFor(s: SessionSummary) { return ( doResume(s.id)} onView={() => setPanelSession(s)} onRename={() => setRenameTarget(s)} onMove={(g) => move(s, g)} onNewGroup={() => newGroup(s)} onArchiveToggle={() => requestArchiveToggle(s)} onDelete={() => requestDelete([s])} /> ); } // The harness "holds the ball" whenever a session is live, no widget is waiting on // the human, AND the Pi turn is still in flight (agentActive). Without the last // condition the final workflow step — which ends with no follow-up gate — would // leave the working state on forever. const pendingWidget = useSessionStore((s) => s.pendingWidget); const sessionToasts = useSessionStore((s) => s.toasts); const resetSession = useSessionStore((s) => s.resetSession); const recordLifecycle = useSessionStore((s) => s.recordLifecycle); const setPhase = useSessionStore((s) => s.setPhase); const setAgentActive = useSessionStore((s) => s.setAgentActive); const lastSystemEvent = useSessionStore((s) => s.lastSystemEvent); const agentActive = useSessionStore((s) => s.agentActive); const deliveredToastCountRef = useRef(0); useEffect(() => { if (sessionToasts.length < deliveredToastCountRef.current) { deliveredToastCountRef.current = 0; } for (const notification of sessionToasts.slice(deliveredToastCountRef.current)) { if (notification.level === "error") toast.error(notification.text); else if (notification.level === "success") toast.success(notification.text); else if (notification.level === "warning") toast.warning(notification.text); else toast.info(notification.text); } deliveredToastCountRef.current = sessionToasts.length; }, [sessionToasts]); const sessionViewOpen = Boolean(activeSessionId) || creatingSession; const working = sessionViewOpen && !pendingWidget && agentActive; // The workflow bar runs only while the harness works, not while a finalized // session sits idle or a gate awaits the reviewer (pendingWidget). const running = working && !finalized; useSessionStream( activeSessionId, 0, streamCursorResetEpoch, Boolean(authenticatedUser), authGeneration, ); // A backend "session_exit" system event (e.g. the replay server emitting it // when the reviewer picks "Esci") asks us to leave the live session view and // return to the landing state. We deliberately do NOT also POST /close here — // stopSession already does, and this effect must stay side-effect-free on the // backend if the session is already inactive. useEffect(() => { if (lastSystemEvent?.type !== "system_event") return; const ev = (lastSystemEvent as any).event; if (ev === "session_exit") { // Never let a streamed event terminate the managed Pi child. Only the // explicit “Stop & save” action is allowed to call /close. invalidateResumeIntent(); resetSession(); selectActiveSession(null); setAwaitingQuestion(false); } // The final workflow turn ends with the session already finalized on disk: // refetch now instead of waiting for the 10s poll, so the completed state // (and the new-session invite below the transcript) appears immediately. if (ev === "agent_end") refresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastSystemEvent]); function startNewSession() { if (preprocessingBlocksNewSession) { toast.warning(preprocessingQuery.data?.detail ?? "Workspace preprocessing is required."); return; } if (!canLeaveDatabaseManagement()) return; setActiveSurface("core"); setActiveManagementPanel(null); invalidateResumeIntent(); newSessionOperationRef.current = null; resetSession(); // Starting a new question closes any open session detail panel: the reader is // moving away from that session, so its left-hand box must not linger. setPanelSession(null); setAwaitingQuestion(true); setCreatingSession(false); selectActiveSession(null); // Best effort only: session creation keeps the authoritative readiness gate. // Composer focus is deliberately independent of this network request. void prewarmRuntime().catch(() => undefined); requestAnimationFrame(() => composerRef.current?.focus()); } function beginSessionCreation() { newSessionOperationRef.current = { target: activeSessionIdRef.current, epoch: activeSessionEpochRef.current, }; setAwaitingQuestion(false); setCreatingSession(true); } function finishSessionCreation(id: string) { const operation = newSessionOperationRef.current; newSessionOperationRef.current = null; if (!operation || operation.target !== activeSessionIdRef.current || operation.epoch !== activeSessionEpochRef.current) return; // React batches these updates, preserving the provisional session view // while useSessionStream opens the durable session's SSE channel. selectActiveSession(id); setCreatingSession(false); setAwaitingQuestion(false); refresh(); } function failSessionCreation(message?: string) { const operation = newSessionOperationRef.current; newSessionOperationRef.current = null; if (!operation || operation.target !== activeSessionIdRef.current || operation.epoch !== activeSessionEpochRef.current) return; setCreatingSession(false); resetSession(); toast.error(message ?? "Failed to create session. Your question is ready to retry."); } async function stopSession() { const id = activeSessionIdRef.current; if (!id) return; const guard = captureAuthOperation({ sessionId: id, disposalEpoch: operationEpochRef.current }); if (!guard) return; invalidateResumeIntent(); try { await closeSession(id); } finally { if (!isAuthOperationCurrent(guard, { sessionId: id, disposalEpoch: operationEpochRef.current }) || activeSessionIdRef.current !== id) return; resetSession(); selectActiveSession(null); setAwaitingQuestion(false); } } async function signOut() { if (!canLeaveDatabaseManagement()) return; await logoutUser(); } const currentNavigation = activeSurface === "database-management" ? "database" : activeManagementPanel ?? "core"; const adminNavigationOpen = adminNavigationValue.includes("administration"); const managementNavigationCurrent = currentNavigation !== "core"; return (
{panelSession && ( setPanelSession(null)} onResume={doResume} desktopSplit={sessionDesktopSplit} hidden={activeSurface !== "core"} /> )} {activeSurface === "core" && panelSession && sessionDesktopSplit && (
)} {showActivity && setShowActivity(false)} onOpenWorkspaceManager={() => setActiveManagementPanel("workspace")} hidden={activeSurface !== "core"} />} {activeSurface === "core" && showActivity && desktopSplit && (
)} {/* Conversation column */} {activeSurface === "database-management" && ( )}
{activeSessionId && (
{/* Arrow toggle for the left Model-activity panel: → opens it, ← closes it. */}
)}
{sessionViewOpen ? ( <> {activeSession?.question && (

Question

{activeSession.question}

)} {activeSessionId && } {finalized && !agentActive && (

Session completed and finalized — the SQL and all phase documents are saved.

)} ) : ( )}
{/* Sticky composer: the box stays pinned while the conversation scrolls above. */}
setStopConfirm(true)} inputRef={composerRef} awaitingInput={awaitingQuestion || pendingWidget?.widget === "freetext"} working={running} />
setActiveManagementPanel(null)} canManageWorkspace={canManageWorkspace} canManageSecrets={canManageWorkspaceSecrets} /> {canManagePi && ( setActiveManagementPanel(null)} /> )}
{/* Right session rail */} {(activeSurface === "database-management" || !showActivity) && ( )} {renameTarget && ( { if (!o) setRenameTarget(null); }} onSubmit={(name) => renameSessionFromDialog(renameTarget.id, name)} /> )} {renameGroupTarget && ( { if (!o) setRenameGroupTarget(null); }} onSubmit={async (name) => { const target = renameGroupTarget; const guard = captureAuthOperation({ disposalEpoch: operationEpochRef.current }); if (await renameGroup(target, name, guard) && guard && isAuthOperationCurrent(guard, { disposalEpoch: operationEpochRef.current })) { setRenameGroupTarget(null); } }} title="Rename group" /> )} {deleteTargets.length > 0 && ( session.name || session.question || session.id)} onOpenChange={(o) => { if (!o) setDeleteTargets([]); }} onConfirm={() => { const targets = deleteTargets; setDeleteTargets([]); void deleteSessions(targets); }} /> )}
); } function EmptyState() { return (
ThothII

Build datamarts from your database through a guided, human-in-the-loop workflow. Type your question in the box below to begin a session.

); }