feat(quick-pane): add constrained agent window
This commit is contained in:
+3
-1
@@ -1,9 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import App from "./App";
|
||||
import QuickPane from "./quick-pane/QuickPane";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
{getCurrentWindow().label === "quick-pane" ? <QuickPane /> : <App />}
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ExtensionDialog } from "../components/ExtensionDialog";
|
||||
import type {
|
||||
Extension,
|
||||
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<QuickPaneSettings>(defaults);
|
||||
const [uiState, setUiState] = useState<UiState>();
|
||||
const [request, setRequest] = useState("");
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [status, setStatus] = useState("Loading Quick Pane settings…");
|
||||
const [error, setError] = useState<string>();
|
||||
const [runtime, setRuntime] = useState<Runtime>();
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [escalation, setEscalation] = useState<string>();
|
||||
const [extension, setExtension] = useState<Extension>();
|
||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
invoke<UiState>("load_ui_state")
|
||||
.then((state) => {
|
||||
if (!active) return;
|
||||
setUiState(state);
|
||||
setSettings({ ...defaults, ...state.quickPane });
|
||||
setStatus("");
|
||||
})
|
||||
.catch(
|
||||
(reason) =>
|
||||
active && setError(`Could not load settings: ${String(reason)}`),
|
||||
);
|
||||
return () => {
|
||||
active = false;
|
||||
const current = runtimeRef.current;
|
||||
if (current)
|
||||
invoke("close_quick_runtime", { runtimeId: current.runtimeId }).catch(
|
||||
() => undefined,
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtime) return;
|
||||
let active = true;
|
||||
const refresh = async () => {
|
||||
try {
|
||||
const snapshot = await invoke<RuntimeSnapshot>(
|
||||
"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 save() {
|
||||
if (!uiState) return;
|
||||
if (invalid) return setError(invalid);
|
||||
try {
|
||||
await invoke("save_ui_state", {
|
||||
state: { ...uiState, quickPane: settings },
|
||||
});
|
||||
setUiState({ ...uiState, quickPane: settings });
|
||||
setError(undefined);
|
||||
setShowSettings(false);
|
||||
setStatus("Quick Pane settings saved.");
|
||||
} catch (reason) {
|
||||
setError(`Could not save settings: ${String(reason)}`);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
await invoke("set_model", {
|
||||
agentId: result.runtime.agentId,
|
||||
provider: settings.provider,
|
||||
modelId: settings.model,
|
||||
});
|
||||
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,
|
||||
});
|
||||
setStatus(
|
||||
"Quick request running. Advisory limits are not trusted enforcement.",
|
||||
);
|
||||
} catch (reason) {
|
||||
setError(`Could not start quick request: ${String(reason)}`);
|
||||
setStatus("");
|
||||
}
|
||||
}
|
||||
|
||||
async function respond(response: Record<string, unknown>) {
|
||||
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 (
|
||||
<main
|
||||
style={{
|
||||
padding: 18,
|
||||
fontFamily: "system-ui",
|
||||
color: "#e7e7e7",
|
||||
background: "#1d2027",
|
||||
minHeight: "100vh",
|
||||
}}
|
||||
>
|
||||
<header style={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<strong>Quick Pane</strong>
|
||||
<span>
|
||||
<button onClick={() => setShowSettings(!showSettings)}>
|
||||
Settings
|
||||
</button>
|
||||
<button onClick={() => getCurrentWindow().hide()}>Close</button>
|
||||
</span>
|
||||
</header>
|
||||
{showSettings && (
|
||||
<section>
|
||||
<label>
|
||||
Workspace{" "}
|
||||
<input
|
||||
value={settings.workspace}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, workspace: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Quick provider/model{" "}
|
||||
<input
|
||||
value={settings.provider}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, provider: e.target.value })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
value={settings.model}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, model: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Follow-up provider/model{" "}
|
||||
<input
|
||||
value={settings.followUpProvider}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, followUpProvider: e.target.value })
|
||||
}
|
||||
/>
|
||||
<input
|
||||
value={settings.followUpModel}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, followUpModel: e.target.value })
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Seconds/tools{" "}
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maximumSeconds}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
maximumSeconds: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={settings.maximumToolCalls}
|
||||
onChange={(e) =>
|
||||
setSettings({
|
||||
...settings,
|
||||
maximumToolCalls: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
<textarea
|
||||
value={settings.instructions}
|
||||
onChange={(e) =>
|
||||
setSettings({ ...settings, instructions: e.target.value })
|
||||
}
|
||||
/>
|
||||
<button onClick={save}>Save</button>
|
||||
</section>
|
||||
)}
|
||||
<textarea
|
||||
autoFocus
|
||||
value={request}
|
||||
onChange={(e) => setRequest(e.target.value)}
|
||||
placeholder="Ask a quick question or give a quick instruction"
|
||||
style={{ width: "100%", minHeight: 90, marginTop: 14 }}
|
||||
/>
|
||||
<button disabled={!uiState || !!invalid || !request.trim()} onClick={run}>
|
||||
Ask
|
||||
</button>
|
||||
{(escalation || error) && (
|
||||
<button onClick={continueFull}>Continue in full session</button>
|
||||
)}
|
||||
{status && <p>{status}</p>}
|
||||
{answer && <pre style={{ whiteSpace: "pre-wrap" }}>{answer}</pre>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<ExtensionDialog extension={extension} onRespond={respond} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -219,6 +219,17 @@ export type WorkspaceState = {
|
||||
interfaceScale: number;
|
||||
};
|
||||
|
||||
export type QuickPaneSettings = {
|
||||
workspace: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
followUpProvider: string;
|
||||
followUpModel: string;
|
||||
instructions: string;
|
||||
maximumSeconds: number;
|
||||
maximumToolCalls: number;
|
||||
};
|
||||
|
||||
export type UiState = {
|
||||
version: 1;
|
||||
directoryOrder: string[];
|
||||
@@ -234,4 +245,5 @@ export type UiState = {
|
||||
interfaceScale?: number;
|
||||
defaultNewSessionWorktree?: string;
|
||||
dismissedCollisionWarning?: boolean;
|
||||
quickPane?: QuickPaneSettings;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user