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 ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; 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 ESCALATION_MARKER = "[ESCALATE]"; 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 }; type ActivitySnapshot = { activeTool?: string; hasAssistantText: boolean; }; 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"); } function toolActivityLabel(tool: string) { const segments = tool.split("."); const name = segments[segments.length - 1]?.replace(/[_-]+/g, " ") ?? tool; const labels: Record = { bash: "Running command", edit: "Editing file", find: "Finding files", grep: "Searching files", ls: "Listing files", read: "Reading file", write: "Writing file", }; return ( labels[name.toLowerCase()] ?? `${name.charAt(0).toUpperCase()}${name.slice(1)}` ); } export default function QuickPane() { const [settings, setSettings] = useState(defaults); const [settingsLoaded, setSettingsLoaded] = useState(false); const [request, setRequest] = useState(""); const [currentRequest, setCurrentRequest] = useState(""); const [submissionStarting, setSubmissionStarting] = useState(false); const [thinkingLevel, setThinkingLevel] = useState("medium"); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState({ provider: "", modelId: "", }); const [notice, setNotice] = 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 [failedRuntimeId, setFailedRuntimeId] = useState(); const [history, setHistory] = useState([]); const [selectedHistoryId, setSelectedHistoryId] = useState(""); const [escalation, setEscalation] = useState(); const [extension, setExtension] = useState(); const [activitySnapshot, setActivitySnapshot] = useState({ hasAssistantText: false, }); const [elapsedSeconds, setElapsedSeconds] = useState(0); const runtimeRef = useRef(undefined); const submittedRequestRef = useRef< { runtimeId: string; request: string } | undefined >(undefined); const promptSubmittedRuntimeRef = useRef(undefined); const completedRuntimeRef = useRef(undefined); const submissionStartingRef = useRef(false); const runStartedAtRef = useRef(undefined); 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 runActive = submissionStarting || (!!runtime && completedRuntimeId !== runtime.runtimeId && failedRuntimeId !== runtime.runtimeId); const viewingHistory = !!selectedHistoryId && !runActive; const displayedEntry = viewingHistory ? history.find((entry) => entry.id === selectedHistoryId) : undefined; const displayedRequest = displayedEntry?.request ?? currentRequest; const displayedAnswer = displayedEntry?.answer ?? answer; const displayedAnswerComplete = !viewingHistory && !!runtime && completedRuntimeId === runtime.runtimeId; const activityPhase = submissionStarting ? "Starting" : runActive && activitySnapshot.activeTool ? toolActivityLabel(activitySnapshot.activeTool) : runActive && activitySnapshot.hasAssistantText ? "Answering" : runActive ? "Thinking" : ""; const activity = activityPhase === "Thinking" ? `${activityPhase} · ${elapsedSeconds}s` : activityPhase; 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, }); setNotice(""); 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(() => { if (!runActive) return; const updateElapsed = () => setElapsedSeconds( Math.max( 0, Math.floor( (Date.now() - (runStartedAtRef.current ?? Date.now())) / 1000, ), ), ); updateElapsed(); const interval = window.setInterval(updateElapsed, 1_000); return () => window.clearInterval(interval); }, [runActive]); 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); const responseComplete = snapshot.state?.data?.isStreaming === false && promptSubmittedRuntimeRef.current === runtime.runtimeId; const escalated = text.startsWith(ESCALATION_MARKER); const awaitingEscalationMarker = !!text && ESCALATION_MARKER.startsWith(text); const result = awaitingEscalationMarker ? "" : escalated ? text.slice(ESCALATION_MARKER.length).trim() : text; setActivitySnapshot({ activeTool: snapshot.runtime.activeTool, hasAssistantText: !!text, }); if (awaitingEscalationMarker) { setAnswer(""); } else if (text) { setAnswer(result); } if (escalated && responseComplete) { setEscalation(result || "Quick agent requested a full session."); } if ( responseComplete && completedRuntimeRef.current !== runtime.runtimeId ) { completedRuntimeRef.current = runtime.runtimeId; setCompletedRuntimeId(runtime.runtimeId); setNotice(""); const submitted = submittedRequestRef.current; if (submitted?.runtimeId === runtime.runtimeId) { setRequest((current) => current === submitted.request ? "" : current, ); setHistory((current) => [ { id: runtime.runtimeId, request: submitted.request, answer: result, }, ...current.filter((entry) => entry.id !== runtime.runtimeId), ].slice(0, 5), ); } window.requestAnimationFrame(focusInput); } setExtension(snapshot.extensions[0]); if (snapshot.runtime.error?.message) { setError(snapshot.runtime.error.message); setFailedRuntimeId(runtime.runtimeId); 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.`, ); setFailedRuntimeId(runtime.runtimeId); invoke("abort", { agentId: runtime.agentId }).catch(() => undefined); }, settings.maximumSeconds * 1000); return () => window.clearTimeout(timeout); }, [completedRuntimeId, runtime, settings.maximumSeconds]); async function run() { if (submissionStartingRef.current || runActive) return; if (invalid || !request.trim()) return setError(invalid ?? "Enter a request."); submissionStartingRef.current = true; setSubmissionStarting(true); const submittedRequest = request; setCurrentRequest(submittedRequest); setSelectedHistoryId(""); runStartedAtRef.current = Date.now(); setElapsedSeconds(0); try { const previous = runtimeRef.current; if (previous) await invoke("close_quick_runtime", { runtimeId: previous.runtimeId, }).catch(() => undefined); runtimeRef.current = undefined; submittedRequestRef.current = undefined; promptSubmittedRuntimeRef.current = undefined; completedRuntimeRef.current = undefined; setRuntime(undefined); setCompletedRuntimeId(undefined); setFailedRuntimeId(undefined); setError(undefined); setAnswer(""); setEscalation(undefined); setExtension(undefined); setActivitySnapshot({ hasAssistantText: false }); setNotice(""); 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, }); promptSubmittedRuntimeRef.current = result.runtime.runtimeId; } catch (reason) { setError(`Could not start quick request: ${String(reason)}`); setFailedRuntimeId(runtimeRef.current?.runtimeId); } finally { submissionStartingRef.current = false; setSubmissionStarting(false); focusInput(); window.requestAnimationFrame(focusInput); } } 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 || currentRequest; 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}`, }); setNotice("Full session started with follow-up model."); } catch (reason) { setError(`Could not start full session: ${String(reason)}`); } } return (
{history.length > 0 && ( )}
{displayedRequest && (
You

{displayedRequest}

)} {(viewingHistory ? !!displayedAnswer : runActive || !!displayedAnswer || displayedAnswerComplete) && (
{displayedAnswer && ( <> Assistant {displayedAnswer} )} {displayedAnswerComplete && ( Response complete. )}
)} {!viewingHistory && activity && ( )} {viewingHistory ? "" : activityPhase} {escalation && ( )} {error && ( )} {catalogError && ( )} {notice && (

{notice}

)}