import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ExtensionDialog } from "../components/ExtensionDialog"; import { catalogModels, getModelCatalog, modelIdentity, modelIdentityFromValue, modelIdentityValue, type ModelIdentity, } from "../model-catalog"; import "./quick-pane.css"; import type { Extension, Model, QuickPaneSettings, RuntimeSnapshot, UiState, } from "../workspace/types"; const defaults: 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, }; type Runtime = { agentId: string; runtimeId: string }; type HistoryEntry = { id: string; request: string; answer: string }; function historyLabel(request: string) { const concise = request.replace(/\s+/g, " ").trim(); return concise.length > 42 ? `${concise.slice(0, 39)}…` : concise; } function settingsError(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."; } function assistantText(snapshot: RuntimeSnapshot) { const messages = snapshot.transcript?.data?.messages ?? []; const message = [...messages] .reverse() .find((candidate) => candidate.role === "assistant"); if (!message) return ""; if (typeof message.content === "string") return message.content; return (message.content ?? []) .map((part) => part.text ?? "") .filter(Boolean) .join("\n"); } export default function QuickPane() { const [settings, setSettings] = useState(defaults); const [settingsLoaded, setSettingsLoaded] = useState(false); const [request, setRequest] = useState(""); const [submissionStarting, setSubmissionStarting] = useState(false); const [thinkingLevel, setThinkingLevel] = useState("medium"); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState({ provider: "", modelId: "", }); const [status, setStatus] = useState("Loading Quick Pane settings…"); const [error, setError] = useState(); const [catalogError, setCatalogError] = useState(); const [runtime, setRuntime] = useState(); const [answer, setAnswer] = useState(""); const [completedRuntimeId, setCompletedRuntimeId] = useState(); const [history, setHistory] = useState([]); const [selectedHistoryId, setSelectedHistoryId] = useState(""); const [escalation, setEscalation] = useState(); const [extension, setExtension] = useState(); const runtimeRef = useRef(undefined); const submittedRequestRef = useRef< { runtimeId: string; request: string } | undefined >(undefined); const completedRuntimeRef = useRef(undefined); const submissionStartingRef = useRef(false); const inputRef = useRef(null); const invalid = useMemo(() => settingsError(settings), [settings]); const modelOptions = useMemo( () => catalogModels(models, [selectedModel]), [models, selectedModel], ); const selectedModelValue = selectedModel.provider && selectedModel.modelId ? modelIdentityValue(selectedModel) : ""; const canContinue = !!escalation || (!!runtime && !!error); const displayedAnswer = selectedHistoryId ? (history.find((entry) => entry.id === selectedHistoryId)?.answer ?? answer) : answer; const dismissPane = () => getCurrentWindow().hide(); const focusInput = () => inputRef.current?.focus(); const loadModelCatalog = useCallback(async () => { try { setModels(await getModelCatalog()); setCatalogError(undefined); } catch (reason) { setCatalogError(`Could not load model catalog: ${String(reason)}`); } }, []); useEffect(() => { let active = true; void (async () => { let nextSettings: QuickPaneSettings; try { const state = await invoke("load_ui_state"); if (!active) return; nextSettings = { ...defaults, ...state.quickPane }; } catch (reason) { if (active) setError(`Could not load settings: ${String(reason)}`); return; } setSettings(nextSettings); setSettingsLoaded(true); setSelectedModel({ provider: nextSettings.provider, modelId: nextSettings.model, }); setStatus(""); if (active) await loadModelCatalog(); })(); return () => { active = false; const current = runtimeRef.current; if (current) invoke("close_quick_runtime", { runtimeId: current.runtimeId }).catch( () => undefined, ); }; }, [loadModelCatalog]); useEffect(() => { if (settingsLoaded) focusInput(); }, [settingsLoaded]); useEffect(() => { const paneWindow = getCurrentWindow(); const settingsListener = listen( "quick-pane-settings-saved", (event) => { const nextSettings = { ...defaults, ...event.payload }; setSettings(nextSettings); setSelectedModel({ provider: nextSettings.provider, modelId: nextSettings.model, }); }, ); const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") void dismissPane(); }; window.addEventListener("keydown", onKeyDown); const focusListener = paneWindow.onFocusChanged((event) => { if (event.payload) { focusInput(); void loadModelCatalog(); } }); return () => { window.removeEventListener("keydown", onKeyDown); void settingsListener.then((unlisten) => unlisten()); void focusListener.then((unlisten) => unlisten()); }; }, [loadModelCatalog]); useEffect(() => { if (!runtime) return; let active = true; const refresh = async () => { try { const snapshot = await invoke( "get_session_runtime_snapshot", { runtimeId: runtime.runtimeId, }, ); if (!active) return; const text = assistantText(snapshot); if (text) { setAnswer(text); if (text.includes("[ESCALATE]")) { setEscalation( text.replace("[ESCALATE]", "").trim() || "Quick agent requested a full session.", ); } if ( snapshot.state?.data?.isStreaming === false && completedRuntimeRef.current !== runtime.runtimeId ) { completedRuntimeRef.current = runtime.runtimeId; setCompletedRuntimeId(runtime.runtimeId); setStatus("Quick answer ready."); const submitted = submittedRequestRef.current; if (submitted?.runtimeId === runtime.runtimeId) { setRequest((current) => current === submitted.request ? "" : current, ); setHistory((current) => [ { id: runtime.runtimeId, request: submitted.request, answer: text, }, ...current.filter((entry) => entry.id !== runtime.runtimeId), ].slice(0, 5), ); } } } setExtension(snapshot.extensions[0]); if (snapshot.runtime.error?.message) { setError(snapshot.runtime.error.message); setEscalation( "Quick runtime failed. Continue in a full session if needed.", ); } } catch (reason) { if (active) setError(`Could not refresh quick result: ${String(reason)}`); } }; void refresh(); const interval = window.setInterval(() => void refresh(), 800); return () => { active = false; window.clearInterval(interval); }; }, [runtime]); useEffect(() => { if (!runtime || completedRuntimeId === runtime.runtimeId) return; const timeout = window.setTimeout(() => { setEscalation( `Quick time budget of ${settings.maximumSeconds} seconds reached.`, ); setStatus( "Quick time budget reached. Showing partial result if available.", ); invoke("abort", { agentId: runtime.agentId }).catch(() => undefined); }, settings.maximumSeconds * 1000); return () => window.clearTimeout(timeout); }, [completedRuntimeId, runtime, settings.maximumSeconds]); async function run() { if (submissionStartingRef.current) return; if (invalid || !request.trim()) return setError(invalid ?? "Enter a request."); submissionStartingRef.current = true; setSubmissionStarting(true); const submittedRequest = request; try { const previous = runtimeRef.current; if (previous) await invoke("close_quick_runtime", { runtimeId: previous.runtimeId, }).catch(() => undefined); runtimeRef.current = undefined; submittedRequestRef.current = undefined; completedRuntimeRef.current = undefined; setRuntime(undefined); setCompletedRuntimeId(undefined); setSelectedHistoryId(""); setError(undefined); setAnswer(""); setEscalation(undefined); setExtension(undefined); setStatus("Starting advisory quick agent…"); const result = await invoke<{ runtime: Runtime }>( "create_quick_runtime", { worktreePath: settings.workspace, }, ); runtimeRef.current = result.runtime; submittedRequestRef.current = { runtimeId: result.runtime.runtimeId, request: submittedRequest, }; setRuntime(result.runtime); await invoke("set_model", { agentId: result.runtime.agentId, provider: selectedModel.provider || settings.provider, modelId: selectedModel.modelId || settings.model, }); await invoke("set_thinking_level", { agentId: result.runtime.agentId, level: thinkingLevel, }); const guardrails = `[Quick Pane advisory mode: target ${settings.maximumSeconds}s and ${settings.maximumToolCalls} read-only tool calls. Do not use subagents. Do not mutate files or system state. If request needs mutation, multi-step work, subagents, or more budget, start final response with [ESCALATE] and explain why.]\n${settings.instructions}\n\nUser request: ${submittedRequest}`; await invoke("submit_prompt", { agentId: result.runtime.agentId, message: guardrails, }); if (completedRuntimeRef.current !== result.runtime.runtimeId) { setStatus( "Quick request running. Advisory limits are not trusted enforcement.", ); } } catch (reason) { setError(`Could not start quick request: ${String(reason)}`); setStatus(""); } finally { submissionStartingRef.current = false; setSubmissionStarting(false); } } async function respond(response: Record) { if (!runtime || !extension?.id) return; try { await invoke("respond_to_extension", { agentId: runtime.agentId, requestId: extension.id, response, }); setExtension(undefined); } catch (reason) { setError(`Could not answer Pi request: ${String(reason)}`); } } async function continueFull() { const handoffRequest = request.trim() || submittedRequestRef.current?.request || ""; if (invalid || !handoffRequest) return setError(invalid ?? "Enter a request."); try { const result = await invoke<{ runtime: Runtime }>( "create_session_runtime", { worktreePath: settings.workspace, }, ); await invoke("set_model", { agentId: result.runtime.agentId, provider: settings.followUpProvider, modelId: settings.followUpModel, }); await invoke("submit_prompt", { agentId: result.runtime.agentId, message: `${handoffRequest}\n\nQuick Pane escalation: ${escalation ?? "Continue this in a full session."}\n\nPartial quick result:\n${answer}`, }); setStatus("Full session started with follow-up model."); } catch (reason) { setError(`Could not start full session: ${String(reason)}`); } } return (
Quick Pane
{history.length > 0 && ( )}