feat(quick-pane): add chat-style exchange

This commit is contained in:
2026-08-26 15:26:03 +02:00
parent 0ec7d224a2
commit d34099771c
6 changed files with 2655 additions and 287 deletions
+413 -9
View File
@@ -112,8 +112,20 @@ describe("QuickPane", () => {
screen.getByPlaceholderText(/Ask a quick question/),
"Inspect",
);
await user.click(screen.getByRole("button", { name: "Ask" }));
await screen.findByText(/Quick request running/);
await user.click(screen.getByRole("button", { name: "Send request" }));
const exchange = screen.getByRole("region", { name: "Quick exchange" });
expect(within(exchange).getByText("Inspect")).toBeInTheDocument();
const visualActivity = within(exchange).getByText(/Thinking · \d+s/);
expect(visualActivity.closest(".quick-pane-activity")).toHaveAttribute(
"aria-hidden",
"true",
);
const activityAnnouncement = within(exchange).getByText("Thinking", {
selector: ".quick-pane-activity-announcement",
});
expect(activityAnnouncement).toHaveAttribute("aria-live", "polite");
expect(activityAnnouncement).toHaveTextContent("Thinking");
expect(activityAnnouncement).not.toHaveTextContent("·");
await act(async () => mocks.focusHandler?.({ payload: true }));
expect(
@@ -167,7 +179,7 @@ describe("QuickPane", () => {
render(<QuickPane />);
const input = await screen.findByPlaceholderText(/Ask a quick question/);
fireEvent.change(input, { target: { value: "Inspect once" } });
const ask = screen.getByRole("button", { name: "Ask" });
const ask = screen.getByRole("button", { name: "Send request" });
fireEvent.click(ask);
fireEvent.click(ask);
@@ -176,7 +188,11 @@ describe("QuickPane", () => {
([command]) => command === "create_quick_runtime",
),
).toHaveLength(1);
expect(screen.getByRole("button", { name: "Starting…" })).toBeDisabled();
expect(
screen.getByRole("button", { name: "Starting request" }),
).toBeDisabled();
expect(input).toHaveFocus();
expect(input).toHaveAttribute("readonly");
await act(async () => {
resolveCreate?.({
@@ -186,6 +202,65 @@ describe("QuickPane", () => {
});
});
it("mounts an empty busy live region before the first snapshot and keeps it through completion", async () => {
const user = userEvent.setup();
let resolveSnapshot!: (value: ReturnType<typeof snapshot>) => void;
const firstSnapshot = new Promise<ReturnType<typeof snapshot>>(
(resolve) => {
resolveSnapshot = resolve;
},
);
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
case "get_session_runtime_snapshot":
return firstSnapshot;
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
await user.type(input, "Inspect this");
await user.click(screen.getByRole("button", { name: "Send request" }));
const assistantResponse = screen.getByRole("region", {
name: "Assistant response",
});
expect(assistantResponse).toBeEmptyDOMElement();
expect(assistantResponse).toHaveAttribute("aria-live", "polite");
expect(assistantResponse).toHaveAttribute("aria-busy", "true");
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
agentId: "agent-1",
message: expect.any(String),
}),
);
await act(async () => {
resolveSnapshot(snapshot("Final answer", false));
await firstSnapshot;
});
expect(await screen.findByText("Final answer")).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Assistant response" })).toBe(
assistantResponse,
);
expect(assistantResponse).toHaveAttribute("aria-busy", "false");
expect(
within(assistantResponse).getByText("Response complete."),
).toBeInTheDocument();
});
it("clears only the submitted request after a completed non-streaming answer", async () => {
const user = userEvent.setup();
let currentSnapshot = snapshot("Partial answer", true);
@@ -211,20 +286,44 @@ describe("QuickPane", () => {
render(<QuickPane />);
const input = await screen.findByPlaceholderText(/Ask a quick question/);
await user.type(input, "Inspect this");
await user.click(screen.getByRole("button", { name: "Ask" }));
await user.click(screen.getByRole("button", { name: "Send request" }));
expect(await screen.findByText("Partial answer")).toBeInTheDocument();
expect(
within(screen.getByRole("region", { name: "Quick exchange" })).getByText(
"Inspect this",
),
).toBeInTheDocument();
const assistantResponse = screen.getByRole("region", {
name: "Assistant response",
});
expect(assistantResponse).toHaveAttribute("aria-live", "polite");
expect(assistantResponse).toHaveAttribute("aria-busy", "true");
expect(
within(assistantResponse).queryByText("Response complete."),
).toBeNull();
expect(input).toHaveValue("Inspect this");
expect(input).toHaveAttribute("readonly");
expect(input).toHaveFocus();
currentSnapshot = snapshot("Final answer", false);
expect(
await screen.findByText("Final answer", {}, { timeout: 2_000 }),
).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
expect(assistantResponse).toHaveAttribute("aria-busy", "false");
expect(
within(assistantResponse).getByText("Response complete."),
).toBeInTheDocument();
expect(input).not.toHaveAttribute("readonly");
expect(input).toHaveFocus();
await user.type(input, "Next draft");
await new Promise((resolve) => window.setTimeout(resolve, 900));
expect(input).toHaveValue("Next draft");
expect(
within(assistantResponse).getByText("Response complete."),
).toBeInTheDocument();
});
it("keeps five in-memory answers and history selection preserves current answer", async () => {
@@ -263,7 +362,7 @@ describe("QuickPane", () => {
const input = await screen.findByPlaceholderText(/Ask a quick question/);
for (let index = 1; index <= 6; index += 1) {
await user.type(input, `Request ${index}`);
await user.click(screen.getByRole("button", { name: "Ask" }));
await user.click(screen.getByRole("button", { name: "Send request" }));
expect(await screen.findByText(`Answer ${index}`)).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
}
@@ -277,17 +376,320 @@ describe("QuickPane", () => {
within(history).getByRole("option", { name: "Request 2" }),
).toBeInTheDocument();
await user.type(input, "Composer draft");
await user.selectOptions(history, "runtime-2");
expect(screen.getByText("Answer 2")).toBeInTheDocument();
const exchange = screen.getByRole("region", { name: "Quick exchange" });
expect(within(exchange).getByText("Request 2")).toBeInTheDocument();
expect(within(exchange).getByText("Answer 2")).toBeInTheDocument();
expect(screen.queryByText("Answer 6")).toBeNull();
expect(
within(exchange).getByRole("region", { name: "Assistant response" }),
).toHaveAttribute("aria-live", "off");
expect(input).toHaveValue("Composer draft");
await user.selectOptions(history, "");
expect(screen.getByText("Answer 6")).toBeInTheDocument();
expect(within(exchange).getByText("Request 6")).toBeInTheDocument();
expect(within(exchange).getByText("Answer 6")).toBeInTheDocument();
expect(
within(exchange).getByRole("region", { name: "Assistant response" }),
).toHaveAttribute("aria-live", "polite");
expect(input).toHaveValue("Composer draft");
expect(mocks.invoke).not.toHaveBeenCalledWith(
"save_ui_state",
expect.anything(),
);
});
it("suppresses partial escalation prefixes and delays its notice until completion", async () => {
const user = userEvent.setup();
let currentSnapshot = snapshot("", true);
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
case "get_session_runtime_snapshot":
return Promise.resolve(currentSnapshot);
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
await user.type(input, "Change files");
await user.click(screen.getByRole("button", { name: "Send request" }));
const assistantResponse = screen.getByRole("region", {
name: "Assistant response",
});
const prefixes = ["[", "[ESCAL", "[ESCALATE]"];
for (const [index, prefix] of prefixes.entries()) {
currentSnapshot = {
...snapshot(prefix, true),
runtime: { activeTool: `functions.prefix_${index + 1}` },
};
expect(
await screen.findByText(
`Prefix ${index + 1}`,
{ selector: ".quick-pane-activity > span:last-child" },
{ timeout: 2_000 },
),
).toBeInTheDocument();
expect(assistantResponse).toBeEmptyDOMElement();
expect(
screen.queryByText("Continue in full session"),
).not.toBeInTheDocument();
}
currentSnapshot = snapshot("[ESCALATE]Needs full session.", true);
expect(
await within(assistantResponse).findByText(
"Needs full session.",
{},
{ timeout: 2_000 },
),
).toBeInTheDocument();
expect(assistantResponse).toHaveAttribute("aria-busy", "true");
expect(
screen.queryByText("Continue in full session"),
).not.toBeInTheDocument();
currentSnapshot = snapshot("[ESCALATE]Needs full session.", false);
await waitFor(
() => expect(assistantResponse).toHaveAttribute("aria-busy", "false"),
{ timeout: 2_000 },
);
expect(assistantResponse).toHaveAttribute("aria-live", "polite");
expect(assistantResponse).toHaveAttribute("aria-busy", "false");
expect(
within(assistantResponse).getAllByText("Response complete."),
).toHaveLength(1);
expect(screen.getByText("Continue in full session")).toBeInTheDocument();
});
it("never renders or stores a completed partial escalation marker", async () => {
const user = userEvent.setup();
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
case "get_session_runtime_snapshot":
return Promise.resolve(snapshot("[ESCAL", false));
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
await user.type(input, "Needs review");
await user.click(screen.getByRole("button", { name: "Send request" }));
const assistantResponse = screen.getByRole("region", {
name: "Assistant response",
});
await waitFor(
() => expect(assistantResponse).toHaveAttribute("aria-busy", "false"),
{ timeout: 2_000 },
);
expect(assistantResponse).not.toHaveTextContent("[ESCAL");
expect(screen.queryByText("Continue in full session")).toBeNull();
expect(
screen.getByRole("combobox", { name: "Answer history" }),
).toBeInTheDocument();
});
it("keeps runtime error and extension visible while viewing history", async () => {
const user = userEvent.setup();
let runtimeCount = 0;
let runtimeFailed = false;
mocks.invoke.mockImplementation(
(command: string, payload?: { runtimeId?: string }) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
runtimeCount += 1;
return Promise.resolve({
runtime: {
agentId: `agent-${runtimeCount}`,
runtimeId: `runtime-${runtimeCount}`,
},
});
case "get_session_runtime_snapshot":
if (payload?.runtimeId === "runtime-1")
return Promise.resolve(snapshot("First answer", false));
return Promise.resolve({
runtime: {
activeTool: "functions.read",
error: runtimeFailed
? { message: "Runtime disconnected." }
: undefined,
},
state: { data: { isStreaming: true } },
transcript: { data: { messages: [] } },
extensions: [
{
id: "permission-1",
method: "confirm",
title: "Run guarded command",
message: "Approval needed",
},
],
});
default:
return Promise.resolve(undefined);
}
},
);
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
await user.type(input, "First request");
await user.click(screen.getByRole("button", { name: "Send request" }));
expect(
await screen.findByText("First answer", {}, { timeout: 2_000 }),
).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
const history = screen.getByRole("combobox", { name: "Answer history" });
await user.type(input, "Active request");
await user.click(screen.getByRole("button", { name: "Send request" }));
expect(
await screen.findByRole("dialog", { name: "Review request" }),
).toBeInTheDocument();
expect(history).toBeDisabled();
expect(history).toHaveValue("");
runtimeFailed = true;
expect(
await screen.findByText("Runtime disconnected.", {}, { timeout: 2_000 }),
).toBeInTheDocument();
await waitFor(() => expect(history).not.toBeDisabled());
await user.selectOptions(history, "runtime-1");
const exchange = screen.getByRole("region", { name: "Quick exchange" });
expect(within(exchange).getByText("First request")).toBeInTheDocument();
expect(within(exchange).getByText("First answer")).toBeInTheDocument();
expect(within(exchange).getByRole("alert")).toHaveTextContent(
"Runtime disconnected.",
);
expect(within(exchange).getByRole("status")).toHaveTextContent(
"Quick runtime failed.",
);
expect(
within(exchange).getByRole("dialog", { name: "Review request" }),
).toBeInTheDocument();
expect(screen.getByText("Run guarded command")).toBeInTheDocument();
expect(screen.getByText("Approval needed")).toBeInTheDocument();
});
it("renders safe GFM Markdown and reports active tools without tool cards", async () => {
const user = userEvent.setup();
let currentSnapshot: ReturnType<typeof snapshot> & {
runtime: { activeTool?: string };
} = {
...snapshot("", true),
runtime: { activeTool: "functions.grep" },
};
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
case "get_session_runtime_snapshot":
return Promise.resolve(currentSnapshot);
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
await user.type(input, "Explain output");
await user.click(screen.getByRole("button", { name: "Send request" }));
expect(
await screen.findByText("Searching files", {
selector: ".quick-pane-activity > span:last-child",
}),
).toBeInTheDocument();
currentSnapshot = snapshot(
"## 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();
expect(document.querySelector("details.tool-result")).toBeNull();
});
it("uses Enter to send and leaves Shift+Enter available for newlines", async () => {
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "get_model_catalog")
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
if (command === "create_quick_runtime")
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
if (command === "get_session_runtime_snapshot")
return Promise.resolve(snapshot("", true));
return Promise.resolve(undefined);
});
render(<QuickPane />);
const input = await screen.findByRole("textbox", { name: "Request" });
fireEvent.change(input, { target: { value: "Inspect" } });
fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
expect(mocks.invoke).not.toHaveBeenCalledWith(
"create_quick_runtime",
expect.anything(),
);
fireEvent.keyDown(input, { key: "Enter" });
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("create_quick_runtime", {
worktreePath: "/repo",
}),
);
});
it("leaves close interception native while preserving close and Escape hiding", async () => {
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
@@ -300,7 +702,9 @@ describe("QuickPane", () => {
await screen.findByPlaceholderText(/Ask a quick question/);
expect(mocks.onCloseRequested).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole("button", { name: "Close" }));
await userEvent.click(
screen.getByRole("button", { name: "Close Quick Pane" }),
);
fireEvent.keyDown(window, { key: "Escape" });
expect(mocks.hide).toHaveBeenCalledTimes(2);
});