feat(quick-pane): promote chats into full sessions
This commit is contained in:
@@ -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/);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user