fix(quick-pane): restore model catalog controls

This commit is contained in:
2026-08-25 22:34:58 +02:00
parent ab482d3daa
commit 1b0b34fad8
10 changed files with 706 additions and 170 deletions
+111 -65
View File
@@ -1,8 +1,16 @@
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ExtensionDialog } from "../components/ExtensionDialog";
import {
catalogModels,
getModelCatalog,
modelIdentity,
modelIdentityFromValue,
modelIdentityValue,
type ModelIdentity,
} from "../model-catalog";
import "./quick-pane.css";
import type {
Extension,
@@ -68,9 +76,13 @@ export default function QuickPane() {
const [request, setRequest] = useState("");
const [thinkingLevel, setThinkingLevel] = useState("medium");
const [models, setModels] = useState<Model[]>([]);
const [selectedModel, setSelectedModel] = useState("");
const [selectedModel, setSelectedModel] = useState<ModelIdentity>({
provider: "",
modelId: "",
});
const [status, setStatus] = useState("Loading Quick Pane settings…");
const [error, setError] = useState<string>();
const [catalogError, setCatalogError] = useState<string>();
const [runtime, setRuntime] = useState<Runtime>();
const [answer, setAnswer] = useState("");
const [escalation, setEscalation] = useState<string>();
@@ -78,9 +90,26 @@ export default function QuickPane() {
const runtimeRef = useRef<Runtime | undefined>(undefined);
const inputRef = useRef<HTMLTextAreaElement>(null);
const invalid = useMemo(() => settingsError(settings), [settings]);
const modelOptions = useMemo(
() => catalogModels(models, [selectedModel]),
[models, selectedModel],
);
const selectedModelValue =
selectedModel.provider && selectedModel.modelId
? modelIdentityValue(selectedModel)
: "";
const canContinue = !!escalation || (!!runtime && !!error);
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;
@@ -91,22 +120,18 @@ export default function QuickPane() {
if (!active) return;
nextSettings = { ...defaults, ...state.quickPane };
} catch (reason) {
if (active)
setError(`Could not load settings: ${String(reason)}`);
if (active) setError(`Could not load settings: ${String(reason)}`);
return;
}
setSettings(nextSettings);
setSettingsLoaded(true);
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
setSelectedModel({
provider: nextSettings.provider,
modelId: 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)}`);
}
if (active) await loadModelCatalog();
})();
return () => {
active = false;
@@ -116,7 +141,7 @@ export default function QuickPane() {
() => undefined,
);
};
}, []);
}, [loadModelCatalog]);
useEffect(() => {
if (settingsLoaded) focusInput();
@@ -129,7 +154,10 @@ export default function QuickPane() {
(event) => {
const nextSettings = { ...defaults, ...event.payload };
setSettings(nextSettings);
setSelectedModel(`${nextSettings.provider}/${nextSettings.model}`);
setSelectedModel({
provider: nextSettings.provider,
modelId: nextSettings.model,
});
},
);
const onKeyDown = (event: KeyboardEvent) => {
@@ -137,7 +165,10 @@ export default function QuickPane() {
};
window.addEventListener("keydown", onKeyDown);
const focusListener = paneWindow.onFocusChanged((event) => {
if (event.payload) focusInput();
if (event.payload) {
focusInput();
void loadModelCatalog();
}
});
const closeListener = paneWindow.onCloseRequested((event) => {
event.preventDefault();
@@ -149,7 +180,7 @@ export default function QuickPane() {
void focusListener.then((unlisten) => unlisten());
void closeListener.then((unlisten) => unlisten());
};
}, []);
}, [loadModelCatalog]);
useEffect(() => {
if (!runtime) return;
@@ -231,11 +262,10 @@ 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: provider || settings.provider,
modelId: modelId || settings.model,
provider: selectedModel.provider || settings.provider,
modelId: selectedModel.modelId || settings.model,
});
await invoke("set_thinking_level", {
agentId: result.runtime.agentId,
@@ -296,17 +326,16 @@ export default function QuickPane() {
return (
<main className="quick-pane">
<header style={{ display: "flex", justifyContent: "space-between" }}>
<header>
<strong>Quick Pane</strong>
<button onClick={dismissPane}>Close</button>
</header>
<textarea
className="quick-pane-request"
autoFocus
ref={inputRef}
value={request}
onChange={(e) => {
setRequest(e.target.value);
}}
onChange={(event) => setRequest(event.currentTarget.value)}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
@@ -318,51 +347,68 @@ export default function QuickPane() {
}
}}
placeholder="Ask a quick question or give a quick instruction"
style={{ width: "100%", minHeight: 90, marginTop: 14 }}
/>
<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}
<section className="quick-pane-output" aria-live="polite">
{status && <p>{status}</p>}
{answer && <pre>{answer}</pre>}
{error && <p role="alert">{error}</p>}
{catalogError && <p role="alert">{catalogError}</p>}
<ExtensionDialog extension={extension} onRespond={respond} />
</section>
<footer className="quick-pane-controls">
<label>
<span>Model</span>
<select
aria-label="Model"
value={selectedModelValue}
onChange={(event) => {
const identity = modelIdentityFromValue(
event.currentTarget.value,
);
if (identity) setSelectedModel(identity);
}}
>
<option value="" disabled>
Select model
</option>
))}
</select>
</label>
<label>
Thinking
<select
value={thinkingLevel}
onChange={(event) => setThinkingLevel(event.currentTarget.value)}
{modelOptions.map((model) => {
const identity = modelIdentity(model);
if (!identity) return null;
return (
<option
key={modelIdentityValue(identity)}
value={modelIdentityValue(identity)}
>
{identity.provider}/{identity.modelId}
</option>
);
})}
</select>
</label>
<label>
<span>Thinking</span>
<select
aria-label="Thinking"
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}
>
<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) && (
<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} />
Ask
</button>
{canContinue && (
<button className="quiet" onClick={continueFull}>
Continue
</button>
)}
</footer>
</main>
);
}