feat(quick-pane): polish compact chat controls
This commit is contained in:
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
|
||||
invoke: vi.fn(),
|
||||
listen: vi.fn(),
|
||||
onFocusChanged: vi.fn(),
|
||||
startDragging: vi.fn(),
|
||||
focusHandler: undefined as
|
||||
| ((event: { payload: boolean }) => void)
|
||||
| undefined,
|
||||
@@ -28,7 +29,10 @@ const mocks = vi.hoisted(() => ({
|
||||
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 }),
|
||||
getCurrentWindow: () => ({
|
||||
onFocusChanged: mocks.onFocusChanged,
|
||||
startDragging: mocks.startDragging,
|
||||
}),
|
||||
}));
|
||||
|
||||
import QuickPane from "./QuickPane";
|
||||
@@ -212,6 +216,7 @@ describe("QuickPane", () => {
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
mocks.focusHandler = undefined;
|
||||
mocks.startDragging.mockResolvedValue(undefined);
|
||||
mocks.listen.mockResolvedValue(vi.fn());
|
||||
mocks.onFocusChanged.mockImplementation((handler) => {
|
||||
mocks.focusHandler = handler;
|
||||
@@ -227,12 +232,95 @@ describe("QuickPane", () => {
|
||||
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]);
|
||||
@@ -371,6 +459,7 @@ describe("QuickPane", () => {
|
||||
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(),
|
||||
@@ -448,7 +537,7 @@ describe("QuickPane", () => {
|
||||
"YouFirst request",
|
||||
"AssistantFirst answer",
|
||||
"YouSecond request",
|
||||
"AssistantSecond answer",
|
||||
"AssistantSecond answerContinue in full session",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user