f1bdc56892
* refactor(layout): swap pane width caps for viewport-proportional runtime cap Replace the hard 350px sidebar / 450px right caps with max(800, viewport*0.7) so wide displays get a roomy file/terminal panel and the sidebar can grow with the screen. Pinned minimum is raised from 50px to 180px so panels don't collapse into unusable slivers. Caps now live in lib/state/layout-manager/caps.ts; constants module no longer exports per-column max-px constants. * feat(layout): persist preferred pane widths, flush on unload, roomier tablet split - Mirror sidebar / right widths into a sessionStorage pinned-defaults slot on every resize; seed brand-new task envs with those widths so the user's last-set sizes follow them across tasks. - Re-apply the runtime cap to every pinned group on each ResizeObserver tick so a panel pinned to the old cap grows when the user widens the window (and re-clamps when they shrink it). - Flush the debounced layout save on `beforeunload`, so a reload less than 300ms after the last drag doesn't lose the new width. - Drop the tablet right-panel inner split minSize from 30/20 to 15/15 so the files vs. terminal proportions match the loosened desktop feel. * test(e2e): cover pane resize, persistence, and edge cases Four spec files under apps/web/e2e/tests/layout/ exercise the runtime cap and pinned-defaults persistence end-to-end via dockview's `.dv-sash` drag handles and `window.__dockviewApi__`: - pane-resize-right: drag past the old 450px cap, viewport-shrink re-clamp, reload + cross-env propagation of preferred width. - pane-resize-sidebar: same coverage for the sidebar including toggle-off/on width retention. - pane-persistence-tablet: localStorage round-trip for the tablet layout keys (`task-layout-tablet-v1`, `task-layout-right-v2`). - pane-resize-edge-cases: double-click smoke, rapid drags + beforeunload flush, maximize round-trip, sash off-viewport, resize with sidebar hidden. Adds e2e/helpers/dockview-resize.ts for the shared drag + width read-out helpers. * fix(layout): cap sidebar at 30% screen width, preserve legacy initial widths - Split the runtime cap into a sidebar-specific function (max(350, vw*0.3)) and the right-pane function (max(800, vw*0.7)). File-tree / task-list content rarely benefits from more than ~30% of the screen. - Preserve legacy initial widths: getPinnedWidth now clamps the ratio-based default to the old hard limits (350 sidebar, 450 right) so a fresh task env opens at exactly the same width as before. Users can still drag past these via setConstraints — only the *initial* default is capped tightly. * fix(layout): address PR review — narrow viewport, hidden sidebar, e2e flake Bot-review fixups for #1005: - caps: bound runtime caps by `viewport - VIEWPORT_RESERVE_PX` so the 800px right-cap floor can't collapse the center column on narrow screens (Cubic P1). - applier: respect `column.maxWidth` on the sidebar too, so the `compact` preset's 260px cap is honored at runtime (Cubic P2). - dockview-store: skip the pinned-defaults mirror when the column is hidden — auto-sync infers sidebar from grid index 0, which is the center when sidebar is hidden, so an unguarded mirror would persist the center's width as the sidebar's (Greptile/Cubic P1). - trackPinnedWidths: gate the auto-sync on `sidebarVisible` / `rightPanelsVisible` for the same reason. - sizing: use `LAYOUT_RIGHT_RATIO` for non-sidebar columns instead of silently reusing the sidebar ratio (Greptile P2). - e2e: tablet viewport bumped from 1024 → 700 (≥1024 is desktop, not tablet — the old viewport never rendered the tablet layout and every assertion timed out). Tests now also verify rendered panel proportions, not just the localStorage echo (CodeRabbit #2/#3). - e2e helpers: throw on missing `.dv-sash` instead of returning `-1` (CodeRabbit #1); shared `openWideTask` extracted from three spec files (CodeRabbit #5); silent `return` in evaluate blocks replaced with `throw` for fail-fast (CodeRabbit #4). * fix(layout): swap drag-mouse e2e for programmatic resize + claude/cr followups Round 2 of PR review fixups: - e2e: convert pane-resize-right/sidebar/edge-cases specs from `page.mouse` drag-by-delta to a programmatic `splitview.resizeView()` call. The mouse-drag approach was flaky in headless CI (5 chromium failures across shards 2-4); the programmatic path tests the same cap-enforcement and persistence contract without depending on pointer-events targeting through a 1px .dv-sash overlay. - dockview-layout-setup: remove an unreachable guard in trackPinnedWidths — `rightIdx === 0 && sidebarVisible` cannot occur given the `sv.length < 2` early return (Claude). - dockview-layout-setup: cancel any in-flight debounce timer in the setupLayoutPersistence disposer so a fire can't race with teardown and persist stale layout (CodeRabbit). - constants: document why LAYOUT_SIDEBAR_RATIO and LAYOUT_RIGHT_RATIO are both 0.25 today — separate constants stay decoupled so a future tweak to one doesn't silently change the other (Claude). * fix(layout): drop cross-env width propagation — defaults stay unchanged User feedback: the initial sidebar width on a brand-new task should be the same as it was before this PR (350px on typical viewports). The plan's "propagate preferred widths across envs" feature changed that in practice: after the first resize, every new task opened at the resized width instead of the legacy default. - Remove `getPinnedDefaults` / `setPinnedDefaults` helpers and the sessionStorage `kandev.dockview.pinned-defaults` slot. - Drop the write-through in `setPinnedWidth` and the seed in `performBuildDefault`. `freshPinned` is back to `new Map()`, so a brand-new env always renders at the ratio-based default (same as before this PR, since `getPinnedWidth` already clamps the initial ratio to the legacy 350/450 hard caps). - Per-env layout persistence still works via `setEnvLayout` — when a user resizes inside a task and switches back later, the width is restored. Only the cross-env *default* is no longer carried over. - Remove e2e tests + helpers that exercised the propagation contract. - Drop the flaky `rapid resizes persist across reload` edge test — too brittle around the debounce/beforeunload race. - `LAYOUT_INITIAL_RATIO` introduced as the canonical name; `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` kept as deprecated aliases for backwards compatibility while sizing.ts dispatches. * fix(layout): bump saved-layout storage keys to v2 Layouts saved by intermediate builds of this PR captured sidebar/right widths beyond the legacy initial caps (when the larger runtime cap was the only thing constraining serialized widths). Restoring those layouts on the current build replays the over-wide widths, defeating the "defaults stay the same" guarantee. Bumping the storage key prefixes (`env-layout` → `env-layout-v2`, `env-maximize` → `env-maximize-v2`, `dockview-layout-v1` → `dockview-layout-v2`) invalidates legacy saves once on first open; subsequent opens persist + restore under v2 normally. Existing users lose stored column widths once and re-customize from defaults — a one-time cost in exchange for the upgrade being clean. * fix(layout): apply target env's widths after fast-path switch Fast-path env switch (`tryFastEnvSwitch`) skips `fromJSON` for speed, which means column widths carry over from the *outgoing* env. If a user resized the sidebar to 480px in task A and then opens task B, B inherits A's 480px sidebar — even with empty sessionStorage on different dev-server ports, because the widths live in dockview's in-memory grid state, not in storage. Explicitly resize sidebar/right after the panel swap: - saved layout exists for target env → use its serialized sizes - no saved layout (brand-new env) → compute defaults via `getPinnedWidth` (legacy initial caps: 350 sidebar / 450 right) * fix(layout): pass measured dims to fromJSON so widths don't grow on rescale Even on a fresh dev instance with no prior storage, brand-new tasks were opening with oversized sidebar/right widths. Root cause: applyLayout fed `api.width` into toSerializedDockview, but `api.width` lags the live container during the dockview mount sequence. fromJSON wrote a layout sized for the stale width; the subsequent `api.layout(safeWidth, ...)` in performBuildDefault's rAF rescaled the grid proportionally — and without the old hard cap (350/450) clamping it, the sidebar grew past its legacy default. Pass the measured container dimensions through applyLayout into toSerializedDockview so fromJSON's grid.width already matches the live container. api.layout in the rAF is then a no-op, no rescale, and sidebar/right open at exactly their legacy defaults. Also applies to applyBuiltInPreset and applyCustomLayout for the same flow. * fix(layout): lock pinned column max width to current size, unlock on drag The real reason brand-new tasks opened with oversized sidebar/right: dockview's splitview rebalances pinned columns proportionally on every `api.layout` call. After we relaxed setConstraints from the legacy 350/450 hard caps to viewport-proportional runtime caps (576/1344), nothing clamped the rebalance — sidebar grew from 350 → ~450, right from 450 → ~530 on the first ResizeObserver tick. New invariant: pinned-column `maximumWidth` is locked to the column's *current* width whenever the user is not actively dragging a sash. The rebalance has zero room to grow. - `applyLayout` / `applyLayoutFixups` set the lock as soon as fromJSON finishes — sidebar/right open at exactly their just-computed widths. - `setupContainerResizeSync` re-applies the lock on every viewport ResizeObserver tick before and after `api.layout`. - `setupSashDragCapToggle` listens for mousedown/mouseup on `.dv-sash`: on mousedown, widen the cap to the runtime viewport-proportional cap so the user can drag past the legacy default; on mouseup, snap the cap back to the new current width. Also addresses Claude's latest review: - caps.test.ts: rename "hits the 350px floor on roomy viewports" → "… when vw*0.3 falls below the minimum" (test name now matches assertion). - dockview-resize.ts: drop the stale "pinned-defaults mirror" comment; guard the sidebar splitview index against the hidden-sidebar footgun. - pane-resize-right.spec.ts: rename the reload test to mention the correct localStorage key (`dockview-layout-v2`). - new `dockview-env-switch-pinned.test.ts` unit-tests applyPinned- ColumnSizes with a spy splitview — verifies sidebar/right are explicitly resized after a fast-path env switch. * fix(layout): constrain all groups, loosen e2e resize cap, drop stale docs Round of PR-review fixes: - applier: `applyConstraintsToAllPanelGroups` replaces the previous first-only helper. The default right column has separate top + bottom groups; constraining only the first left the bottom group unbounded and let the column grow on rebalance via that group (cubic P2 + Claude regression). - sizing: collapse `LAYOUT_SIDEBAR_RATIO` / `LAYOUT_RIGHT_RATIO` dispatch — they alias the same `LAYOUT_INITIAL_RATIO`, so the ternary was a silent no-op (Claude). - sizing: doc — drop "widths seeded from pinned-defaults sessionStorage" reference; that feature was removed infb3dbe4(Claude). - e2e/dockview-resize: the latest fix locks pinned-column maxWidth to current width, which clamps `sv.resizeView` in tests. Loosen the constraint to the same runtime cap the sash-drag handler uses on user mousedown — so cap-enforcement assertions still work end-to-end without burning the 10-minute CI timeout retrying clamped resizes. * fix(layout): mirror viewportBound clamp in e2e test cap helper Claude & cubic flagged that `resizeColumnViaSplitview`'s runtime-cap computation skipped the `viewportBound(value, vw - 300)` step that production caps go through. On narrow viewports (vw < ~1143px) the helper's cap would exceed what dockview actually enforces, so cap-clamp assertions would drift from real app behaviour. Mirror the caps.ts logic exactly in the helper. * fix(layout): pin-by-target, not lock-by-current — defeats the ratchet bug My previous attempt (lock max=current width except during sash drag) was fundamentally broken: any transient container shrink would lock the cap at the smaller size, and dockview's proportional rebalance could never restore the column. Under heavy ResizeObserver fluctuation on reload, the sidebar marched all the way down to LAYOUT_PINNED_MIN_PX (180px). New invariant: pinned-column max is the *runtime cap* (loose enough for the user to drag past the legacy default), and a separate "target width" is tracked per pinned column. After every `onDidLayoutChange` event the column is force-restored to its target via `sv.resizeView`. Target is updated only by explicit actions: - `applyLayout` sets it to the computed default after `fromJSON`. - `applyLayoutFixups` sets it to the just-restored width on saved-layout restore. - `applyPinnedColumnSizes` (fast-path env switch) sets it to the saved / default width. - The sash-drag handler sets it to the new width on mouseup. `pinnedTargets` live in a new module `lib/state/layout-manager/pinned-targets.ts`. `setPinnedTarget` / `getPinnedTarget` are exported from the layout-manager barrel. An `enforcing` guard around `enforcePinnedTargets` prevents the restore-via-resizeView from re-triggering itself. E2E helper additions: - `__persistDockviewLayout__` exposed on `window` (test seam) — calls the setupLayoutPersistence `persistNow` directly. `sv.resizeView` doesn't emit `onDidLayoutChange` in dockview 4.x, so the helper now force-flushes the layout after each programmatic resize. Without this, reload tests never saw a saved layout. - `__setPinnedTarget__` exposed too so the helper can update the target after a programmatic resize, mirroring sash-drag mouseup. - Dropped the brittle `data-panel-id` width assertion in the tablet persistence test; the localStorage round-trip is the persistence contract, and `react-resizable-panels`' internal DOM is version-dependent. Local validation: all 15 layout e2e tests pass; full unit suite (2087) green; format + lint + typecheck clean. * fix(layout): pause target enforcement during sash drag + cleanup test seam Three follow-ups from the latest review round: - cubic P1: pause `enforcePinnedTargets` while the user is actively dragging a sash. Without this, an in-progress layout-change event during the drag would revert the in-flight resize before mouseup records the new target. The new `sashDragging` flag is set on mousedown over `.dv-sash` and cleared on mouseup. - claude: clear `window.__persistDockviewLayout__` in the setupLayoutPersistence disposer so a remount can't be racing with a stale closure from the previous mount. - claude: third unit test exercises the right column's saved-layout resize path (sv.length = 3 covering both pinned columns), now that the previous test bounded the loop at sv.length = 2 and only reached the sidebar. * test(e2e): bump dockview-layout-v1 → v2 in plan-panel-indicator spec The storage key was bumped from v1 → v2 infb3dbe4(drop cross-env width propagation). The plan-panel-indicator spec was waiting for the old `dockview-layout-v1` localStorage key to materialise after a debounced save — that key is never written under v2, so the waitForFunction always timed out. * fix(layout): reset sashDragging on disposer + only track left-button drags Two safety fixes for the module-scope `sashDragging` flag: - claude: clear it in the setupSashDragCapToggle disposer. If the user starts dragging a sash and unmounts mid-drag (e.g. navigates away), the flag stayed `true` forever and `enforcePinnedTargets` would never fire again on the next mount. - cubic P2: only set the flag on left-button mousedown. A right/middle click over a sash currently flipped it true, and the mouseup handler ignores non-left buttons — so the flag would stick and pause enforcement until a left mousedown→up cycle. * fix(ui): preserve session mode selector state * fix(ui): format fallback session mode labels