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() { let resolve!: (value: T) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((resolvePromise, rejectPromise) => { resolve = resolvePromise; reject = rejectPromise; }); return { promise, resolve, reject }; } function bridgeMock( catalogs: Model[][], activeSnapshot?: ( index: number, ) => RuntimeSnapshot | Promise, ) { let runtimeCount = 0; const submitted = new Set(); const submittedPrompts = new Map(); 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(); 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(); 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(); 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(); 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(); 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) => { if (command === "abort") return Promise.reject(new Error("abort unavailable")); return baseImplementation?.(command, payload); }); render(); 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(); 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(); 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(); const tracker = bridgeMock([defaultModels]); const baseImplementation = mocks.invoke.getMockImplementation(); mocks.invoke.mockImplementation( (command: string, payload?: Record) => { if (command === "set_model") return pendingModel.promise; return baseImplementation?.(command, payload); }, ); render(); 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(); bridgeMock([defaultModels], () => pendingSnapshot.promise); render(); 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(); 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) => { if (command === "create_session_runtime") return Promise.resolve({ runtime: { agentId: "agent-full", runtimeId: "runtime-full" }, }); return baseImplementation?.(command, payload); }, ); render(); 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) => { if (command === "submit_prompt") turn += 1; return baseImplementation?.(command, payload); }, ); render(); 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) => { 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(); 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) => { if (command === "load_ui_state") return Promise.resolve(collisionState); if (command === "submit_prompt") reflectedPrompt = String(payload?.message ?? ""); return baseImplementation?.(command, payload); }, ); render(); 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(); 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(); 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", 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); 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(); let runtimeCount = 0; let firstCloseAttempts = 0; const activeRuntimeIds = new Set(); 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(); 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) => { if (command === "close_quick_runtime") { closeAttempts += 1; if (closeAttempts === 1) return Promise.reject(new Error("close unavailable")); } return baseImplementation?.(command, payload); }, ); render(); 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(); 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(); 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(); 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(); 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(); 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(); }); });