Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .papercuts/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Troubleshooting

- `.papercuts/` is ignored even when its troubleshooting file is present in the PR branch, so persisting a required update needs an explicit `git add -f`.
- Layout stabilization must race `animation.finished` against a short timeout because paused or infinite document animations never settle; keep geometry polling as the authoritative E2E readiness check.
- Pi 0.80.10 can choose the oldest oversized user turn as `firstKeptEntryId`, leaving both summary inputs empty and producing a no-op checkpoint. When the journal has a newer turn, retry `prepareCompaction` with a minimal retained-tail budget; still refuse the checkpoint if both summary inputs remain empty.
- `Session.getEntries()` includes abandoned branches. Synchronization markers must be read from `Session.getBranch()` or a rolled-back partial write can still look committed.
- Child-runtime unit tests load outside Electron. Keep usage accounting behind an injected callback (with a production-only dynamic import) instead of statically importing the Electron-backed singleton into the reusable child registry.
Expand Down
2 changes: 1 addition & 1 deletion docs/settings-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The `.settings-responsive` container defines `--settings-card-radius`, `--settin

Rows respond to their allocated content width, not the whole window. Below 540px complex controls stack under descriptions, while switches remain on the right. Grid groups must use `minmax(0, 1fr)` / `grid-cols-1` so long provider names or endpoints cannot force horizontal overflow. Controls and text must stay reachable without horizontal page scrolling.

Model Pad measures the actual scrollport, wrapped toolbar, labels, and legend. Its square is constrained by both remaining height and column width. On very short or highly zoomed windows, it retains a usable 160px square and the Settings page scrolls; the Pad and its labels remain reachable. Ordinary window allocations show the full canvas and legend together. Opening model or benchmark panels uses the same measurement.
Model Pad measures the actual scrollport and remaining column. Axis captions and the legend use a reserved height so the square outline stays put while surrounding copy, marker labels, and catalog text change. The square is constrained by remaining height, column width, and the visible scrollport. On very short or highly zoomed windows, it keeps a usable canvas (160px when the scrollport allows) and the Settings page scrolls; the Pad and its labels remain reachable. Ordinary window allocations show the full canvas and legend together. Opening model or benchmark panels uses the same measurement.

## Workspace labels

Expand Down
2 changes: 1 addition & 1 deletion renderer/components/composer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ test("model picker details sit beside the menu without overlapping the pad", ()
);
assert.match(
modelPicker,
/className="pointer-events-none w-56 shrink-0 rounded-popover bg-popover p-3 text-primary shadow-popover"/u,
/className="pointer-events-auto flex h-\[min\(22\.5rem,70vh\)\] w-56 shrink-0 flex-col overflow-hidden rounded-popover bg-popover p-3 text-primary shadow-popover"/u,
);
assert.doesNotMatch(modelPicker, /left-\[calc\(100%\+0\.5rem\)\]/u);
assert.doesNotMatch(modelPicker, /right: showExternalDetails/u);
Expand Down
2 changes: 1 addition & 1 deletion renderer/components/git-push-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ export function GitPushDialog({
</div>
</div>

<Label className="items-start justify-between rounded-control border border-field px-3 py-2.5">
<Label className="items-center justify-between rounded-control border border-field px-3 py-2.5">
<span className="min-w-0 pr-3">
<span className="block text-regular text-primary">Remember as upstream</span>
<span className="mt-0.5 block text-small text-secondary">
Expand Down
6 changes: 4 additions & 2 deletions renderer/components/model-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ function ModelHoverDetails({
].filter((row): row is [string, string] => Boolean(row));

return (
<aside className="pointer-events-none w-56 shrink-0 rounded-popover bg-popover p-3 text-primary shadow-popover">
<aside className="pointer-events-auto flex h-[min(22.5rem,70vh)] w-56 shrink-0 flex-col overflow-hidden rounded-popover bg-popover p-3 text-primary shadow-popover">
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto overscroll-contain">
<div className="flex min-w-0 items-start gap-2">
<span className="mt-0.5 shrink-0 text-tertiary">
<ProviderIcon
Expand Down Expand Up @@ -321,14 +322,15 @@ function ModelHoverDetails({
href={attributionUrl}
target="_blank"
rel="noreferrer"
className="pointer-events-auto mt-1 inline-block text-mini leading-4 text-tertiary underline decoration-separator underline-offset-2 hover:text-secondary"
className="mt-1 inline-block text-mini leading-4 text-tertiary underline decoration-separator underline-offset-2 hover:text-secondary"
>
{info?.benchmark
? `${info.benchmark.sourceLabel} · ${info.benchmark.license}`
: `${model.ranking ? "Benchmark data" : "Model data"} · Artificial Analysis`}
</a>
) : null}
</div>
</div>
</aside>
);
}
Expand Down
2 changes: 1 addition & 1 deletion renderer/components/scheduled-tasks-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ export function ScheduledTasksView() {
/>
</div>
) : (
<div className="overflow-hidden rounded-card bg-well">
<div className="overflow-visible rounded-card bg-well">
{visible.map((task, index) => {
const status = statusPresentation(task);
const busy = busyTaskId === task.id;
Expand Down
2 changes: 2 additions & 0 deletions renderer/components/settings/memory-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ test("Memory settings expose authoritative global and workspace switches", () =>
assert.match(settings, /workspacesApi\.update\(workspace\.id, \{ memoryEnabled: enabled \}\)/u);
assert.match(settings, /Existing approved[\s\S]*stay on this Mac/u);
assert.match(settings, /Bot memory has its own scope/u);
assert.match(settings, /<\/FieldSet>\s*<Text as="p"[\s\S]*Existing approved/u);
assert.match(settings, /<\/FieldSet>\s*<Text as="p"[\s\S]*Workspace switches affect/u);
});

test("the chat toolbar no longer exposes the manual memory manager", () => {
Expand Down
16 changes: 8 additions & 8 deletions renderer/components/settings/memory-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ export function MemorySettings() {
aria-label="Use memory globally"
/>
</Field>
<Text as="p" variant="small" color="secondary" className="px-4 pb-4 text-pretty">
Turning memory off stops memory tools, prompt context, and new indexing. Existing approved
facts stay on this Mac and become available again if you turn it back on.
</Text>
</FieldSet>
<Text as="p" variant="small" color="secondary" className="text-pretty">
Turning memory off stops memory tools, prompt context, and new indexing. Existing approved
facts stay on this Mac and become available again if you turn it back on.
</Text>

<FieldSet title="Workspaces">
{workspaces.isLoading ? (
Expand Down Expand Up @@ -159,11 +159,11 @@ export function MemorySettings() {
No workspaces yet.
</Text>
)}
<Text as="p" variant="small" color="secondary" className="px-4 pb-4 text-pretty">
Workspace switches affect regular workspace chats. Bot memory has its own scope and
follows the global switch.
</Text>
</FieldSet>
<Text as="p" variant="small" color="secondary" className="text-pretty">
Workspace switches affect regular workspace chats. Bot memory has its own scope and
follows the global switch.
</Text>
</div>
);
}
12 changes: 12 additions & 0 deletions renderer/components/settings/model-pad-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ test("Model Pad progressively discloses supporting and advanced controls", () =>
);
assert.match(styles, /\.model-pad-canvas\s*\{\s*width: min\(100%, 40rem\)/u);
assert.match(styles, /\.model-pad\s*\{\s*width: min\(100%, var\(--model-pad-available-size/u);
assert.match(source, /measureModelPadAvailableSize/u);
assert.match(source, /observer\.observe\(grid\)/u);
assert.doesNotMatch(source, /labelsHeight/u);
assert.doesNotMatch(
source,
/canvas\.getBoundingClientRect\(\)\.height - pad\.getBoundingClientRect\(\)\.height/u,
);
assert.match(
styles,
/\.model-pad\s*\{[\s\S]*aspect-ratio: 1 \/ 1;[\s\S]*flex-shrink: 0/u,
);
assert.match(styles, /\.model-pad-legend\s*\{\s*min-height: 2\.5rem/u);
assert.match(styles, /@container model-pad-fieldset \(max-width: 760px\)/u);
assert.match(
styles,
Expand Down
22 changes: 13 additions & 9 deletions renderer/components/settings/model-pad-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
reflowVisibleModelPadPlacements,
snapToModelPadGrid,
writeModelPadLayout,
measureModelPadAvailableSize,
MODEL_PAD_INSET_PERCENT,
MODEL_PAD_RANGE_PERCENT,
type ModelPadDirection,
Expand Down Expand Up @@ -388,14 +389,19 @@ export function ModelPadSettings() {
let frame = 0;
const measure = () => {
frame = 0;
const scrollportRect = scrollport?.getBoundingClientRect();
const viewportBottom = Math.min(
window.innerHeight,
scrollport?.getBoundingClientRect().bottom ?? window.innerHeight,
scrollportRect?.bottom ?? window.innerHeight,
);
// Compensate for page scrolling so scrolling down never grows the Pad.
const canvasTop = canvas.getBoundingClientRect().top + (scrollport?.scrollTop ?? 0);
const labelsHeight = canvas.getBoundingClientRect().height - pad.getBoundingClientRect().height;
const size = Math.floor(Math.max(160, viewportBottom - canvasTop - labelsHeight - 24));
const size = measureModelPadAvailableSize({
viewportBottom,
canvasTop,
canvasWidth: canvas.clientWidth,
scrollportClientHeight: scrollport?.clientHeight ?? window.innerHeight,
});
const value = `${size}px`;
if (grid.style.getPropertyValue("--model-pad-available-size") !== value) {
grid.style.setProperty("--model-pad-available-size", value);
Expand All @@ -405,12 +411,10 @@ export function ModelPadSettings() {
if (!frame) frame = requestAnimationFrame(measure);
};
const observer = new ResizeObserver(schedule);
// Include ancestors to catch a title or toolbar wrapping after a font or
// window change, and labels to converge when a narrow legend wraps.
for (let element: HTMLElement | null = canvas; element; element = element.parentElement) {
observer.observe(element);
if (element === scrollport) break;
}
// Watch the grid and scrollport only. Observing the canvas height (labels +
// square) fed legend wrapping back into the square size and made the outline jump.
observer.observe(grid);
if (scrollport) observer.observe(scrollport);
measure();
window.addEventListener("resize", schedule);
return () => {
Expand Down
2 changes: 1 addition & 1 deletion renderer/components/settings/remote-access-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ export function RemoteAccessSettings() {
)}
description="Connect Aiden On The Go while Aiden is running."
>
<div className="flex items-center justify-end gap-2 max-[540px]:justify-start">
<div className="flex items-center justify-end gap-2">
{busy === "enabled" ? <Loader2 className="size-4 animate-spin text-secondary" /> : null}
<Badge color={status.running ? "green" : status.error ? "red" : undefined}>{summary}</Badge>
<Switch
Expand Down
8 changes: 7 additions & 1 deletion renderer/components/settings/settings-design.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,14 @@ test("every settings destination uses the shared page and grouped row system", (
assert.match(css, /--settings-card-fill:/u);
assert.match(
css,
/settings-field-horizontal:has\(> \.settings-field-control > \[role="switch"\]\)/u,
/settings-field-horizontal:has\(\.settings-field-control \[role="switch"\]\)/u,
);
assert.doesNotMatch(
css,
/settings-field-control > \[role="switch"\][\s\S]{0,80}display:\s*flex/u,
);
assert.match(css, /@container settings-content \(max-width: 540px\)[\s\S]*settings-field-horizontal:has\(\.settings-field-control \[role="switch"\]\)/u);
assert.match(ui, /settings-group-card overflow-visible/u);
assert.match(css, /outline: 2px solid var\(--focus-ring\)/u);
});

Expand Down
2 changes: 1 addition & 1 deletion renderer/components/settings/shortcut-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function ShortcutRow({

return (
<div className="px-4 py-3.5 after:mt-3.5 after:block after:h-px after:bg-separator last:after:hidden">
<div className="flex items-start gap-3">
<div className="flex items-center gap-3">
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<Text variant="strong">{command.title}</Text>
Expand Down
8 changes: 4 additions & 4 deletions renderer/components/ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export function FieldSet({
return (
<section className={cn("settings-group mb-7", className)}>
{title ? <h2 className="settings-group-title mb-3 px-4 text-large-strong text-primary">{title}</h2> : null}
<div className="settings-group-card overflow-hidden rounded-card bg-well">{children}</div>
<div className="settings-group-card overflow-visible rounded-card bg-well">{children}</div>
</section>
);
}
Expand All @@ -332,7 +332,7 @@ export function Field({
className={cn(
"settings-field relative p-4 after:absolute after:inset-x-4 after:bottom-0 after:h-px after:bg-separator last:after:hidden",
orientation === "horizontal"
? "settings-field-horizontal grid min-h-12 grid-cols-[minmax(120px,0.8fr)_minmax(160px,1.2fr)] items-center gap-5 max-[540px]:grid-cols-1 max-[540px]:items-start max-[540px]:gap-2"
? "settings-field-horizontal grid min-h-12 grid-cols-[minmax(120px,0.8fr)_minmax(160px,1.2fr)] items-center gap-5 has-[[role=switch]]:grid-cols-[minmax(0,1fr)_auto] max-[540px]:grid-cols-1 max-[540px]:items-start max-[540px]:has-[[role=switch]]:grid-cols-[minmax(0,1fr)_auto] max-[540px]:has-[[role=switch]]:items-center max-[540px]:gap-2"
: "flex flex-col gap-3",
className,
)}
Expand Down Expand Up @@ -1423,12 +1423,12 @@ export const Switch = React.forwardRef<
<SwitchPrimitive.Root
ref={ref}
className={cn(
"relative h-6 w-10 rounded-pill bg-control-hover shadow-control-pressed outline-none transition-[background-color,box-shadow,opacity] duration-150 ease-out hover:bg-control-active focus-visible:bg-control-active focus-visible:outline-none data-[state=checked]:bg-accent data-[state=checked]:shadow-control data-[state=checked]:hover:bg-accent-hover data-[state=checked]:focus-visible:bg-accent-hover disabled:pointer-events-none disabled:opacity-45",
"relative inline-flex h-6 w-10 shrink-0 items-center overflow-visible rounded-pill bg-control-hover shadow-control-pressed outline-none transition-[background-color,box-shadow,opacity] duration-150 ease-out hover:bg-control-active focus-visible:bg-control-active focus-visible:outline-none data-[state=checked]:bg-accent data-[state=checked]:shadow-control data-[state=checked]:hover:bg-accent-hover data-[state=checked]:focus-visible:bg-accent-hover disabled:pointer-events-none disabled:opacity-45",
className,
)}
{...props}
>
<SwitchPrimitive.Thumb className="block size-5 translate-x-0.5 rounded-full bg-white shadow-control transition-[background-color,transform] duration-150 ease-out data-[state=checked]:translate-x-[18px] data-[state=checked]:bg-accent-foreground" />
<SwitchPrimitive.Thumb className="pointer-events-none block size-5 shrink-0 translate-x-0.5 rounded-full bg-white shadow-control transition-[background-color,transform] duration-150 ease-out data-[state=checked]:translate-x-[18px] data-[state=checked]:bg-accent-foreground" />
</SwitchPrimitive.Root>
);
});
Expand Down
2 changes: 2 additions & 0 deletions renderer/lib/button-appearance-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ test("legacy actions and button links share the shape without clipping or changi
assert.match(rule[1], /corner-shape: squircle/u);
assert.doesNotMatch(rule[1], /overflow|clip-path|outline/u);
assert.match(styles, /--radius-button: 16px/u);
assert.match(uiSource, /inline-flex h-6 w-10 shrink-0 items-center overflow-visible/u);
assert.doesNotMatch(styles, /\.settings-field-control > \[role="switch"\][\s\S]{0,60}display:\s*flex/u);
});

test("design guidance and interactive specimen document reusable button and composer geometry", () => {
Expand Down
41 changes: 41 additions & 0 deletions renderer/lib/model-pad-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import test from "node:test";
import {
distributeCapabilityOnlyModelPadSuggestions,
emptyModelPadLayout,
measureModelPadAvailableSize,
MODEL_PAD_MIN_SIZE_PX,
MODEL_PAD_SETTINGS_CHROME_PX,
MODEL_PAD_VIEWPORT_GUTTER_PX,
modelPadGridSize,
modelPadLayoutsEqual,
modelPadPointKey,
Expand Down Expand Up @@ -215,6 +219,43 @@ test("capability-only distribution fails closed for invalid and duplicate sugges
assert.equal(placements.valid.y, 5 / 6);
});

test("Model Pad available size ignores label wrap and never exceeds the scrollport", () => {
const canvasTop = 120;
const viewportBottom = 600;
const canvasWidth = 480;
const scrollportClientHeight = 360;
const size = measureModelPadAvailableSize({
viewportBottom,
canvasTop,
canvasWidth,
scrollportClientHeight,
});
const heightBudget =
viewportBottom - canvasTop - MODEL_PAD_SETTINGS_CHROME_PX - MODEL_PAD_VIEWPORT_GUTTER_PX;
assert.equal(
size,
Math.floor(Math.min(scrollportClientHeight - 1, canvasWidth, heightBudget)),
);
assert.ok(size <= scrollportClientHeight);
assert.equal(
measureModelPadAvailableSize({
viewportBottom: 800,
canvasTop: 100,
canvasWidth: 500,
scrollportClientHeight: 140,
}),
139,
);
assert.ok(
measureModelPadAvailableSize({
viewportBottom: 400,
canvasTop: 280,
canvasWidth: 400,
scrollportClientHeight: 500,
}) >= MODEL_PAD_MIN_SIZE_PX,
);
});

test("keyboard movement advances by nodes and skips occupied points on its axis", () => {
const next = moveModelPadPoint({ x: 0.5, y: 0.5 }, "right", 1, 7, [{ x: 4 / 6, y: 0.5 }]);
assert.deepEqual(next, { x: 5 / 6, y: 0.5 });
Expand Down
26 changes: 26 additions & 0 deletions renderer/lib/model-pad-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ export const BASE_MODEL_PAD_GRID_SIZE = 7;
export const MODEL_PAD_GRID_DENSITY = 6;
export const MODEL_PAD_INSET_PERCENT = 8;
export const MODEL_PAD_RANGE_PERCENT = 100 - MODEL_PAD_INSET_PERCENT * 2;
export const MODEL_PAD_MIN_SIZE_PX = 160;
/** Gutter below the measured canvas so the square does not kiss the scrollport edge. */
export const MODEL_PAD_VIEWPORT_GUTTER_PX = 24;
/**
* Reserved height for the two axis captions and the legend. Using a fixed
* reservation keeps the square outline stable when legend copy wraps or
* marker labels appear; those must not feed back into `--model-pad-available-size`.
*/
export const MODEL_PAD_SETTINGS_CHROME_PX = 128;

export function measureModelPadAvailableSize(input: {
viewportBottom: number;
canvasTop: number;
canvasWidth: number;
scrollportClientHeight: number;
}): number {
const heightBudget =
input.viewportBottom -
input.canvasTop -
MODEL_PAD_SETTINGS_CHROME_PX -
MODEL_PAD_VIEWPORT_GUTTER_PX;
const preferred = Math.min(input.canvasWidth, heightBudget);
const scrollportCap = Math.max(0, input.scrollportClientHeight - 1);
const size = Math.min(scrollportCap, Math.max(MODEL_PAD_MIN_SIZE_PX, preferred));
return Math.floor(Math.max(0, size));
}

export type ModelPadPlacementSource = "user" | "benchmark" | "neutral";

Expand Down
2 changes: 1 addition & 1 deletion renderer/shared/appearance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ test("the shared focus treatment separates text entry from non-text keyboard foc
);
assert.match(
ui,
/SwitchPrimitive\.Thumb className="[^"]*bg-white[^"]*data-\[state=checked\]:bg-accent-foreground[^"]*"/u,
/SwitchPrimitive\.Root[\s\S]*inline-flex h-6 w-10 shrink-0 items-center overflow-visible[\s\S]*SwitchPrimitive\.Thumb className="[^"]*pointer-events-none[^"]*bg-white[^"]*data-\[state=checked\]:bg-accent-foreground[^"]*"/u,
);
assert.match(assistantBubble, /bg-support-red[\s\S]*?text-support-red-foreground/u);
});
Expand Down
Loading