feat(quick-pane): promote active runtime

This commit is contained in:
2026-08-28 12:07:50 +02:00
parent 0a318f5ab6
commit b44b1be4b7
19 changed files with 1298 additions and 126 deletions
+29 -35
View File
@@ -389,7 +389,8 @@ describe("QuickPane", () => {
provider: "provider",
id: "quick/model",
name: "Quick Model",
thinkingLevelMap: { off: "off", high: "high" },
reasoning: true,
thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high" },
},
];
bridgeMock([thinkingModels], () =>
@@ -663,23 +664,13 @@ describe("QuickPane", () => {
expect(response.scrollTop).toBe(100);
});
it("shows manual continuation after a completed normal answer and hands off transcript", async () => {
it("promotes completed quick runtime without copying a handoff", async () => {
bridgeMock([defaultModels], () =>
snapshotWithTranscript([
{ role: "user", content: "Inspect" },
{ role: "assistant", content: "Completed answer" },
]),
);
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "create_session_runtime")
return Promise.resolve({
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
});
return baseImplementation?.(command, payload);
},
);
render(<QuickPane />);
await preparedTrigger();
@@ -690,19 +681,26 @@ describe("QuickPane", () => {
expect(
continueButton.closest(".quick-pane-assistant-footer"),
).not.toBeNull();
const submittedPromptCount = mocks.invoke.mock.calls.filter(
([command]) => command === "submit_prompt",
).length;
await userEvent.click(continueButton);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("promote_quick_runtime", {
runtimeId: "runtime-1",
}),
);
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane");
expect(mocks.invoke).not.toHaveBeenCalledWith(
"create_session_runtime",
expect.anything(),
);
await userEvent.click(continueButton);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
agentId: "agent-full",
message:
"Inspect\n\nQuick Pane handoff: Continue this chat in a full session.\n\nQuick Pane transcript:\nUser: Inspect\n\nAssistant: Completed answer",
}),
);
expect(
mocks.invoke.mock.calls.filter(
([command]) => command === "submit_prompt",
),
).toHaveLength(submittedPromptCount);
});
it("keeps unlimited follow-ups on one runtime and renders snapshot messages in order", async () => {
@@ -988,10 +986,6 @@ describe("QuickPane", () => {
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "create_session_runtime")
return Promise.resolve({
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
});
if (command === "submit_prompt" && payload?.agentId === "agent-1")
submittedPrompts.push(String(payload.message));
return baseImplementation?.(command, payload);
@@ -1011,18 +1005,18 @@ describe("QuickPane", () => {
await screen.findByText("Second runtime failed."),
).toBeInTheDocument();
expect(screen.getByText("First answer")).toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("create_session_runtime", {
worktreePath: "/repo",
await userEvent.click(
within(screen.getByRole("status")).getByRole("button", {
name: "Continue",
}),
);
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
agentId: "agent-full",
message:
"Second\n\nQuick Pane handoff: Quick runtime failed. Continue in a full session if needed.\n\nQuick Pane transcript:\nUser: First\n\nAssistant: First answer\n\nUser: Second",
});
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("promote_quick_runtime", {
runtimeId: "runtime-1",
}),
);
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane");
});
it("maps the exact submitted payload to the entered multiline request despite delimiter collisions", async () => {
+23 -32
View File
@@ -213,6 +213,7 @@ export default function QuickPane() {
const [submissionStarting, setSubmissionStarting] = useState(false);
const [requestActive, setRequestActive] = useState(false);
const [chatReplacing, setChatReplacing] = useState(false);
const [promoting, setPromoting] = useState(false);
const [thinkingLevel, setThinkingLevel] = useState("medium");
const [models, setModels] = useState<Model[]>([]);
const [catalogStatus, setCatalogStatus] = useState<
@@ -229,7 +230,6 @@ export default function QuickPane() {
const [catalogNotice, setCatalogNotice] = useState<string>();
const [runtime, setRuntime] = useState<Runtime>();
const [messages, setMessages] = useState<TranscriptMessage[]>([]);
const [answer, setAnswer] = useState("");
const [responseComplete, setResponseComplete] = useState(false);
const [escalation, setEscalation] = useState<string>();
const [extension, setExtension] = useState<Extension>();
@@ -634,7 +634,6 @@ export default function QuickPane() {
setRequestActive(false);
setSubmissionStarting(false);
setMessages([]);
setAnswer("");
setResponseComplete(false);
setEscalation(undefined);
setExtension(undefined);
@@ -894,8 +893,6 @@ export default function QuickPane() {
activeTool: snapshot.runtime.activeTool,
hasAssistantText: !!text,
});
if (promptSubmittedRuntimeRef.current === runtime.runtimeId)
setAnswer(result);
setExtension(snapshot.extensions[0]);
if (
complete &&
@@ -983,7 +980,6 @@ export default function QuickPane() {
};
promptSubmittedRuntimeRef.current = undefined;
completionHandledRef.current = false;
setAnswer("");
setResponseComplete(false);
setError(undefined);
setEscalation(undefined);
@@ -1050,9 +1046,7 @@ export default function QuickPane() {
}
}
const handoffRequest =
request.trim() || submittedRequestRef.current?.request || "";
const canContinue = !invalid && !!handoffRequest;
const canContinue = !invalid && !!runtime && !promoting;
const completedAssistantAnswer =
responseComplete &&
messages.some((message) => message.role === "assistant");
@@ -1069,35 +1063,22 @@ export default function QuickPane() {
responseComplete ||
submittedRequestRef.current !== undefined;
const hasTranscript = messages.length > 0 || runActive || responseComplete;
const transcriptHandoff = messages
.map(
(message) =>
`${message.role === "user" ? "User" : "Assistant"}: ${message.markdown}`,
)
.join("\n\n");
async function continueFull() {
if (invalid || !handoffRequest)
return setError(invalid ?? "Enter a request.");
const current = runtimeRef.current;
if (invalid || !current) return setError(invalid ?? "Quick runtime is unavailable.");
setPromoting(true);
try {
const result = await invoke<{ runtime: Runtime }>(
"create_session_runtime",
{
worktreePath: settings.workspace,
},
);
await invoke("set_model", {
agentId: result.runtime.agentId,
provider: settings.followUpProvider,
modelId: settings.followUpModel,
await invoke<{ runtime: Runtime }>("promote_quick_runtime", {
runtimeId: current.runtimeId,
});
await invoke("submit_prompt", {
agentId: result.runtime.agentId,
message: `${handoffRequest}\n\nQuick Pane handoff: ${escalation ?? "Continue this chat in a full session."}\n\nQuick Pane transcript:\n${transcriptHandoff || answer}`,
});
setNotice("Full session started with follow-up model.");
cleanupQueueRef.current.delete(current.runtimeId);
clearRuntimeIfCurrent(current.runtimeId);
await dismissPane();
} catch (reason) {
setError(`Could not start full session: ${String(reason)}`);
setError(`Could not continue in full session: ${String(reason)}`);
} finally {
setPromoting(false);
}
}
@@ -1187,6 +1168,16 @@ export default function QuickPane() {
/>
</div>
<div className="quick-pane-header-actions">
{runtime && (
<button
className="quick-pane-action"
type="button"
disabled={catalogOverlayActive || promoting}
onClick={continueFull}
>
{promoting ? "Continuing…" : "Continue"}
</button>
)}
<button
className="quick-pane-close"
type="button"