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
+54 -27
View File
@@ -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(
<ThinkingSelector
model={{
reasoning: true,
thinkingLevelMap: {
minimal: null,
low: null,
medium: null,
high: "high",
},
}}
value="medium"
onChange={onChange}
/>,
);
const onChange = vi.fn();
render(
<ThinkingSelector
model={{
reasoning: true,
thinkingLevelMap: {
minimal: null,
low: null,
medium: null,
high: "high",
},
}}
value="medium"
onChange={onChange}
/>,
);
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(
<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();
render(<ThinkingSelector value="off" disabled onChange={vi.fn()} />);
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;