) => {
+ if (command === "close_quick_runtime") return closing.promise;
+ return baseImplementation?.(command, payload);
+ },
+ );
+ render();
+ await preparedTrigger();
+ const thinking = screen.getByRole("button", { name: "Thinking Medium" });
+ await userEvent.click(thinking);
+ expect(
+ screen.getByRole("listbox", { name: "Thinking options" }),
+ ).toBeInTheDocument();
+
+ fireEvent.keyDown(window, { key: "n", ctrlKey: true, cancelable: true });
+ await waitFor(() => expect(thinking).toBeDisabled());
+ expect(
+ screen.queryByRole("listbox", { name: "Thinking options" }),
+ ).toBeNull();
+
+ await act(async () => closing.resolve());
+ await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
+ });
+
it("keeps Abort active-only and Renew idle after an exchange", async () => {
let streaming = true;
const tracker = bridgeMock([defaultModels, defaultModels], () =>
@@ -1422,7 +1457,13 @@ describe("QuickPane fresh runtime catalog", () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
- mocks.listen.mockResolvedValue(vi.fn());
+ mocks.shownHandler = undefined;
+ mocks.listen.mockImplementation(
+ (event: string, handler: (event: { payload: undefined }) => void) => {
+ if (event === "quick-pane-shown") mocks.shownHandler = handler;
+ return Promise.resolve(vi.fn());
+ },
+ );
mocks.onFocusChanged.mockImplementation((handler) => {
mocks.focusHandler = handler;
return Promise.resolve(vi.fn());
@@ -1488,7 +1529,7 @@ describe("QuickPane fresh runtime catalog", () => {
);
await act(async () => mocks.focusHandler?.({ payload: false }));
now += 180_000;
- await act(async () => mocks.focusHandler?.({ payload: true }));
+ await act(async () => mocks.shownHandler?.({ payload: undefined }));
expect(tracker.runtimeCount()).toBe(1);
expect(trigger).toBeInTheDocument();
@@ -1564,7 +1605,7 @@ describe("QuickPane fresh runtime catalog", () => {
),
);
- await act(async () => mocks.focusHandler?.({ payload: true }));
+ await act(async () => mocks.shownHandler?.({ payload: undefined }));
expect(await preparedTrigger()).toBeInTheDocument();
await act(async () =>
firstSnapshot.resolve(runtimeSnapshot(defaultModels)),
@@ -1575,7 +1616,7 @@ describe("QuickPane fresh runtime catalog", () => {
expect(activeRuntimeIds).toEqual(new Set(["runtime-1", "runtime-2"]));
now += 180_001;
- await act(async () => mocks.focusHandler?.({ payload: true }));
+ await act(async () => mocks.shownHandler?.({ payload: undefined }));
await waitFor(() => expect(runtimeCount).toBe(3));
expect(firstCloseAttempts).toBe(2);
expect(activeRuntimeIds).toEqual(new Set(["runtime-3"]));
@@ -1660,7 +1701,7 @@ describe("QuickPane fresh runtime catalog", () => {
expect(screen.queryByText("Partial")).toBeNull();
});
- it("renews a stale idle chat when reopened after three minutes", async () => {
+ it("renews stale idle chat only after Quick Pane show, never focus while open", async () => {
let now = 1_000_000;
vi.spyOn(Date, "now").mockImplementation(() => now);
const freshModels: Model[] = [
@@ -1682,12 +1723,21 @@ describe("QuickPane fresh runtime catalog", () => {
).not.toHaveAttribute("readonly"),
);
+ now += 180_001;
+ fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
+ target: { value: "Still open" },
+ });
+ 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",
+ });
+
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 act(async () => mocks.shownHandler?.({ payload: undefined }));
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
diff --git a/ui/src/quick-pane/QuickPane.tsx b/ui/src/quick-pane/QuickPane.tsx
index 560c6ed..4497dbe 100644
--- a/ui/src/quick-pane/QuickPane.tsx
+++ b/ui/src/quick-pane/QuickPane.tsx
@@ -706,7 +706,7 @@ export default function QuickPane() {
[clearChatState, clearRuntimeIfCurrent, closeRuntime, prepareRuntime],
);
- const renewChatOnFocus = useCallback(async () => {
+ const renewChatOnShow = useCallback(async () => {
if (requestActiveRef.current || submissionStartingRef.current) return;
if (!runtimeRef.current) {
clearChatState(false);
@@ -832,6 +832,10 @@ export default function QuickPane() {
void prepareRuntime(configuredModel);
},
);
+ const paneShownListener = listen("quick-pane-shown", () => {
+ focusInput();
+ void renewChatOnShow();
+ });
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Tab") inputFocusIntentRef.current = "keyboard";
if (catalogOverlayActive) {
@@ -856,20 +860,19 @@ export default function QuickPane() {
};
window.addEventListener("keydown", onKeyDown);
const focusListener = paneWindow.onFocusChanged((event) => {
- if (!event.payload) return;
- focusInput();
- void renewChatOnFocus();
+ if (event.payload) focusInput();
});
return () => {
window.removeEventListener("keydown", onKeyDown);
void settingsListener.then((unlisten) => unlisten());
+ void paneShownListener.then((unlisten) => unlisten());
void focusListener.then((unlisten) => unlisten());
};
}, [
catalogOverlayActive,
dismissPane,
prepareRuntime,
- renewChatOnFocus,
+ renewChatOnShow,
replaceChat,
]);
@@ -1138,7 +1141,7 @@ export default function QuickPane() {
{
- if (event.button !== 0) return;
+ if (event.button && event.button !== 0) return;
void getCurrentWindow()
.startDragging()
.catch(() => undefined);
@@ -1176,6 +1179,7 @@ export default function QuickPane() {
option.getAttribute("value"));
@@ -127,15 +128,18 @@ describe("thinking selector parity", () => {
"matches Pi %s semantics in Conversation Workspace and Quick Pane",
async (_name, metadata, expected) => {
renderSurfaces({ provider: "provider", id: "model", ...metadata });
- await waitFor(() =>
- expect(
- screen.getAllByRole("combobox", { name: "Thinking" }),
- ).toHaveLength(2),
- );
- for (const selector of screen.getAllByRole("combobox", {
- name: "Thinking",
- }) as HTMLSelectElement[])
- expect(options(selector)).toEqual(expected);
+ const user = userEvent.setup();
+ const triggers = await screen.findAllByRole("button", {
+ name: /Thinking /,
+ });
+ expect(triggers).toHaveLength(2);
+ for (const trigger of triggers) {
+ await user.click(trigger);
+ expect(options(screen.getByRole("listbox", { name: "Thinking options" }))).toEqual(
+ expected,
+ );
+ await user.click(trigger);
+ }
},
);
});