import { invoke } from "@tauri-apps/api/core"; import { emitTo } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { open } from "@tauri-apps/plugin-dialog"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import "./App.css"; import { ConversationWorkspace } from "./components/ConversationWorkspace"; import { DirectorySidebar } from "./components/DirectorySidebar"; import { ExtensionDialog } from "./components/ExtensionDialog"; import { CommandFormDialog } from "./components/CommandFormDialog"; import type { RpcCommand } from "./commands/rpc"; import { SessionPicker } from "./components/SessionPicker"; import { SessionTabs } from "./components/SessionTabs"; import { catalogModels, getModelCatalog, modelIdentity, modelIdentityFromValue, modelIdentityValue, } from "./model-catalog"; import type { DirectorySession, DirectoryWorkspace, Model, QuickPaneSettings, RuntimeView, UiState, } from "./workspace/types"; import { useWorkspace } from "./workspace/useWorkspace"; const minimumUiScale = 0.9, maximumUiScale = 1.4; const clamp = (value: number) => Math.min(maximumUiScale, Math.max(minimumUiScale, value)); const defaultQuickPaneSettings: QuickPaneSettings = { workspace: "", provider: "", model: "", followUpProvider: "", followUpModel: "", instructions: "Answer quickly. Do not use subagents. Prefer read-only inspection. Escalate mutations, multi-step work, or work likely to exceed budget.", maximumSeconds: 60, maximumToolCalls: 6, }; function validateQuickPaneSettings(settings: QuickPaneSettings) { if (!settings.workspace.startsWith("/")) return "Workspace must be an absolute path."; if (!settings.provider || !settings.model) return "Quick provider and model are required."; if (!settings.followUpProvider || !settings.followUpModel) return "Follow-up provider and model are required."; if (!settings.instructions.trim()) return "Supplemental instructions are required."; if ( !Number.isInteger(settings.maximumSeconds) || settings.maximumSeconds < 1 || settings.maximumSeconds > 600 ) return "Seconds must be an integer from 1 to 600."; if ( !Number.isInteger(settings.maximumToolCalls) || settings.maximumToolCalls < 1 || settings.maximumToolCalls > 64 ) return "Tool calls must be an integer from 1 to 64."; } let submissionId = 0; type LocalOperation = { id: number; kind: | "creating" | "opening" | "closing" | "listing" | "submitting" | "command" | "restarting" | "renewing"; detail: string; runtimeId?: string; command?: string; }; type CommandNotice = { kind: "error"; message: string; runtimeId: string; }; const modelRpcCommands = new Set([ "get_available_models", "set_model", "cycle_model", "set_thinking_level", "cycle_thinking_level", ]); function requestedCommand(command: string, args: Record) { return command === "pi_rpc_command" && typeof args.command === "string" ? args.command : command; } function sessionStateLabel(state?: string) { return !state || state === "idle" ? "Pi ready" : state; } function commandProgress(command: string) { if (command === "restart") return "Restarting Pi and restoring the session…"; if (command === "renew_session") return "Clearing session history and starting anew…"; if (command === "get_available_models") return "Loading available Pi models…"; if (command === "set_model") return "Changing Pi model…"; if (command === "cycle_model") return "Cycling Pi model…"; if (command === "set_thinking_level") return "Updating thinking level…"; if (command === "cycle_thinking_level") return "Cycling thinking level…"; return "Updating Pi session…"; } export default function App() { const { state, dispatch, refresh, loadSnapshot } = useWorkspace(); const [status, setStatus] = useState("Connecting to Pi Status Bridge…"); const [runtimeStatuses, setRuntimeStatuses] = useState>( {}, ); const [commandForms, setCommandForms] = useState>( {}, ); const [commandNotices, setCommandNotices] = useState< Record >({}); const [view, setView] = useState<"conversation" | "settings">("conversation"); const [adding, setAdding] = useState(false); const [folderPath, setFolderPath] = useState(""); const [sessions, setSessions] = useState( undefined, ); const selected = state.selectedRuntimeId ? state.runtimesById[state.selectedRuntimeId] : undefined; const directory = state.selectedDirectoryPath ? state.directoriesByPath[state.selectedDirectoryPath] : undefined; const runtimes = useMemo( () => (state.runtimeOrderByDirectory[state.selectedDirectoryPath ?? ""] ?? []) .map((id) => state.runtimesById[id]) .filter(Boolean), [ state.runtimeOrderByDirectory, state.selectedDirectoryPath, state.runtimesById, ], ); const mountedRef = useRef(true); const operationSequence = useRef(0); const activeAgentCommands = useRef(new Set()); const [operations, setOperations] = useState>( {}, ); const [abortingRuntimeIds, setAbortingRuntimeIds] = useState>( () => new Set(), ); const [restoreTabFocus, setRestoreTabFocus] = useState(false); const [uiState, setUiState] = useState(); const [quickPaneSettings, setQuickPaneSettings] = useState( defaultQuickPaneSettings, ); const [quickPaneSettingsStatus, setQuickPaneSettingsStatus] = useState(""); const [quickPaneSettingsError, setQuickPaneSettingsError] = useState(); const [modelCatalog, setModelCatalog] = useState([]); const setRuntimeStatus = (runtimeId: string, detail: string) => setRuntimeStatuses((current) => ({ ...current, [runtimeId]: detail })); const clearRuntimeStatus = (runtimeId: string) => setRuntimeStatuses((current) => { if (!(runtimeId in current)) return current; const next = { ...current }; delete next[runtimeId]; return next; }); const startOperation = ( kind: LocalOperation["kind"], detail: string, runtimeId?: string, command?: string, ) => { const operation = { id: ++operationSequence.current, kind, detail, runtimeId, command, }; setOperations((current) => ({ ...current, [operation.id]: operation })); if (runtimeId) clearRuntimeStatus(runtimeId); else setStatus(detail); return operation.id; }; const finishOperation = (id: number) => setOperations((current) => { if (!(id in current)) return current; const next = { ...current }; delete next[id]; return next; }); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); useEffect(() => { let active = true; void invoke("load_ui_state") .then((loaded) => { if (!active) return; setUiState(loaded); setQuickPaneSettings({ ...defaultQuickPaneSettings, ...loaded.quickPane, }); }) .catch((error) => { if (active) setQuickPaneSettingsError( `Could not load Quick Pane settings: ${String(error)}`, ); }); void getModelCatalog() .then((models) => { if (active) setModelCatalog(models); }) .catch((error) => { if (active) setQuickPaneSettingsError( `Could not load model catalog: ${String(error)}`, ); }); return () => { active = false; }; }, []); const selectedRuntimeId = selected?.summary.runtimeId; useEffect(() => { if (!selectedRuntimeId) return; requestAnimationFrame(() => dispatch({ type: "markRead", runtimeId: selectedRuntimeId }), ); }, [selectedRuntimeId, dispatch]); useEffect(() => { setAbortingRuntimeIds((current) => { const next = new Set( [...current].filter( (runtimeId) => state.runtimesById[runtimeId]?.summary.state === "streaming", ), ); return next.size === current.size ? current : next; }); }, [state.runtimesById]); async function saveQuickPaneSettings() { const validationError = validateQuickPaneSettings(quickPaneSettings); if (validationError) { setQuickPaneSettingsError(validationError); return; } if (!uiState) return; try { const current = await invoke("load_ui_state"); const next = { ...current, quickPane: quickPaneSettings }; await invoke("save_ui_state", { state: next }); void emitTo( "quick-pane", "quick-pane-settings-saved", quickPaneSettings, ).catch(() => undefined); setUiState(next); setQuickPaneSettingsError(undefined); setQuickPaneSettingsStatus("Quick Pane settings saved."); } catch (error) { setQuickPaneSettingsError( `Could not save Quick Pane settings: ${String(error)}`, ); } } async function createRuntime(path = state.selectedDirectoryPath) { if (!path || !mountedRef.current) return; const operationId = startOperation("creating", "Starting a new Pi session…"); try { const result = await invoke<{ runtime: RuntimeView["summary"] }>( "create_session_runtime", { worktreePath: path }, ); if (!mountedRef.current) return; await refresh(); if (!mountedRef.current) return; dispatch({ type: "selectRuntime", runtimeId: result.runtime.runtimeId }); await loadSnapshot(result.runtime.runtimeId); if (mountedRef.current) setStatus("New session ready"); } catch (error) { if (mountedRef.current) setStatus(`Could not start session: ${String(error)}`); } finally { if (mountedRef.current) finishOperation(operationId); } } async function closeRuntime(runtime: RuntimeView) { if ( runtime.summary.state === "streaming" && !window.confirm( "Close this working session? Pi will be stopped, but its history is kept.", ) ) return; const operationId = startOperation( "closing", `Closing ${runtime.summary.label}…`, runtime.summary.runtimeId, ); try { await invoke("close_session_runtime", { runtimeId: runtime.summary.runtimeId, }); if (!mountedRef.current) return; await refresh(); if (mountedRef.current) setRestoreTabFocus(true); } catch (error) { if (mountedRef.current) setRuntimeStatus( runtime.summary.runtimeId, `Could not close session: ${String(error)}`, ); } finally { if (mountedRef.current) finishOperation(operationId); } } async function showSessions() { if (!state.selectedDirectoryPath) return; const operationId = startOperation("listing", "Loading saved sessions…"); try { const result = await invoke<{ sessions: DirectorySession[] }>( "list_directory_sessions", { worktreePath: state.selectedDirectoryPath }, ); setSessions(result.sessions); } catch (error) { setStatus(`Could not load saved sessions: ${String(error)}`); } finally { if (mountedRef.current) finishOperation(operationId); } } async function openSession(session: DirectorySession) { if (session.runtimeId) { dispatch({ type: "selectRuntime", runtimeId: session.runtimeId }); setSessions(undefined); return; } if (!state.selectedDirectoryPath || !mountedRef.current) return; const operationId = startOperation("opening", "Opening saved Pi session…"); try { const result = await invoke<{ runtime: RuntimeView["summary"] }>( "open_session_runtime", { worktreePath: state.selectedDirectoryPath, sessionPath: session.path, }, ); if (!mountedRef.current) return; await refresh(); if (!mountedRef.current) return; dispatch({ type: "selectRuntime", runtimeId: result.runtime.runtimeId }); await loadSnapshot(result.runtime.runtimeId); if (mountedRef.current) setSessions(undefined); } catch (error) { if (mountedRef.current) setStatus(`Could not open session: ${String(error)}`); } finally { if (mountedRef.current) finishOperation(operationId); } } async function addDirectory() { const path = folderPath.trim(); if (!path.startsWith("/")) { setStatus("Enter an absolute folder path"); return; } await createRuntime(path); if (mountedRef.current) { setFolderPath(""); setAdding(false); } } async function forgetDirectory(entry: DirectoryWorkspace) { if (!window.confirm(`Forget ${entry.worktreePath}?`)) return; try { await invoke("forget_directory", { worktreePath: entry.worktreePath }); await refresh(); } catch (error) { setStatus(String(error)); } } async function agentCommand( command: string, args: Record = {}, reload = true, target = selected, showOperation = true, ) { const runtimeId = target?.summary.runtimeId, agentId = target?.summary.agentId; if (!runtimeId || !agentId) { if (runtimeId) setRuntimeStatus(runtimeId, "This session is not currently available"); else setStatus("This session is not currently available"); return false; } if (activeAgentCommands.current.has(runtimeId)) return false; const operationCommand = requestedCommand(command, args); const restarting = operationCommand === "restart"; const renewing = operationCommand === "renew_session"; activeAgentCommands.current.add(runtimeId); setCommandNotices((current) => { if (!(runtimeId in current)) return current; const next = { ...current }; delete next[runtimeId]; return next; }); const operationId = showOperation ? startOperation( renewing ? "renewing" : restarting ? "restarting" : "command", commandProgress(operationCommand), runtimeId, operationCommand, ) : undefined; try { await invoke(command, { agentId, ...args }); if (reload) await loadSnapshot(runtimeId, { throwOnError: true }); return true; } catch (error) { const subject = modelRpcCommands.has(operationCommand) ? "Pi model command failed" : renewing ? "Session renewal failed" : restarting ? "Pi restart failed" : "Pi command failed"; const message = `${subject}: ${String(error)}`; setCommandNotices((current) => ({ ...current, [runtimeId]: { kind: "error", message, runtimeId }, })); return false; } finally { activeAgentCommands.current.delete(runtimeId); if (mountedRef.current && operationId) finishOperation(operationId); } } async function submit() { if (!selected) return; const text = selected.draft.trim(); if (!text) return; if (text === "/new") { dispatch({ type: "draftChanged", runtimeId: selected.summary.runtimeId, draft: "", }); await createRuntime(); return; } if (text === "/resume") { dispatch({ type: "draftChanged", runtimeId: selected.summary.runtimeId, draft: "", }); await showSessions(); return; } const name = text.match(/^\/name\s+([\s\S]+)$/)?.[1]?.trim(); if (text === "/name" || name) { if (!name) { setRuntimeStatus( selected.summary.runtimeId, "Enter a session name after /name", ); return; } await agentCommand("set_session_name", { name }); dispatch({ type: "draftChanged", runtimeId: selected.summary.runtimeId, draft: "", }); return; } const compact = text.match(/^\/compact(?:\s+([\s\S]+))?$/); if (compact) { await agentCommand("compact", { customInstructions: compact[1]?.trim() || undefined, }); dispatch({ type: "draftChanged", runtimeId: selected.summary.runtimeId, draft: "", }); return; } const runtimeId = selected.summary.runtimeId, id = ++submissionId, initialDelivery = selected.summary.state === "streaming" ? "follow_up" : "prompt", operationId = startOperation( "submitting", initialDelivery === "follow_up" ? "Queueing follow-up…" : "Sending prompt to Pi…", selected.summary.runtimeId, ); dispatch({ type: "submissionAdded", runtimeId, submission: { id, text, userMessageCount: selected.messages.filter( (message) => message.role === "user", ).length, phase: "sending", delivery: initialDelivery, }, }); dispatch({ type: "draftChanged", runtimeId, draft: "" }); try { const result = await invoke<{ delivery?: "prompt" | "follow_up" }>( "submit_prompt", { agentId: selected.summary.agentId, message: text }, ); const delivery = result.delivery ?? initialDelivery; dispatch({ type: "submissionSent", runtimeId, id, delivery }); clearRuntimeStatus(runtimeId); } catch (error) { setRuntimeStatus(runtimeId, `Pi command failed: ${String(error)}`); dispatch({ type: "submissionRemoved", runtimeId, id }); dispatch({ type: "draftChanged", runtimeId, draft: text }); } finally { finishOperation(operationId); } } async function abortSelected() { const target = selected; if (!target?.summary.agentId) return; const runtimeId = target.summary.runtimeId; setAbortingRuntimeIds((current) => new Set(current).add(runtimeId)); clearRuntimeStatus(runtimeId); try { await invoke("abort", { agentId: target.summary.agentId }); } catch (error) { setRuntimeStatus(runtimeId, `Pi command failed: ${String(error)}`); setAbortingRuntimeIds((current) => { const next = new Set(current); next.delete(runtimeId); return next; }); } } async function runRpcCommand( command: RpcCommand, input: Record = {}, target = selected, ) { if (!target) return; const runtimeId = target.summary.runtimeId; const succeeded = await agentCommand( "pi_rpc_command", { command: command.command, input }, true, target, ); if (succeeded) setCommandForms((current) => { const next = { ...current }; delete next[runtimeId]; return next; }); } async function respond(response: Record) { const target = selected; const extension = target?.extensions[0]; if (!extension?.id) return; await agentCommand( "respond_to_extension", { requestId: extension.id, response }, true, target, ); } const closePicker = useCallback(() => setSessions(undefined), []); const activeOperations = Object.values(operations); const globalOperation = activeOperations .filter((entry) => !entry.runtimeId) .reduce( (latest, entry) => (!latest || entry.id > latest.id ? entry : latest), undefined, ); const selectedOperations = selectedRuntimeId ? activeOperations.filter((entry) => entry.runtimeId === selectedRuntimeId) : []; const selectedOperation = selectedOperations.reduce< LocalOperation | undefined >( (latest, entry) => (!latest || entry.id > latest.id ? entry : latest), undefined, ); const pendingSubmissions = selected?.pendingSubmissions ?? []; const pendingSubmission = pendingSubmissions[pendingSubmissions.length - 1]; const isSubmitting = pendingSubmissions.some((entry) => entry.phase === "sending") || selectedOperations.some((entry) => entry.kind === "submitting"); const isRestarting = selectedOperations.some( (entry) => entry.kind === "restarting", ); const isRenewing = selectedOperations.some( (entry) => entry.kind === "renewing", ); const isAborting = selectedRuntimeId ? abortingRuntimeIds.has(selectedRuntimeId) : false; const isSessionLocked = isRestarting || isRenewing; const visibleCommandForm = selectedRuntimeId ? commandForms[selectedRuntimeId] : undefined; const commandOperation = selectedOperations.find( (entry) => entry.kind === "command", ); const commandFormPending = Boolean(commandOperation); const modelCommandPending = commandFormPending && modelRpcCommands.has(commandOperation?.command ?? ""); const visibleCommandNotice = selectedRuntimeId ? commandNotices[selectedRuntimeId] : undefined; const displayedStatus = state.newSessionLaunch?.detail ?? globalOperation?.detail ?? (isAborting ? "Aborting Pi…" : undefined) ?? selectedOperation?.detail ?? (pendingSubmission ? pendingSubmission.phase === "sending" ? pendingSubmission.delivery === "follow_up" ? "Queueing follow-up…" : "Sending prompt to Pi…" : pendingSubmission.delivery === "follow_up" ? "Follow-up queued · waiting for Pi…" : "Prompt accepted · waiting for Pi…" : undefined) ?? (selectedRuntimeId ? runtimeStatuses[selectedRuntimeId] : undefined) ?? (selected?.progress.phase === "working" || selected?.progress.phase === "recovering" ? selected.progress.detail : undefined) ?? selected?.error ?? selected?.summary.error?.message ?? selected?.progress.detail ?? status; const isWorking = state.newSessionLaunch?.phase === "starting" || isSessionLocked || isSubmitting || selected?.progress.phase === "working" || selected?.progress.phase === "recovering"; const sessionStatusLabel = state.newSessionLaunch?.phase === "starting" ? "Starting" : isRenewing ? "Renewing" : isRestarting ? "Restarting" : isWorking ? "Working" : sessionStateLabel(selected?.summary.state); const sessionStatusPhase = isWorking ? ("working" as const) : (selected?.progress.phase ?? "idle"); const isStartingSession = state.newSessionLaunch?.phase === "starting" || activeOperations.some((entry) => entry.kind === "creating"); const sessionTabsBusy = activeOperations.some( (entry) => !entry.runtimeId && (entry.kind === "creating" || entry.kind === "opening" || entry.kind === "listing"), ); const closingRuntimeIds = new Set( activeOperations .filter((entry) => entry.kind === "closing" && entry.runtimeId) .map((entry) => entry.runtimeId as string), ); const renewingRuntimeIds = new Set( activeOperations .filter((entry) => entry.kind === "renewing" && entry.runtimeId) .map((entry) => entry.runtimeId as string), ); const sameDirectoryCollision = directory && directory.openCount > 1; const quickPaneModelOptions = catalogModels(modelCatalog, [ { provider: quickPaneSettings.provider, modelId: quickPaneSettings.model, }, { provider: quickPaneSettings.followUpProvider, modelId: quickPaneSettings.followUpModel, }, ]); const quickModelValue = quickPaneSettings.provider && quickPaneSettings.model ? modelIdentityValue({ provider: quickPaneSettings.provider, modelId: quickPaneSettings.model, }) : ""; const followUpModelValue = quickPaneSettings.followUpProvider && quickPaneSettings.followUpModel ? modelIdentityValue({ provider: quickPaneSettings.followUpProvider, modelId: quickPaneSettings.followUpModel, }) : ""; return (
{ if ( event.button === 0 && !(event.target as HTMLElement).closest("button,input,select,textarea") ) void getCurrentWindow().startDragging(); }} >
PI

{selected?.summary.label ?? "Pi workspace"}

{state.resourceWarning && (
Many Pi sessions are open. Startup and memory use may be high.
)} {sameDirectoryCollision && !state.dismissedCollisionWarning && (
Multiple sessions are open in the same directory. Concurrent work can collide in files and Git; separate worktrees are safer.
)} {visibleCommandNotice && !visibleCommandForm && (
{visibleCommandNotice.message}
)} {selected?.modelError && (
{selected.modelError}
)} {selected?.notifications.map((notice, index) => (
{notice.message}
))}
dispatch({ type: "selectDirectory", worktreePath: path }) } onAdd={() => setAdding(true)} onForget={forgetDirectory} />
{adding && (
{ event.preventDefault(); void addDirectory(); }} > setFolderPath(event.currentTarget.value)} placeholder="/absolute/path" />
)} dispatch({ type: "selectRuntime", runtimeId })} onClose={(runtime) => void closeRuntime(runtime)} onRenew={(runtime) => void agentCommand("renew_session", {}, true, runtime) } onNew={() => void createRuntime()} onResume={() => void showSessions()} busy={sessionTabsBusy} closingRuntimeIds={closingRuntimeIds} renewingRuntimeIds={renewingRuntimeIds} restoreFocus={restoreTabFocus} onFocusRestored={() => setRestoreTabFocus(false)} /> {view === "settings" ? (

Session controls

{ event.preventDefault(); void saveQuickPaneSettings(); }} >

Quick Pane