fix(quick-pane): preserve lifecycle and answer history
This commit is contained in:
+111
-28
@@ -33,6 +33,12 @@ const defaults: QuickPaneSettings = {
|
||||
};
|
||||
|
||||
type Runtime = { agentId: string; runtimeId: string };
|
||||
type HistoryEntry = { id: string; request: string; answer: string };
|
||||
|
||||
function historyLabel(request: string) {
|
||||
const concise = request.replace(/\s+/g, " ").trim();
|
||||
return concise.length > 42 ? `${concise.slice(0, 39)}…` : concise;
|
||||
}
|
||||
|
||||
function settingsError(settings: QuickPaneSettings) {
|
||||
if (!settings.workspace.startsWith("/"))
|
||||
@@ -74,6 +80,7 @@ export default function QuickPane() {
|
||||
const [settings, setSettings] = useState<QuickPaneSettings>(defaults);
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
const [request, setRequest] = useState("");
|
||||
const [submissionStarting, setSubmissionStarting] = useState(false);
|
||||
const [thinkingLevel, setThinkingLevel] = useState("medium");
|
||||
const [models, setModels] = useState<Model[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState<ModelIdentity>({
|
||||
@@ -85,9 +92,17 @@ export default function QuickPane() {
|
||||
const [catalogError, setCatalogError] = useState<string>();
|
||||
const [runtime, setRuntime] = useState<Runtime>();
|
||||
const [answer, setAnswer] = useState("");
|
||||
const [completedRuntimeId, setCompletedRuntimeId] = useState<string>();
|
||||
const [history, setHistory] = useState<HistoryEntry[]>([]);
|
||||
const [selectedHistoryId, setSelectedHistoryId] = useState("");
|
||||
const [escalation, setEscalation] = useState<string>();
|
||||
const [extension, setExtension] = useState<Extension>();
|
||||
const runtimeRef = useRef<Runtime | undefined>(undefined);
|
||||
const submittedRequestRef = useRef<
|
||||
{ runtimeId: string; request: string } | undefined
|
||||
>(undefined);
|
||||
const completedRuntimeRef = useRef<string | undefined>(undefined);
|
||||
const submissionStartingRef = useRef(false);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const invalid = useMemo(() => settingsError(settings), [settings]);
|
||||
const modelOptions = useMemo(
|
||||
@@ -99,6 +114,10 @@ export default function QuickPane() {
|
||||
? modelIdentityValue(selectedModel)
|
||||
: "";
|
||||
const canContinue = !!escalation || (!!runtime && !!error);
|
||||
const displayedAnswer = selectedHistoryId
|
||||
? (history.find((entry) => entry.id === selectedHistoryId)?.answer ??
|
||||
answer)
|
||||
: answer;
|
||||
|
||||
const dismissPane = () => getCurrentWindow().hide();
|
||||
const focusInput = () => inputRef.current?.focus();
|
||||
@@ -170,15 +189,10 @@ export default function QuickPane() {
|
||||
void loadModelCatalog();
|
||||
}
|
||||
});
|
||||
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());
|
||||
};
|
||||
}, [loadModelCatalog]);
|
||||
|
||||
@@ -202,8 +216,30 @@ export default function QuickPane() {
|
||||
text.replace("[ESCALATE]", "").trim() ||
|
||||
"Quick agent requested a full session.",
|
||||
);
|
||||
} else if (!snapshot.state?.data?.isStreaming) {
|
||||
}
|
||||
if (
|
||||
snapshot.state?.data?.isStreaming === false &&
|
||||
completedRuntimeRef.current !== runtime.runtimeId
|
||||
) {
|
||||
completedRuntimeRef.current = runtime.runtimeId;
|
||||
setCompletedRuntimeId(runtime.runtimeId);
|
||||
setStatus("Quick answer ready.");
|
||||
const submitted = submittedRequestRef.current;
|
||||
if (submitted?.runtimeId === runtime.runtimeId) {
|
||||
setRequest((current) =>
|
||||
current === submitted.request ? "" : current,
|
||||
);
|
||||
setHistory((current) =>
|
||||
[
|
||||
{
|
||||
id: runtime.runtimeId,
|
||||
request: submitted.request,
|
||||
answer: text,
|
||||
},
|
||||
...current.filter((entry) => entry.id !== runtime.runtimeId),
|
||||
].slice(0, 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
setExtension(snapshot.extensions[0]);
|
||||
@@ -227,7 +263,7 @@ export default function QuickPane() {
|
||||
}, [runtime]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!runtime) return;
|
||||
if (!runtime || completedRuntimeId === runtime.runtimeId) return;
|
||||
const timeout = window.setTimeout(() => {
|
||||
setEscalation(
|
||||
`Quick time budget of ${settings.maximumSeconds} seconds reached.`,
|
||||
@@ -238,22 +274,32 @@ export default function QuickPane() {
|
||||
invoke("abort", { agentId: runtime.agentId }).catch(() => undefined);
|
||||
}, settings.maximumSeconds * 1000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [runtime, settings.maximumSeconds]);
|
||||
}, [completedRuntimeId, runtime, settings.maximumSeconds]);
|
||||
|
||||
async function run() {
|
||||
if (submissionStartingRef.current) return;
|
||||
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…");
|
||||
submissionStartingRef.current = true;
|
||||
setSubmissionStarting(true);
|
||||
const submittedRequest = request;
|
||||
try {
|
||||
const previous = runtimeRef.current;
|
||||
if (previous)
|
||||
await invoke("close_quick_runtime", {
|
||||
runtimeId: previous.runtimeId,
|
||||
}).catch(() => undefined);
|
||||
runtimeRef.current = undefined;
|
||||
submittedRequestRef.current = undefined;
|
||||
completedRuntimeRef.current = undefined;
|
||||
setRuntime(undefined);
|
||||
setCompletedRuntimeId(undefined);
|
||||
setSelectedHistoryId("");
|
||||
setError(undefined);
|
||||
setAnswer("");
|
||||
setEscalation(undefined);
|
||||
setExtension(undefined);
|
||||
setStatus("Starting advisory quick agent…");
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
"create_quick_runtime",
|
||||
{
|
||||
@@ -261,6 +307,10 @@ export default function QuickPane() {
|
||||
},
|
||||
);
|
||||
runtimeRef.current = result.runtime;
|
||||
submittedRequestRef.current = {
|
||||
runtimeId: result.runtime.runtimeId,
|
||||
request: submittedRequest,
|
||||
};
|
||||
setRuntime(result.runtime);
|
||||
await invoke("set_model", {
|
||||
agentId: result.runtime.agentId,
|
||||
@@ -271,17 +321,22 @@ export default function QuickPane() {
|
||||
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: ${submittedRequest}`;
|
||||
await invoke("submit_prompt", {
|
||||
agentId: result.runtime.agentId,
|
||||
message: guardrails,
|
||||
});
|
||||
setStatus(
|
||||
"Quick request running. Advisory limits are not trusted enforcement.",
|
||||
);
|
||||
if (completedRuntimeRef.current !== result.runtime.runtimeId) {
|
||||
setStatus(
|
||||
"Quick request running. Advisory limits are not trusted enforcement.",
|
||||
);
|
||||
}
|
||||
} catch (reason) {
|
||||
setError(`Could not start quick request: ${String(reason)}`);
|
||||
setStatus("");
|
||||
} finally {
|
||||
submissionStartingRef.current = false;
|
||||
setSubmissionStarting(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +355,9 @@ export default function QuickPane() {
|
||||
}
|
||||
|
||||
async function continueFull() {
|
||||
if (invalid || !request.trim())
|
||||
const handoffRequest =
|
||||
request.trim() || submittedRequestRef.current?.request || "";
|
||||
if (invalid || !handoffRequest)
|
||||
return setError(invalid ?? "Enter a request.");
|
||||
try {
|
||||
const result = await invoke<{ runtime: Runtime }>(
|
||||
@@ -316,7 +373,7 @@ export default function QuickPane() {
|
||||
});
|
||||
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}`,
|
||||
message: `${handoffRequest}\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) {
|
||||
@@ -328,7 +385,28 @@ export default function QuickPane() {
|
||||
<main className="quick-pane">
|
||||
<header>
|
||||
<strong>Quick Pane</strong>
|
||||
<button onClick={dismissPane}>Close</button>
|
||||
<div className="quick-pane-header-controls">
|
||||
{history.length > 0 && (
|
||||
<label className="quick-pane-history">
|
||||
<span>History</span>
|
||||
<select
|
||||
aria-label="Answer history"
|
||||
value={selectedHistoryId}
|
||||
onChange={(event) =>
|
||||
setSelectedHistoryId(event.currentTarget.value)
|
||||
}
|
||||
>
|
||||
<option value="">Current answer</option>
|
||||
{history.map((entry) => (
|
||||
<option key={entry.id} value={entry.id}>
|
||||
{historyLabel(entry.request)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<button onClick={dismissPane}>Close</button>
|
||||
</div>
|
||||
</header>
|
||||
<textarea
|
||||
className="quick-pane-request"
|
||||
@@ -350,7 +428,7 @@ export default function QuickPane() {
|
||||
/>
|
||||
<section className="quick-pane-output" aria-live="polite">
|
||||
{status && <p>{status}</p>}
|
||||
{answer && <pre>{answer}</pre>}
|
||||
{displayedAnswer && <pre>{displayedAnswer}</pre>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{catalogError && <p role="alert">{catalogError}</p>}
|
||||
<ExtensionDialog extension={extension} onRespond={respond} />
|
||||
@@ -398,10 +476,15 @@ export default function QuickPane() {
|
||||
</select>
|
||||
</label>
|
||||
<button
|
||||
disabled={!settingsLoaded || !!invalid || !request.trim()}
|
||||
disabled={
|
||||
submissionStarting ||
|
||||
!settingsLoaded ||
|
||||
!!invalid ||
|
||||
!request.trim()
|
||||
}
|
||||
onClick={run}
|
||||
>
|
||||
Ask
|
||||
{submissionStarting ? "Starting…" : "Ask"}
|
||||
</button>
|
||||
{canContinue && (
|
||||
<button className="quiet" onClick={continueFull}>
|
||||
|
||||
Reference in New Issue
Block a user