feat(quick-pane): add follow-up chat renewal

This commit is contained in:
2026-08-27 16:26:17 +02:00
parent 05874a0be1
commit 60fb91f375
14 changed files with 1147 additions and 549 deletions
+510 -208
View File
@@ -9,6 +9,7 @@ import {
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
Message,
Model,
QuickPaneSettings,
RuntimeSnapshot,
@@ -39,7 +40,6 @@ const settings: QuickPaneSettings = {
followUpProvider: "provider",
followUpModel: "full/model",
instructions: "Stay read-only.",
maximumSeconds: 60,
maximumToolCalls: 6,
};
const uiState: UiState = {
@@ -83,6 +83,16 @@ function runtimeSnapshot(
};
}
function snapshotWithTranscript(
messages: Message[],
isStreaming = false,
runtime: RuntimeSnapshot["runtime"] = runtimeSnapshot(defaultModels).runtime,
) {
const snapshot = runtimeSnapshot(defaultModels, "", isStreaming, runtime);
snapshot.transcript = { data: { messages } };
return snapshot;
}
function deferred<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -101,8 +111,12 @@ function bridgeMock(
) {
let runtimeCount = 0;
const submitted = new Set<string>();
const submittedPrompts = new Map<string, string>();
mocks.invoke.mockImplementation(
(command: string, payload?: { runtimeId?: string; agentId?: string }) => {
(
command: string,
payload?: { runtimeId?: string; agentId?: string; message?: string },
) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "create_quick_runtime") {
runtimeCount += 1;
@@ -114,14 +128,27 @@ function bridgeMock(
});
}
if (command === "submit_prompt") {
if (payload?.agentId) submitted.add(payload.agentId);
if (payload?.agentId) {
submitted.add(payload.agentId);
if (payload.message)
submittedPrompts.set(payload.agentId, payload.message);
}
return Promise.resolve(undefined);
}
if (command === "get_session_runtime_snapshot") {
const segments = payload?.runtimeId?.split("-") ?? [];
const index = Number(segments[segments.length - 1] ?? 1) - 1;
if (submitted.has(`agent-${index + 1}`) && activeSnapshot)
return Promise.resolve(activeSnapshot(index));
return Promise.resolve(activeSnapshot(index)).then((snapshot) => {
const messages = snapshot.transcript?.data?.messages;
const prompt = submittedPrompts.get(`agent-${index + 1}`);
if (
prompt &&
messages?.every((message) => message.role === "assistant")
)
messages.unshift({ role: "user", content: prompt });
return snapshot;
});
return Promise.resolve(
runtimeSnapshot(
catalogs[index] ?? catalogs[catalogs.length - 1] ?? [],
@@ -182,6 +209,7 @@ async function ask(text = "Inspect") {
describe("QuickPane", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
mocks.focusHandler = undefined;
mocks.listen.mockResolvedValue(vi.fn());
@@ -205,24 +233,23 @@ describe("QuickPane", () => {
]);
});
it("keeps downward model menu unclipped while trigger text stays truncatable", async () => {
it("keeps downward model menu inside the scroll-contained pane", async () => {
const user = userEvent.setup();
bridgeMock([defaultModels]);
const { container } = render(<QuickPane />);
const trigger = await preparedTrigger();
await user.click(trigger);
const menu = screen.getByRole("dialog", { name: "Choose model" });
let ancestor = menu.parentElement;
while (ancestor && ancestor !== document.body) {
expect(getComputedStyle(ancestor).overflow).not.toMatch(/hidden|clip/);
ancestor = ancestor.parentElement;
}
const value = trigger.querySelector("span");
expect(value).toHaveTextContent("provider · Quick Model");
expect(trigger).toHaveClass("model-trigger");
expect(container.querySelector(".model-control-down .model-menu")).toBe(
menu,
);
expect(menu).toHaveClass("model-menu");
expect(
screen.getByRole("listbox", { name: "Available models" }),
).toHaveClass("model-results");
});
it("closes an open selector on first Escape and dismisses pane on second", async () => {
@@ -320,48 +347,239 @@ describe("QuickPane", () => {
expect(input).toHaveValue("Next draft");
});
it("keeps five answers and previews history without changing composer draft", async () => {
const tracker = bridgeMock(
Array.from({ length: 6 }, () => defaultModels),
(index) => runtimeSnapshot(defaultModels, `Answer ${index + 1}`, false),
it("shows manual continuation after a completed normal answer and hands off transcript", 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();
await ask("Inspect");
const continueButton = await screen.findByRole("button", {
name: "Continue in full session",
});
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",
}),
);
});
it("keeps unlimited follow-ups on one runtime and renders snapshot messages in order", async () => {
let turn = 0;
const tracker = bridgeMock([defaultModels], () => {
const messages: Message[] = [
{ role: "user", content: "**First request**" },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "private reasoning" },
{ type: "text", text: "First answer" },
{ type: "toolCall", name: "read" },
],
},
{ role: "toolResult", content: "private tool output" },
];
if (turn > 1)
messages.push(
{ role: "user", content: "Second request" },
{ role: "assistant", content: "## Second answer" },
);
return snapshotWithTranscript(messages);
});
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "submit_prompt") turn += 1;
return baseImplementation?.(command, payload);
},
);
render(<QuickPane />);
await preparedTrigger();
const input = screen.getByRole("textbox", { name: "Request" });
for (let index = 1; index <= 6; index += 1) {
await ask(`Request ${index}`);
expect(await screen.findByText(`Answer ${index}`)).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
}
expect(tracker.runtimeCount()).toBe(6);
const history = screen.getByRole("combobox", { name: "Answer history" });
expect(within(history).getAllByRole("option")).toHaveLength(6);
await ask("First");
expect(await screen.findByText("First answer")).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
await ask("Second");
expect(
within(history).queryByRole("option", { name: "Request 1" }),
).toBeNull();
await screen.findByRole("heading", { name: "Second answer" }),
).toBeInTheDocument();
await userEvent.type(input, "Composer draft");
await userEvent.selectOptions(history, "runtime-2");
const exchange = screen.getByRole("region", { name: "Quick exchange" });
expect(within(exchange).getByText("Request 2")).toBeInTheDocument();
expect(within(exchange).getByText("Answer 2")).toBeInTheDocument();
expect(input).toHaveValue("Composer draft");
expect(tracker.runtimeCount()).toBe(1);
expect(
within(exchange).getByRole("region", { name: "Assistant response" }),
).toHaveAttribute("aria-live", "off");
mocks.invoke.mock.calls.filter(
([command]) => command === "submit_prompt",
),
).toEqual([
["submit_prompt", expect.objectContaining({ agentId: "agent-1" })],
["submit_prompt", expect.objectContaining({ agentId: "agent-1" })],
]);
expect(screen.queryByText("private reasoning")).toBeNull();
expect(screen.queryByText("private tool output")).toBeNull();
expect(
screen.queryByRole("combobox", { name: "Answer history" }),
).toBeNull();
const cards = screen
.getByRole("region", { name: "Assistant response" })
.querySelectorAll("article");
expect(Array.from(cards).map((card) => card.textContent)).toEqual([
"YouFirst request",
"AssistantFirst answer",
"YouSecond request",
"AssistantSecond answer",
]);
});
it("keeps runtime error and extension request visible in history preview", async () => {
it("does not use prior-turn answer after follow-up runtime failure", async () => {
const submittedPrompts: string[] = [];
bridgeMock([defaultModels], () => {
const messages: Message[] = [
{ role: "user", content: submittedPrompts[0] ?? "" },
{ role: "assistant", content: "First answer" },
];
if (submittedPrompts[1])
messages.push({ role: "user", content: submittedPrompts[1] });
return snapshotWithTranscript(
messages,
false,
submittedPrompts[1]
? {
...runtimeSnapshot(defaultModels).runtime,
error: { message: "Second runtime failed." },
}
: undefined,
);
});
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);
},
);
render(<QuickPane />);
await preparedTrigger();
await ask("First");
expect(await screen.findByText("First answer")).toBeInTheDocument();
await waitFor(() =>
expect(screen.getByRole("textbox", { name: "Request" })).toHaveValue(""),
);
await ask("Second");
expect(
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",
}),
);
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",
});
});
it("maps the exact submitted payload to the entered multiline request despite delimiter collisions", async () => {
const supplementalInstructions =
"Stay read-only.\n\nUser request: supplemental collision.";
const enteredRequest =
"First line\n\nUser request: request collision.\nThird **bold** line";
const collisionState: UiState = {
...uiState,
quickPane: { ...settings, instructions: supplementalInstructions },
};
let reflectedPrompt = "";
bridgeMock([defaultModels], () =>
snapshotWithTranscript([
{ role: "user", content: "Normal earlier user text" },
{
role: "user",
content: "[Quick Pane advisory mode: unknown reflected prompt]",
},
{ role: "user", content: reflectedPrompt },
{ role: "assistant", content: "Done" },
]),
);
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "load_ui_state") return Promise.resolve(collisionState);
if (command === "submit_prompt")
reflectedPrompt = String(payload?.message ?? "");
return baseImplementation?.(command, payload);
},
);
render(<QuickPane />);
await preparedTrigger();
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
target: { value: enteredRequest },
});
await userEvent.click(screen.getByRole("button", { name: "Send request" }));
const expectedPayload = `[Quick Pane advisory mode: use at most 6 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 tool calls, start final response with [ESCALATE] and explain why.]\n${supplementalInstructions}\n\nUser request: ${enteredRequest}`;
await waitFor(() => expect(reflectedPrompt).toBe(expectedPayload));
await screen.findByText("Done");
const userArticles = document.querySelectorAll(".quick-pane-user-message");
expect(userArticles).toHaveLength(2);
expect(userArticles[0]).toHaveTextContent("YouNormal earlier user text");
expect(userArticles[1]).toHaveTextContent(
/YouFirst line\s+User request: request collision\.\s+Third bold line/,
);
expect(screen.queryByText(/unknown reflected prompt/)).toBeNull();
expect(userArticles[1]).not.toHaveTextContent("supplemental collision");
expect(userArticles[1]).not.toHaveTextContent("Quick Pane advisory mode");
});
it("keeps runtime errors and extension requests visible", async () => {
let failed = false;
bridgeMock([defaultModels, defaultModels], (index) => {
if (index === 0)
return runtimeSnapshot(defaultModels, "First answer", false);
const result = runtimeSnapshot(defaultModels, "", true, {
...runtimeSnapshot(defaultModels).runtime,
activeTool: "functions.read",
error: failed ? { message: "Runtime disconnected." } : undefined,
});
result.extensions = [
bridgeMock([defaultModels], () => {
const snapshot = snapshotWithTranscript(
[
{ role: "user", content: "Inspect" },
{ role: "assistant", content: "Partial answer" },
],
true,
{
...runtimeSnapshot(defaultModels).runtime,
activeTool: "functions.read",
error: failed ? { message: "Runtime disconnected." } : undefined,
},
);
snapshot.extensions = [
{
id: "permission-1",
method: "confirm",
@@ -369,34 +587,25 @@ describe("QuickPane", () => {
message: "Approval needed",
},
];
return result;
return snapshot;
});
render(<QuickPane />);
await preparedTrigger();
const input = screen.getByRole("textbox", { name: "Request" });
await ask("First request");
expect(await screen.findByText("First answer")).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
await ask("Active request");
await ask("Inspect");
expect(
await screen.findByRole("dialog", { name: "Review request" }),
).toBeInTheDocument();
expect(screen.getByText("Partial answer")).toBeInTheDocument();
failed = true;
expect(
await screen.findByText("Runtime disconnected.", {}, { timeout: 2_000 }),
).toBeInTheDocument();
const history = screen.getByRole("combobox", { name: "Answer history" });
await waitFor(() => expect(history).toBeEnabled());
await userEvent.selectOptions(history, "runtime-1");
const exchange = screen.getByRole("region", { name: "Quick exchange" });
expect(within(exchange).getByText("First answer")).toBeInTheDocument();
expect(within(exchange).getByRole("alert")).toHaveTextContent(
"Runtime disconnected.",
);
expect(
within(exchange).getByRole("dialog", { name: "Review request" }),
screen.getByRole("dialog", { name: "Review request" }),
).toBeInTheDocument();
expect(screen.getByText("Partial answer")).toBeInTheDocument();
});
it("renders sanitized GFM Markdown and reports active tools", async () => {
@@ -482,20 +691,17 @@ describe("QuickPane", () => {
await ask("Change files");
current = runtimeSnapshot(defaultModels, "[ESCAL", false);
const history = await screen.findByRole(
"combobox",
{ name: "Answer history" },
await waitFor(
() =>
expect(
screen.getByRole("textbox", { name: "Request" }),
).not.toHaveAttribute("readonly"),
{ timeout: 2_000 },
);
const response = screen.getByRole("region", { name: "Assistant response" });
expect(response).not.toHaveTextContent("[ESCAL");
await userEvent.selectOptions(history, "runtime-1");
expect(response).not.toHaveTextContent("[ESCAL");
expect(
within(
screen.getByRole("region", { name: "Quick exchange" }),
).queryByText("[ESCAL"),
screen.queryByRole("combobox", { name: "Answer history" }),
).toBeNull();
});
@@ -522,7 +728,7 @@ describe("QuickPane", () => {
expect(tracker.runtimeCount()).toBe(1);
});
it("routes Close and pane-level Escape through native dismissal", async () => {
it("routes Close and pane-level Escape only through native dismissal", async () => {
bridgeMock([[]]);
render(<QuickPane />);
await screen.findByText("No models are currently available.");
@@ -537,39 +743,94 @@ describe("QuickPane", () => {
),
).toHaveLength(2),
);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});
it("retries catalog preparation after an error", async () => {
let attempts = 0;
it("retries startup settings loading from the visible error action", async () => {
let loadAttempts = 0;
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "create_quick_runtime") {
attempts += 1;
if (command === "load_ui_state") {
loadAttempts += 1;
return loadAttempts === 1
? Promise.reject(new Error("state unavailable"))
: Promise.resolve(uiState);
}
if (command === "create_quick_runtime")
return Promise.resolve({
runtime: {
agentId: `agent-${attempts}`,
runtimeId: `runtime-${attempts}`,
},
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
}
if (command === "get_session_runtime_snapshot") {
if (attempts === 1) return Promise.reject(new Error("offline"));
if (command === "get_session_runtime_snapshot")
return Promise.resolve(runtimeSnapshot(defaultModels));
}
return Promise.resolve(undefined);
});
render(<QuickPane />);
expect(await screen.findByRole("alert")).toHaveTextContent(
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent(
"Could not load settings: Error: state unavailable",
);
await userEvent.click(within(alert).getByRole("button", { name: "Retry" }));
expect(await preparedTrigger()).toBeInTheDocument();
expect(loadAttempts).toBe(2);
expect(screen.queryByText(/Could not load settings/)).toBeNull();
});
it("retries catalog preparation visibly after failed-runtime cleanup", async () => {
let attempts = 0;
mocks.invoke.mockImplementation(
(command: string, payload?: { runtimeId?: string }) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "create_quick_runtime") {
attempts += 1;
return Promise.resolve({
runtime: {
agentId: `agent-${attempts}`,
runtimeId: `runtime-${attempts}`,
},
});
}
if (command === "get_session_runtime_snapshot") {
if (payload?.runtimeId === "runtime-1")
return Promise.reject(new Error("offline"));
return Promise.resolve(runtimeSnapshot(defaultModels));
}
return Promise.resolve(undefined);
},
);
render(<QuickPane />);
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent(
"Could not prepare model catalog: Error: offline",
);
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(within(alert).getByRole("button", { name: "Retry" })).toBeVisible();
expect(screen.getByRole("button", { name: /model/i })).toBeDisabled();
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
target: { value: "Blocked until ready" },
});
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
await userEvent.click(within(alert).getByRole("button", { name: "Retry" }));
expect(await preparedTrigger()).toBeInTheDocument();
expect(screen.queryByText(/Could not prepare model catalog/)).toBeNull();
const closeIndex = mocks.invoke.mock.calls.findIndex(
([command, payload]) =>
command === "close_quick_runtime" && payload?.runtimeId === "runtime-1",
);
const retryCreateIndex = mocks.invoke.mock.calls.findIndex(
([command], index) =>
command === "create_quick_runtime" && index > closeIndex,
);
expect(closeIndex).toBeGreaterThan(-1);
expect(retryCreateIndex).toBeGreaterThan(closeIndex);
});
});
describe("QuickPane fresh runtime catalog", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
mocks.listen.mockResolvedValue(vi.fn());
mocks.onFocusChanged.mockImplementation((handler) => {
@@ -613,6 +874,40 @@ describe("QuickPane fresh runtime catalog", () => {
});
});
it("preserves idle runtime, transcript, and catalog when hidden and reopened within three minutes", async () => {
let now = 1_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const tracker = bridgeMock([defaultModels], () =>
snapshotWithTranscript([
{ role: "user", content: "Question" },
{ role: "assistant", content: "Remembered answer" },
]),
);
render(<QuickPane />);
const trigger = await preparedTrigger();
await ask("Question");
expect(await screen.findByText("Remembered answer")).toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("textbox", { name: "Request" }),
).not.toHaveAttribute("readonly"),
);
await userEvent.click(
screen.getByRole("button", { name: "Close Quick Pane" }),
);
await act(async () => mocks.focusHandler?.({ payload: false }));
now += 180_000;
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(tracker.runtimeCount()).toBe(1);
expect(trigger).toBeInTheDocument();
expect(screen.getByText("Remembered answer")).toBeInTheDocument();
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});
it.each([
{
action: "Close",
@@ -629,7 +924,7 @@ describe("QuickPane fresh runtime catalog", () => {
},
},
])(
"discards deferred catalog preparation after $action dismissal",
"retains deferred catalog preparation across $action dismissal",
async ({ dismiss }) => {
const tracker = deferredCatalogBridge();
render(<QuickPane />);
@@ -644,39 +939,23 @@ describe("QuickPane fresh runtime catalog", () => {
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane"),
);
await act(async () => mocks.focusHandler?.({ payload: false }));
await act(async () =>
tracker.firstSnapshot.resolve(runtimeSnapshot(tracker.staleModels)),
);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
}),
);
expect(
screen.queryByRole("button", { name: /provider · Stale/i }),
).toBeNull();
await screen.findByRole("button", { name: /provider · Stale/i }),
).toBeInTheDocument();
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(
await screen.findByRole("button", { name: /provider · Fresh/i }),
).toBeInTheDocument();
expect(tracker.runtimeCount()).toBe(2);
await ask();
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("set_model", {
agentId: "agent-2",
provider: "provider",
modelId: "fresh/model",
}),
);
expect(mocks.invoke).not.toHaveBeenCalledWith(
"set_model",
expect.objectContaining({ agentId: "agent-1" }),
);
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
},
);
it("discards deferred catalog preparation after idle focus loss", async () => {
it("retains deferred catalog preparation across idle focus loss", async () => {
const tracker = deferredCatalogBridge();
render(<QuickPane />);
await waitFor(() =>
@@ -687,86 +966,22 @@ describe("QuickPane fresh runtime catalog", () => {
);
await act(async () => mocks.focusHandler?.({ payload: false }));
expect(mocks.invoke).not.toHaveBeenCalledWith("dismiss_quick_pane");
await act(async () =>
tracker.firstSnapshot.resolve(runtimeSnapshot(tracker.staleModels)),
);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
}),
);
expect(
screen.queryByRole("button", { name: /provider · Stale/i }),
).toBeNull();
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(
await screen.findByRole("button", { name: /provider · Fresh/i }),
await screen.findByRole("button", { name: /provider · Stale/i }),
).toBeInTheDocument();
expect(tracker.runtimeCount()).toBe(2);
await ask();
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("set_model", {
agentId: "agent-2",
provider: "provider",
modelId: "fresh/model",
}),
);
expect(mocks.invoke).not.toHaveBeenCalledWith(
"set_model",
expect.objectContaining({ agentId: "agent-1" }),
);
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("dismiss_quick_pane");
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});
it("retains a prior runtime when close fails and retries before replacement", async () => {
let runtimeCount = 0;
let closeAttempts = 0;
const activeRuntimeIds = new Set<string>();
let maximumActive = 0;
mocks.invoke.mockImplementation(
(command: string, payload?: { runtimeId?: string }) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "create_quick_runtime") {
runtimeCount += 1;
const runtimeId = `runtime-${runtimeCount}`;
activeRuntimeIds.add(runtimeId);
maximumActive = Math.max(maximumActive, activeRuntimeIds.size);
return Promise.resolve({
runtime: { agentId: `agent-${runtimeCount}`, runtimeId },
});
}
if (command === "get_session_runtime_snapshot")
return Promise.resolve(runtimeSnapshot(defaultModels));
if (command === "close_quick_runtime") {
closeAttempts += 1;
if (closeAttempts === 1)
return Promise.reject(new Error("close unavailable"));
activeRuntimeIds.delete(payload?.runtimeId ?? "");
return Promise.resolve(undefined);
}
return Promise.resolve(undefined);
},
);
render(<QuickPane />);
await preparedTrigger();
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not prepare model catalog: Error: close unavailable",
);
expect(runtimeCount).toBe(1);
expect(activeRuntimeIds).toEqual(new Set(["runtime-1"]));
await act(async () => mocks.focusHandler?.({ payload: true }));
await waitFor(() => expect(runtimeCount).toBe(2));
expect(closeAttempts).toBe(2);
expect(activeRuntimeIds).toEqual(new Set(["runtime-2"]));
expect(maximumActive).toBe(1);
expect(screen.queryByText(/close unavailable/)).toBeNull();
});
it("queues a created stale runtime after close failure and drains it before preparing again", async () => {
it("retries stale-created-runtime cleanup before stale chat renewal", async () => {
let now = 1_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const firstSnapshot = deferred<RuntimeSnapshot>();
let runtimeCount = 0;
let firstCloseAttempts = 0;
@@ -816,6 +1031,7 @@ describe("QuickPane fresh runtime catalog", () => {
);
expect(activeRuntimeIds).toEqual(new Set(["runtime-1", "runtime-2"]));
now += 180_001;
await act(async () => mocks.focusHandler?.({ payload: true }));
await waitFor(() => expect(runtimeCount).toBe(3));
expect(firstCloseAttempts).toBe(2);
@@ -830,46 +1046,132 @@ describe("QuickPane fresh runtime catalog", () => {
).toHaveLength(1);
});
it("recycles idle prepared runtime on focus and exposes new catalog", async () => {
const tracker = bridgeMock([
defaultModels,
[{ provider: "provider", id: "new/model", name: "Fresh" }],
]);
render(<QuickPane />);
await preparedTrigger();
await act(async () => mocks.focusHandler?.({ payload: true }));
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
expect(
await screen.findByRole("button", { name: /provider · Fresh/i }),
).toBeInTheDocument();
});
it("prepares a fresh runtime before a repeated request without reopen", async () => {
const tracker = bridgeMock(
[defaultModels, [{ provider: "provider", id: "second/model" }]],
(index) => runtimeSnapshot(defaultModels, `Done ${index + 1}`, false),
it("Ctrl+N resets an idle chat and retries cleanup before replacement", async () => {
const tracker = bridgeMock([defaultModels, defaultModels], () =>
runtimeSnapshot(defaultModels, "Old answer", false),
);
const baseImplementation = mocks.invoke.getMockImplementation();
let closeAttempts = 0;
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "close_quick_runtime") {
closeAttempts += 1;
if (closeAttempts === 1)
return Promise.reject(new Error("close unavailable"));
}
return baseImplementation?.(command, payload);
},
);
render(<QuickPane />);
await preparedTrigger();
await ask("First");
expect(await screen.findByText("Done 1")).toBeInTheDocument();
await ask("Old question");
expect(await screen.findByText("Old answer")).toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("textbox", { name: "Request" }),
).not.toHaveAttribute("readonly"),
);
await ask("Second");
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
target: { value: "draft" },
});
const event = new KeyboardEvent("keydown", {
key: "n",
ctrlKey: true,
cancelable: true,
});
window.dispatchEvent(event);
expect(event.defaultPrevented).toBe(true);
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
expect(closeAttempts).toBe(2);
expect(screen.queryByText("Old answer")).toBeNull();
expect(screen.getByRole("textbox", { name: "Request" })).toHaveValue("");
expect(mocks.invoke).not.toHaveBeenCalledWith("abort", expect.anything());
});
it("Ctrl+N aborts active work before closing and preparing a fresh runtime", async () => {
const tracker = bridgeMock([defaultModels, defaultModels], () =>
runtimeSnapshot(defaultModels, "Partial", true),
);
render(<QuickPane />);
await preparedTrigger();
await ask("Active question");
expect(await screen.findByText("Partial")).toBeInTheDocument();
const event = new KeyboardEvent("keydown", {
key: "N",
ctrlKey: true,
cancelable: true,
});
window.dispatchEvent(event);
expect(event.defaultPrevented).toBe(true);
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
const abortIndex = mocks.invoke.mock.calls.findIndex(
([command]) => command === "abort",
);
const closeIndex = mocks.invoke.mock.calls.findIndex(
([command]) => command === "close_quick_runtime",
);
expect(mocks.invoke).toHaveBeenCalledWith("abort", { agentId: "agent-1" });
expect(abortIndex).toBeLessThan(closeIndex);
expect(screen.queryByText("Partial")).toBeNull();
});
it("renews a stale idle chat when reopened after three minutes", async () => {
let now = 1_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const freshModels: Model[] = [
{ provider: "provider", id: "fresh/model", name: "Fresh" },
];
const tracker = bridgeMock([defaultModels, freshModels], () =>
snapshotWithTranscript([
{ role: "user", content: "Question" },
{ role: "assistant", content: "Expired answer" },
]),
);
render(<QuickPane />);
await preparedTrigger();
await ask("Question");
expect(await screen.findByText("Expired answer")).toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("textbox", { name: "Request" }),
).not.toHaveAttribute("readonly"),
);
await userEvent.click(
screen.getByRole("button", { name: "Close Quick Pane" }),
);
await act(async () => mocks.focusHandler?.({ payload: false }));
now += 180_001;
await act(async () => mocks.focusHandler?.({ payload: true }));
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
expect(mocks.invoke).toHaveBeenCalledWith("set_model", {
agentId: "agent-2",
provider: "provider",
modelId: "second/model",
expect(screen.queryByText("Expired answer")).toBeNull();
expect(
await screen.findByRole("button", { name: /provider · Fresh/i }),
).toBeInTheDocument();
});
it("does not renew or queue renewal while request is active", async () => {
let now = 1_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const tracker = bridgeMock([defaultModels], () =>
runtimeSnapshot(defaultModels, "Still working", true),
);
render(<QuickPane />);
await preparedTrigger();
await ask("Long request");
expect(await screen.findByText("Still working")).toBeInTheDocument();
now += 180_001;
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});