Commit Graph

29 Commits

Author SHA1 Message Date
Carlos Florêncio d35f36ae73 fix(web): make left sidebar width global + steady monitor-switch settling (#1140) 2026-05-30 19:16:26 +01:00
Carlos Florêncio 72441fb69a feat(web): add PR closed banner and hide CI chip on terminal state (#1161) 2026-05-30 12:54:32 +01:00
Carlos Florêncio 2a68a9b7c7 feat(web): persist PR-merged banner dismissal in sessionStorage (#1129) 2026-05-29 00:10:33 +01:00
José Almeida f1bdc56892 feat(layout): roomier pane caps + remember user-set widths (#1005)
* 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
2026-05-24 11:26:56 +01:00
João Salavisa 35242b2982 feat(web): drag-reorder subtasks within a parent task (#962)
* feat(web): drag-reorder subtasks within a parent task

Adds per-parent subtask ordering in the sidebar. Drag a subtask under its
parent to reorder; the chosen order is independent of the global sort and
persists per browser via localStorage.

- New `sidebarTaskPrefs.subtaskOrderByParentId` slice field and matching
  `setSubtaskOrder(parentId, ids)` action, persisted to localStorage under
  `kandev.sidebar.subtaskOrderByParentId`.
- `applyView` applies the per-parent override after `separateSubtasks`, so
  it never touches root order or the active sort spec.
- TaskSwitcher exposes `onReorderSubtasks(parentId, ids)`; each parent's
  subtask list lives in its own nested DndContext / SortableContext using
  the existing dnd-kit setup.
- Task-deletion cleanup strips the deleted id as both parent key and
  subtask entry from `subtaskOrderByParentId` in store + localStorage.

* refactor(web): address PR review feedback on subtask reordering

- Extract `pruneSubtaskOrder(map, taskId)` helper in `local-storage.ts`;
  use it in both `cleanupTaskStorage` and `removeTaskFromSidebarPrefs`
  to keep the two cleanup paths in lockstep.
- Move `SortableSubtaskRow`, `SortableSubtaskList`, and `useSubtaskDnd`
  into a new sibling file `task-switcher-subtask-dnd.tsx` so the
  switcher stays under the 600-line soft limit.
- `SortableSubtaskList` now short-circuits to a plain row map when
  `onReorderSubtasks` is undefined — read-only callers no longer get a
  misleading grab cursor or unused DnD scaffolding.

* fix(web): add stable keys to non-DnD subtask render path

When `onReorderSubtasks` is undefined, the early-return branch in
`SortableSubtaskList` mapped subtasks without keys, falling back to
React's index-based reconciliation. Wrap each child in a keyed
`Fragment` so reorders or size changes reconcile correctly.

* refactor(web): single-source DRAG_ACTIVATION_DISTANCE

Export the constant from `task-switcher-subtask-dnd.tsx` and import it
in `task-switcher.tsx` instead of declaring the same value twice.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-21 01:31:42 +01:00
Luan Martins 1359811187 feat(mobile): replace merged-diff view with timeline + overlay sheets on Changes tab (#902) 2026-05-14 10:27:38 +01:00
João Salavisa 5e3248a66b feat(web): pin tasks and drag-to-reorder in sidebar (#829)
Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-05 22:06:24 +01:00
João Salavisa 5f4b3ec0c0 feat(web): allow renaming quick-chat tabs locally (#830)
* feat(web): allow renaming quick-chat tabs locally

Double-click a quick-chat tab to rename it. The name is stored in
localStorage and overlays the SSR-derived task title on hydration, so
renames survive reload without touching the backend.

* fix(web): address quick-chat rename review feedback

- Drop unused sessionId prop from QuickChatTabItem
- Enter handler now blurs the input instead of calling commit(),
  preventing a duplicate commit when onBlur fires on unmount
- Add hydrator tests covering the localStorage name overlay

* fix(web): make Escape actually cancel quick-chat rename

Escape was setting isEditing(false), which unmounted the focused input,
causing onBlur (commit) to fire synchronously with the typed draft still
in the closure — committing the rename instead of discarding it.

Cancel now flips a cancelledRef and blurs the input; commit short-circuits
when the ref is set, so onBlur is the single edit-exit path for both
Enter and Escape. Adds a render test covering both flows.

* fix(web): guard quick-chat rename against IME and orphan storage writes

- Skip Enter/Escape handling while IME composition is active so candidate
  confirmation does not commit/cancel the rename
- renameQuickChatSession only writes to localStorage when the session was
  actually found in state, preventing orphan entries for stale session IDs

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-05 21:47:35 +01:00
José Almeida 45ee2ff48f feat: key terminals + dockview layout to TaskEnvironment (#755)
* feat(backend): key user shells by TaskEnvironmentID

Sessions in the same task share a TaskEnvironment. Re-key the user shell
runner so terminals are scoped per env, not per session — switching between
sessions of the same task now sees the same shell list.

- Add TaskEnvironmentID to AgentExecution / LaunchRequest / ExecutorCreateRequest
  / executor.LaunchAgentRequest, propagated through ToAgentExecution and the
  orchestrator launch path.
- Add Manager.ResolveScopeKey(sessionID) — returns env when execution carries
  it, falls back to sessionID for legacy paths.
- Rename runner param sessionID -> scopeID in interactive_shells.go; map keys
  are now scopeID:terminalID.
- Shell handlers + WS terminal handler resolve sid -> scopeID via
  ResolveScopeKey before calling the runner.

* feat(web): key dockview layout + portals to TaskEnvironmentID

Switching between sessions of the same task no longer rebuilds the dockview
layout — terminals and panels persist because layouts are env-scoped, not
session-scoped. The bottom terminal also reuses a stable sessionId across
same-env switches so it doesn't reconnect to a different shell.

- Rename layout persistence: setSessionLayout/getSessionLayout/*SessionMaximizeState
  -> setEnvLayout/getEnvLayout/*EnvMaximizeState (kandev.dockview.env-layout. prefix).
- Rename dockview-session-switch.ts -> dockview-env-switch.ts; performSessionSwitch
  -> performEnvSwitch with oldEnvId/newEnvId/activeSessionId.
- dockview-store.ts: currentLayoutSessionId -> currentLayoutEnvId; switchSessionLayout
  -> switchEnvLayout(oldEnvId, newEnvId, activeSessionId). Same-env switches no-op.
  Drop _skipLayoutSwitchForSession + consumeSkipFlag — env-keyed action handles it
  structurally.
- panel-portal-manager: PortalEntry.envId; releaseBySession -> releaseByEnv.
  ENV_SCOPED_COMPONENTS in dockview-desktop-layout (renamed from SESSION_SCOPED).
- useEnvSwitchCleanup replaces useSessionSwitchCleanup; tracks env changes only.
- All performLayoutSwitch callers (sidebar, dropdown, header, dialog) resolve
  sid -> envId via environmentIdBySessionId before switching.
- bottom-terminal-panel: drop sessionId prop, use useEnvironmentSessionId().
- cleanupTaskStorage now takes envIds[] alongside sessionIds[] for env-scoped
  layout/maximize cleanup.

* test: add coverage for env-keyed terminals + layout

Playwright e2e (terminal-env-keyed.spec.ts) — switching between sessions of
the same task keeps terminal scrollback intact and asserts both sessions
share one task_environment_id. Update the comment in
sessionless-task-switch.spec.ts to reference performEnvSwitch.

* fix: address env layout PR feedback

* test: fix flaky handshake auth check

* fix: address env scope review findings
2026-05-01 11:00:33 +01:00
Carlos Florêncio e1d21e39a9 feat(web): collapse subtasks in sidebar (#662) 2026-04-23 18:40:58 +01:00
José Almeida f6f45b5777 feat: add filter/group/sort and saved views to task sidebar (#644)
* feat(web): add sidebar view types and apply-view engine

Introduces the typed representation of a sidebar view (filter clauses,
sort spec, group key) and the pure engine that applies a view to a list
of tasks to produce a filtered, sorted, grouped result.

* feat(web): add built-in sidebar view presets

Provides the four shipped presets (All tasks, No PR reviews, Active,
Archived). The 'No PR reviews' preset filters out PR-watcher tasks that
would otherwise crowd the sidebar.

* feat(web): persist sidebar views in ui store

Adds sidebar view state to the ui slice with draft semantics for saved
views, localStorage persistence of active view + user-created views, and
root store wiring.

* feat(web): add filter dimension registry and clause editor

Registry of filterable task dimensions (status, workspace, workflow, task
type, etc.) plus the primitive UI pieces — clause editor, sort picker,
group picker, and a hook that supplies operator and value options for a
selected dimension.

* feat(web): add sidebar filter bar, popover, and view manager

Composes the filter primitives into the user-facing surfaces: the top
filter bar, the popover that hosts clause editing and sort/group
selection, the chips that switch between views, and the manager for
saving, renaming, and deleting user views.

* refactor(web): extract NewTaskButton component

Pulls the Create Task button and its command-palette registration into a
standalone memoized component in preparation for reuse across sidebar
surfaces.

* feat(web): integrate filter/view system into task sidebar

Wires the filter bar, view chips, and apply-view engine into the task
sidebar (desktop and mobile sheet). Updates task-switcher to consume the
grouped list from the engine and adds mock data for tests and previews.

* test(web): add e2e coverage for sidebar filter/view system

Page object for the filter popover plus Playwright specs covering built-in
presets, clause editing, sort/group, saved-view draft semantics, and
persistence across reloads.

* refactor(web): trim sidebar built-in views to a single seeded default

Drop the "No PR reviews", "Active", and "Archived" built-in view presets
and the isBuiltIn protection flag. The sidebar now seeds a single "All
tasks" view on first load; users can rename, overwrite, or delete it like
any other view. Deletion is guarded against removing the last remaining
view so the user can never lock themselves out.

* fix(web): humanize executor type labels in sidebar filter and grouping

The executor-type filter dropdown and group-by label were rendering raw
backend identifiers like "worktree" and "local_docker". Route both
through the existing getExecutorLabel() helper so the sidebar shows
"Worktree" and "Local Docker" like the rest of the app.

* feat(web): replace "Has PR" filter with "Is PR review"

The old dimension matched any task with an associated PR, including
tasks a user manually linked. The new dimension isolates tasks created
by the GitHub PR-review watcher specifically, which is what the filter
is useful for — hiding a wall of auto-generated review tasks.

The flag is derived from the review_watch_id metadata key the backend
already stamps onto watcher-created tasks, threaded through the kanban
snapshot and into the sidebar TaskSwitcherItem. A small migration drops
unknown dimensions (including the old "hasPR") from stored views so the
popover does not crash when reading legacy data.

* fix(web): tighten sidebar filter popover layout

Reduce the popover width (w-96 → w-[22rem]), trim section padding
(p-3 → p-2.5) and label margins (mb-2 → mb-1.5), and drop the
"No filters applied." placeholder. Extract a shared SectionLabel to
keep the three section headers visually aligned.

* feat(web): show workflow step color dot in sidebar filter dropdown

Surface the step color on the workflow-step filter's value dropdown so
each option renders with the same coloured circle used elsewhere (task
switcher, kanban column, command panel). Adds an optional color field
on filter value options; only workflow-step options populate it today.

* feat(web): replace cramped in/not-in filter editor with a multiselect dropdown

The previous "in/not in" value input rendered selected values as tiny
10px pills inside a fixed-height row that overflowed and truncated as
the list of options grew. Replace it with a combobox-style multiselect
(Popover + Command) that shows a compact summary ("N selected" or the
single label with its color dot) and opens a searchable, scrollable
list with check indicators on click.

* fix(web): show workflow and step names (not UUIDs) in sidebar grouping

Group-by Workflow and Workflow Step rendered raw UUID strings as group
headers because `TaskSwitcherItem` did not carry the resolved names.
Pass `workflowName` and `workflowStepTitle` through `toSidebarItem` and
prefer them in the group extractors.

* fix(web): drop redundant true/false selector on boolean sidebar filters

Boolean dimensions (PR review, Archived, Has diff) previously exposed
both an operator (is / is not) and a value selector (true / false),
which gives four user inputs for two real states. Hide the value input
for boolean clauses and always store `true`; negation is expressed via
the operator. Rename "Is PR review" to "PR review" since the dimension
label no longer needs to read as a question.

* refactor(web): drop Sort by Repository and relabel State to Status

Sorting by repository duplicates what Group by Repository already
offers, and "State" leaks a backend term into the picker. Remove the
repository sort key (comparator, type, picker option) and rename the
state option to "Status". Stored views referencing the removed key are
migrated back to `state` at load time so the popover does not crash.

* fix(web): tighten sidebar filter popover section spacing

The FILTERS, SORT, and GROUP BY section labels each sat above their
controls with both padded containers (p-2.5) and a 6px bottom margin,
leaving visible dead space above each header. Reduce container padding
to p-2 and label margin to mb-1 so sections read as a compact stack.

* chore(web): apply prettier formatting drift in sidebar-filter area

* revert(web): undo store.ts prettier reformat to stay under max-lines cap

The packed single-line UIA declaration in store.ts is load-bearing for
the 600-line ESLint cap: the previous chore commit unpacked it to 9
lines and pushed the file to 609 lines, turning the max-lines warning
into a lint failure. Restore the packed layout (the /* prettier-ignore */
comment is there for exactly this reason).

* refactor(web): label boolean filter ops as Show/Hide instead of is/is not

* fix(web): pull section labels flush to sidebar filter popover borders

* refactor(web): inline rename and save-as into sidebar view header row

* refactor(web): split ViewHeaderRow into mode, display, and action subcomponents

* feat(backend): persist sidebar views in user settings

Adds a SidebarViews field to UserSettings (stored in the existing
users.settings JSON blob) plus validation in the service layer:
views are capped at 50, ids and names must be non-empty and unique.

* feat(web): hydrate and sync sidebar views to backend

Moves the source of truth for saved sidebar views from localStorage
to the user settings endpoint. Views hydrate from SSR on first load;
create/rename/overwrite/delete/duplicate/toggle-collapsed all PATCH
the backend with optimistic updates and revert-on-failure. The
active view id and draft remain per-device in localStorage. A
sync bridge surfaces PATCH errors as toasts and runs a one-time
migration to upload any pre-existing local views.

As a side effect, localStorage writes and PATCH side effects are
moved out of immer producers to avoid proxy-serialization risk.

* chore(web): apply prettier to store.ts sidebar actions block

* fix(web): run migrateView on SSR-hydrated sidebar views

Defensively drop clauses with unknown dimensions and reset stale sort
keys when views come from the backend, matching the localStorage load
path. Also notes the default-state module and sidebar views in the
store structure docs.

* docs: note default-state module and sidebar views in store structure

* fix(web): type sidebar view storage wrappers properly

Drop the as-unknown-as-SidebarView and as-never casts at call sites by
making the local-storage getters/setters accept the domain type and do
the JsonValue cast once inside the wrapper.

* refactor(web): stop PATCHing on sidebar group collapse toggles

collapsedGroups is per-device visual state; routing it through
mutateViews made every expand/collapse click fire a user-settings
PATCH. Keep it local (in-memory + localStorage cache); the next real
view mutation picks up whatever state is current.

* fix(web): skip sidebar view migration when settings hydrated from server

After SSR hydration the in-memory views are already the backend's
truth, so migrating re-POSTs the same data every page load. Gate the
one-shot migration on userSettings.loaded — only run if the server
didn't answer and the slice fell back to localStorage.
2026-04-20 13:44:24 +01:00
Carlos Florêncio 4b55a69145 feat(web): vscode-style preview tabs for files, diffs, and commits (#622) 2026-04-19 17:39:07 +01:00
Carlos Florêncio 52d6f7e9bf fix(web): persist PR panel dismissal across page refreshes (#547) 2026-04-06 13:52:53 +01:00
Carlos Florêncio fb5886600a feat(web): add markdown preview mode and PR screenshot capture (#461) 2026-03-24 23:38:02 +00:00
João Salavisa b1c848ed80 fix(ui): persist task creation draft when modal closes (#455) 2026-03-23 21:16:41 +00:00
Carlos Florêncio bb1caaba11 feat: add ACP agent variants for Claude, Codex, Copilot, and Amp (#387) 2026-03-10 17:30:04 +00:00
Carlos Florêncio 76c1dcce89 fix: sidebar task switcher shows outdated session state and custom maximize layout (#345) 2026-03-02 14:12:11 +00:00
Carlos Florêncio 61ef224ec7 style: fix format issues (#255) 2026-02-19 22:18:01 +00:00
Carlos Florêncio 160dba0aaf feat: improve comment system (#247) 2026-02-18 19:52:40 +00:00
Carlos Florêncio f4d1dfd3a7 feat: better context files (#222)
feat: context files
2026-02-14 20:13:11 +00:00
Carlos Florêncio 813bfd1215 feat: dockview new ui (#219) 2026-02-13 00:00:04 +00:00
Carlos Florêncio b22e66750c feat: improved file tree (#164)
feat: improved editor
2026-02-03 04:14:07 +00:00
Carlos Florêncio e474c0c0b5 feat: custom commands (#162) 2026-02-03 01:56:56 +00:00
Carlos Florêncio ed6c6e8d1b fix: show approve button wrongly + improve plan ux (#158) 2026-02-02 22:45:35 +00:00
João Salavisa d951b9cb47 Fix: Persist plan notification state across page refreshes (#150) 2026-02-02 01:28:07 +00:00
Carlos Florêncio 6ec8b31e6b feat: kanban page refactoring (#52)
* feat: kanban page refactoring

* feat: remove mds
2026-01-20 02:02:34 +00:00
Carlos Florêncio 0ac6a87132 feat: preview panel option (#49)
* feat: preview panel option

* feat: improved card navigation
2026-01-19 23:49:06 +00:00
Carlos Florêncio 84252719b4 feat: right panel overlay 2026-01-15 18:27:05 +00:00
Carlos Florêncio a797335ecd feat: improved task page 2026-01-10 23:17:26 +00:00