feat(quick-pane): refine spotlight workflow

This commit is contained in:
2026-08-25 19:14:30 +02:00
parent dd23898769
commit 67063464e7
11 changed files with 418 additions and 113 deletions
+103 -112
View File
@@ -1,9 +1,12 @@
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,
@@ -61,9 +64,11 @@ function assistantText(snapshot: RuntimeSnapshot) {
export default function QuickPane() {
const [settings, setSettings] = useState<QuickPaneSettings>(defaults);
const [uiState, setUiState] = useState<UiState>();
const [settingsLoaded, setSettingsLoaded] = useState(false);
const [request, setRequest] = useState("");
const [showSettings, setShowSettings] = useState(false);
const [thinkingLevel, setThinkingLevel] = useState("medium");
const [models, setModels] = useState<Model[]>([]);
const [selectedModel, setSelectedModel] = useState("");
const [status, setStatus] = useState("Loading Quick Pane settings…");
const [error, setError] = useState<string>();
const [runtime, setRuntime] = useState<Runtime>();
@@ -71,16 +76,26 @@ export default function QuickPane() {
const [escalation, setEscalation] = useState<string>();
const [extension, setExtension] = useState<Extension>();
const runtimeRef = useRef<Runtime | undefined>(undefined);
const requestRef = useRef(request);
const phaseRef = useRef<"idle" | "starting" | "running">("idle");
const invalid = useMemo(() => settingsError(settings), [settings]);
const dismissPane = () => getCurrentWindow().hide();
useEffect(() => {
let active = true;
invoke<UiState>("load_ui_state")
.then((state) => {
if (!active) return;
setUiState(state);
setSettings({ ...defaults, ...state.quickPane });
const nextSettings = { ...defaults, ...state.quickPane };
setSettings(nextSettings);
setSettingsLoaded(true);
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
setStatus("");
return invoke<{ models?: Model[] }>("get_model_catalog");
})
.then((catalog) => {
if (active) setModels(catalog?.models ?? []);
})
.catch(
(reason) =>
@@ -96,6 +111,40 @@ export default function QuickPane() {
};
}, []);
useEffect(() => {
const paneWindow = getCurrentWindow();
const settingsListener = listen<QuickPaneSettings>(
"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 &&
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;
@@ -154,22 +203,6 @@ export default function QuickPane() {
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.");
@@ -182,6 +215,7 @@ export default function QuickPane() {
setAnswer("");
setEscalation(undefined);
setExtension(undefined);
phaseRef.current = "starting";
setStatus("Starting advisory quick agent…");
try {
const result = await invoke<{ runtime: Runtime }>(
@@ -192,20 +226,27 @@ export default function QuickPane() {
);
runtimeRef.current = result.runtime;
setRuntime(result.runtime);
const [provider, modelId] = selectedModel.split("/", 2);
await invoke("set_model", {
agentId: result.runtime.agentId,
provider: settings.provider,
modelId: settings.model,
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("");
}
@@ -251,105 +292,55 @@ export default function QuickPane() {
}
return (
<main
style={{
padding: 18,
fontFamily: "system-ui",
color: "#e7e7e7",
background: "#1d2027",
minHeight: "100vh",
}}
>
<main className="quick-pane">
<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>
<button onClick={dismissPane}>Close</button>
</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)}
onChange={(e) => {
requestRef.current = e.target.value;
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}>
<label>
Model
<select
value={selectedModel}
onChange={(event) => setSelectedModel(event.currentTarget.value)}
>
<option value={`${settings.provider}/${settings.model}`}>
{settings.provider}/{settings.model}
</option>
{models.map((model) => (
<option
key={`${model.provider}/${model.id}`}
value={`${model.provider}/${model.id}`}
>
{model.provider}/{model.id ?? model.name}
</option>
))}
</select>
</label>
<label>
Thinking
<select
value={thinkingLevel}
onChange={(event) => setThinkingLevel(event.currentTarget.value)}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</label>
<button
disabled={!settingsLoaded || !!invalid || !request.trim()}
onClick={run}
>
Ask
</button>
{(escalation || error) && (
+20
View File
@@ -0,0 +1,20 @@
:root { color-scheme: dark; }
body { margin: 0; background: transparent; }
.quick-pane {
box-sizing: border-box;
min-height: 100vh;
padding: 18px;
color: #f4f2ee;
font: 14px/1.4 system-ui, sans-serif;
background: radial-gradient(circle at top, #46494f, #23252a 62%);
border: 1px solid rgba(255,255,255,.16);
border-radius: 14px;
box-shadow: 0 22px 70px rgba(0,0,0,.55);
}
.quick-pane header { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; color:#c9c7c2; font-size:12px; letter-spacing:.08em; text-transform:uppercase; }
.quick-pane button, .quick-pane select { border:0; border-radius:7px; padding:7px 10px; color:inherit; background:#45484e; }
.quick-pane textarea { box-sizing:border-box; width:100%; min-height:92px; padding:14px; border:0; border-radius:10px; outline:1px solid rgba(255,255,255,.14); color:#fff; background:rgba(0,0,0,.24); font:inherit; font-size:16px; resize:none; }
.quick-pane textarea:focus { outline:2px solid #8aa9d6; background:rgba(0,0,0,.38); }
.quick-pane > label { display:inline-flex; align-items:center; gap:7px; margin:10px 8px 10px 0; color:#c9c7c2; }
.quick-pane > pre, .quick-pane > p { margin:10px 0 0; padding:10px 12px; border-radius:9px; white-space:pre-wrap; background:rgba(0,0,0,.22); }
.quick-pane [role="alert"] { color:#ffb8bd; background:rgba(119,42,52,.42); }