import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useEffect, useMemo, useRef, useState } from "react"; import { ExtensionDialog } from "../components/ExtensionDialog"; 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 }; 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 [thinkingLevel, setThinkingLevel] = useState("medium"); const [models, setModels] = useState([]); const [selectedModel, setSelectedModel] = useState(""); const [status, setStatus] = useState("Loading Quick Pane settings…"); const [error, setError] = useState(); const [runtime, setRuntime] = useState(); const [answer, setAnswer] = useState(""); const [escalation, setEscalation] = useState(); const [extension, setExtension] = useState(); const runtimeRef = useRef(undefined); const requestRef = useRef(request); const inputRef = useRef(null); const phaseRef = useRef<"idle" | "starting" | "running">("idle"); const invalid = useMemo(() => settingsError(settings), [settings]); const dismissPane = () => getCurrentWindow().hide(); const focusInput = () => inputRef.current?.focus(); 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(`${nextSettings.provider}/${nextSettings.model}`); setStatus(""); try { const catalog = await invoke<{ models?: Model[] }>("get_model_catalog"); if (active) setModels(catalog?.models ?? []); } catch (reason) { if (active) setError(`Could not load model catalog: ${String(reason)}`); } })(); return () => { active = false; const current = runtimeRef.current; if (current) invoke("close_quick_runtime", { runtimeId: current.runtimeId }).catch( () => undefined, ); }; }, []); 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(`${nextSettings.provider}/${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(); } else if (phaseRef.current === "idle" && !requestRef.current.trim()) { void dismissPane(); } }); const closeListener = paneWindow.onCloseRequested((event) => { event.preventDefault(); void dismissPane(); }); return () => { window.removeEventListener("keydown", onKeyDown); void settingsListener.then((unlisten) => unlisten()); void focusListener.then((unlisten) => unlisten()); void closeListener.then((unlisten) => unlisten()); }; }, []); 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.", ); } else if (!snapshot.state?.data?.isStreaming) { setStatus("Quick answer ready."); } } 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) 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); }, [runtime, settings.maximumSeconds]); async function run() { if (invalid || !request.trim()) return setError(invalid ?? "Enter a request."); const previous = runtimeRef.current; if (previous) await invoke("close_quick_runtime", { runtimeId: previous.runtimeId, }).catch(() => undefined); setError(undefined); setAnswer(""); setEscalation(undefined); setExtension(undefined); phaseRef.current = "starting"; setStatus("Starting advisory quick agent…"); try { const result = await invoke<{ runtime: Runtime }>( "create_quick_runtime", { worktreePath: settings.workspace, }, ); runtimeRef.current = result.runtime; setRuntime(result.runtime); const [provider, modelId] = selectedModel.split("/", 2); await invoke("set_model", { agentId: result.runtime.agentId, provider: provider || settings.provider, modelId: 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: ${request}`; await invoke("submit_prompt", { agentId: result.runtime.agentId, message: guardrails, }); phaseRef.current = "running"; setStatus( "Quick request running. Advisory limits are not trusted enforcement.", ); } catch (reason) { phaseRef.current = "idle"; setError(`Could not start quick request: ${String(reason)}`); setStatus(""); } } 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() { if (invalid || !request.trim()) 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: `${request}\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