feat(quick-pane): promote chats into full sessions

This commit is contained in:
2026-08-28 13:58:03 +02:00
parent 0eb163b6c4
commit 1ce03ef986
5 changed files with 168 additions and 68 deletions
+49 -9
View File
@@ -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('<header className="quick-pane-toolbar">'),
source.indexOf(
"</header>",
source.indexOf('<header className="quick-pane-toolbar">'),
),
);
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/);
+28 -1
View File
@@ -31,7 +31,34 @@ it("uses selected model thinking capabilities and reconciles unavailable values"
expect(onChange).toHaveBeenCalledWith("high");
});
it("keeps complete native labels available through XHigh", () => {
render(
<ThinkingSelector
model={{
reasoning: true,
thinkingLevelMap: {
minimal: "minimal",
low: "low",
medium: "medium",
high: "high",
xhigh: "xhigh",
},
}}
value="medium"
onChange={vi.fn()}
/>,
);
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(<ThinkingSelector value="off" disabled onChange={vi.fn()} />);
expect(screen.getByRole("combobox", { name: "Thinking" })).toBeDisabled();
expect(
screen.getByRole("combobox", { name: "Thinking" }),
).toBeDisabled();
});
+34 -10
View File
@@ -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(<QuickPane />);
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(<QuickPane />);
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(<QuickPane />);
const pane = container.querySelector<HTMLElement>(".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 () => {
+6 -12
View File
@@ -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() {
/>
</div>
<div className="quick-pane-header-actions">
{runtime && (
<button
className="quick-pane-toolbar-control quick-pane-action"
type="button"
disabled={catalogOverlayActive || promoting}
onClick={continueFull}
>
{promoting ? "Continuing…" : "Continue"}
</button>
)}
<button
className="quick-pane-toolbar-control quick-pane-close"
type="button"
+25 -10
View File
@@ -77,6 +77,7 @@ body {
.quick-pane-toolbar {
flex: 0 0 auto;
flex-wrap: nowrap;
justify-content: space-between;
gap: 8px;
}
@@ -88,6 +89,7 @@ body {
.quick-pane-primary-controls {
flex: 1 1 0;
min-width: 0;
}
.quick-pane-header-actions {
@@ -106,6 +108,7 @@ body {
.quick-pane-model-selector {
flex: 1 1 0;
min-width: 0;
color: #bbb7b0;
font-weight: 750;
letter-spacing: 0.04em;
@@ -180,7 +183,10 @@ body {
}
.quick-pane-thinking {
flex: 0 1 122px;
display: grid;
flex: 0 0 143px;
grid-template-columns: auto minmax(84px, 1fr);
gap: 4px;
}
.quick-pane .compact-label {
@@ -230,36 +236,46 @@ body {
outline-offset: 2px;
}
.quick-pane-toolbar-control {
.quick-pane button.quick-pane-toolbar-control,
.quick-pane select.quick-pane-toolbar-control,
.quick-pane .model-trigger.quick-pane-toolbar-control,
.quick-pane
.thinking-selector.thinking-selector
select.quick-pane-toolbar-control {
box-sizing: border-box;
height: 27px;
min-width: 0;
min-width: 48px;
padding: 0 8px;
border: 1px solid #70522f;
border-radius: 5px;
color: #e6e3df;
background: #353535;
font: 500 11px / 1 Inter, ui-sans-serif, system-ui, sans-serif;
font:
650 11px / 1 Inter,
ui-sans-serif,
system-ui,
sans-serif;
cursor: pointer;
}
.quick-pane .model-trigger.quick-pane-toolbar-control,
.quick-pane .thinking-selector.thinking-selector select.quick-pane-toolbar-control {
.quick-pane
.thinking-selector.thinking-selector
select.quick-pane-toolbar-control {
appearance: auto;
background: #353535;
}
.quick-pane-toolbar-control:hover:not(:disabled) {
.quick-pane .quick-pane-toolbar-control:hover:not(:disabled) {
border-color: #d88735;
background: #44382d;
}
.quick-pane-toolbar-control:focus-visible {
.quick-pane .quick-pane-toolbar-control:focus-visible {
outline: 3px solid #f0a347;
outline-offset: 2px;
}
.quick-pane-toolbar-control:disabled {
.quick-pane .quick-pane-toolbar-control:disabled {
opacity: 0.58;
cursor: not-allowed;
}
@@ -598,7 +614,6 @@ body {
gap: 4px;
}
.quick-pane-send {
position: absolute;
right: 8px;