feat(quick-pane): promote chats into full sessions
This commit is contained in:
@@ -334,7 +334,7 @@ fn quick_pane_height(height: f64) -> Result<f64, String> {
|
||||
if !height.is_finite() {
|
||||
return Err("Quick Pane height must be finite".to_owned());
|
||||
}
|
||||
Ok(height.clamp(240.0, 520.0))
|
||||
Ok(height.clamp(360.0, 520.0))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -891,9 +891,9 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(quick_pane.width, 640.0);
|
||||
assert_eq!(quick_pane.height, 240.0);
|
||||
assert_eq!(quick_pane.height, 360.0);
|
||||
assert_eq!(quick_pane.min_width, Some(520.0));
|
||||
assert_eq!(quick_pane.min_height, Some(240.0));
|
||||
assert_eq!(quick_pane.min_height, Some(360.0));
|
||||
assert_eq!(quick_pane.url.to_string(), "index.html");
|
||||
assert!(!quick_pane.visible);
|
||||
assert!(quick_pane.transparent);
|
||||
@@ -903,7 +903,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn quick_pane_height_stays_within_content_limit() {
|
||||
assert_eq!(quick_pane_height(200.0), Ok(240.0));
|
||||
assert_eq!(quick_pane_height(200.0), Ok(360.0));
|
||||
assert_eq!(quick_pane_height(420.0), Ok(420.0));
|
||||
assert_eq!(quick_pane_height(900.0), Ok(520.0));
|
||||
}
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
"label": "quick-pane",
|
||||
"title": "Pi Quick Pane",
|
||||
"width": 640,
|
||||
"height": 240,
|
||||
"height": 360,
|
||||
"minWidth": 520,
|
||||
"minHeight": 240,
|
||||
"minHeight": 360,
|
||||
"center": true,
|
||||
"visible": false,
|
||||
"decorations": false,
|
||||
|
||||
@@ -92,6 +92,7 @@ export function ModelSelector({
|
||||
label = "Model",
|
||||
placement = "up",
|
||||
className = "",
|
||||
triggerClassName = "",
|
||||
onOpen,
|
||||
onClose,
|
||||
}: {
|
||||
@@ -103,6 +104,7 @@ export function ModelSelector({
|
||||
label?: string;
|
||||
placement?: "up" | "down";
|
||||
className?: string;
|
||||
triggerClassName?: string;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
@@ -209,7 +211,7 @@ export function ModelSelector({
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
className="model-trigger"
|
||||
className={["model-trigger", triggerClassName].filter(Boolean).join(" ")}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="dialog"
|
||||
aria-controls={open ? menuId : undefined}
|
||||
|
||||
@@ -3,35 +3,35 @@ 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("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();
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ type ThinkingSelectorProps = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
controlClassName?: string;
|
||||
onChange: (level: string) => void;
|
||||
};
|
||||
|
||||
@@ -15,6 +16,7 @@ export function ThinkingSelector({
|
||||
value,
|
||||
disabled = false,
|
||||
className,
|
||||
controlClassName,
|
||||
onChange,
|
||||
}: ThinkingSelectorProps) {
|
||||
const selected = reconcileThinkingLevel(model, value);
|
||||
@@ -24,6 +26,7 @@ export function ThinkingSelector({
|
||||
<label className={classes}>
|
||||
<span>Thinking</span>
|
||||
<select
|
||||
className={controlClassName}
|
||||
aria-label="Thinking"
|
||||
value={selected}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -239,6 +239,21 @@ describe("QuickPane", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("applies shared toolbar-control classes to compact controls", 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",
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Close Quick Pane" }),
|
||||
).toHaveClass("quick-pane-toolbar-control");
|
||||
});
|
||||
|
||||
it("starts native dragging only from the strip above toolbar controls", async () => {
|
||||
bridgeMock([defaultModels]);
|
||||
const { container } = render(<QuickPane />);
|
||||
@@ -390,7 +405,12 @@ describe("QuickPane", () => {
|
||||
id: "quick/model",
|
||||
name: "Quick Model",
|
||||
reasoning: true,
|
||||
thinkingLevelMap: { minimal: null, low: null, medium: null, high: "high" },
|
||||
thinkingLevelMap: {
|
||||
minimal: null,
|
||||
low: null,
|
||||
medium: null,
|
||||
high: "high",
|
||||
},
|
||||
},
|
||||
];
|
||||
bridgeMock([thinkingModels], () =>
|
||||
@@ -424,7 +444,10 @@ describe("QuickPane", () => {
|
||||
expect(screen.queryByRole("button", { name: "Abort" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Renew" })).toBeNull();
|
||||
await ask("Inspect");
|
||||
expect(await screen.findByRole("button", { name: "Abort" })).toBeEnabled();
|
||||
expect(await screen.findByRole("button", { name: "Abort" })).toHaveClass(
|
||||
"quick-pane-toolbar-control",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Abort" })).toBeEnabled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Abort" }));
|
||||
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
|
||||
expect(mocks.invoke).toHaveBeenCalledWith("abort", { agentId: "agent-1" });
|
||||
@@ -436,7 +459,10 @@ describe("QuickPane", () => {
|
||||
|
||||
streaming = false;
|
||||
await ask("Again");
|
||||
expect(await screen.findByRole("button", { name: "Renew" })).toBeEnabled();
|
||||
expect(await screen.findByRole("button", { name: "Renew" })).toHaveClass(
|
||||
"quick-pane-toolbar-control",
|
||||
);
|
||||
expect(screen.getByRole("button", { name: "Renew" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("reports Abort failures without replacing active chat", async () => {
|
||||
@@ -510,7 +536,7 @@ describe("QuickPane", () => {
|
||||
expect(resizeCalls.length).toBeGreaterThan(resizeCallCountBeforeClose);
|
||||
expect(resizeCalls[resizeCalls.length - 1]).toEqual([
|
||||
"resize_quick_pane",
|
||||
{ height: 240 },
|
||||
{ height: 360 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -71,6 +71,12 @@ type TextSelection = {
|
||||
};
|
||||
|
||||
const CHAT_RENEWAL_MS = 180_000;
|
||||
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));
|
||||
}
|
||||
|
||||
function messageText(message: Message) {
|
||||
if (typeof message.content === "string") return message.content;
|
||||
@@ -391,6 +397,7 @@ export default function QuickPane() {
|
||||
if (intent !== "pointer") scheduleInputSelectionRestore();
|
||||
};
|
||||
const requestPaneResize = useCallback((height: number) => {
|
||||
height = clampQuickPaneHeight(height);
|
||||
if (requestedPaneHeightRef.current !== height)
|
||||
resizeRetryCountRef.current = 0;
|
||||
requestedPaneHeightRef.current = height;
|
||||
@@ -420,7 +427,7 @@ export default function QuickPane() {
|
||||
}
|
||||
if (
|
||||
!mountedRef.current ||
|
||||
(modelMenuOpenRef.current && height !== 520) ||
|
||||
(modelMenuOpenRef.current && height !== QUICK_PANE_MAX_HEIGHT) ||
|
||||
resizeRetryCountRef.current >= 1
|
||||
)
|
||||
return;
|
||||
@@ -430,7 +437,7 @@ export default function QuickPane() {
|
||||
if (
|
||||
mountedRef.current &&
|
||||
requestedPaneHeightRef.current === height &&
|
||||
(!modelMenuOpenRef.current || height === 520)
|
||||
(!modelMenuOpenRef.current || height === QUICK_PANE_MAX_HEIGHT)
|
||||
)
|
||||
requestPaneResizeRef.current?.(height);
|
||||
}, 250);
|
||||
@@ -441,7 +448,7 @@ export default function QuickPane() {
|
||||
const handleModelMenuOpen = useCallback(() => {
|
||||
modelMenuOpenRef.current = true;
|
||||
setModelMenuOpen(true);
|
||||
requestPaneResize(520);
|
||||
requestPaneResize(QUICK_PANE_MAX_HEIGHT);
|
||||
}, [requestPaneResize]);
|
||||
const handleModelMenuClose = useCallback(() => {
|
||||
modelMenuOpenRef.current = false;
|
||||
@@ -750,16 +757,9 @@ export default function QuickPane() {
|
||||
(Number.parseFloat(styles.rowGap || styles.gap || "0") || 0) +
|
||||
(Number.parseFloat(styles.paddingTop) || 0) +
|
||||
(Number.parseFloat(styles.paddingBottom) || 0);
|
||||
const height = Math.min(
|
||||
520,
|
||||
Math.max(
|
||||
240,
|
||||
Math.ceil(
|
||||
pane.clientHeight - exchange.clientHeight + contentHeight,
|
||||
),
|
||||
),
|
||||
requestPaneResize(
|
||||
pane.clientHeight - exchange.clientHeight + contentHeight,
|
||||
);
|
||||
requestPaneResize(height);
|
||||
});
|
||||
};
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
@@ -1137,6 +1137,7 @@ export default function QuickPane() {
|
||||
<div className="quick-pane-primary-controls">
|
||||
<ModelSelector
|
||||
className="quick-pane-model-selector"
|
||||
triggerClassName="quick-pane-toolbar-control"
|
||||
placement="down"
|
||||
models={models}
|
||||
current={{
|
||||
@@ -1161,6 +1162,7 @@ export default function QuickPane() {
|
||||
/>
|
||||
<ThinkingSelector
|
||||
className="quick-pane-thinking"
|
||||
controlClassName="quick-pane-toolbar-control"
|
||||
model={selectedCatalogModel}
|
||||
value={thinkingLevel}
|
||||
disabled={catalogOverlayActive || runActive || chatReplacing}
|
||||
@@ -1170,7 +1172,7 @@ export default function QuickPane() {
|
||||
<div className="quick-pane-header-actions">
|
||||
{runtime && (
|
||||
<button
|
||||
className="quick-pane-action"
|
||||
className="quick-pane-toolbar-control quick-pane-action"
|
||||
type="button"
|
||||
disabled={catalogOverlayActive || promoting}
|
||||
onClick={continueFull}
|
||||
@@ -1179,7 +1181,7 @@ export default function QuickPane() {
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="quick-pane-close"
|
||||
className="quick-pane-toolbar-control quick-pane-close"
|
||||
type="button"
|
||||
aria-label="Close Quick Pane"
|
||||
title="Close Quick Pane (Esc)"
|
||||
@@ -1236,7 +1238,7 @@ export default function QuickPane() {
|
||||
>
|
||||
{requestActive && (
|
||||
<button
|
||||
className="quick-pane-action"
|
||||
className="quick-pane-toolbar-control quick-pane-action"
|
||||
type="button"
|
||||
disabled={submissionStarting || chatReplacing}
|
||||
onClick={() => void replaceChat(false, true)}
|
||||
@@ -1246,7 +1248,7 @@ export default function QuickPane() {
|
||||
)}
|
||||
{!runActive && hasExchange && (
|
||||
<button
|
||||
className="quick-pane-action"
|
||||
className="quick-pane-toolbar-control quick-pane-action"
|
||||
type="button"
|
||||
disabled={submissionStarting || chatReplacing}
|
||||
onClick={() => void replaceChat(false, false)}
|
||||
|
||||
@@ -87,11 +87,11 @@ body {
|
||||
}
|
||||
|
||||
.quick-pane-primary-controls {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.quick-pane-header-actions {
|
||||
flex: 0 1 auto;
|
||||
flex: 0 0 auto;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
@@ -105,8 +105,7 @@ body {
|
||||
}
|
||||
|
||||
.quick-pane-model-selector {
|
||||
flex: 1 1 230px;
|
||||
max-width: 310px;
|
||||
flex: 1 1 0;
|
||||
color: #bbb7b0;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0.04em;
|
||||
@@ -150,16 +149,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.quick-pane .model-trigger {
|
||||
height: 27px;
|
||||
border: 0;
|
||||
padding: 5px 25px 5px 8px;
|
||||
color: #e6e3df;
|
||||
background-color: #353535;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.quick-pane .model-menu {
|
||||
width: min(400px, calc(100vw - 24px));
|
||||
max-height: min(350px, calc(100vh - 56px));
|
||||
@@ -236,16 +225,48 @@ body {
|
||||
}
|
||||
|
||||
.quick-pane button:focus-visible,
|
||||
.quick-pane select:focus-visible,
|
||||
.quick-pane-request:focus {
|
||||
.quick-pane select:focus-visible {
|
||||
outline: 3px solid #f0a347;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.quick-pane-close {
|
||||
color: #e6e3df !important;
|
||||
background: #45413c !important;
|
||||
font-size: 11px !important;
|
||||
.quick-pane-toolbar-control {
|
||||
box-sizing: border-box;
|
||||
height: 27px;
|
||||
min-width: 0;
|
||||
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;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.quick-pane .model-trigger.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) {
|
||||
border-color: #d88735;
|
||||
background: #44382d;
|
||||
}
|
||||
|
||||
.quick-pane-toolbar-control:focus-visible {
|
||||
outline: 3px solid #f0a347;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.quick-pane-toolbar-control:disabled {
|
||||
opacity: 0.58;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.quick-pane-request:focus {
|
||||
outline: 3px solid #f0a347;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.quick-pane-exchange {
|
||||
@@ -577,13 +598,6 @@ body {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.quick-pane-action {
|
||||
min-height: 30px;
|
||||
padding: 4px 7px !important;
|
||||
color: #e6e3df !important;
|
||||
background: #45413c !important;
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.quick-pane-send {
|
||||
position: absolute;
|
||||
@@ -643,13 +657,6 @@ body {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.quick-pane-close {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
padding: 0 !important;
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.quick-pane-user-message {
|
||||
max-width: 88%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user