Files
pi-gui/ui/src/quick-pane/QuickPane.test.tsx
T

1307 lines
46 KiB
TypeScript

import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type {
Message,
Model,
QuickPaneSettings,
RuntimeSnapshot,
UiState,
} from "../workspace/types";
const mocks = vi.hoisted(() => ({
invoke: vi.fn(),
listen: vi.fn(),
onFocusChanged: vi.fn(),
startDragging: vi.fn(),
focusHandler: undefined as
| ((event: { payload: boolean }) => void)
| undefined,
}));
vi.mock("@tauri-apps/api/core", () => ({ invoke: mocks.invoke }));
vi.mock("@tauri-apps/api/event", () => ({ listen: mocks.listen }));
vi.mock("@tauri-apps/api/window", () => ({
getCurrentWindow: () => ({
onFocusChanged: mocks.onFocusChanged,
startDragging: mocks.startDragging,
}),
}));
import QuickPane from "./QuickPane";
const settings: QuickPaneSettings = {
workspace: "/repo",
provider: "provider",
model: "quick/model",
followUpProvider: "provider",
followUpModel: "full/model",
instructions: "Stay read-only.",
maximumToolCalls: 6,
};
const uiState: UiState = {
version: 1,
directoryOrder: [],
runtimeOrderByDirectory: {},
drafts: {},
scroll: {},
lastSeen: {},
quickPane: settings,
};
const defaultModels: Model[] = [
{ provider: "provider", id: "quick/model", name: "Quick Model" },
];
function runtimeSnapshot(
models: Model[],
answer = "",
isStreaming = false,
runtime: RuntimeSnapshot["runtime"] = {
runtimeId: "unused",
worktreePath: "/repo",
state: "idle",
label: "Quick",
attention: false,
queueCount: 0,
lastActivity: "now",
openedAt: "now",
},
): RuntimeSnapshot {
return {
bridgeInstanceId: "bridge",
latestSeq: 1,
runtime,
models: { data: { models } },
state: { data: { isStreaming } },
transcript: answer
? { data: { messages: [{ role: "assistant", content: answer }] } }
: undefined,
extensions: [],
};
}
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;
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
function bridgeMock(
catalogs: Model[][],
activeSnapshot?: (
index: number,
) => RuntimeSnapshot | Promise<RuntimeSnapshot>,
) {
let runtimeCount = 0;
const submitted = new Set<string>();
const submittedPrompts = new Map<string, string>();
mocks.invoke.mockImplementation(
(
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;
return Promise.resolve({
runtime: {
agentId: `agent-${runtimeCount}`,
runtimeId: `runtime-${runtimeCount}`,
},
});
}
if (command === "submit_prompt") {
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)).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] ?? [],
),
);
}
return Promise.resolve(undefined);
},
);
return { runtimeCount: () => runtimeCount };
}
function deferredCatalogBridge() {
const firstSnapshot = deferred<RuntimeSnapshot>();
const staleModels: Model[] = [
{ provider: "provider", id: "stale/model", name: "Stale" },
];
const freshModels: Model[] = [
{ provider: "provider", id: "fresh/model", name: "Fresh" },
];
let runtimeCount = 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;
return Promise.resolve({
runtime: {
agentId: `agent-${runtimeCount}`,
runtimeId: `runtime-${runtimeCount}`,
},
});
}
if (command === "get_session_runtime_snapshot") {
if (payload?.runtimeId === "runtime-1") return firstSnapshot.promise;
return Promise.resolve(runtimeSnapshot(freshModels));
}
return Promise.resolve(undefined);
},
);
return {
firstSnapshot,
staleModels,
runtimeCount: () => runtimeCount,
};
}
async function preparedTrigger() {
return screen.findByRole("button", { name: /provider ·/i });
}
async function ask(text = "Inspect") {
const user = userEvent.setup();
const input = screen.getByRole("textbox", { name: "Request" });
await user.type(input, text);
await user.click(screen.getByRole("button", { name: "Send request" }));
}
describe("QuickPane", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
mocks.focusHandler = undefined;
mocks.startDragging.mockResolvedValue(undefined);
mocks.listen.mockResolvedValue(vi.fn());
mocks.onFocusChanged.mockImplementation((handler) => {
mocks.focusHandler = handler;
return Promise.resolve(vi.fn());
});
});
it("renders toolbar before composer before exchange", async () => {
bridgeMock([defaultModels]);
const { container } = render(<QuickPane />);
await preparedTrigger();
const pane = container.querySelector(".quick-pane");
expect(
Array.from(pane?.children ?? []).map((element) => element.className),
).toEqual([
"quick-pane-drag-strip",
"quick-pane-toolbar",
"quick-pane-composer",
"quick-pane-exchange",
]);
});
it("starts native dragging only from the strip above toolbar controls", async () => {
bridgeMock([defaultModels]);
const { container } = render(<QuickPane />);
await preparedTrigger();
fireEvent.pointerDown(container.querySelector(".quick-pane-drag-strip")!);
expect(mocks.startDragging).toHaveBeenCalledTimes(1);
fireEvent.pointerDown(screen.getByRole("button", { name: "Close Quick Pane" }));
expect(mocks.startDragging).toHaveBeenCalledTimes(1);
});
it("derives thinking choices from selected model metadata and falls back to Off", async () => {
const thinkingModels: Model[] = [
{
provider: "provider",
id: "quick/model",
name: "Quick Model",
thinkingLevelMap: { off: "off", high: "high" },
},
];
bridgeMock([thinkingModels], () => runtimeSnapshot(thinkingModels, "", true));
render(<QuickPane />);
await preparedTrigger();
const thinking = screen.getByRole("combobox", { name: "Thinking" });
expect(within(thinking).getAllByRole("option").map((option) => option.textContent)).toEqual([
"Off",
"High",
]);
await userEvent.selectOptions(thinking, "high");
await ask("Inspect");
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("set_thinking_level", {
agentId: "agent-1",
level: "high",
}),
);
});
it("keeps Abort active-only and Renew idle after an exchange", async () => {
let streaming = true;
const tracker = bridgeMock([defaultModels, defaultModels], () =>
runtimeSnapshot(defaultModels, "Partial answer", streaming),
);
render(<QuickPane />);
await preparedTrigger();
expect(screen.queryByRole("button", { name: "Abort" })).toBeNull();
expect(screen.queryByRole("button", { name: "Renew" })).toBeNull();
await ask("Inspect");
expect(await screen.findByRole("button", { name: "Abort" })).toBeEnabled();
await userEvent.click(screen.getByRole("button", { name: "Abort" }));
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
expect(mocks.invoke).toHaveBeenCalledWith("abort", { agentId: "agent-1" });
await waitFor(() =>
expect(screen.getByRole("button", { name: "Send request" })).toBeEnabled(),
);
streaming = false;
await ask("Again");
expect(await screen.findByRole("button", { name: "Renew" })).toBeEnabled();
});
it("reports Abort failures without replacing active chat", async () => {
bridgeMock([defaultModels], () =>
runtimeSnapshot(defaultModels, "Partial answer", true),
);
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation((command: string, payload?: Record<string, unknown>) => {
if (command === "abort") return Promise.reject(new Error("abort unavailable"));
return baseImplementation?.(command, payload);
});
render(<QuickPane />);
await preparedTrigger();
await ask("Inspect");
await userEvent.click(await screen.findByRole("button", { name: "Abort" }));
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not abort quick request: Error: abort unavailable",
);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
expect(screen.getByRole("button", { name: "Abort" })).toBeEnabled();
});
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" });
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 () => {
const user = userEvent.setup();
bridgeMock([defaultModels]);
render(<QuickPane />);
const trigger = await preparedTrigger();
await user.click(trigger);
await waitFor(() => expect(screen.getByRole("searchbox")).toHaveFocus());
fireEvent.keyDown(window, { key: "Escape" });
expect(screen.queryByRole("dialog", { name: "Choose model" })).toBeNull();
expect(mocks.invoke).not.toHaveBeenCalledWith("dismiss_quick_pane");
await waitFor(() => expect(trigger).toHaveFocus());
fireEvent.keyDown(window, { key: "Escape" });
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane"),
);
});
it("allows only one submission start while commands are pending", async () => {
const pendingModel = deferred<unknown>();
const tracker = bridgeMock([defaultModels]);
const baseImplementation = mocks.invoke.getMockImplementation();
mocks.invoke.mockImplementation(
(command: string, payload?: Record<string, unknown>) => {
if (command === "set_model") return pendingModel.promise;
return baseImplementation?.(command, payload);
},
);
render(<QuickPane />);
await preparedTrigger();
const input = screen.getByRole("textbox", { name: "Request" });
fireEvent.change(input, { target: { value: "Inspect once" } });
const send = screen.getByRole("button", { name: "Send request" });
fireEvent.click(send);
fireEvent.click(send);
expect(tracker.runtimeCount()).toBe(1);
expect(
mocks.invoke.mock.calls.filter(([command]) => command === "set_model"),
).toHaveLength(1);
expect(input).toHaveAttribute("readonly");
await act(async () => pendingModel.resolve(undefined));
});
it("keeps live assistant region stable through completion", async () => {
const pendingSnapshot = deferred<RuntimeSnapshot>();
bridgeMock([defaultModels], () => pendingSnapshot.promise);
render(<QuickPane />);
await preparedTrigger();
await ask("Inspect this");
const response = screen.getByRole("region", { name: "Assistant response" });
expect(response).toHaveAttribute("aria-live", "polite");
expect(response).toHaveAttribute("aria-busy", "true");
expect(response.querySelector(".quick-pane-activity")).toBeInTheDocument();
await act(async () =>
pendingSnapshot.resolve(
runtimeSnapshot(defaultModels, "Final answer", false),
),
);
expect(await screen.findByText("Final answer")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Assistant response" })).toBe(
response,
);
expect(response).toHaveAttribute("aria-busy", "false");
expect(
within(response).getByText("Response complete."),
).toBeInTheDocument();
});
it("clears only submitted draft and restores focus after completion", async () => {
let current = runtimeSnapshot(defaultModels, "Partial answer", true);
bridgeMock([defaultModels], () => current);
render(<QuickPane />);
await preparedTrigger();
const input = screen.getByRole("textbox", { name: "Request" });
await ask("Inspect this");
expect(await screen.findByText("Partial answer")).toBeInTheDocument();
expect(input).toHaveValue("Inspect this");
expect(input).toHaveAttribute("readonly");
current = runtimeSnapshot(defaultModels, "Final answer", false);
expect(
await screen.findByText("Final answer", {}, { timeout: 2_000 }),
).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
expect(input).not.toHaveAttribute("readonly");
expect(input).toHaveFocus();
await userEvent.type(input, "Next draft");
await new Promise((resolve) => window.setTimeout(resolve, 850));
expect(input).toHaveValue("Next draft");
});
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(continueButton.closest(".quick-pane-assistant-footer")).not.toBeNull();
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" });
await ask("First");
expect(await screen.findByText("First answer")).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
await ask("Second");
expect(
await screen.findByRole("heading", { name: "Second answer" }),
).toBeInTheDocument();
expect(tracker.runtimeCount()).toBe(1);
expect(
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 answerContinue in full session",
]);
});
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], () => {
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",
title: "Run guarded command",
message: "Approval needed",
},
];
return snapshot;
});
render(<QuickPane />);
await preparedTrigger();
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();
expect(
screen.getByRole("dialog", { name: "Review request" }),
).toBeInTheDocument();
expect(screen.getByText("Partial answer")).toBeInTheDocument();
});
it("renders sanitized GFM Markdown and reports active tools", async () => {
let current = runtimeSnapshot(defaultModels, "", true, {
...runtimeSnapshot(defaultModels).runtime,
activeTool: "functions.grep",
});
bridgeMock([defaultModels], () => current);
render(<QuickPane />);
await preparedTrigger();
await ask("Explain output");
expect(
await screen.findByText("Searching files", {
selector: ".quick-pane-activity > span:last-child",
}),
).toBeInTheDocument();
current = runtimeSnapshot(
defaultModels,
"## Result\n\n- item\n\n`const safe = true`\n\n[unsafe](javascript:alert(1))\n\n<img src=x onerror=alert(1)>",
false,
);
expect(
await screen.findByRole(
"heading",
{ name: "Result" },
{ timeout: 2_000 },
),
).toBeInTheDocument();
expect(screen.getByText("const safe = true")).toBeInTheDocument();
expect(screen.getByText("unsafe").closest("a")).toHaveAttribute("href", "");
expect(document.querySelector("img")).toBeNull();
});
it("suppresses escalation prefixes until content and completion are ready", async () => {
let current = runtimeSnapshot(defaultModels, "", true);
bridgeMock([defaultModels], () => current);
render(<QuickPane />);
await preparedTrigger();
await ask("Change files");
const response = screen.getByRole("region", { name: "Assistant response" });
for (const prefix of ["[", "[ESCAL", "[ESCALATE]"]) {
current = runtimeSnapshot(defaultModels, prefix, true);
await new Promise((resolve) => window.setTimeout(resolve, 820));
expect(response).not.toHaveTextContent(prefix);
expect(screen.queryByText("Continue in full session")).toBeNull();
}
current = runtimeSnapshot(
defaultModels,
"[ESCALATE]Needs full session.",
true,
);
expect(
await within(response).findByText(
"Needs full session.",
{},
{ timeout: 2_000 },
),
).toBeInTheDocument();
expect(screen.queryByText("Continue in full session")).toBeNull();
current = runtimeSnapshot(
defaultModels,
"[ESCALATE]Needs full session.",
false,
);
expect(
await screen.findByText(
"Continue in full session",
{},
{ timeout: 2_000 },
),
).toBeInTheDocument();
expect(response).not.toHaveTextContent("[ESCALATE]");
}, 10_000);
it("does not render or retain a completed partial escalation marker", async () => {
let current = runtimeSnapshot(defaultModels, "", true);
bridgeMock([defaultModels], () => current);
render(<QuickPane />);
await preparedTrigger();
await ask("Change files");
current = runtimeSnapshot(defaultModels, "[ESCAL", false);
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");
expect(
screen.queryByRole("combobox", { name: "Answer history" }),
).toBeNull();
});
it("uses Enter to send and leaves Shift+Enter for a newline", async () => {
const tracker = bridgeMock([defaultModels], () =>
runtimeSnapshot(defaultModels, "", true),
);
render(<QuickPane />);
await preparedTrigger();
const input = screen.getByRole("textbox", { name: "Request" });
fireEvent.change(input, { target: { value: "Inspect" } });
fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
expect(mocks.invoke).not.toHaveBeenCalledWith(
"submit_prompt",
expect.anything(),
);
fireEvent.keyDown(input, { key: "Enter" });
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith(
"submit_prompt",
expect.objectContaining({ agentId: "agent-1" }),
),
);
expect(tracker.runtimeCount()).toBe(1);
});
it("routes Close and pane-level Escape only through native dismissal", async () => {
bridgeMock([[]]);
render(<QuickPane />);
await screen.findByText("No models are currently available.");
await userEvent.click(
screen.getByRole("button", { name: "Close Quick Pane" }),
);
fireEvent.keyDown(window, { key: "Escape" });
await waitFor(() =>
expect(
mocks.invoke.mock.calls.filter(
([command]) => command === "dismiss_quick_pane",
),
).toHaveLength(2),
);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});
it("retries startup settings loading from the visible error action", async () => {
let loadAttempts = 0;
mocks.invoke.mockImplementation((command: string) => {
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-1", runtimeId: "runtime-1" },
});
if (command === "get_session_runtime_snapshot")
return Promise.resolve(runtimeSnapshot(defaultModels));
return Promise.resolve(undefined);
});
render(<QuickPane />);
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",
);
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) => {
mocks.focusHandler = handler;
return Promise.resolve(vi.fn());
});
});
it("uses snapshot catalog, removes stale model, and submits on that exact prepared runtime", async () => {
const tracker = bridgeMock([
[{ provider: "provider", id: "new/model", name: "New Model" }],
]);
render(<QuickPane />);
const trigger = await preparedTrigger();
expect(trigger).toHaveAccessibleName(/provider · New Model/i);
expect(
screen.getByText("Selected model unavailable; using provider/new/model."),
).toBeInTheDocument();
await ask();
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("set_model", {
agentId: "agent-1",
provider: "provider",
modelId: "new/model",
}),
);
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("get_model_catalog");
});
it("preserves prepared runtime and catalog when focus changes during active request", async () => {
const tracker = bridgeMock([defaultModels]);
render(<QuickPane />);
await preparedTrigger();
await ask();
await act(async () => mocks.focusHandler?.({ payload: false }));
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
});
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",
dismiss: async () => {
await userEvent.click(
screen.getByRole("button", { name: "Close Quick Pane" }),
);
},
},
{
action: "Escape",
dismiss: async () => {
fireEvent.keyDown(window, { key: "Escape" });
},
},
])(
"retains deferred catalog preparation across $action dismissal",
async ({ dismiss }) => {
const tracker = deferredCatalogBridge();
render(<QuickPane />);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith(
"get_session_runtime_snapshot",
{ runtimeId: "runtime-1" },
),
);
await dismiss();
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)),
);
expect(
await screen.findByRole("button", { name: /provider · Stale/i }),
).toBeInTheDocument();
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(tracker.runtimeCount()).toBe(1);
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
},
);
it("retains deferred catalog preparation across idle focus loss", async () => {
const tracker = deferredCatalogBridge();
render(<QuickPane />);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith(
"get_session_runtime_snapshot",
{ runtimeId: "runtime-1" },
),
);
await act(async () => mocks.focusHandler?.({ payload: false }));
await act(async () =>
tracker.firstSnapshot.resolve(runtimeSnapshot(tracker.staleModels)),
);
expect(
await screen.findByRole("button", { name: /provider · Stale/i }),
).toBeInTheDocument();
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("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;
const activeRuntimeIds = new Set<string>();
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);
return Promise.resolve({
runtime: { agentId: `agent-${runtimeCount}`, runtimeId },
});
}
if (command === "get_session_runtime_snapshot") {
if (payload?.runtimeId === "runtime-1") return firstSnapshot.promise;
return Promise.resolve(runtimeSnapshot(defaultModels));
}
if (command === "close_quick_runtime") {
if (payload?.runtimeId === "runtime-1") {
firstCloseAttempts += 1;
if (firstCloseAttempts === 1)
return Promise.reject(new Error("stale close unavailable"));
}
activeRuntimeIds.delete(payload?.runtimeId ?? "");
return Promise.resolve(undefined);
}
return Promise.resolve(undefined);
},
);
render(<QuickPane />);
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith(
"get_session_runtime_snapshot",
{ runtimeId: "runtime-1" },
),
);
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(await preparedTrigger()).toBeInTheDocument();
await act(async () =>
firstSnapshot.resolve(runtimeSnapshot(defaultModels)),
);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not clean up stale quick runtime: Error: stale close unavailable",
);
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);
expect(activeRuntimeIds).toEqual(new Set(["runtime-3"]));
expect(screen.queryByText(/stale close unavailable/)).toBeNull();
expect(
mocks.invoke.mock.calls.filter(
([command, payload]) =>
command === "close_quick_runtime" &&
payload?.runtimeId === "runtime-2",
),
).toHaveLength(1);
});
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("Old question");
expect(await screen.findByText("Old answer")).toBeInTheDocument();
await waitFor(() =>
expect(
screen.getByRole("textbox", { name: "Request" }),
).not.toHaveAttribute("readonly"),
);
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(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",
});
});
it("blocks Send for empty snapshot catalog", async () => {
bridgeMock([[]]);
render(<QuickPane />);
expect(
await screen.findByText("No models are currently available."),
).toBeInTheDocument();
const input = screen.getByRole("textbox", { name: "Request" });
fireEvent.change(input, { target: { value: "Must not run" } });
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
fireEvent.keyDown(input, { key: "Enter" });
expect(mocks.invoke).not.toHaveBeenCalledWith(
"submit_prompt",
expect.anything(),
);
});
it("closes failed preparation runtime and blocks Send", async () => {
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "create_quick_runtime")
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
if (command === "get_session_runtime_snapshot")
return Promise.reject(new Error("offline"));
return Promise.resolve(undefined);
});
render(<QuickPane />);
expect(await screen.findByRole("alert")).toHaveTextContent(
"Could not prepare model catalog: Error: offline",
);
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
runtimeId: "runtime-1",
});
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
target: { value: "Blocked" },
});
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
});
});