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
13 changes: 9 additions & 4 deletions apps/desktop/src/features/app/useAppShellRuntime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function useAppShellRuntime() {

const [searchOpen, setSearchOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [sidebarWidth] = useState(() => loadSidebarWidth());
const [sidebarWidth, setSidebarWidth] = useState(() => loadSidebarWidth());
const [sidebarExiting, setSidebarExiting] = useState(false);
const [shellWidth, setShellWidth] = useState(0);
const appShellRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -100,9 +100,14 @@ export function useAppShellRuntime() {
observer.observe(shell);
return () => observer.disconnect();
}, []);
// The sidebar is a fixed-width column: it only collapses and opens.
const handleSidebarWidthChange = useCallback(() => {}, []);
const handleSidebarWidthCommit = useCallback(() => {}, []);
const handleSidebarWidthChange = useCallback((width: number) => {
setSidebarWidth(clampSidebarWidth(width));
}, []);
const handleSidebarWidthCommit = useCallback((width: number) => {
const nextWidth = clampSidebarWidth(width);
setSidebarWidth(nextWidth);
saveSidebarWidth(nextWidth);
}, []);
// Reopening prefers the right column: the work panel gives up width first so
// MainChat keeps the width it already had, and only a would-be breach of the
// 450px floor falls back to the 460px reopen target.
Expand Down
23 changes: 12 additions & 11 deletions apps/desktop/src/lib/sidebar-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,13 @@ export const SIDEBAR_WIDTH_MIN = 240;
export const SIDEBAR_WIDTH_DEFAULT = 275;
export const SIDEBAR_WIDTH_MAX = 520;

/**
* The sidebar is a fixed-width column: it collapses and opens, but its width is
* not resizable. The historical fixed value is 275px, which the design tokens
* already use as the preferred value of `--ds-sidebar-width`; a persisted
* preference from the resizable era is ignored on purpose.
*/
export function clampSidebarWidth(value?: number): number {
void value;
return SIDEBAR_WIDTH_DEFAULT;
if (typeof value !== "number" || !Number.isFinite(value)) {
return SIDEBAR_WIDTH_DEFAULT;
}
return Math.round(
Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, value)),
);
}

function storage(): Storage | null {
Expand Down Expand Up @@ -224,11 +222,14 @@ export function saveSidebarPreferences(value: SidebarPreferences): void {
}

export function loadSidebarWidth(): number {
return SIDEBAR_WIDTH_DEFAULT;
const value = read(SIDEBAR_WIDTH_KEY);
return typeof value === "number"
? clampSidebarWidth(value)
: SIDEBAR_WIDTH_DEFAULT;
}

export function saveSidebarWidth(): void {
// The width is fixed; nothing to persist.
export function saveSidebarWidth(value: number): void {
write(SIDEBAR_WIDTH_KEY, clampSidebarWidth(value));
}

export function sessionIsPinned(id: string, meta: Record<string, SessionMeta>): boolean {
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/styles/chrome.css
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@
}

.sidebar-resize-handle {
/* The sidebar is fixed-width: collapse/open only, never dragged. */
display: none;
position: absolute;
top: 0;
right: 0;
Expand Down
13 changes: 6 additions & 7 deletions apps/desktop/test/sidebar-preferences.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,11 @@ test("persists retained project paths and per-project collapse state", () => {
}
});

test("keeps the expanded sidebar at its fixed width without persistence", () => {
test("clamps and persists the expanded sidebar width", () => {
assert.equal(clampSidebarWidth(Number.NaN), SIDEBAR_WIDTH_DEFAULT);
assert.equal(clampSidebarWidth(SIDEBAR_WIDTH_MIN - 1), SIDEBAR_WIDTH_DEFAULT);
assert.equal(clampSidebarWidth(312.4), SIDEBAR_WIDTH_DEFAULT);
assert.equal(clampSidebarWidth(SIDEBAR_WIDTH_MAX + 1), SIDEBAR_WIDTH_DEFAULT);
assert.equal(clampSidebarWidth(SIDEBAR_WIDTH_MIN - 1), SIDEBAR_WIDTH_MIN);
assert.equal(clampSidebarWidth(312.4), 312);
assert.equal(clampSidebarWidth(SIDEBAR_WIDTH_MAX + 1), SIDEBAR_WIDTH_MAX);

const values = new Map();
const previousStorage = globalThis.localStorage;
Expand Down Expand Up @@ -410,10 +410,9 @@ test("keeps the expanded sidebar at its fixed width without persistence", () =>
try {
assert.equal(loadSidebarWidth(), SIDEBAR_WIDTH_DEFAULT);
saveSidebarWidth(SIDEBAR_WIDTH_MAX + 100);
assert.equal(loadSidebarWidth(), SIDEBAR_WIDTH_DEFAULT);
assert.equal(loadSidebarWidth(), SIDEBAR_WIDTH_MAX);
saveSidebarWidth(SIDEBAR_WIDTH_MIN - 100);
assert.equal(loadSidebarWidth(), SIDEBAR_WIDTH_DEFAULT);
assert.equal(values.size, 0);
assert.equal(loadSidebarWidth(), SIDEBAR_WIDTH_MIN);
} finally {
globalThis.localStorage = previousStorage;
}
Expand Down
26 changes: 15 additions & 11 deletions apps/desktop/test/sidebar-resize.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,29 @@ const sidebarSource = await readFile(
const appSource = await readAppSource();
const globalStyles = await loadStyles();

test("the sidebar keeps a fixed width and only exposes collapse/open chrome", () => {
test("the sidebar exposes an accessible pointer and keyboard resize handle", () => {
assert.match(sidebarSource, /className=\{cx\("sidebar-resize-handle no-drag"/);
assert.match(sidebarSource, /role="separator"/);
assert.match(sidebarSource, /aria-orientation="vertical"/);
assert.match(appSource, /loadSidebarWidth\(\)/);
assert.doesNotMatch(appSource, /saveSidebarWidth\(nextWidth\)/);
assert.match(
globalStyles,
/\.sidebar-resize-handle\s*\{[\s\S]*?display:\s*none;/,
);
assert.match(appSource, /saveSidebarWidth\(nextWidth\)/);
assert.match(sidebarSource, /onPointerDown=\{startSidebarResize\}/);
assert.match(sidebarSource, /onPointerMove=\{moveSidebarResize\}/);
assert.match(sidebarSource, /onPointerCancel=\{cancelSidebarResize\}/);
assert.match(sidebarSource, /onLostPointerCapture=\{cancelSidebarResize\}/);
assert.match(sidebarSource, /requestAnimationFrame\(\(\) =>/);
assert.match(sidebarSource, /event\.key === "ArrowRight"/);
assert.match(sidebarSource, /event\.key === "Home"/);
assert.match(sidebarSource, /finishSidebarResize\(true\)/);
});

test("sidebar width is shell-owned and the hidden edge has no resize affordance", () => {
test("sidebar width is shell-owned and the resize affordance is edge-anchored", () => {
assert.match(appSource, /loadSidebarWidth\(\)/);
assert.match(appSource, /saveSidebarWidth\(nextWidth\)/);
assert.match(appSource, /"--ds-sidebar-width": `\$\{sidebarWidth\}px`/);
assert.match(globalStyles, /\.sidebar\s*\{[\s\S]*?position:\s*relative/);
assert.match(
globalStyles,
/\.sidebar-resize-handle\s*\{[\s\S]*?display:\s*none;/,
);
assert.match(globalStyles, /\.sidebar-resize-handle\s*\{[\s\S]*?right:\s*0;[\s\S]*?cursor:\s*col-resize/);
assert.match(globalStyles, /\.sidebar-resize-handle\s*\{[\s\S]*?touch-action:\s*none/);
});

test("sidebar hover does not paint a full-height resize rail", () => {
Expand Down
5 changes: 3 additions & 2 deletions docs/adr/0238-three-column-width-priority.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ MainChat usable in the fixed window.
### Let the sidebar resize continuously to preserve every column

Rejected because the sidebar remains a discrete expanded/collapsed column for
this interaction. Its user-selected preferred width is not silently mutated by
window pressure.
automatic window-pressure behavior: its user-selected preferred width is not
silently mutated by layout pressure. Explicit user resizing is restored by
D434; the MainChat floor and automatic yield rules in this ADR remain in force.

### Mirror the committed panel width into native window bounds

Expand Down
5 changes: 3 additions & 2 deletions docs/spec/04-ux/01-ui-ia.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ destination, chat as the home surface, tools and permissions inline.
non-destructive pin/archive actions, an independent conversation-branch
command, and sortable views. Projects not retained in the sidebar remain
discoverable through Settings → Project archive.
Collapsible to an icon rail (Cmd/Ctrl+B). Its expanded column is fixed at
275px; persisted resize preferences from older builds are ignored.
Collapsible to an icon rail (Cmd/Ctrl+B). Its expanded column defaults to
275px and keeps a persisted 240px–520px preferred width across relaunches;
the preference is independent from the collapsed rail.
- **Product identity**: runtime shell copy uses `PI-Desktop`; the home hero and
sidebar reuse the derived `src/assets/brand/logo-*.png` marks, while composer prompt
rows have no leading brand icon and session-creation controls use a dedicated
Expand Down
12 changes: 7 additions & 5 deletions docs/spec/04-ux/07-ui-design-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,11 @@ same 6px contract and scroll-reveal mark. This keeps first-party surfaces such
as the Files view aligned with the host renderer; the external page loaded
inside the Browser guest remains page-owned and keeps its own scrollbar style.

The expanded sidebar is a fixed 275px column. Collapse/open changes only whether
the column is present; the historical resize handle is hidden and legacy width
preferences are not persisted.
The expanded sidebar defaults to 275px and keeps a persisted preferred width
between 240px and 520px. Its right-edge separator previews pointer changes and
commits the final width on release; keyboard changes commit immediately.
Collapse/open changes only whether the column is present, so the preferred
expanded width remains independent from the 48px icon rail.

The profile menu is `280px` wide, opens `8px` above the footer, and uses the
standard opaque elevated-menu surface, subtle border, and dialog shadow. Its
Expand Down Expand Up @@ -1019,12 +1021,12 @@ Codex parity decisions (D034/D070) supersede any older value here.
|---|---|---|
| Titlebar row height | 46px | Codex toolbar rhythm (D034); traffic lights {x:16,y:16} |
| Sidebar width (collapsed) | 48px | Icon-only rail |
| Sidebar width (expanded) | 275px | Fixed column; collapse/open does not resize it |
| Sidebar width (expanded) | `240px–520px` (default 275px) | Right-edge resize handle; persisted preferred width |
| Main pane minimum readable width | 450px | The MainChat hard floor; the sidebar yields before it is breached (ADR 0238) |
| Work panel width (closed) | 0px | Hidden by default |
| Work panel width (open) | `≥244px` (new-profile default 360px), capped by `client width - 450px - expanded sidebar` with no fixed pixel cap | the panel is an in-flow column whose width is taken from the existing client area; the renderer owns its divider (ADR 0033 / ADR 0151 / ADR 0238); saved widths remain unchanged |
| Composer shell minimum | ~80px | One-line draft + toolbar padding |
| Composer toolbar | MainChat `≥450px` | Left/right control groups stay on one row and do not shrink; mode/permission labels stay single-line and ellipsize |
| Composer toolbar | MainChat `≥450px` | Left/right control groups stay on one row; mode/permission labels stay single-line and ellipsize while the model chip adapts at narrower container widths |
| Composer draft height | 1–7 text lines | Auto-grow; internal scroll beyond line 7 |
| Chat message max width | 720px assistant / 560px user plate | Prevent eye-span over-stretch; user turns stay compact |
| Window min width | 1040px | Enforced by Electron for the whole app; opening the panel never changes native bounds |
Expand Down
23 changes: 14 additions & 9 deletions docs/spec/04-ux/08-component-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
> Interaction behavior: [09-interaction-patterns.md](09-interaction-patterns.md)


> Shell layout is Codex-aligned: left thread sidebar (fixed 275px), main transcript, floating bottom composer with runtime mode/permission/model controls, and a compact action-only top bar. Prefer neutral charcoal surfaces over blue-slate chrome.
> Shell layout is Codex-aligned: left thread sidebar (240px–520px, 275px
> default), main transcript, floating bottom composer with runtime
> mode/permission/model controls, and a compact action-only top bar. Prefer
> neutral charcoal surfaces over blue-slate chrome.
>
> **Precedence rule**: where a metric or copy string below disagrees with a
> Codex parity decision in [decisions-log §D](../08-meta/decisions-log.md)
> (D034+), the decision log wins — it tracks the live gold captures. Known
> updated values: sidebar 275px fixed, toolbar 46px (not 44px),
> updated values: sidebar 240px–520px (275px default), toolbar 46px (not 44px),
> composer placeholder per D094/D066, home empty stack and bottom composer per
> D111/D204/D206,
> Projects index table per D066/D133, settings full-page shell per D063 with the
Expand All @@ -30,7 +33,7 @@ Outer frame that positions Topbar, Sidebar, MainChat, and WorkPanel. Owns resize
```text
+------------------+------------------------------+------------------+
| Sidebar | MainChat | WorkPanel |
| (275px / 48px) | (flex-1) | (≥244px / dynamic|
| (240–520px / 48px) | (flex-1) | (≥244px / dynamic|
| | | hidden) |
+------------------+------------------------------+------------------+
| Titlebar row: 46px, traffic lights at {x:16,y:16} (D034/D070) |
Expand All @@ -55,9 +58,10 @@ Outer frame that positions Topbar, Sidebar, MainChat, and WorkPanel. Owns resize
`sidebar-in`, exit `sidebar-out` keyframes) that mirrors the work-panel dock:
the aside stays in the tree through the exit keyframe, then unmounts
(`is-exiting` flag + `animationend` guard, with a timeout fallback)
- Sidebar width: the expanded column is fixed at 275px. Collapse/open changes
only whether the column is present; the historical resize handle is hidden
and legacy persisted width preferences are ignored.
- Sidebar width: the expanded column defaults to 275px and is resizable from
240px to 520px through its right-edge handle. Collapse/open changes only
whether the column is present; the preferred expanded width is retained
independently from the 48px collapsed icon rail.
- Work panel collapse: the sole control is the viewport-fixed toggle in the
window's top-right corner, available on every non-Settings route whether the
panel is open or closed. It does not sit in the work-panel content header.
Expand Down Expand Up @@ -87,8 +91,9 @@ Outer frame that positions Topbar, Sidebar, MainChat, and WorkPanel. Owns resize

### 1.6 MVP constraints

- Sidebar width is fixed at 275px and remains independent from the collapsed
icon-rail state; the work panel remains adjustable from its own divider
- Sidebar width defaults to 275px, is retained independently from the collapsed
icon-rail state, and can be adjusted from 240px to 520px; the work panel
remains adjustable from its own divider
- The main pane renders one active transcript and one selected workspace while
the sidebar may retain several project tabs/groups
- Sidebar and work-panel dock transitions animate their flex allocation as well
Expand Down Expand Up @@ -394,7 +399,7 @@ visually distinct from list content.
| State | Behavior |
|---|---|
| Expanded | Full session titles visible |
| Sidebar width | Fixed at 275px; collapse/open changes only column presence |
| Sidebar width | 240px–520px (275px default); collapse/open changes only column presence |
| Collapsed | Icon rail — hover shows tooltip with session title |
| Active session | Accent-blue outlined status ring plus active row background |
| Selecting session | Destination row receives the active treatment immediately while transcript/workspace resolution continues |
Expand Down
8 changes: 5 additions & 3 deletions docs/spec/04-ux/09-interaction-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -1022,9 +1022,11 @@ Work-panel and application-window resizing are implemented in MVP:
Native pointer clicks must operate the controls and dragging empty header
space must move the window; DOM/CDP clicks alone do not establish native hit testing.

The expanded sidebar is fixed at 275px. Collapse/open changes only whether the
column is present; the historical resize handle is hidden and legacy width
preferences are ignored.
The expanded sidebar defaults to 275px and keeps a persisted preferred width
between 240px and 520px. Its right-edge separator previews pointer changes and
commits the final width on release; keyboard changes commit immediately.
Collapse/open changes only whether the column is present, so the preferred
expanded width remains independent from the 48px icon rail.

Project ordering is implemented for retained project groups. There is no
reorder grip. Pressing the project title and moving 8px starts a project drag,
Expand Down
13 changes: 7 additions & 6 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -9577,23 +9577,24 @@ This test plan spec is accepted when:
- **Expected**: The handle is discoverable on direct hover/focus without a
full-height white/accent rail when the sidebar body is hovered, has no native
window drag or text-selection side effect, and remains anchored to the press
point. MainChat follows the live width until its 515px floor. Pointer release saves one clamped
point. MainChat follows the live width until its 450px floor. Pointer release saves one clamped
preferred width; Escape/cancellation restores the starting width without
saving it. Keyboard changes commit immediately and expose localized width
semantics. The saved width survives relaunch and is restored after sidebar
collapse; collapse does not convert the preferred width into the icon-rail
width. MainChat never falls below its reserved 515px width, and the composer
width. MainChat never falls below its 450px floor, and the composer
toolbar keeps its left and right groups on one row without squeezed buttons.
Mode/permission labels remain single-line and ellipsized; no toolbar text is
vertically split or overlapped.
- **Specs linked**: `04-ux/01-ui-ia.md`, `04-ux/07-ui-design-system.md`,
`04-ux/08-component-spec.md`, `04-ux/09-interaction-patterns.md`,
ADR 0141, ADR 0226, D280, D401
ADR 0141, ADR 0238, D280, D408, D434
- **Acceptance**: A (app shell), F (persistence), Quality
- **Milestone**: M6+
- **Status**: Unit/source-contract covered (`sidebar-preferences.test.mjs`,
`sidebar-resize.test.mjs`); rendered desktop drag and relaunch journey
remains pending
- **Status**: Unit/source-contract and rendered desktop drag/bounds covered
(`sidebar-preferences.test.mjs`, `sidebar-resize.test.mjs`,
`scripts/e2e-three-column-layout.mjs`); relaunch persistence journey remains
pending

#### E2E-162: A vendor account and an AI service offer the same model picker

Expand Down
Loading
Loading