fix(quick-pane): refresh runtime model catalog
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+235
-116
@@ -5,11 +5,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { ExtensionDialog } from "../components/ExtensionDialog";
|
||||
import { ModelSelector } from "../components/ModelSelector";
|
||||
import {
|
||||
catalogModels,
|
||||
getModelCatalog,
|
||||
modelIdentity,
|
||||
modelIdentityFromValue,
|
||||
modelIdentityValue,
|
||||
reconcileModelIdentity,
|
||||
type ModelIdentity,
|
||||
@@ -24,6 +22,8 @@ import type {
|
||||
} from "../workspace/types";
|
||||
|
||||
const ESCALATION_MARKER = "[ESCALATE]";
|
||||
const pendingQuickRuntimeCleanupIds = new Set<string>();
|
||||
const pendingQuickRuntimeCleanupPromises = new Map<string, Promise<void>>();
|
||||
|
||||
const defaults: QuickPaneSettings = {
|
||||
workspace: "",
|
||||
@@ -38,6 +38,7 @@ const defaults: QuickPaneSettings = {
|
||||
};
|
||||
|
||||
type Runtime = { agentId: string; runtimeId: string };
|
||||
type PreparedRuntime = { runtime: Runtime; models: Model[] };
|
||||
type HistoryEntry = { id: string; request: string; answer: string };
|
||||
type ActivitySnapshot = {
|
||||
activeTool?: string;
|
||||
@@ -109,6 +110,7 @@ export default function QuickPane() {
|
||||
const [request, setRequest] = useState("");
|
||||
const [currentRequest, setCurrentRequest] = useState("");
|
||||
const [submissionStarting, setSubmissionStarting] = useState(false);
|
||||
const [requestActive, setRequestActive] = useState(false);
|
||||
const [thinkingLevel, setThinkingLevel] = useState("medium");
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
const [catalogStatus, setCatalogStatus] = useState<
|
||||
@@ -125,7 +127,6 @@ export default function QuickPane() {
|
||||
const [runtime, setRuntime] = useState<Runtime>();
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [completedRuntimeId, setCompletedRuntimeId] = useState<string>();
|
||||
const [failedRuntimeId, setFailedRuntimeId] = useState<string>();
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState("");
|
||||
const [escalation, setEscalation] = useState<string>();
|
||||
@@ -135,6 +136,12 @@ export default function QuickPane() {
|
||||
});
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||
const preparedRuntimeRef = useRef<PreparedRuntime | undefined>(undefined);
|
||||
const cleanupQueueRef = useRef(pendingQuickRuntimeCleanupIds);
|
||||
const mountedRef = useRef(true);
|
||||
const requestActiveRef = useRef(false);
|
||||
const settingsRef = useRef(settings);
|
||||
settingsRef.current = settings;
|
||||
const submittedRequestRef = useRef<
|
||||
{ runtimeId: string; request: string } | undefined
|
||||
>(undefined);
|
||||
@@ -142,21 +149,13 @@ export default function QuickPane() {
|
||||
const completedRuntimeRef = useRef<string | undefined>(undefined);
|
||||
const submissionStartingRef = useRef(false);
|
||||
const runStartedAtRef = useRef<number | undefined>(undefined);
|
||||
const catalogRequestRef = useRef(0);
|
||||
const preparationGenerationRef = useRef(0);
|
||||
const preparationLockRef = useRef(false);
|
||||
const catalogReadyRef = useRef(false);
|
||||
const selectedModelRef = useRef(selectedModel);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||
const modelOptions = useMemo(() => catalogModels(models, []), [models]);
|
||||
const selectedModelValue =
|
||||
selectedModel.provider && selectedModel.modelId
|
||||
? modelIdentityValue(selectedModel)
|
||||
: "";
|
||||
const runActive =
|
||||
submissionStarting ||
|
||||
(!!runtime &&
|
||||
completedRuntimeId !== runtime.runtimeId &&
|
||||
failedRuntimeId !== runtime.runtimeId);
|
||||
const runActive = submissionStarting || requestActive;
|
||||
const viewingHistory = !!selectedHistoryId && !runActive;
|
||||
const displayedEntry = viewingHistory
|
||||
? history.find((entry) => entry.id === selectedHistoryId)
|
||||
@@ -179,48 +178,160 @@ export default function QuickPane() {
|
||||
? `${activityPhase} · ${elapsedSeconds}s`
|
||||
: activityPhase;
|
||||
|
||||
const dismissPane = () => invoke("dismiss_quick_pane");
|
||||
const focusInput = () => inputRef.current?.focus();
|
||||
const loadModelCatalog = useCallback(
|
||||
async (preferredModel?: ModelIdentity) => {
|
||||
const requestId = ++catalogRequestRef.current;
|
||||
const closeRuntime = useCallback(async (runtimeId: string) => {
|
||||
const pending = pendingQuickRuntimeCleanupPromises.get(runtimeId);
|
||||
if (pending) return pending;
|
||||
|
||||
cleanupQueueRef.current.add(runtimeId);
|
||||
const cleanup = invoke("close_quick_runtime", { runtimeId })
|
||||
.then(() => {
|
||||
cleanupQueueRef.current.delete(runtimeId);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingQuickRuntimeCleanupPromises.delete(runtimeId);
|
||||
});
|
||||
pendingQuickRuntimeCleanupPromises.set(runtimeId, cleanup);
|
||||
return cleanup;
|
||||
}, []);
|
||||
const clearRuntimeIfCurrent = useCallback((runtimeId: string) => {
|
||||
if (runtimeRef.current?.runtimeId !== runtimeId) return;
|
||||
runtimeRef.current = undefined;
|
||||
preparedRuntimeRef.current = undefined;
|
||||
setRuntime(undefined);
|
||||
}, []);
|
||||
const prepareRuntime = useCallback(
|
||||
async (preferredModel?: ModelIdentity, ignoreLock = false) => {
|
||||
if (
|
||||
requestActiveRef.current ||
|
||||
(preparationLockRef.current && !ignoreLock)
|
||||
)
|
||||
return false;
|
||||
const generation = ++preparationGenerationRef.current;
|
||||
catalogReadyRef.current = false;
|
||||
setCatalogStatus("loading");
|
||||
setCatalogError(undefined);
|
||||
setCatalogNotice(undefined);
|
||||
setModels([]);
|
||||
|
||||
try {
|
||||
const freshModels = await getModelCatalog();
|
||||
if (requestId !== catalogRequestRef.current) return;
|
||||
for (const runtimeId of [...cleanupQueueRef.current]) {
|
||||
await closeRuntime(runtimeId);
|
||||
clearRuntimeIfCurrent(runtimeId);
|
||||
}
|
||||
|
||||
const previous = runtimeRef.current;
|
||||
if (previous) {
|
||||
await closeRuntime(previous.runtimeId);
|
||||
clearRuntimeIfCurrent(previous.runtimeId);
|
||||
}
|
||||
} catch (reason) {
|
||||
if (generation === preparationGenerationRef.current) {
|
||||
setCatalogStatus("error");
|
||||
setCatalogError(`Could not prepare model catalog: ${String(reason)}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (generation !== preparationGenerationRef.current) return false;
|
||||
|
||||
let created: Runtime | undefined;
|
||||
let retained = false;
|
||||
let preparationFailure: unknown;
|
||||
try {
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
"create_quick_runtime",
|
||||
{ worktreePath: settingsRef.current.workspace },
|
||||
);
|
||||
created = result.runtime;
|
||||
if (generation !== preparationGenerationRef.current) return false;
|
||||
const snapshot = await invoke<RuntimeSnapshot>(
|
||||
"get_session_runtime_snapshot",
|
||||
{ runtimeId: created.runtimeId },
|
||||
);
|
||||
if (generation !== preparationGenerationRef.current) return false;
|
||||
if (snapshot.models?.error) throw new Error(snapshot.models.error);
|
||||
const freshModels = snapshot.models?.data?.models ?? [];
|
||||
const preferred = preferredModel ?? selectedModelRef.current;
|
||||
const reconciled = reconcileModelIdentity(freshModels, preferred);
|
||||
|
||||
runtimeRef.current = created;
|
||||
preparedRuntimeRef.current = { runtime: created, models: freshModels };
|
||||
setRuntime(created);
|
||||
setModels(freshModels);
|
||||
setCatalogError(undefined);
|
||||
catalogReadyRef.current = true;
|
||||
setCatalogStatus("ready");
|
||||
retained = true;
|
||||
if (!reconciled) {
|
||||
const emptyModel = { provider: "", modelId: "" };
|
||||
selectedModelRef.current = emptyModel;
|
||||
setSelectedModel(emptyModel);
|
||||
setCatalogStatus("ready");
|
||||
setCatalogNotice("No models are currently available.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
selectedModelRef.current = reconciled;
|
||||
setSelectedModel(reconciled);
|
||||
catalogReadyRef.current = true;
|
||||
setCatalogStatus("ready");
|
||||
setCatalogNotice(
|
||||
modelIdentityValue(reconciled) === modelIdentityValue(preferred)
|
||||
? undefined
|
||||
: `Selected model unavailable; using ${reconciled.provider}/${reconciled.modelId}.`,
|
||||
);
|
||||
return true;
|
||||
} catch (reason) {
|
||||
if (requestId !== catalogRequestRef.current) return;
|
||||
catalogReadyRef.current = false;
|
||||
setCatalogStatus("error");
|
||||
setCatalogError(`Could not load model catalog: ${String(reason)}`);
|
||||
preparationFailure = reason;
|
||||
if (generation === preparationGenerationRef.current) {
|
||||
catalogReadyRef.current = false;
|
||||
preparedRuntimeRef.current = undefined;
|
||||
setCatalogStatus("error");
|
||||
setCatalogError(`Could not prepare model catalog: ${String(reason)}`);
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (created && !retained) {
|
||||
try {
|
||||
await closeRuntime(created.runtimeId);
|
||||
} catch (cleanupReason) {
|
||||
if (generation === preparationGenerationRef.current) {
|
||||
catalogReadyRef.current = false;
|
||||
setCatalogStatus("error");
|
||||
setCatalogError(
|
||||
preparationFailure
|
||||
? `Could not prepare model catalog: ${String(preparationFailure)}; cleanup failed: ${String(cleanupReason)}`
|
||||
: `Could not clean up stale quick runtime: ${String(cleanupReason)}`,
|
||||
);
|
||||
} else if (mountedRef.current) {
|
||||
setCatalogError(
|
||||
`Could not clean up stale quick runtime: ${String(cleanupReason)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
[clearRuntimeIfCurrent, closeRuntime],
|
||||
);
|
||||
|
||||
const closeIdleRuntime = useCallback(async () => {
|
||||
const current = runtimeRef.current;
|
||||
if (!current || requestActiveRef.current) return true;
|
||||
try {
|
||||
await closeRuntime(current.runtimeId);
|
||||
clearRuntimeIfCurrent(current.runtimeId);
|
||||
return true;
|
||||
} catch (reason) {
|
||||
setCatalogStatus("error");
|
||||
setCatalogError(`Could not close Quick runtime: ${String(reason)}`);
|
||||
return false;
|
||||
}
|
||||
}, [clearRuntimeIfCurrent, closeRuntime]);
|
||||
|
||||
const dismissPane = useCallback(async () => {
|
||||
if (await closeIdleRuntime()) await invoke("dismiss_quick_pane");
|
||||
}, [closeIdleRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
mountedRef.current = true;
|
||||
void (async () => {
|
||||
let nextSettings: QuickPaneSettings;
|
||||
try {
|
||||
@@ -232,6 +343,7 @@ export default function QuickPane() {
|
||||
return;
|
||||
}
|
||||
|
||||
settingsRef.current = nextSettings;
|
||||
setSettings(nextSettings);
|
||||
setSettingsLoaded(true);
|
||||
const configuredModel = {
|
||||
@@ -241,17 +353,16 @@ export default function QuickPane() {
|
||||
selectedModelRef.current = configuredModel;
|
||||
setSelectedModel(configuredModel);
|
||||
setNotice("");
|
||||
if (active) await loadModelCatalog(configuredModel);
|
||||
if (active) await prepareRuntime(configuredModel);
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
mountedRef.current = false;
|
||||
preparationGenerationRef.current += 1;
|
||||
const current = runtimeRef.current;
|
||||
if (current)
|
||||
invoke("close_quick_runtime", { runtimeId: current.runtimeId }).catch(
|
||||
() => undefined,
|
||||
);
|
||||
if (current) void closeRuntime(current.runtimeId).catch(() => undefined);
|
||||
};
|
||||
}, [loadModelCatalog]);
|
||||
}, [closeRuntime, prepareRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (settingsLoaded) focusInput();
|
||||
@@ -279,6 +390,7 @@ export default function QuickPane() {
|
||||
"quick-pane-settings-saved",
|
||||
(event) => {
|
||||
const nextSettings = { ...defaults, ...event.payload };
|
||||
settingsRef.current = nextSettings;
|
||||
setSettings(nextSettings);
|
||||
const configuredModel = {
|
||||
provider: nextSettings.provider,
|
||||
@@ -286,17 +398,30 @@ export default function QuickPane() {
|
||||
};
|
||||
selectedModelRef.current = configuredModel;
|
||||
setSelectedModel(configuredModel);
|
||||
void loadModelCatalog(configuredModel);
|
||||
if (!requestActiveRef.current && !submissionStartingRef.current)
|
||||
void prepareRuntime(configuredModel);
|
||||
},
|
||||
);
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") void dismissPane();
|
||||
if (event.key !== "Escape" || event.defaultPrevented) return;
|
||||
const target = event.target;
|
||||
const selector =
|
||||
target instanceof Element ? target.closest(".model-selector") : null;
|
||||
if (
|
||||
selector?.querySelector(".model-menu") ||
|
||||
document.querySelector(".model-selector .model-menu")
|
||||
)
|
||||
return;
|
||||
void dismissPane();
|
||||
};
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
const focusListener = paneWindow.onFocusChanged((event) => {
|
||||
if (event.payload) {
|
||||
focusInput();
|
||||
void loadModelCatalog();
|
||||
if (!requestActiveRef.current && !submissionStartingRef.current)
|
||||
void prepareRuntime();
|
||||
} else if (!submissionStartingRef.current) {
|
||||
void closeIdleRuntime();
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -304,10 +429,10 @@ export default function QuickPane() {
|
||||
void settingsListener.then((unlisten) => unlisten());
|
||||
void focusListener.then((unlisten) => unlisten());
|
||||
};
|
||||
}, [loadModelCatalog]);
|
||||
}, [closeIdleRuntime, dismissPane, prepareRuntime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtime) return;
|
||||
if (!runtime || !requestActive) return;
|
||||
let active = true;
|
||||
const refresh = async () => {
|
||||
try {
|
||||
@@ -347,6 +472,8 @@ export default function QuickPane() {
|
||||
completedRuntimeRef.current !== runtime.runtimeId
|
||||
) {
|
||||
completedRuntimeRef.current = runtime.runtimeId;
|
||||
requestActiveRef.current = false;
|
||||
setRequestActive(false);
|
||||
setCompletedRuntimeId(runtime.runtimeId);
|
||||
setNotice("");
|
||||
const submitted = submittedRequestRef.current;
|
||||
@@ -370,7 +497,8 @@ export default function QuickPane() {
|
||||
setExtension(snapshot.extensions[0]);
|
||||
if (snapshot.runtime.error?.message) {
|
||||
setError(snapshot.runtime.error.message);
|
||||
setFailedRuntimeId(runtime.runtimeId);
|
||||
requestActiveRef.current = false;
|
||||
setRequestActive(false);
|
||||
setEscalation(
|
||||
"Quick runtime failed. Continue in a full session if needed.",
|
||||
);
|
||||
@@ -386,84 +514,94 @@ export default function QuickPane() {
|
||||
active = false;
|
||||
window.clearInterval(interval);
|
||||
};
|
||||
}, [runtime]);
|
||||
}, [requestActive, runtime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtime || completedRuntimeId === runtime.runtimeId) return;
|
||||
if (!runtime || !requestActive) return;
|
||||
const timeout = window.setTimeout(() => {
|
||||
setEscalation(
|
||||
`Quick time budget of ${settings.maximumSeconds} seconds reached.`,
|
||||
);
|
||||
setFailedRuntimeId(runtime.runtimeId);
|
||||
requestActiveRef.current = false;
|
||||
setRequestActive(false);
|
||||
invoke("abort", { agentId: runtime.agentId }).catch(() => undefined);
|
||||
}, settings.maximumSeconds * 1000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [completedRuntimeId, runtime, settings.maximumSeconds]);
|
||||
}, [requestActive, runtime, settings.maximumSeconds]);
|
||||
|
||||
async function run() {
|
||||
if (submissionStartingRef.current || runActive) return;
|
||||
if (!catalogReadyRef.current || !selectedModelRef.current.modelId)
|
||||
return setError("Model catalog is not ready.");
|
||||
if (submissionStartingRef.current || requestActiveRef.current) return;
|
||||
if (invalid || !request.trim())
|
||||
return setError(invalid ?? "Enter a request.");
|
||||
if (!catalogReadyRef.current || !selectedModelRef.current.modelId)
|
||||
return setError("Model catalog is not ready.");
|
||||
|
||||
submissionStartingRef.current = true;
|
||||
preparationLockRef.current = true;
|
||||
setSubmissionStarting(true);
|
||||
const submittedRequest = request;
|
||||
const submittedModel = selectedModelRef.current;
|
||||
setCurrentRequest(submittedRequest);
|
||||
setSelectedHistoryId("");
|
||||
runStartedAtRef.current = Date.now();
|
||||
setElapsedSeconds(0);
|
||||
try {
|
||||
const previous = runtimeRef.current;
|
||||
if (previous)
|
||||
await invoke("close_quick_runtime", {
|
||||
runtimeId: previous.runtimeId,
|
||||
}).catch(() => undefined);
|
||||
runtimeRef.current = undefined;
|
||||
submittedRequestRef.current = undefined;
|
||||
if (!preparedRuntimeRef.current) {
|
||||
const prepared = await prepareRuntime(selectedModelRef.current, true);
|
||||
if (!prepared) throw new Error("Model catalog is not ready.");
|
||||
}
|
||||
const prepared = preparedRuntimeRef.current;
|
||||
const submittedModel = selectedModelRef.current;
|
||||
if (
|
||||
!prepared ||
|
||||
!prepared.models.some((model) => {
|
||||
const identity = modelIdentity(model);
|
||||
return (
|
||||
identity?.provider === submittedModel.provider &&
|
||||
identity.modelId === submittedModel.modelId
|
||||
);
|
||||
})
|
||||
)
|
||||
throw new Error("Selected model is not in prepared runtime catalog.");
|
||||
|
||||
const submittedRuntime = prepared.runtime;
|
||||
preparedRuntimeRef.current = undefined;
|
||||
submittedRequestRef.current = {
|
||||
runtimeId: submittedRuntime.runtimeId,
|
||||
request: submittedRequest,
|
||||
};
|
||||
promptSubmittedRuntimeRef.current = undefined;
|
||||
completedRuntimeRef.current = undefined;
|
||||
setRuntime(undefined);
|
||||
setCurrentRequest(submittedRequest);
|
||||
setSelectedHistoryId("");
|
||||
setCompletedRuntimeId(undefined);
|
||||
setFailedRuntimeId(undefined);
|
||||
setError(undefined);
|
||||
setAnswer("");
|
||||
setEscalation(undefined);
|
||||
setExtension(undefined);
|
||||
setActivitySnapshot({ hasAssistantText: false });
|
||||
setNotice("");
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
"create_quick_runtime",
|
||||
{
|
||||
worktreePath: settings.workspace,
|
||||
},
|
||||
);
|
||||
runtimeRef.current = result.runtime;
|
||||
submittedRequestRef.current = {
|
||||
runtimeId: result.runtime.runtimeId,
|
||||
request: submittedRequest,
|
||||
};
|
||||
setRuntime(result.runtime);
|
||||
runStartedAtRef.current = Date.now();
|
||||
setElapsedSeconds(0);
|
||||
requestActiveRef.current = true;
|
||||
setRequestActive(true);
|
||||
|
||||
await invoke("set_model", {
|
||||
agentId: result.runtime.agentId,
|
||||
agentId: submittedRuntime.agentId,
|
||||
provider: submittedModel.provider,
|
||||
modelId: submittedModel.modelId,
|
||||
});
|
||||
await invoke("set_thinking_level", {
|
||||
agentId: result.runtime.agentId,
|
||||
agentId: submittedRuntime.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: ${submittedRequest}`;
|
||||
await invoke("submit_prompt", {
|
||||
agentId: result.runtime.agentId,
|
||||
agentId: submittedRuntime.agentId,
|
||||
message: guardrails,
|
||||
});
|
||||
promptSubmittedRuntimeRef.current = result.runtime.runtimeId;
|
||||
promptSubmittedRuntimeRef.current = submittedRuntime.runtimeId;
|
||||
} catch (reason) {
|
||||
requestActiveRef.current = false;
|
||||
setRequestActive(false);
|
||||
setError(`Could not start quick request: ${String(reason)}`);
|
||||
setFailedRuntimeId(runtimeRef.current?.runtimeId);
|
||||
} finally {
|
||||
preparationLockRef.current = false;
|
||||
submissionStartingRef.current = false;
|
||||
setSubmissionStarting(false);
|
||||
focusInput();
|
||||
@@ -516,42 +654,23 @@ export default function QuickPane() {
|
||||
<main className="quick-pane">
|
||||
<header className="quick-pane-toolbar">
|
||||
<div className="quick-pane-primary-controls">
|
||||
<label className="quick-pane-field quick-pane-model">
|
||||
<span className="full-label">Model</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
M
|
||||
</span>
|
||||
<select
|
||||
aria-label="Model"
|
||||
value={selectedModelValue}
|
||||
disabled={runActive}
|
||||
onChange={(event) => {
|
||||
const identity = modelIdentityFromValue(
|
||||
event.currentTarget.value,
|
||||
);
|
||||
if (identity) {
|
||||
selectedModelRef.current = identity;
|
||||
setSelectedModel(identity);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled>
|
||||
Select model
|
||||
</option>
|
||||
{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>
|
||||
<ModelSelector
|
||||
className="quick-pane-model-selector"
|
||||
placement="down"
|
||||
models={models}
|
||||
current={{
|
||||
provider: selectedModel.provider,
|
||||
id: selectedModel.modelId,
|
||||
}}
|
||||
disabled={runActive || catalogStatus !== "ready"}
|
||||
onSelect={async (model) => {
|
||||
const identity = modelIdentity(model);
|
||||
if (!identity) return false;
|
||||
selectedModelRef.current = identity;
|
||||
setSelectedModel(identity);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
<label className="quick-pane-field quick-pane-thinking">
|
||||
<span className="full-label">Thinking</span>
|
||||
<span className="compact-label" aria-hidden="true">
|
||||
|
||||
@@ -22,7 +22,7 @@ body {
|
||||
flex-direction: column;
|
||||
gap: 9px;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
color: #e6e3df;
|
||||
font:
|
||||
@@ -58,13 +58,11 @@ body {
|
||||
|
||||
.quick-pane-primary-controls {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-pane-header-actions {
|
||||
flex: 0 1 auto;
|
||||
justify-content: end;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-pane-field {
|
||||
@@ -76,9 +74,53 @@ body {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.quick-pane-model {
|
||||
.quick-pane-model-selector {
|
||||
flex: 1 1 230px;
|
||||
max-width: 310px;
|
||||
color: #bbb7b0;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.quick-pane .model-trigger {
|
||||
height: 27px;
|
||||
border: 0;
|
||||
padding: 5px 25px 5px 8px;
|
||||
color: #e6e3df;
|
||||
background-color: #353535;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-pane .model-menu {
|
||||
width: min(400px, calc(100vw - 24px));
|
||||
max-height: min(350px, calc(100vh - 56px));
|
||||
color: #e6e3df;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.quick-pane .model-option {
|
||||
display: grid;
|
||||
padding: 6px 7px;
|
||||
border: 1px solid transparent;
|
||||
color: #f2ece4;
|
||||
background: #35322e;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-pane .model-option:hover {
|
||||
border-color: #8c6338;
|
||||
background: #44382d;
|
||||
}
|
||||
|
||||
.quick-pane .model-option[aria-selected="true"] {
|
||||
border-color: #f0a347;
|
||||
background: #4b3420;
|
||||
}
|
||||
|
||||
.quick-pane-thinking {
|
||||
|
||||
Reference in New Issue
Block a user