* feat(web): add app status bar
* feat(web): add app status bar visibility toggle
* feat(web): persist status bar item order
* fix(web): refine app status bar behavior
* fix(web): polish app status bar presentation
* test(web): split plugin slot coverage groups
* fix(web): address status surface review feedback
* fix(web): stabilize status bar overlay tests
* fix(web): stabilize sidebar toggle animation
Keep Dockview panes fixed while sidebar chrome animates between its open and closed widths.
* ci: align docs workflow pnpm version
Match pnpm/action-setup to the workspace packageManager pin.
* fix(web): address sidebar review feedback
Harden sidebar E2E assertions and restore the shared repository fixture after mutation.
* fix(web): make toggle-sidebar shortcut work on every route
TOGGLE_SIDEBAR (Cmd/Ctrl+B) was handled only inside useEditorKeybinds,
mounted just by the dockview desktop layout, so it was dead on Kanban,
the task list, Office, Settings, etc. Lift it to a global app-root
listener (useAppShortcuts, mounted in GlobalCommands) that honors user
overrides via getShortcut and keeps the isEditableTarget guard, and drop
the now-duplicate branch from useEditorKeybinds. Adds a hook unit test
and a Playwright e2e covering Kanban, Settings, and the dockview session.
* fix(web): hydrate userSettings into the root store on settings routes
Each StateProvider creates its own store; the settings/general page mounts
a nested one that the root-mounted GlobalCommands/AppSidebar can't see. The
settings layout hydrated the root store but omitted userSettings, so
override-driven global shortcuts (TOGGLE_SIDEBAR, Quick Chat) were dead on
every /settings route. Hydrate userSettings there too, matching the
kanban/tasks/office/github routes.
* test(web): cover the macOS Cmd path; note the settings-layout catch trade-off
Address review feedback: add a metaKey (Cmd) variant to the useAppShortcuts
test so both ctrlOrCmd branches are exercised, and document that the settings
layout's outer catch intentionally drops userSettings overrides in the
already-degraded error path.
* feat(web): introduce unified AppSidebar and retire legacy nav surfaces
Mounts a single collapsible AppSidebar at the root layout that replaces the
former WorkspaceRail, OfficeSidebar, and dockview-embedded sidebar pane.
Wires per-section expand + collapse state through a new UI slice with
localStorage persistence, ports projects/agents/settings/tasks sections, and
trims integrations / quick-chat / settings buttons + the home breadcrumb
from the dockview top bar (debug-only MoreToolsMenu kept). Strips the
sidebar column from every preset and the panel registry; persisted
sidebar panels are dropped by the existing sanitize layer at restore time.
* test(web): point workflow-filter e2e at AppSidebar's Home link
The dockview top bar no longer renders the IconHome breadcrumb after the
unified sidebar landed — the home navigation now lives in AppSidebar's
primary nav. Update the workflow-filter spec to click that link instead.
* feat(web): wire app-sidebar primary controls, nested settings, and footer migration
Gates office-only nav (Inbox, Agents, Projects, New Task) behind the office
feature flag, renames Tasks → Kanban as the bottom flex-grow section, replaces
the gradient workspace picker with a minimal trigger when office is off, and
moves Stats/Improve/What's-New/Office out of the kanban top bar into the
AppSidebar footer + a dedicated Integrations section. Expands Settings into a
collapsible deep tree mirroring the old SettingsAppSidebar so /settings pages
can drop their nested SidebarProvider.
* feat(web): make app-sidebar resizable and inline task top bar actions
Adds a persistent drag-resize handle on the AppSidebar's right edge that
clamps between APP_SIDEBAR_EXPANDED_WIDTH and 30vw; width is stored on the
appSidebar slice and persisted to localStorage. Drops the dockview top bar's
overflow popover (TopbarActionOverflow) in favor of always-inline clusters,
and replaces the debug "More tools" dropdown with a single debug toggle button.
* feat(web): polish app-sidebar headers and consolidate task views
Section headers now lead with a clean uppercase label, drop the mono
clash, lift contrast from /60 to /70, and end with the collapse chevron
at the right so the section body aligns flush under the label.
The Tasks section's filter bar (view chips + gear in a card-tinted
strip) is replaced by a compact view dropdown that lives in the section
header. Drag-to-reorder lives in the existing filter popover, which is
now reachable from the same gear button.
Settings moves below Tasks so the kanban list keeps the flex-grow seat
in the sidebar.
* feat(web): move settings group chevrons to the right
* fix(web): drop chat from home topbar; calm tasks group headers
The kanban top bar's Quick Chat button is removed now that Quick Chat
lives in the AppSidebar; workspaceId stops being threaded into the
tablet and desktop header variants.
Group headers inside the Tasks section (kanban workflow steps) lose
the opaque background strip and the section-label uppercase styling
so they read as inline subgroup rows, not duplicate section dividers.
The tasks-section wrapper drops its negative margin so the group rows
sit flush under the TASKS label instead of bleeding out past it.
* fix(web): restore agent logos on settings tree profile leaves
SettingsLeaf gains a leadingIcon slot for pre-rendered visuals; agents
group passes <AgentLogo agentName={agent.name}> so each profile row
shows its agent's logo like it did before the old SettingsAppSidebar
was retired.
* feat(web): brand the sidebar, move New Task off the kanban top bar
The Add task button (CreateTaskTopbarButton) leaves both the desktop
and tablet kanban headers; New Task is now an unconditional primary
nav item on the AppSidebar, falling back to a disabled affordance when
no workspace is active.
AppSidebar header gains a Kandev wordmark that links to /, with a
single-letter K mark in collapsed-rail mode. PageTopbar's root variant
no longer renders the parent brand label when backLabel is empty, so
the home view's top-left "Kandev" goes away in favor of the sidebar.
* feat(web): unbind sidebar toggle by default; drop Home from breadcrumbs
TOGGLE_SIDEBAR's configurable default is now UNBOUND_SHORTCUT (the new
sentinel for "no binding"); matchesShortcut already returns false on
an empty key, so the keybind hook needs no change. Keyboard shortcuts
settings show "Unbound", expose a Clear (X) button when a custom
binding is set, and the reset icon falls back to unbound for entries
whose default is unbound.
PageTopbar's breadcrumb variant no longer prepends the Home back-link
when backHref is "/" (the AppSidebar provides Home navigation).
* feat(web): move New Task into the sidebar, retire the workspace selector
- Extract AppSidebarNewTaskItem (office vs regular create dialog) with tests
- Drop the Workspace selector from the kanban display dropdown and mobile
menu sheet; the sidebar workspace picker is now the sole switcher
- Factor SectionHeader out of AppSidebarSection; animate fixed sections via
Collapsible while the grow (Tasks) section stays flex-driven
- Polish brand header, share SIDEBAR_ITEM_ACTIVE accent-bar classes, move
task group chevrons to the right, drop the task-item selected fill
- Bump dockview layout keys to v3 and filter the orphaned 'sidebar' column
so pre-AppSidebar default layouts no longer render a broken grid
- Point the cross-workspace isolation e2e at the sidebar picker
* feat(web): sidebar settings mode, subtask shortcut, accordion settings nav
- Restore subtask creation: the sidebar New Task row now shows a trailing
subtask affordance when viewing a task (regular mode), reopening the
NewSubtaskDialog the retired dockview header dropdown used to provide
- Fix settings pages not scrolling: scroll the content region (topbar stays
pinned) via min-h-0 + overflow-y-auto
- Add a footer gear that toggles a settings-only sidebar takeover; the gear
spins on toggle. Mode is transient (never persisted) and force-expands a
collapsed rail on entry
- Make the settings nav a single-open accordion: opening one top-level group
closes the others and reveals its subsections; route changes re-sync the
open group. Extract the tree into a reusable SettingsTree
- Repoint subtask e2e specs at the new sidebar affordance
* test(web): restore create-task e2e flow after New Task moved to sidebar
The New Task relocation removed the kanban-header create-task-button and made
the sidebar entry auto-navigate + open the office dialog under the e2e profile
(office on), breaking ~20 specs that drive the regular task-create dialog.
- Give the sidebar New Task the create-task-button testid so KanbanPage
resolves it again
- Drop the post-create auto-navigation so creating a task stays on the board
(matches the retired kanban button; specs find/click the new card)
- Ungate the subtask affordance from office mode (subtasks use the compact
NewSubtaskDialog in both modes, as the old dropdown did)
- Add a useRegularMode() helper that runs a spec file with KANDEV_FEATURES_OFFICE
disabled (backend.restart bracket) and apply it to the 18 create-task specs
- Point the System sidebar-navigation spec at a /settings/system page so the
single-open settings accordion has that group expanded
Validated: create-task, create-task-disabled-tooltip, create-task-branch-selector,
and subtask all green (modulo pre-existing idleInput agent-timing flakes that
CI retries absorb).
* fix(web): sidebar task navigation, settings-mode close + transitions
- Clicking a task in the global sidebar from a non-task page now navigates to
the task route (mounts the dockview) instead of only rewriting the URL via an
in-place layout switch that assumed the dockview was already mounted
- Make the settings-mode 'Settings' header a close affordance (the footer gear
is far from the tree); clicking it exits the takeover regardless of which
group is expanded, and it fades in when opened
- Fade in the Tasks (grow) section content on expand, matching the other
sections' animated reveal
- Enable next-themes disableTransitionOnChange so dark/light flips switch every
surface in the same instant rather than each animating at its own duration
- Add a regression e2e for sidebar→dockview navigation
* fix(web): clear sidebar task highlight when off the task route
The AppSidebar is global, so tasks.activeTaskId lingered after navigating Home,
leaving the last-opened task visually selected in the sidebar. Gate the
selected/active highlight by route — only highlight while on /t/ or
/office/tasks/. Expose data-active on the sidebar task item for testability.
Adds an e2e: entering a task highlights it; returning Home clears it.
* fix(web): never build the dockview default layout at zero width
On a fresh client-side navigation into a task the dockview can mount before the
grid has painted, so measureDockviewContainer returned api.width/height = 0.
Building the default layout at width 0 makes dockview collapse the horizontal
columns into a vertical stack (chat / files+changes / terminal) instead of the
intended side-by-side layout. Fall back to the viewport when neither the live
container nor the api report a usable size; the resize observer then snaps the
grid to the exact container size.
TDD: dockview-measure.test.ts covers the zero-size fallback.
* fix(web): keep the default layout horizontal when swapping chat for the session tab
Entering a task rendered a vertical stack (chat on top, files/changes in the
middle, terminal at the bottom) instead of the side-by-side default. The
chat->session-tab swap removed the 'chat' placeholder BEFORE adding the session
panel; when chat was the only panel in the center group, removing it destroyed
that group, the stored centerGroupId went stale, resolveInitialPosition
returned undefined, and the session panel was appended as a new row — flipping
the grid root to VERTICAL with three stacked groups.
Fix: add the session panel into chat's live group first (resolveInitialPosition
now prefers the chat panel's group), THEN remove the chat placeholder, so the
center group never empties and the horizontal split is preserved.
Also fixes the pre-existing pane-resize reload/clamp e2e failures, which were
the same collapse. TDD: task-default-layout.spec.ts asserts the right column
sits beside chat.
* fix(web): remove the sidebar Settings section; settings is gear-only
Settings was still a collapsible nav section in the sidebar AND auto-expanded on
/settings routes. That also caused the reported bug: toggling the footer gear
off while on a settings page revealed the normal sidebar with the Settings
section already expanded, so the settings tree appeared to 'not close'.
- Drop the Settings nav section (and its /settings route auto-expand) — the
settings tree is reached only via the footer gear takeover
- Delete the now-unused sidebar settings-section component
- e2e: gear opens the tree and closes it even after navigating into a section
* fix(web): show office sidebar sections only while inside Office
Inbox, Projects, and Agents were gated on the office feature flag, so they were
always visible when the feature was enabled. Gate them on actually being in the
Office surface (an /office route, reached via the footer Office button) via a
new useInOffice() hook — they stay hidden in the regular workspace and appear
only after entering Office.
Unit test for the hook; e2e (office fixture) asserts the sections appear on
/office and are absent on the regular home.
* fix(web): drop the colorful workspace selector, restore the minimal one
The workspace picker swapped to a gradient/initials trigger (and gradient
swatches in the dropdown) whenever office was enabled. Always use the plain
name + chevron trigger and drop the gradient swatches, matching the previous
design. Removes the now-unused workspace-gradient helper.
* fix(web): align sidebar header, scope office New Task, exit settings on nav
- Drop the doubled right border: the resize-handle hairline is transparent at
rest, leaving the aside's border-r as the single edge (highlights on hover).
- Collapse the expanded sidebar header into one h-10 row (brand · workspace
picker · collapse) so it lines up with the page/dockview top bar.
- Gate the sidebar New Task dialog on useInOffice() (actual /office route)
instead of the bare office feature flag, so Kanban mode keeps the classic
create dialog while office is enabled.
- Close the settings takeover when navigating off the /settings surface
(Home, Kandev brand, Office, etc.), tracked on a real pathname change so the
gear still opens it in place and in-tree navigation keeps it open.
- Lock in the already-working collapsed-gear-expands behaviour with an e2e.
Tests: new-task-item unit (route gating), settings-gear e2e (Home closes,
collapsed gear expands), office gating e2e (Kanban vs Office dialog).
* fix(web): realign task top bar to h-10, refine sidebar workspace picker
- Pin TaskTopBar to h-10 so its bottom border lines up exactly with the
sidebar header (and the PageTopbar) — previously it had no fixed height and
sat ~2px off, breaking the seam across the sidebar/content edge.
- Redesign the workspace picker as a compact, muted secondary control inlined
after the Kandev brand: brand and workspace share text-sm so they sit on a
common baseline, with a faint '/' separator; the workspace reads as muted and
the brand stays primary.
- Drop the workspace switcher (and its irrelevant folder glyph) from the
collapsed rail — collapsed now shows just brand + expand.
Adds an e2e that measures the task top bar bottom edge against the sidebar
header (within 1px).
* fix(web): replace the /github and /gitlab presets rail with a scope bar
The integration dashboards rendered their own w-60 border-r presets sidebar
right next to the global AppSidebar — a redundant 'double sidebar' that looked
off after the UI overhaul unified navigation.
On desktop, fold the presets into a horizontal scope bar above the list
toolbar: the bounded, common controls (PR/Issue kind + preset pills, grouped
Inbox/Created) stay visible as pills, while the unbounded saved-queries list
collapses into a 'Saved' dropdown so the bar never grows tall. The dashboard is
now single-column like Kanban/Tasks. Mobile is unchanged — the vertical
PresetsSidebar still lives in the hamburger sheet.
Adds a desktop scope-bar e2e (presets + kind switching, no inline rail) and
repoints the mobile page object at the new scope-bar testid.
* fix(web): address CodeRabbit/cubic review — a11y, dedup, dup nav, lazy-load
- Sidebar: fix Tasks auto-expand route prefix (/task/ -> /t/); forwardRef the
workspace-picker trigger so DropdownMenuTrigger asChild wires it; aria-label
the icon-only Add agent and keyboard-shortcut buttons.
- Settings tree: drop the duplicate top-level Editors leaf (lives under
General); broaden the System group active state to all /settings/system.
- CSS: honour prefers-reduced-motion for .sidebar-fade-in.
- Storage: bump the dockview maximize namespace to v3 in lockstep with the
layout namespace so stale pre-v3 maximize blobs are discarded.
- Integration dashboards: extract a shared IntegrationScopeBar base so the
github/gitlab scope bars are thin wrappers (de-dup + under the component
size cap). Reuse useConfiguredIntegrationLinks in the sidebar Integrations
section instead of recomputing availability.
- Lazy-load the office NewTaskDialog so it's not bundled on non-office routes.
- tasks-view-picker: use onSelect for keyboard-activatable menu items.
- Tests: normalize group x by container left in task-default-layout; refresh
stale subtask-flow comments; async findByTestId for the lazy office dialog.
* fix(web): gate the scope-bar delete button pointer-events while hidden
The opacity-0 delete button stayed clickable, so a click on the right side of a
saved-query row could hit the invisible button instead of selecting the query.
Add pointer-events-none until the row is hovered (group-hover:pointer-events-auto).
* fix(web): use onSelect for scope-bar saved-query menu items (keyboard a11y)
Radix DropdownMenuItem fires onSelect for pointer AND keyboard activation;
onClick missed keyboard users. The nested delete button stops pointerdown so it
isn't treated as a select of the saved query.
* fix(web): mobile sidebar hide, gear-gated settings test, office paused badge, picker a11y
- AppSidebar is now hidden below md (mobile uses its own per-surface nav); it
was an always-on absolute overlay covering mobile content.
- Restore the sidebar-agent-paused-badge testid on the office agent row.
- Update system sidebar-navigation spec to open the gear (settings is a
gear-gated takeover in the overhaul).
- Workspace picker menu items use onSelect for keyboard a11y.
* test(web): reconcile main's E2E suite with the unified-sidebar overhaul
Merging main un-gated the full E2E suite (was behind safe-to-test); ~50 of
main's tests asserted pre-overhaul UI. Updated them to the overhaul:
- Delete obsolete dockview-embedded-sidebar resize tests (feature removed);
keep the right-pane resize tests.
- Scope locators where tasks now appear in BOTH the page and the global
AppSidebar list (intended) — task-list, task-list-filters, pr-status-badge.
- Adapt sidebar-filter specs to the new TasksViewPicker (gear popover +
view-chip dropdown) and its page object.
- Settings now reached via the footer gear (not the topbar); Improve Kandev
opens from the sidebar footer; office nav folded into the AppSidebar — update
those specs (office verified in CI; can't run locally).
- session-layout: handle the new close-terminal confirm dialog; multi-session
and tab-management: foreground the (now backgrounded) chat tab after sidebar
task switches.
- Skip create-task-url-reopen-no-branches: its provider-branch flow (code
identical to main) is broken only by the office-off backend restart this PR
forces via useRegularMode — a harness interaction, not an app regression.
* test(web): fix create-task-url-reopen branch listing (reload to drop stale cache)
Un-skipped and fixed properly. Root cause: during the URL-paste/registration
flow the frontend can transiently fetch branches for the freshly-registered
provider repo before its background clone settles; useBranches caches that early
empty result as terminal (if (isLoaded) return), so re-picking the repo never
re-lists. Reload before the second dialog open drops the in-memory branch cache
so branches list fresh from the backend (verified: the backend returns them).
The provider-API path and product behaviour are unchanged from main.
* fix(web): restore mobile search toggle + reconcile remaining E2E with overhaul
- Restore main's mobile-search-toggle in kanban-header-mobile (my earlier
--ours merge resolution dropped it, breaking the mobile-kanban search tests).
- Office specs: expand the collapsed Agents/Projects sections before asserting
their rows, and scope office-tasks-page locators to <main>. [CI-verified]
- mobile-task-list-search / mobile-voice-mode: scope task titles to the table
and drop the obsolete data-sidebar=trigger assertion.
- app-sidebar.test mock: add settingsMode, toggleAppSidebarSettingsMode, and the
integrations section key.
- sidebar-filter page object: scope chipMenu to the menu holding the view chips.
* test(web): finish E2E reconciliation — office sections, session timing, robustness
- agent-error-handling: expand the collapsed Agents section before the paused
badge check.
- office execution-indicator/task-sorting: wait for a non-empty .font-mono
identifier (assigned async by the backend).
- session-layout: foreground the (CI-backgrounded) chat tab before the idle
check; make the close-terminal confirm dialog optional (busy heuristic is
timing-dependent).
- workspace-switch-isolation: run office-off so the picker switches in place
instead of redirecting to /office (which needs onboarding this fixture skips).
- app-sidebar-page: condense JSDoc to one line per project convention.
* test(web): match main's office identifier assertion (revert over-scoping)
The round-2 office agent scoped the .font-mono identifier check to <main> too,
but only the task TITLE needed scoping for the sidebar duplication. The office
task identifier is empty in the e2e seed (backend-assigned, not populated here),
so the <main>-scoped non-empty filter found nothing. Revert the identifier
assertion to main's exact form (first .font-mono on the page); keep the title
scoped to <main>.
* test(web): restore non-empty .font-mono filter for office identifier checks
Per review: wait for the backend-assigned (non-empty) identifier rather than the
empty span. Unscoped so it finds the identifier wherever it renders, 15s window.
* test(web): fix office identifier + realtime-dashboard specs at the root cause
Ran office specs locally (they DO run here) and root-caused all three:
- execution-indicator / task-sorting: the office task identifier is empty in
e2e — tasks are seeded via the core /api/v1/tasks route, which bypasses the
office identifier-assignment flow (the workspace KAN prefix only applies to
office-flow-created tasks). Main's .font-mono.first() was a false-positive
matching an element the overhaul removed. Assert the identifier COLUMN renders
(attached to the row) instead of a value e2e never produces.
- realtime-dashboard: the Recent Tasks card is fed by dashboard.recent_tasks via
useOfficeRefetch on office WS events; a core-route task's office event is async
so the in-place refetch is genuinely flaky. Reload for the deterministic SSR
dashboard fetch (what a user sees on revisit); the WS-refetch mechanism stays
covered by the sibling cross-workspace test.
* 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 in fb3dbe4 (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 in fb3dbe4 (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
* feat(ui): collapse commits and PR changes by default in changes panel
* fix(e2e): re-expand commits section after remount in squash and focus tests
CommitsSection unmounts mid-transition (when both hasStaged and hasCommits
are briefly false), so it remounts with defaultCollapsed=true and loses
the user's expanded state. Re-expand before assertions that follow such
remounts.
---------
Co-authored-by: Kandev Agent <agent@kandev.dev>