feat(quick-pane): refine spotlight workflow
This commit is contained in:
@@ -1205,6 +1205,16 @@ export function createAgentRegistry({
|
|||||||
if (!runtime.ephemeral) throw new Error("runtime is not a quick runtime");
|
if (!runtime.ephemeral) throw new Error("runtime is not a quick runtime");
|
||||||
return closeRuntime(runtimeId);
|
return closeRuntime(runtimeId);
|
||||||
},
|
},
|
||||||
|
async getModelCatalog() {
|
||||||
|
const runtime = [...runtimesByAgentId.values()].find(
|
||||||
|
(candidate) => candidate.adapter,
|
||||||
|
);
|
||||||
|
if (!runtime?.adapter) return [];
|
||||||
|
const response = await enqueue(runtime, () =>
|
||||||
|
runtime.adapter.send({ type: "get_available_models" }),
|
||||||
|
);
|
||||||
|
return response?.data?.models ?? [];
|
||||||
|
},
|
||||||
listAgents() {
|
listAgents() {
|
||||||
return [...runtimesByAgentId.values()]
|
return [...runtimesByAgentId.values()]
|
||||||
.map(publicAgent)
|
.map(publicAgent)
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export async function startBridgeService({
|
|||||||
return registry.getWorkspace();
|
return registry.getWorkspace();
|
||||||
case "get_workspace_summary":
|
case "get_workspace_summary":
|
||||||
return registry.getWorkspaceSummary();
|
return registry.getWorkspaceSummary();
|
||||||
|
case "get_model_catalog":
|
||||||
|
return { models: await registry.getModelCatalog() };
|
||||||
case "create_session_runtime":
|
case "create_session_runtime":
|
||||||
return {
|
return {
|
||||||
runtime: await registry.createSessionRuntime(
|
runtime: await registry.createSessionRuntime(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const requestOperations = new Map([
|
|||||||
["forget_directory", { agent: false, payload: "worktree" }],
|
["forget_directory", { agent: false, payload: "worktree" }],
|
||||||
["get_workspace", { agent: false, payload: "none" }],
|
["get_workspace", { agent: false, payload: "none" }],
|
||||||
["get_workspace_summary", { agent: false, payload: "none" }],
|
["get_workspace_summary", { agent: false, payload: "none" }],
|
||||||
|
["get_model_catalog", { agent: false, payload: "none" }],
|
||||||
["create_session_runtime", { agent: false, payload: "worktree" }],
|
["create_session_runtime", { agent: false, payload: "worktree" }],
|
||||||
["create_quick_runtime", { agent: false, payload: "worktree" }],
|
["create_quick_runtime", { agent: false, payload: "worktree" }],
|
||||||
["open_session_runtime", { agent: false, payload: "worktreeSession" }],
|
["open_session_runtime", { agent: false, payload: "worktreeSession" }],
|
||||||
|
|||||||
@@ -19,6 +19,29 @@ test("Quick Pane polls snapshots, renders answers, and gates handoff", async ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("main Settings owns Quick Pane configuration while pane keeps request controls", async () => {
|
||||||
|
const [pane, app, persistence] = await Promise.all([
|
||||||
|
readFile(
|
||||||
|
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
readFile(new URL("../ui/src/App.tsx", import.meta.url), "utf8"),
|
||||||
|
readFile(
|
||||||
|
new URL("../ui/src/workspace/persistence.ts", import.meta.url),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
assert.match(app, /load_ui_state/);
|
||||||
|
assert.match(app, /save_ui_state/);
|
||||||
|
assert.match(app, /Save Quick Pane settings/);
|
||||||
|
assert.match(pane, /get_model_catalog/);
|
||||||
|
assert.match(pane, /set_thinking_level/);
|
||||||
|
assert.doesNotMatch(pane, /showSettings/);
|
||||||
|
assert.doesNotMatch(pane, /Save Quick Pane settings/);
|
||||||
|
assert.doesNotMatch(pane, /setRequest\(""\)/);
|
||||||
|
assert.match(persistence, /quickPane: current\.quickPane/);
|
||||||
|
});
|
||||||
|
|
||||||
test("Quick Pane routes extension responses through existing bridge command", async () => {
|
test("Quick Pane routes extension responses through existing bridge command", async () => {
|
||||||
const source = await readFile(
|
const source = await readFile(
|
||||||
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
|
new URL("../ui/src/quick-pane/QuickPane.tsx", import.meta.url),
|
||||||
|
|||||||
@@ -203,6 +203,9 @@ pub async fn get_workspace(socket_path: &str) -> Result<Workspace, String> {
|
|||||||
pub async fn get_workspace_summary(socket_path: &str) -> Result<WorkspaceSummary, String> {
|
pub async fn get_workspace_summary(socket_path: &str) -> Result<WorkspaceSummary, String> {
|
||||||
typed_request(socket_path, "get_workspace_summary", None).await
|
typed_request(socket_path, "get_workspace_summary", None).await
|
||||||
}
|
}
|
||||||
|
pub async fn get_model_catalog(socket_path: &str) -> Result<Value, String> {
|
||||||
|
request(socket_path, "get_model_catalog", None, None).await
|
||||||
|
}
|
||||||
pub async fn create_session_runtime(
|
pub async fn create_session_runtime(
|
||||||
socket_path: &str,
|
socket_path: &str,
|
||||||
worktree_path: &str,
|
worktree_path: &str,
|
||||||
|
|||||||
@@ -122,6 +122,11 @@ async fn get_workspace_summary() -> Result<bridge::WorkspaceSummary, String> {
|
|||||||
bridge::get_workspace_summary(&socket_path()?).await
|
bridge::get_workspace_summary(&socket_path()?).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_model_catalog() -> Result<Value, String> {
|
||||||
|
bridge::get_model_catalog(&socket_path()?).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn create_session_runtime(worktree_path: String) -> Result<bridge::RuntimeResult, String> {
|
async fn create_session_runtime(worktree_path: String) -> Result<bridge::RuntimeResult, String> {
|
||||||
bridge::create_session_runtime(&socket_path()?, &worktree_path).await
|
bridge::create_session_runtime(&socket_path()?, &worktree_path).await
|
||||||
@@ -410,6 +415,7 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_workspace,
|
get_workspace,
|
||||||
get_workspace_summary,
|
get_workspace_summary,
|
||||||
|
get_model_catalog,
|
||||||
create_session_runtime,
|
create_session_runtime,
|
||||||
create_quick_runtime,
|
create_quick_runtime,
|
||||||
open_session_runtime,
|
open_session_runtime,
|
||||||
|
|||||||
@@ -1043,6 +1043,44 @@ select:focus-visible {
|
|||||||
color: #b9afa4;
|
color: #b9afa4;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
.quick-pane-settings {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
max-width: 560px;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid #4b4b4b;
|
||||||
|
}
|
||||||
|
.quick-pane-settings h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.quick-pane-settings label {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
color: #ddd8d0;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.quick-pane-settings input,
|
||||||
|
.quick-pane-settings textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #70522f;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #f2ece4;
|
||||||
|
background: #2c2a27;
|
||||||
|
}
|
||||||
|
.quick-pane-settings textarea {
|
||||||
|
min-height: 90px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.quick-pane-settings button {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.quick-pane-settings p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
.extension-widget {
|
.extension-widget {
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border-left: 3px solid #f0a347;
|
border-left: 3px solid #f0a347;
|
||||||
|
|||||||
+208
@@ -1,4 +1,5 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { emitTo } from "@tauri-apps/api/event";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
@@ -14,7 +15,9 @@ import type {
|
|||||||
DirectorySession,
|
DirectorySession,
|
||||||
DirectoryWorkspace,
|
DirectoryWorkspace,
|
||||||
Model,
|
Model,
|
||||||
|
QuickPaneSettings,
|
||||||
RuntimeView,
|
RuntimeView,
|
||||||
|
UiState,
|
||||||
} from "./workspace/types";
|
} from "./workspace/types";
|
||||||
import { useWorkspace } from "./workspace/useWorkspace";
|
import { useWorkspace } from "./workspace/useWorkspace";
|
||||||
|
|
||||||
@@ -22,6 +25,39 @@ const minimumUiScale = 0.9,
|
|||||||
maximumUiScale = 1.4;
|
maximumUiScale = 1.4;
|
||||||
const clamp = (value: number) =>
|
const clamp = (value: number) =>
|
||||||
Math.min(maximumUiScale, Math.max(minimumUiScale, value));
|
Math.min(maximumUiScale, Math.max(minimumUiScale, value));
|
||||||
|
const defaultQuickPaneSettings: 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,
|
||||||
|
};
|
||||||
|
function validateQuickPaneSettings(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.";
|
||||||
|
}
|
||||||
let submissionId = 0;
|
let submissionId = 0;
|
||||||
type LocalOperation = {
|
type LocalOperation = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -119,6 +155,12 @@ export default function App() {
|
|||||||
() => new Set(),
|
() => new Set(),
|
||||||
);
|
);
|
||||||
const [restoreTabFocus, setRestoreTabFocus] = useState(false);
|
const [restoreTabFocus, setRestoreTabFocus] = useState(false);
|
||||||
|
const [uiState, setUiState] = useState<UiState>();
|
||||||
|
const [quickPaneSettings, setQuickPaneSettings] = useState<QuickPaneSettings>(
|
||||||
|
defaultQuickPaneSettings,
|
||||||
|
);
|
||||||
|
const [quickPaneSettingsStatus, setQuickPaneSettingsStatus] = useState("");
|
||||||
|
const [quickPaneSettingsError, setQuickPaneSettingsError] = useState<string>();
|
||||||
const setRuntimeStatus = (runtimeId: string, detail: string) =>
|
const setRuntimeStatus = (runtimeId: string, detail: string) =>
|
||||||
setRuntimeStatuses((current) => ({ ...current, [runtimeId]: detail }));
|
setRuntimeStatuses((current) => ({ ...current, [runtimeId]: detail }));
|
||||||
const clearRuntimeStatus = (runtimeId: string) =>
|
const clearRuntimeStatus = (runtimeId: string) =>
|
||||||
@@ -161,6 +203,28 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true;
|
||||||
|
void invoke<UiState>("load_ui_state")
|
||||||
|
.then((loaded) => {
|
||||||
|
if (!active) return;
|
||||||
|
setUiState(loaded);
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...defaultQuickPaneSettings,
|
||||||
|
...loaded.quickPane,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (active)
|
||||||
|
setQuickPaneSettingsError(
|
||||||
|
`Could not load Quick Pane settings: ${String(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const selectedRuntimeId = selected?.summary.runtimeId;
|
const selectedRuntimeId = selected?.summary.runtimeId;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedRuntimeId) return;
|
if (!selectedRuntimeId) return;
|
||||||
@@ -181,6 +245,32 @@ export default function App() {
|
|||||||
});
|
});
|
||||||
}, [state.runtimesById]);
|
}, [state.runtimesById]);
|
||||||
|
|
||||||
|
async function saveQuickPaneSettings() {
|
||||||
|
const validationError = validateQuickPaneSettings(quickPaneSettings);
|
||||||
|
if (validationError) {
|
||||||
|
setQuickPaneSettingsError(validationError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!uiState) return;
|
||||||
|
try {
|
||||||
|
const current = await invoke<UiState>("load_ui_state");
|
||||||
|
const next = { ...current, quickPane: quickPaneSettings };
|
||||||
|
await invoke("save_ui_state", { state: next });
|
||||||
|
void emitTo(
|
||||||
|
"quick-pane",
|
||||||
|
"quick-pane-settings-saved",
|
||||||
|
quickPaneSettings,
|
||||||
|
).catch(() => undefined);
|
||||||
|
setUiState(next);
|
||||||
|
setQuickPaneSettingsError(undefined);
|
||||||
|
setQuickPaneSettingsStatus("Quick Pane settings saved.");
|
||||||
|
} catch (error) {
|
||||||
|
setQuickPaneSettingsError(
|
||||||
|
`Could not save Quick Pane settings: ${String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function createRuntime(path = state.selectedDirectoryPath) {
|
async function createRuntime(path = state.selectedDirectoryPath) {
|
||||||
if (!path || !mountedRef.current) return;
|
if (!path || !mountedRef.current) return;
|
||||||
const operationId = startOperation("creating", "Starting a new Pi session…");
|
const operationId = startOperation("creating", "Starting a new Pi session…");
|
||||||
@@ -763,6 +853,124 @@ export default function App() {
|
|||||||
Used by Ctrl+Super+Space. Only currently open directories are listed.
|
Used by Ctrl+Super+Space. Only currently open directories are listed.
|
||||||
</small>
|
</small>
|
||||||
</label>
|
</label>
|
||||||
|
<form
|
||||||
|
className="quick-pane-settings"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void saveQuickPaneSettings();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2>Quick Pane</h2>
|
||||||
|
<label>
|
||||||
|
Workspace
|
||||||
|
<input
|
||||||
|
value={quickPaneSettings.workspace}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
workspace: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Quick provider
|
||||||
|
<input
|
||||||
|
value={quickPaneSettings.provider}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
provider: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Quick model
|
||||||
|
<input
|
||||||
|
value={quickPaneSettings.model}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
model: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Follow-up provider
|
||||||
|
<input
|
||||||
|
value={quickPaneSettings.followUpProvider}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
followUpProvider: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Follow-up model
|
||||||
|
<input
|
||||||
|
value={quickPaneSettings.followUpModel}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
followUpModel: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Maximum seconds
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="600"
|
||||||
|
value={quickPaneSettings.maximumSeconds}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
maximumSeconds: Number(event.currentTarget.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Maximum tool calls
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="64"
|
||||||
|
value={quickPaneSettings.maximumToolCalls}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
maximumToolCalls: Number(event.currentTarget.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Supplemental instructions
|
||||||
|
<textarea
|
||||||
|
value={quickPaneSettings.instructions}
|
||||||
|
onChange={(event) =>
|
||||||
|
setQuickPaneSettings({
|
||||||
|
...quickPaneSettings,
|
||||||
|
instructions: event.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="submit" disabled={!uiState}>
|
||||||
|
Save Quick Pane settings
|
||||||
|
</button>
|
||||||
|
{quickPaneSettingsStatus && (
|
||||||
|
<p role="status">{quickPaneSettingsStatus}</p>
|
||||||
|
)}
|
||||||
|
{quickPaneSettingsError && <p role="alert">{quickPaneSettingsError}</p>}
|
||||||
|
</form>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
<>
|
<>
|
||||||
<p>{selected.summary.worktreePath}</p>
|
<p>{selected.summary.worktreePath}</p>
|
||||||
|
|||||||
+103
-112
@@ -1,9 +1,12 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ExtensionDialog } from "../components/ExtensionDialog";
|
import { ExtensionDialog } from "../components/ExtensionDialog";
|
||||||
|
import "./quick-pane.css";
|
||||||
import type {
|
import type {
|
||||||
Extension,
|
Extension,
|
||||||
|
Model,
|
||||||
QuickPaneSettings,
|
QuickPaneSettings,
|
||||||
RuntimeSnapshot,
|
RuntimeSnapshot,
|
||||||
UiState,
|
UiState,
|
||||||
@@ -61,9 +64,11 @@ function assistantText(snapshot: RuntimeSnapshot) {
|
|||||||
|
|
||||||
export default function QuickPane() {
|
export default function QuickPane() {
|
||||||
const [settings, setSettings] = useState<QuickPaneSettings>(defaults);
|
const [settings, setSettings] = useState<QuickPaneSettings>(defaults);
|
||||||
const [uiState, setUiState] = useState<UiState>();
|
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||||
const [request, setRequest] = useState("");
|
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 [status, setStatus] = useState("Loading Quick Pane settings…");
|
||||||
const [error, setError] = useState<string>();
|
const [error, setError] = useState<string>();
|
||||||
const [runtime, setRuntime] = useState<Runtime>();
|
const [runtime, setRuntime] = useState<Runtime>();
|
||||||
@@ -71,16 +76,26 @@ export default function QuickPane() {
|
|||||||
const [escalation, setEscalation] = useState<string>();
|
const [escalation, setEscalation] = useState<string>();
|
||||||
const [extension, setExtension] = useState<Extension>();
|
const [extension, setExtension] = useState<Extension>();
|
||||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||||
|
const requestRef = useRef(request);
|
||||||
|
const phaseRef = useRef<"idle" | "starting" | "running">("idle");
|
||||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||||
|
|
||||||
|
const dismissPane = () => getCurrentWindow().hide();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
invoke<UiState>("load_ui_state")
|
invoke<UiState>("load_ui_state")
|
||||||
.then((state) => {
|
.then((state) => {
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setUiState(state);
|
const nextSettings = { ...defaults, ...state.quickPane };
|
||||||
setSettings({ ...defaults, ...state.quickPane });
|
setSettings(nextSettings);
|
||||||
|
setSettingsLoaded(true);
|
||||||
|
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
|
||||||
setStatus("");
|
setStatus("");
|
||||||
|
return invoke<{ models?: Model[] }>("get_model_catalog");
|
||||||
|
})
|
||||||
|
.then((catalog) => {
|
||||||
|
if (active) setModels(catalog?.models ?? []);
|
||||||
})
|
})
|
||||||
.catch(
|
.catch(
|
||||||
(reason) =>
|
(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(() => {
|
useEffect(() => {
|
||||||
if (!runtime) return;
|
if (!runtime) return;
|
||||||
let active = true;
|
let active = true;
|
||||||
@@ -154,22 +203,6 @@ export default function QuickPane() {
|
|||||||
return () => window.clearTimeout(timeout);
|
return () => window.clearTimeout(timeout);
|
||||||
}, [runtime, settings.maximumSeconds]);
|
}, [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() {
|
async function run() {
|
||||||
if (invalid || !request.trim())
|
if (invalid || !request.trim())
|
||||||
return setError(invalid ?? "Enter a request.");
|
return setError(invalid ?? "Enter a request.");
|
||||||
@@ -182,6 +215,7 @@ export default function QuickPane() {
|
|||||||
setAnswer("");
|
setAnswer("");
|
||||||
setEscalation(undefined);
|
setEscalation(undefined);
|
||||||
setExtension(undefined);
|
setExtension(undefined);
|
||||||
|
phaseRef.current = "starting";
|
||||||
setStatus("Starting advisory quick agent…");
|
setStatus("Starting advisory quick agent…");
|
||||||
try {
|
try {
|
||||||
const result = await invoke<{ runtime: Runtime }>(
|
const result = await invoke<{ runtime: Runtime }>(
|
||||||
@@ -192,20 +226,27 @@ export default function QuickPane() {
|
|||||||
);
|
);
|
||||||
runtimeRef.current = result.runtime;
|
runtimeRef.current = result.runtime;
|
||||||
setRuntime(result.runtime);
|
setRuntime(result.runtime);
|
||||||
|
const [provider, modelId] = selectedModel.split("/", 2);
|
||||||
await invoke("set_model", {
|
await invoke("set_model", {
|
||||||
agentId: result.runtime.agentId,
|
agentId: result.runtime.agentId,
|
||||||
provider: settings.provider,
|
provider: provider || settings.provider,
|
||||||
modelId: settings.model,
|
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}`;
|
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", {
|
await invoke("submit_prompt", {
|
||||||
agentId: result.runtime.agentId,
|
agentId: result.runtime.agentId,
|
||||||
message: guardrails,
|
message: guardrails,
|
||||||
});
|
});
|
||||||
|
phaseRef.current = "running";
|
||||||
setStatus(
|
setStatus(
|
||||||
"Quick request running. Advisory limits are not trusted enforcement.",
|
"Quick request running. Advisory limits are not trusted enforcement.",
|
||||||
);
|
);
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
|
phaseRef.current = "idle";
|
||||||
setError(`Could not start quick request: ${String(reason)}`);
|
setError(`Could not start quick request: ${String(reason)}`);
|
||||||
setStatus("");
|
setStatus("");
|
||||||
}
|
}
|
||||||
@@ -251,105 +292,55 @@ export default function QuickPane() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<main className="quick-pane">
|
||||||
style={{
|
|
||||||
padding: 18,
|
|
||||||
fontFamily: "system-ui",
|
|
||||||
color: "#e7e7e7",
|
|
||||||
background: "#1d2027",
|
|
||||||
minHeight: "100vh",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<header style={{ display: "flex", justifyContent: "space-between" }}>
|
<header style={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
<strong>Quick Pane</strong>
|
<strong>Quick Pane</strong>
|
||||||
<span>
|
<button onClick={dismissPane}>Close</button>
|
||||||
<button onClick={() => setShowSettings(!showSettings)}>
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
<button onClick={() => getCurrentWindow().hide()}>Close</button>
|
|
||||||
</span>
|
|
||||||
</header>
|
</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
|
<textarea
|
||||||
autoFocus
|
autoFocus
|
||||||
value={request}
|
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"
|
placeholder="Ask a quick question or give a quick instruction"
|
||||||
style={{ width: "100%", minHeight: 90, marginTop: 14 }}
|
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
|
Ask
|
||||||
</button>
|
</button>
|
||||||
{(escalation || error) && (
|
{(escalation || error) && (
|
||||||
|
|||||||
@@ -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); }
|
||||||
@@ -42,7 +42,10 @@ export async function loadPresentation() {
|
|||||||
return invoke<UiState>("load_ui_state");
|
return invoke<UiState>("load_ui_state");
|
||||||
}
|
}
|
||||||
export async function savePresentation(state: UiState) {
|
export async function savePresentation(state: UiState) {
|
||||||
await invoke("save_ui_state", { state });
|
const current = await loadPresentation();
|
||||||
|
await invoke("save_ui_state", {
|
||||||
|
state: { ...state, quickPane: current.quickPane },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
export function legacyPreferences(storage: Pick<Storage, "getItem">) {
|
export function legacyPreferences(storage: Pick<Storage, "getItem">) {
|
||||||
const path = storage.getItem(selectedPathKey) ?? undefined;
|
const path = storage.getItem(selectedPathKey) ?? undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user