From 1ce03ef986f04167f36a5c47a137dd5e72f80a68 Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Fri, 28 Aug 2026 13:58:03 +0200 Subject: [PATCH] feat(quick-pane): promote chats into full sessions --- test/quick-pane-ui.test.js | 58 ++++++++++++--- ui/src/components/ThinkingSelector.test.tsx | 81 ++++++++++++++------- ui/src/quick-pane/QuickPane.test.tsx | 44 ++++++++--- ui/src/quick-pane/QuickPane.tsx | 18 ++--- ui/src/quick-pane/quick-pane.css | 35 ++++++--- 5 files changed, 168 insertions(+), 68 deletions(-) diff --git a/test/quick-pane-ui.test.js b/test/quick-pane-ui.test.js index 1cb47a3..773de76 100644 --- a/test/quick-pane-ui.test.js +++ b/test/quick-pane-ui.test.js @@ -13,6 +13,14 @@ test("Quick Pane polls snapshots, renders answers, and promotes runtime without assert.match(source, /className="quick-pane-notice escalation"/); assert.match(source, /className="quick-pane-assistant-footer"/); assert.match(source, /Continue in full session/); + const toolbar = source.slice( + source.indexOf('
'), + source.indexOf( + "
", + source.indexOf('
'), + ), + ); + assert.doesNotMatch(toolbar, /continueFull|Continue/); assert.match( source, /await invoke<\{ runtime: Runtime \}>\("promote_quick_runtime", \{\s+runtimeId: current\.runtimeId,/, @@ -174,15 +182,24 @@ test("Quick Pane dismiss and mutually exclusive window visibility stay native-se test("native promotion owns visibility and Main consumes pending runtime after startup refresh", async () => { const [nativeSource, workspaceSource] = await Promise.all([ readFile(new URL("../ui/src-tauri/src/lib.rs", import.meta.url), "utf8"), - readFile(new URL("../ui/src/workspace/useWorkspace.ts", import.meta.url), "utf8"), + readFile( + new URL("../ui/src/workspace/useWorkspace.ts", import.meta.url), + "utf8", + ), ]); const promotion = nativeSource.slice( nativeSource.indexOf("fn show_main_for_promoted_runtime"), nativeSource.indexOf("fn toggle_quick_pane"), ); assert.match(promotion, /get_or_create_main_with_visibility\(app, false\)/); - assert.ok(promotion.indexOf("quick_pane\n .hide()") < promotion.indexOf("window\n .show()")); - assert.ok(promotion.indexOf('visibility.hidden("quick-pane")') < promotion.indexOf("window\n .show()")); + assert.ok( + promotion.indexOf("quick_pane\n .hide()") < + promotion.indexOf("window\n .show()"), + ); + assert.ok( + promotion.indexOf('visibility.hidden("quick-pane")') < + promotion.indexOf("window\n .show()"), + ); assert.match(promotion, /\*pending = Some\(runtime_id\)/); assert.match(nativeSource, /fn get_pending_promoted_runtime/); assert.match(nativeSource, /fn consume_pending_promoted_runtime/); @@ -329,15 +346,38 @@ test("Quick Pane uses native dimensions and request-first compact layout", async ); assert.match( stylesheet, - /\.quick-pane-toolbar-control\s*\{[\s\S]*?height:\s*27px[\s\S]*?padding:\s*0 8px[\s\S]*?border:[\s\S]*?border-radius:[\s\S]*?background:[\s\S]*?font:/, + /\.quick-pane button\.quick-pane-toolbar-control,[\s\S]*?height:\s*27px[\s\S]*?min-width:\s*48px[\s\S]*?padding:\s*0 8px[\s\S]*?border:[\s\S]*?border-radius:[\s\S]*?background:[\s\S]*?font:/, + ); + assert.match( + stylesheet, + /\.quick-pane \.quick-pane-toolbar-control:hover:not\(:disabled\)/, + ); + assert.match( + stylesheet, + /\.quick-pane \.quick-pane-toolbar-control:focus-visible/, + ); + assert.match( + stylesheet, + /\.quick-pane \.quick-pane-toolbar-control:disabled/, + ); + assert.match( + stylesheet, + /\.quick-pane-thinking\s*\{[\s\S]*?flex:\s*0 0 143px[\s\S]*?grid-template-columns:\s*auto minmax\(84px, 1fr\)/, + ); + assert.match( + stylesheet, + /\.quick-pane-toolbar\s*\{[\s\S]*?flex-wrap:\s*nowrap/, ); - assert.match(stylesheet, /\.quick-pane-toolbar-control:hover:not\(:disabled\)/); - assert.match(stylesheet, /\.quick-pane-toolbar-control:focus-visible/); - assert.match(stylesheet, /\.quick-pane-toolbar-control:disabled/); assert.match(source, /triggerClassName="quick-pane-toolbar-control"/); assert.match(source, /controlClassName="quick-pane-toolbar-control"/); - assert.match(source, /className="quick-pane-toolbar-control quick-pane-action"/); - assert.match(source, /className="quick-pane-toolbar-control quick-pane-close"/); + assert.match( + source, + /className="quick-pane-toolbar-control quick-pane-action"/, + ); + assert.match( + source, + /className="quick-pane-toolbar-control quick-pane-close"/, + ); assert.match(stylesheet, /#f0a347/); assert.doesNotMatch(stylesheet, /#8aa9d6|#46494f|#23252a/); diff --git a/ui/src/components/ThinkingSelector.test.tsx b/ui/src/components/ThinkingSelector.test.tsx index f1c201d..d463a53 100644 --- a/ui/src/components/ThinkingSelector.test.tsx +++ b/ui/src/components/ThinkingSelector.test.tsx @@ -3,35 +3,62 @@ import { expect, it, vi } from "vitest"; import { ThinkingSelector } from "./ThinkingSelector"; it("uses selected model thinking capabilities and reconciles unavailable values", () => { - const onChange = vi.fn(); - render( - , - ); + const onChange = vi.fn(); + render( + , + ); - const selector = screen.getByRole("combobox", { name: "Thinking" }); - expect(selector).toHaveValue("off"); - expect( - within(selector) - .getAllByRole("option") - .map((option) => option.textContent), - ).toEqual(["Off", "High"]); - fireEvent.change(selector, { target: { value: "high" } }); - expect(onChange).toHaveBeenCalledWith("high"); + const selector = screen.getByRole("combobox", { name: "Thinking" }); + expect(selector).toHaveValue("off"); + expect( + within(selector) + .getAllByRole("option") + .map((option) => option.textContent), + ).toEqual(["Off", "High"]); + fireEvent.change(selector, { target: { value: "high" } }); + expect(onChange).toHaveBeenCalledWith("high"); +}); + +it("keeps complete native labels available through XHigh", () => { + render( + , + ); + + expect( + within(screen.getByRole("combobox", { name: "Thinking" })) + .getAllByRole("option") + .map((option) => option.textContent), + ).toEqual(["Off", "Minimal", "Low", "Medium", "High", "XHigh"]); }); it("uses disabled native select semantics", () => { - render(); - expect(screen.getByRole("combobox", { name: "Thinking" })).toBeDisabled(); + render(); + expect( + screen.getByRole("combobox", { name: "Thinking" }), + ).toBeDisabled(); }); diff --git a/ui/src/quick-pane/QuickPane.test.tsx b/ui/src/quick-pane/QuickPane.test.tsx index b39aa4d..158ad7b 100644 --- a/ui/src/quick-pane/QuickPane.test.tsx +++ b/ui/src/quick-pane/QuickPane.test.tsx @@ -239,19 +239,43 @@ describe("QuickPane", () => { ]); }); - it("applies shared toolbar-control classes to compact controls", async () => { + it("keeps full thinking labels and promotion out of the 640px toolbar", async () => { bridgeMock([defaultModels]); - render(); - expect(await preparedTrigger()).toHaveClass("quick-pane-toolbar-control"); - expect(screen.getByRole("combobox", { name: "Thinking" })).toHaveClass( - "quick-pane-toolbar-control", - ); - expect(screen.getByRole("button", { name: "Continue" })).toHaveClass( - "quick-pane-toolbar-control", - ); + const { container } = render(); + const model = await preparedTrigger(); + const thinking = screen.getByRole("combobox", { name: "Thinking" }); + const close = screen.getByRole("button", { name: "Close Quick Pane" }); + + for (const control of [model, thinking, close]) + expect(control).toHaveClass("quick-pane-toolbar-control"); expect( + within(thinking) + .getAllByRole("option") + .map((option) => option.textContent), + ).toEqual(["Off", "Minimal", "Low", "Medium", "High"]); + expect( + within(container.querySelector(".quick-pane-toolbar")!).queryByRole( + "button", + { name: "Continue" }, + ), + ).toBeNull(); + }); + + it("renders one toolbar control set at 640px and 520px screenshot widths", async () => { + bridgeMock([defaultModels]); + const { container } = render(); + const pane = container.querySelector(".quick-pane")!; + const toolbar = container.querySelector(".quick-pane-toolbar")!; + const controls = [ + await preparedTrigger(), + screen.getByRole("combobox", { name: "Thinking" }), screen.getByRole("button", { name: "Close Quick Pane" }), - ).toHaveClass("quick-pane-toolbar-control"); + ]; + + for (const width of [640, 520]) { + pane.style.width = `${width}px`; + for (const control of controls) expect(toolbar).toContainElement(control); + } }); it("starts native dragging only from the strip above toolbar controls", async () => { diff --git a/ui/src/quick-pane/QuickPane.tsx b/ui/src/quick-pane/QuickPane.tsx index 34a366b..f44ee41 100644 --- a/ui/src/quick-pane/QuickPane.tsx +++ b/ui/src/quick-pane/QuickPane.tsx @@ -75,7 +75,10 @@ const QUICK_PANE_MIN_HEIGHT = 360; const QUICK_PANE_MAX_HEIGHT = 520; function clampQuickPaneHeight(height: number) { - return Math.min(QUICK_PANE_MAX_HEIGHT, Math.max(QUICK_PANE_MIN_HEIGHT, height)); + return Math.min( + QUICK_PANE_MAX_HEIGHT, + Math.max(QUICK_PANE_MIN_HEIGHT, height), + ); } function messageText(message: Message) { @@ -1066,7 +1069,8 @@ export default function QuickPane() { async function continueFull() { const current = runtimeRef.current; - if (invalid || !current) return setError(invalid ?? "Quick runtime is unavailable."); + if (invalid || !current) + return setError(invalid ?? "Quick runtime is unavailable."); setPromoting(true); try { await invoke<{ runtime: Runtime }>("promote_quick_runtime", { @@ -1170,16 +1174,6 @@ export default function QuickPane() { />
- {runtime && ( - - )}