Skip to content

feat: cmd+click and shift+click multi-select for kanban and sidebar#1518

Merged
zeval merged 19 commits into
mainfrom
feature/cmd-click-support-si-iwd
Jul 6, 2026
Merged

feat: cmd+click and shift+click multi-select for kanban and sidebar#1518
zeval merged 19 commits into
mainfrom
feature/cmd-click-support-si-iwd

Conversation

@zeval

@zeval zeval commented Jun 28, 2026

Copy link
Copy Markdown
Member

Multi-select previously required clicking a "Multi-select" toggle on the kanban board first and didn't exist at all in the sidebar task list — selecting several tasks to move or archive was slow and inconsistent. Cmd/Ctrl+click and Shift+click now drive selection natively in both surfaces.

Important Changes

  • Shared selection reducer gains an anchor + select_range action so kanban and sidebar use one range algorithm.
  • Kanban: Cmd/Ctrl+click toggles a card and Shift+click range-selects within a column (no toggle button needed; button kept). Plain click still toggles while in multi-select mode.
  • Sidebar: new cmd/shift multi-select with a selection-aware right-click menu — Archive/Move act on the whole selection; plain click navigates only when nothing is selected; Escape clears.

Validation

  • pnpm --filter @kandev/web typecheck && pnpm --filter @kandev/web lint (clean, --max-warnings 0)
  • pnpm --filter @kandev/web test — 3808 unit tests pass (reducer range + sidebar flatten covered)
  • make test-e2e (Playwright, chromium) — 14 new e2e tests pass across e2e/tests/kanban/cmd-shift-multi-select.spec.ts and e2e/tests/task/sidebar-multi-select.spec.ts

Possible Improvements

Low risk. Pressing Escape to dismiss a context menu also clears the active selection — acceptable but could be scoped to ignore Escape while a menu/dialog is open.

Checklist

  • I have performed a self-review of my code.
  • I have manually tested my changes and they work as expected.
  • My changes have tests that cover the new functionality and edge cases.
  • If my change touches UI files (apps/web/), I have added or updated Playwright e2e tests in apps/web/e2e/ and verified them with make test-e2e.

Review in cubic

@coderabbitai

coderabbitai Bot commented Jun 28, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds anchor-based range selection for Kanban and sidebar task rows, threads selection props through the UI layers, and adds sidebar bulk archive/move selection state with updated context-menu behavior and E2E coverage.

Changes

Modifier-key multi-select and bulk actions

Layer / File(s) Summary
anchorId and range selection
apps/web/hooks/use-task-multi-select.ts, apps/web/hooks/use-task-multi-select-range.test.ts, apps/web/hooks/use-task-multi-select.test.ts
useTaskMultiSelect adds anchorId, select_range, anchor realignment, selectRange, and display-order sorting for bulk moves; reducer tests cover range selection and anchor updates.
Visible sidebar row order
apps/web/lib/sidebar/apply-view.ts, apps/web/lib/sidebar/apply-view-flatten.test.ts
flattenVisibleTaskIds walks grouped sidebar rows in render order, sortIdsByVisibleOrder reorders ids by visible order, and tests cover traversal and ordering rules.
Kanban range-select threading
apps/web/components/kanban-card-content.tsx, apps/web/components/kanban-card.tsx, apps/web/components/kanban-column.tsx, apps/web/components/kanban/swipeable-columns.tsx, apps/web/components/kanban/swimlane-kanban-content.tsx, apps/web/components/kanban/swimlane-container.tsx, apps/web/components/kanban-board.tsx, apps/web/lib/kanban/view-registry.ts, apps/web/components/kanban-card-click.test.ts, apps/web/e2e/pages/kanban-page.ts, apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
KanbanCard, KanbanColumn, SwipeableColumns, SwimlaneKanbanContent, SwimlaneContainer, ViewContentProps, and KanbanBoard add onSelectRange/onRangeSelect wiring, column task ordering, and modifier-click dispatch; card click tests and Kanban E2E coverage verify Cmd, Ctrl, Shift, and plain-click routing.
Sidebar row click wiring
apps/web/components/task/task-item.tsx, apps/web/components/task/task-switcher.tsx, apps/web/components/task/task-switcher-click.test.ts
TaskItem adds multi-selected styling and mouse/keyboard select handling; TaskSwitcher adds modifier-aware row click dispatch, multi-select props, and recursive threading through grouped rows; the click-dispatch tests cover the new behaviors.
Sidebar selection and bulk archive
apps/web/hooks/use-sidebar-multi-select.ts, apps/web/components/task/task-session-sidebar-selection.tsx, apps/web/components/task/task-session-sidebar.tsx, apps/web/e2e/pages/sidebar-tasks-page.ts, apps/web/e2e/tests/task/sidebar-multi-select.spec.ts
useSidebarMultiSelect adds workspace-reset selection state with bulk archive and bulk move; useSidebarSelection builds visible row order, mixed-workflow state, escape handling, and bulk archive dialog state; SidebarBulkArchiveDialog renders the confirmation dialog; task-session-sidebar wires the selection model into the sidebar; sidebar E2E coverage exercises multi-select and bulk archive flows.
Selection-aware context menu
apps/web/components/task/task-switcher-context-menu.tsx
TaskItemWithContextMenu and TaskContextMenuItems add selection-aware archive and move actions, compute acting IDs from the current selection, render bulk archive labels, and disable mixed-workflow step moves.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • kdlbs/kandev#573: Shares the Kanban multi-selection plumbing that this PR extends with Shift-based range selection.
  • kdlbs/kandev#492: Related through the sidebar task switcher move-flow code path that this PR extends for bulk move and selection-aware context-menu behavior.

Poem

🐇 I hop by rows with Ctrl and Shift,
A selection ring makes the whole board drift.
One range, one click, the bunny’s tale is true,
Bulk archiving hums in the menu too.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: native cmd/ctrl and shift multi-select for kanban and sidebar.
Description check ✅ Passed The description matches the template well, with the required summary, changes, validation, improvements, and checklist sections.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/cmd-click-support-si-iwd

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 3m 18s —— View job


Code Review

  • Gather context (PR diff, changed files)
  • Review tests and verification
  • Review implementation for issues (security, architecture, logic, performance, complexity, quality)
  • Post findings report and inline comments

Reviewed the full diff against 4d19fb3 (the commit immediately preceding this PR's commits, confirmed to match the PR's reported stats: 23 files, +1200/-62). Note: I was unable to run pnpm typecheck/lint/test in this sandbox (command execution requires approval I don't have in reviewer mode), so the below is based on reading the code and tests directly, not re-running CI.

Findings

Suggestion (recommended, doesn't block)

  1. Sidebar bulk archive silently drops failures and clears selection regardlessapps/web/hooks/use-sidebar-multi-select.ts:48-62

    • Issue: bulkArchive uses Promise.allSettled, only removes succeeded ids from the store, and then unconditionally calls clearSelection() in finally — even when some tasks failed. archiveTaskByIdarchiveTask() is a bare API call with no toast/error surface of its own.
    • Why: On partial failure, the user sees the selection vanish with no indication which task(s) failed to archive and no path to retry, since the failed ids are gone from selectedIds too.
    • Fix: Mirror the kanban useTaskMultiSelect's runBulk (apps/web/hooks/use-task-multi-select.ts:111-133), which keeps failed ids selected via setSelectedIds(failed) instead of clearing unconditionally, and/or surface a toast when results contains rejections. Posted inline.
  2. bulkArchiveState.executorTypes lookup is silent on a stale IDapps/web/components/task/task-session-sidebar-selection.tsx:47-53 — low confidence, not flagged inline since it's an edge case (a task removed from displayTasks between selection and right-click) that the byId.get(id) ?? null fallback already handles gracefully; mentioning only as a non-issue I considered.

Summary

Severity Count
Blocker 0
Suggestion 1

Verdict: Ready with suggestions

Highlights of what's solid:

  • The shared multiSelectReducer + anchor/select_range design is clean and well-tested (use-task-multi-select-range.test.ts covers forward/backward range, union-with-existing-selection, and the "anchor in another column" fallback).
  • Modifier-key dispatch (cmd/ctrl toggle, shift range, plain-click-while-selecting) is consistently implemented across kanban (kanban-card.tsx) and sidebar (task-switcher.tsx), with matching e2e coverage in both cmd-shift-multi-select.spec.ts and sidebar-multi-select.spec.ts.
  • flattenVisibleTaskIds correctly walks the exact rendered order (groups → collapsed-state → subtasks → collapsed-subtask-state), which is what shift-range selection needs to stay visually correct — and it's unit tested.
  • The selection-aware context menu (single vs. bulk archive/move, mixed-workflow gating on "Move to step") is well-scoped and the e2e tests assert both branches (right-clicking a selected row vs. an unselected row).
  • No security, architecture, or complexity-limit concerns found; all touched files stay within the 600-line/100-line-function TS limits.

Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/hooks/use-sidebar-multi-select.ts
Comment thread apps/web/hooks/use-sidebar-multi-select.ts
Comment thread apps/web/hooks/use-task-multi-select-range.test.ts Outdated
Comment thread apps/web/components/kanban-card.tsx Outdated
@greptile-apps

greptile-apps Bot commented Jun 28, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds cmd/ctrl+click and shift+click multi-select to both the kanban board and the sidebar task list. A shared selection reducer gains an anchorId field and a select_range action so both surfaces use a single range-selection algorithm; the new useSidebarMultiSelect hook wires the reducer to the existing bulk-action infrastructure with workspace-scoped reset, prune-to-visible pruning, and clear-on-success semantics.

  • Reducer / state: multiSelectReducer gains select_range, anchor tracking, and realignAnchor for partial-failure recovery; INITIAL_STATE is exported so the sidebar hook can reuse it.
  • Kanban: dispatchKanbanCardClick handles the four click modes (cmd, shift, multi-select plain, default); column task IDs are passed as the ordered list for per-column range selection; sortByDisplayOrder ensures backward range selections land in board order at the destination.
  • Sidebar: useSidebarSelection composes visible-order flattening, mixed-workflow detection, and the bulk confirm dialogs; TaskSwitcher receives the full selection prop bundle and routes every right-click to either the bulk menu or the single-task menu depending on whether the right-clicked row is in the active selection.

Confidence Score: 4/5

Safe to merge for the new sidebar and kanban shift/cmd-click paths; the kanban context-menu Archive and Delete actions remain single-task-only while Move is now selection-aware, an asymmetry already documented in the review thread.

All previously flagged issues in the review thread are fixed in this iteration. The new range-selection, prune-to-visible, mixed-workflow detection, and bulk confirm dialog code is correct and well-tested. The one outstanding gap — kanban context-menu Archive/Delete not acting on the full selection while Move does — was flagged in the previous review cycle and remains open.

apps/web/components/kanban-card.tsx — the context-menu Archive and Delete handlers still open single-task confirm dialogs; the fix is tracked from the previous review cycle.

Important Files Changed

Filename Overview
apps/web/hooks/use-task-multi-select.ts Adds anchor tracking, select_range action, and realignAnchor to the shared reducer; exports INITIAL_STATE and useTaskMultiSelectStore for sidebar reuse; adds sortByDisplayOrder for board-order bulk moves. Well-tested and logically sound.
apps/web/hooks/use-sidebar-multi-select.ts New sidebar multi-select hook reusing the shared reducer; runBulkRemoval correctly handles active-task routing last, keeps failed IDs selected for retry, and clears selection on full success. bulkMove catches errors to prevent unhandled rejections.
apps/web/components/task/task-session-sidebar-selection.tsx Composes sidebar selection lifecycle (Escape-to-clear, prune-to-visible effects), memoized range/move/pin callbacks (useCallback deps correct), and bulk confirm dialogs. onSelectTaskRange is wrapped in useCallback([selectRange, visibleTaskIds]) so TaskSwitcher memo holds.
apps/web/components/task/task-switcher-context-menu.tsx Right-click routing: non-selected rows get single-task menu; selected rows route to bulk or single-selected menu correctly. Archive for a lone selected row uses the bulk path (clears selection). withClear ensures Pin and Delete also clear selection on a single-row selection.
apps/web/components/kanban-card.tsx dispatchKanbanCardClick cleanly handles four click modes; Move is now selection-aware via orderedSelectedIds(). Archive and Delete in the kanban context menu (via menuBase) still open single-task confirm dialogs regardless of selection size — a pre-existing gap flagged in the previous review cycle.
apps/web/components/kanban-column.tsx Adds columnTaskIds memo and wires onSelectRange to onRangeSelect per card correctly; per-column ordered list is the right source of truth for shift-click range selection.
apps/web/components/task/task-switcher.tsx dispatchSidebarRowClick exported for unit testing; prop drilling for selection handlers is mechanical but necessary given the tree depth. TaskRow correctly derives isMultiSelected and isSelecting from the prop Set.
apps/web/lib/sidebar/apply-view.ts Three new pure functions (flattenVisibleTaskIds, sortIdsByVisibleOrder, computeMixedWorkflowSelection) are all well-tested; flattenVisibleTaskIds correctly handles collapsed groups and subtask parents depth-first.
apps/web/lib/state/slices/ui/sidebar-task-prefs-actions.ts pinTasks and unpinTasks follow the same Immer + setStoredPinnedTaskIds + syncSidebarTaskPrefs pattern as the existing togglePinnedTask; the changed flag correctly gates the sync call.
apps/web/components/task/task-item.tsx Adds isMultiSelected styling (ring + lighter bg) and onSelect handler; keyboard events (Enter/Space) correctly delegate to the modifier-aware path. Helper functions cleanly extracted.

Reviews (15): Last reviewed commit: "test: restore sidebar selection console ..." | Re-trigger Greptile

Comment thread apps/web/components/task/task-switcher-context-menu.tsx
Comment thread apps/web/components/task/task-session-sidebar-selection.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/components/task/task-item.tsx`:
- Around line 127-128: The row highlight classes in task-item.tsx conflict
inside the cn(...) call, causing the active state from isSelected to be
overridden when isMultiSelected is also true. Update the class composition in
the task row component so the active-task highlight remains visible in the
combined selected-and-multi-selected state, using the existing isSelected and
isMultiSelected conditions without letting twMerge drop the stronger background.

In `@apps/web/components/task/task-switcher.tsx`:
- Around line 96-107: The modifier-click branches in TaskSwitcher are not truly
optional-safe because they call preventDefault and return even when
onToggleSelectTask or onSelectTaskRange is absent, which blocks the normal
selection fallback. Update the click handling in TaskSwitcher’s onSelect logic
so Cmd/Ctrl-click and Shift-click only intercept when the corresponding handler
exists; otherwise let the event continue to the regular onSelectTask path used
by TaskRow. Keep the existing modifier detection, but gate the
preventDefault/return behavior on handler presence.

In `@apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts`:
- Around line 13-32: The DOM-order helpers are currently querying
`kanban-column-*` directly from `page`, which can pick up hidden or stale
mounted boards and break the selection assertions. Re-anchor `columnTaskIds()`
and `columnSelectedIds()` to the active Kanban container/page-object root before
calling `getByTestId()` and evaluating card order or `ring-primary` classes. Use
the existing `page`/container access pattern in this spec so these helpers only
inspect the visible active board.
- Around line 174-189: This test only verifies that the multi-select toggle
enables the mode, but it never exercises the Cmd/Ctrl-click coexistence path in
cmd-shift-multi-select.spec.ts. Update the test around
KanbanPage.multiSelectToggle and the task card interaction so it first enables
the toggle and then performs a Cmd/Ctrl-click on a task card via
testPage/kanban, asserting that both toggle-based multi-select and
modifier-click selection still work together.

In `@apps/web/e2e/tests/task/sidebar-multi-select.spec.ts`:
- Around line 29-41: The sidebar E2E helpers are too loosely scoped because
sidebarOrder() uses a global page selector and openSidebarOn() discards the
validated session.sidebar locator, which can cause stale or hidden panels to be
queried in dock/mobile layouts. Update the helper flow in sidebarOrder() and
openSidebarOn() so they accept and use the active session.sidebar container for
both ordering reads and row interactions, keeping all sidebar assertions
anchored to the same mounted panel.

In `@apps/web/hooks/use-sidebar-multi-select.ts`:
- Around line 53-58: The bulk-archive flow in use-sidebar-multi-select’s
bulkArchive swallows request failures because it uses Promise.allSettled and
then always clears selection in the finally block. Change the callback to
surface any failed archiveTaskById calls back to the caller (so the dialog can
show an error), and only clear the selection after a fully successful run; keep
partial successes handling via removeTasksFromStore but preserve failed ids for
retry.

In `@apps/web/hooks/use-task-multi-select.ts`:
- Around line 240-245: The `set_selected` branch in `useTaskMultiSelect` leaves
`anchorId` unchanged for non-empty replacement selections, which can leave it
pointing at an item no longer in `selectedIds`. Update this branch so the anchor
is always realigned to the current selection when `action.ids` replaces state,
using an existing selected id or clearing it when appropriate. Make the change
in the reducer logic that handles `set_selected` so Shift+click range extension
continues from a valid anchor after partial bulk failures or other selection
replacements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 201156cb-11b2-4e4c-9bfa-9e82b7cc0631

📥 Commits

Reviewing files that changed from the base of the PR and between adba21e and c8b6abe.

📒 Files selected for processing (23)
  • apps/web/components/kanban-board.tsx
  • apps/web/components/kanban-card-content.tsx
  • apps/web/components/kanban-card.tsx
  • apps/web/components/kanban-column.tsx
  • apps/web/components/kanban/swimlane-container.tsx
  • apps/web/components/kanban/swimlane-kanban-content.tsx
  • apps/web/components/kanban/swipeable-columns.tsx
  • apps/web/components/task/task-item.tsx
  • apps/web/components/task/task-session-sidebar-selection.tsx
  • apps/web/components/task/task-session-sidebar.tsx
  • apps/web/components/task/task-switcher-context-menu.tsx
  • apps/web/components/task/task-switcher.tsx
  • apps/web/e2e/pages/kanban-page.ts
  • apps/web/e2e/pages/sidebar-tasks-page.ts
  • apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
  • apps/web/e2e/tests/task/sidebar-multi-select.spec.ts
  • apps/web/hooks/use-sidebar-multi-select.ts
  • apps/web/hooks/use-task-multi-select-range.test.ts
  • apps/web/hooks/use-task-multi-select.test.ts
  • apps/web/hooks/use-task-multi-select.ts
  • apps/web/lib/kanban/view-registry.ts
  • apps/web/lib/sidebar/apply-view-flatten.test.ts
  • apps/web/lib/sidebar/apply-view.ts

Comment thread apps/web/components/task/task-item.tsx Outdated
Comment thread apps/web/components/task/task-switcher.tsx Outdated
Comment thread apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
Comment thread apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
Comment thread apps/web/e2e/tests/task/sidebar-multi-select.spec.ts
Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/hooks/use-task-multi-select.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8b6abe177

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/components/task/task-switcher-context-menu.tsx Outdated
Comment thread apps/web/hooks/use-task-multi-select.ts Outdated
Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/components/task/task-switcher.tsx Outdated
Comment thread apps/web/components/task/task-switcher-context-menu.tsx Outdated
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 3m 52s —— View job


Claude Code Review

  • Gather context (PR diff, prior review feedback, changed files)
  • Review tests and verification
  • Review implementation for issues (security, architecture, logic, performance, complexity, quality)
  • Post findings report and inline comments

Posted a findings report and one inline comment.

Summary: The latest push (87e51cc, 1a52ec7) cleaned up essentially everything flagged by the earlier CodeRabbit/Greptile/OpenCode/Codex review passes — partial-archive-failure handling, active-task-archive routing, bulk-move selection clearing, anchor realignment, optional-handler safety, the conflicting CSS classes, the misleading test name, and e2e container scoping. One remaining blocker: apps/web/hooks/use-sidebar-multi-select.ts still has no unit test despite now containing meaningfully branching logic (active-task handling, partial-failure recovery, toast). One low-confidence suggestion: sidebar bulkMove doesn't distinguish a partial server-side move (moved_count < requested) from full success.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
apps/web/components/task/task-switcher-click.test.ts (1)

22-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the remaining modifier-routing branches.

dispatchSidebarRowClick also special-cases ctrlKey and the “selection handlers absent” fallback in apps/web/components/task/task-switcher.tsx:88-116. This suite only exercises metaKey with all handlers wired, so Windows/Linux Ctrl+click and the mobile fallback path can regress unnoticed.

Suggested additions
 describe("dispatchSidebarRowClick", () => {
   it("cmd/meta-click toggles and prevents default", () => {
     const h = handlers();
     const e = fakeEvent({ metaKey: true });
     dispatchSidebarRowClick(e, "t1", false, h);
     expect(e.preventDefault).toHaveBeenCalled();
     expect(h.onToggleSelectTask).toHaveBeenCalledWith("t1");
     expect(h.onSelectTask).not.toHaveBeenCalled();
   });

+  it("ctrl-click toggles and prevents default", () => {
+    const h = handlers();
+    const e = fakeEvent({ ctrlKey: true });
+    dispatchSidebarRowClick(e, "t1", false, h);
+    expect(e.preventDefault).toHaveBeenCalled();
+    expect(h.onToggleSelectTask).toHaveBeenCalledWith("t1");
+    expect(h.onSelectTask).not.toHaveBeenCalled();
+  });
+
   it("shift-click range-selects and prevents default", () => {
     const h = handlers();
     const e = fakeEvent({ shiftKey: true });
     dispatchSidebarRowClick(e, "t1", false, h);
     expect(e.preventDefault).toHaveBeenCalled();
     expect(h.onSelectTaskRange).toHaveBeenCalledWith("t1");
   });
+
+  it("modifier clicks fall back to navigation when selection handlers are absent", () => {
+    const onSelectTask = vi.fn();
+    dispatchSidebarRowClick(fakeEvent({ metaKey: true }), "t1", false, { onSelectTask });
+    expect(onSelectTask).toHaveBeenCalledWith("t1");
+  });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/task/task-switcher-click.test.ts` around lines 22 - 53,
The dispatchSidebarRowClick test suite is missing coverage for the ctrlKey
branch and the fallback path when selection handlers are absent. Add cases
around dispatchSidebarRowClick in task-switcher-click.test.ts to verify Ctrl/Cmd
modifier routing behaves the same as metaKey, and that the function falls back
correctly when onToggleSelectTask, onSelectTaskRange, or other selection
handlers are unavailable. Use the existing fakeEvent and handlers helpers to
exercise these remaining branches.
apps/web/hooks/use-task-multi-select-range.test.ts (1)

33-41: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert anchorId in the backward-range case too.

This case only checks selectedIds. If reverse select_range ever re-anchors to the clicked task, this test would still pass even though multiSelectReducer is supposed to preserve the original anchor.

Suggested test assertion
   expect(next.selectedIds).toEqual(new Set(["b", "c", "d"]));
+  expect(next.anchorId).toBe("d");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/hooks/use-task-multi-select-range.test.ts` around lines 33 - 41, The
backward-range test for multiSelectReducer only verifies selectedIds and misses
the anchor behavior. Update the select_range test that starts from the
toggle_select anchor on "d" to also assert next.anchorId remains the original
anchor rather than changing to the clicked task. Use the existing
multiSelectReducer and anchor-related state fields in
use-task-multi-select-range.test.ts to keep the intent explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/components/task/task-switcher-click.test.ts`:
- Around line 22-53: The dispatchSidebarRowClick test suite is missing coverage
for the ctrlKey branch and the fallback path when selection handlers are absent.
Add cases around dispatchSidebarRowClick in task-switcher-click.test.ts to
verify Ctrl/Cmd modifier routing behaves the same as metaKey, and that the
function falls back correctly when onToggleSelectTask, onSelectTaskRange, or
other selection handlers are unavailable. Use the existing fakeEvent and
handlers helpers to exercise these remaining branches.

In `@apps/web/hooks/use-task-multi-select-range.test.ts`:
- Around line 33-41: The backward-range test for multiSelectReducer only
verifies selectedIds and misses the anchor behavior. Update the select_range
test that starts from the toggle_select anchor on "d" to also assert
next.anchorId remains the original anchor rather than changing to the clicked
task. Use the existing multiSelectReducer and anchor-related state fields in
use-task-multi-select-range.test.ts to keep the intent explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39bff29a-ba6b-4bb6-94c2-10d8ec795dac

📥 Commits

Reviewing files that changed from the base of the PR and between c8b6abe and 1a52ec7.

📒 Files selected for processing (13)
  • apps/web/components/kanban-card-click.test.ts
  • apps/web/components/kanban-card.tsx
  • apps/web/components/task/task-item.tsx
  • apps/web/components/task/task-session-sidebar-selection.tsx
  • apps/web/components/task/task-switcher-click.test.ts
  • apps/web/components/task/task-switcher-context-menu.tsx
  • apps/web/components/task/task-switcher.tsx
  • apps/web/e2e/pages/sidebar-tasks-page.ts
  • apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
  • apps/web/e2e/tests/task/sidebar-multi-select.spec.ts
  • apps/web/hooks/use-sidebar-multi-select.ts
  • apps/web/hooks/use-task-multi-select-range.test.ts
  • apps/web/hooks/use-task-multi-select.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • apps/web/components/task/task-session-sidebar-selection.tsx
  • apps/web/e2e/pages/sidebar-tasks-page.ts
  • apps/web/components/kanban-card.tsx
  • apps/web/hooks/use-task-multi-select.ts
  • apps/web/e2e/tests/kanban/cmd-shift-multi-select.spec.ts
  • apps/web/e2e/tests/task/sidebar-multi-select.spec.ts
  • apps/web/components/task/task-item.tsx
  • apps/web/components/task/task-switcher.tsx
  • apps/web/components/task/task-switcher-context-menu.tsx

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Findings (re-review after latest push)

The latest commits (87e51cc, 1a52ec7) addressed essentially every actionable item from the prior CodeRabbit/Greptile/OpenCode/Codex passes: bulkArchive now keeps failed ids selected and toasts instead of silently clearing (use-sidebar-multi-select.ts:74-87), the active-open-task archive path routes through archiveAndSwitch (use-sidebar-multi-select.ts:56-73), bulk move is wired through onBulkMove/bulkMove so the sidebar selection clears after a context-menu move (task-switcher-context-menu.tsx:323-335, use-sidebar-multi-select.ts:92-102), set_selected/toggle_select now realign or clear the anchor via realignAnchor (use-task-multi-select.ts:204-208), the modifier-click dispatchers are gated on handler presence (task-switcher.tsx:98-115), the conflicting bg-primary/5 vs bg-primary/10 classes were split (task-item.tsx:119-134), single-row selections now route through the bulk path (task-switcher-context-menu.tsx:163-164, 286-294), the misleading range test name was corrected, and the e2e DOM-order helpers are now scoped to the active container/task-sidebar.first() instead of bare page selectors. New unit tests (kanban-card-click.test.ts, task-switcher-click.test.ts) cover the four-branch click dispatch that was previously untested.

One gap remains from the testing-policy check:

Blocker (must fix before merge)

  1. useSidebarMultiSelect has no unit testapps/web/hooks/use-sidebar-multi-select.ts
    • Issue: This hook now contains non-trivial branching logic — bulkArchive's active-task-archive-last ordering, partial-failure handling (failed/succeeded split, toast, re-selecting failed ids), and bulkMove's unconditional clearSelection() in finally — none of which has a *.test.ts. The sibling reducer (use-task-multi-select-range.test.ts) and the kanban hook's equivalent logic are unit tested; this hook isn't, despite being equally complex.
    • Why: Per apps/web/AGENTS.md / the repo testing policy, new hooks with business logic require unit tests; the existing e2e spec (sidebar-multi-select.spec.ts) only covers the happy-path bulk-archive-confirm flow, not partial failure, the active-task-archive branch, or bulkMove.
    • Fix: Add apps/web/hooks/use-sidebar-multi-select.test.ts covering: workspace-change reset, bulkArchive with all-success, bulkArchive with partial failure (failed ids stay selected, toast fires), bulkArchive when the active task is in the set (archiveAndSwitch called, failure pushes id into failed), and bulkMove clearing selection on both success and rejection.

Suggestion (recommended, doesn't block)

  1. Sidebar bulkMove ignores partial server-side failureapps/web/hooks/use-sidebar-multi-select.ts:92-102
    • Issue: bulkMove awaits moveTasks(...) (which only throws on a hard request failure) then unconditionally clears the whole selection in finally. useTaskWorkflowMove (use-task-workflow-move.ts) calls a single batched bulkMoveSelectedTasks endpoint and toasts based on moved_count, but if the backend moves fewer tasks than requested (e.g. some ids fail server-side validation) without throwing, the sidebar still reports success and drops the selection with no way to tell which tasks weren't moved.
    • Fix: Low confidence this is reachable in practice (depends on backend semantics for bulkMoveSelectedTasks) — worth a quick check of whether the endpoint can return moved_count < requested without erroring; if so, surface a partial-failure toast and keep the unmoved ids selected, similar to how bulkArchive already handles its own partial-failure case.

Summary

Severity Count
Blocker 1
Suggestion 1

Verdict: Blocked — add the missing use-sidebar-multi-select.ts unit test, otherwise ready to merge.

Comment thread apps/web/hooks/use-sidebar-multi-select.ts
Comment thread apps/web/hooks/use-sidebar-multi-select.ts
Comment thread apps/web/hooks/use-task-multi-select.ts Outdated
Comment thread apps/web/hooks/use-sidebar-multi-select.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

runMoveTasks(selectedTaskIds, moveTargets.currentWorkflowId, stepId);

P2 Badge Block mixed-workflow moves from card menus

When the selected cards span multiple workflows, the toolbar disables bulk Move to, but the card context/dropdown menu still sends the whole selectedTaskIds set to the clicked card's currentWorkflowId. If a user selects cards in workflows A and B, right-clicks a selected card in A, and chooses Move to for an A step, the B cards are moved into workflow A instead of being blocked as ambiguous. Pass the mixed-workflow state into this menu and disable the same-workflow bulk move, or fall back to moving only the clicked card.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/hooks/use-sidebar-multi-select.ts Outdated
Comment thread apps/web/components/task/task-switcher-context-menu.tsx
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 4m 45s —— View job


Claude Code Review

  • Gather context (PR diff against 4d19fb3, prior review threads, latest commit 3c998c9)
  • Review tests and verification
  • Review implementation for issues (security, architecture, logic, performance, complexity, quality)
  • Post findings report

Posted a findings report (corrected here after I initially mis-scoped one finding as introduced by this PR when it's pre-existing code).

Summary: The latest commit (3c998c9) closes out the last open blocker from the prior review round — apps/web/hooks/use-sidebar-multi-select.test.ts now unit-tests workspace reset, full/partial-failure bulk archive (failed ids re-selected + toast), active-task archive routing, and bulkMove success/failure. It also fixes the unhandled-rejection risk in bulkMove (now catches instead of a bare finally) and wires onClearSelection through so deleting a selected sidebar row clears it from the selection.

No blockers found in this PR's actual diff. Two non-blocking suggestions remain: (1) the kanban card context menu's move actions don't guard against mixed-workflow selections — confirmed this is pre-existing code this PR doesn't touch, so it's out of scope here but worth a follow-up since this PR makes that selection easier to build; (2) dispatchSidebarRowClick's unit tests skip the ctrl-click and handler-absent branches (low risk, already e2e-covered).

Verdict: Ready to merge.

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Findings (review of latest push, commit 3c998c9)

The latest commit closes out the remaining blocker from the prior round: apps/web/hooks/use-sidebar-multi-select.test.ts now covers workspace reset, full-success archive, partial-failure archive (failed ids re-selected + toast), active-task archive routing, and bulkMove success/failure — good coverage of the hook's branching logic. It also fixes the unhandled-rejection risk in bulkMove (now catches instead of relying on a bare finally, so a failed move keeps the rows selected instead of silently clearing them) and wires onClearSelection through so a selected row that gets deleted no longer leaves a stale id in the selection set.

Blocker (must fix before merge)

  1. Kanban card context menu can move tasks into the wrong workflow when the selection spans multiple workflowsapps/web/components/kanban-card.tsx:132-140 and :165-171
    • Issue: kanban-board.tsx computes isMixedWorkflowSelection and uses it to disable the toolbar's bulk move (canMove={!s.isMixedWorkflowSelection}, kanban-board.tsx:405), but the per-card context menu built in useKanbanCardMenu never checks this flag. moveSelectedToStep and the context menu's onSendToWorkflow always send the full selectedTaskIds to moveTargets.currentWorkflowId — i.e. the right-clicked card's workflow — regardless of which workflow each selected task actually belongs to.
    • Why: This PR adds Cmd/Ctrl-click multi-select that works across swimlanes (each swimlane snapshot is a separate workflow per useMultiSelectDerived), so a mixed-workflow selection is now trivially reachable without the old toggle-button flow. Right-clicking one of those selected cards and choosing "Move to step" or "Send to workflow" silently relocates every selected task — including ones from other workflows — into the clicked card's workflow/step. This is a data-correctness bug, not just a UX inconsistency, since it moves tasks the user likely didn't intend to move.
    • Fix: Thread isMixedWorkflowSelection (already computed in kanban-board.tsx) down to KanbanCard/useKanbanCardMenu, and disable/hide onMoveToStep/onSendToWorkflow in contextMenuEntries when it's true and the card is acting on the selection (mirror the toolbar's canMove guard and the sidebar's isMixedWorkflowSelection check in task-switcher-context-menu.tsx:355).

Suggestion (recommended, doesn't block)

  1. dispatchSidebarRowClick unit tests don't cover the ctrl-click or handler-absent branchesapps/web/components/task/task-switcher-click.test.ts:22-46
    • The function also special-cases ctrlKey and falls through to onSelectTask when onToggleSelectTask/onSelectTaskRange are absent (used by the mobile switcher), but only metaKey-with-all-handlers and the two non-modifier paths are tested. A regression in either untested branch (e.g. dropping the optional-handler fallback) would not be caught by this suite. Low risk since it's covered by e2e and was flagged by CodeRabbit already; still worth adding two more cases for parity with kanban-card-click.test.ts.

Summary

Severity Count
Blocker 1
Suggestion 1

Verdict: Blocked — guard the kanban card context menu's move actions against mixed-workflow selections before merge; everything else from this round (sidebar hook tests, bulkMove rejection handling, delete-clears-selection) looks solid.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/hooks/use-sidebar-multi-select.ts (1)

28-31: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard bulk-action completions against workspace changes.

Line 29 says this selection is workspace-scoped, but both async bulk handlers still call dispatch/clearSelection after awaiting. If a bulk archive or move starts in workspace A and the user switches to workspace B before it settles, the old request can clear or repopulate B's selection state when it completes.

Suggested fix
+ const workspaceRef = useRef(workspaceId);
+
  useEffect(() => {
+   workspaceRef.current = workspaceId;
    dispatch({ type: "reset" });
  }, [workspaceId]);

  const bulkArchive = useCallback(
    async (ids: string[], opts?: { cascade?: boolean }) => {
      if (ids.length === 0) return;
+     const opWorkspaceId = workspaceRef.current;
      // If the open task is in the set, archive it via the switch-aware path last
      // so the URL/layout moves off it instead of showing stale content.
      const activeId = store.getState().tasks.activeTaskId;
      const activeInSet = activeId != null && ids.includes(activeId);
      const restIds = activeInSet ? ids.filter((id) => id !== activeId) : ids;
      setIsArchiving(true);
      try {
        const results = await Promise.allSettled(restIds.map((id) => archiveTaskById(id, opts)));
        const failed = restIds.filter((_, i) => results[i].status === "rejected");
        const succeeded = restIds.filter((_, i) => results[i].status === "fulfilled");
        if (succeeded.length > 0) removeTasksFromStore(new Set(succeeded));
        if (activeInSet) {
          try {
            await archiveAndSwitch(activeId!, opts);
          } catch {
            failed.push(activeId!);
          }
        }
        if (failed.length > 0) {
+         if (workspaceRef.current !== opWorkspaceId) return;
          dispatch({ type: "set_selected", ids: new Set(failed) });
          toast({
            title: `Failed to archive ${failed.length} task${failed.length === 1 ? "" : "s"}`,
            variant: "error",
          });
          return;
        }
+       if (workspaceRef.current !== opWorkspaceId) return;
        clearSelection();
      } finally {
        setIsArchiving(false);
      }
    },
    [store, archiveTaskById, archiveAndSwitch, removeTasksFromStore, clearSelection, toast],
  );

  const bulkMove = useCallback(
    async (ids: string[], targetWorkflowId: string, targetStepId: string) => {
      if (ids.length === 0) return;
+     const opWorkspaceId = workspaceRef.current;
      try {
        await moveTasks(ids, targetWorkflowId, targetStepId);
+       if (workspaceRef.current !== opWorkspaceId) return;
        clearSelection();
      } catch {
        // useTaskWorkflowMove already toasts the failure; keep the rows selected
        // for retry and swallow the rejection so it isn't unhandled at the
        // fire-and-forget call site.
      }
    },
    [moveTasks, clearSelection],
  );

Also applies to: 53-90, 92-105

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/hooks/use-sidebar-multi-select.ts` around lines 28 - 31, The bulk
action handlers in use-sidebar-multi-select should not update selection state
after the workspace has changed, since the selection is scoped per workspace.
Add a workspace identity guard in the async archive/move flows so dispatch and
clearSelection only run if the current workspaceId still matches the one that
started the request. Apply this check in the bulk handlers and any shared
completion path they use, so stale completions from a previous workspace cannot
reset or repopulate the new workspace’s selection state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/web/hooks/use-sidebar-multi-select.ts`:
- Around line 28-31: The bulk action handlers in use-sidebar-multi-select should
not update selection state after the workspace has changed, since the selection
is scoped per workspace. Add a workspace identity guard in the async
archive/move flows so dispatch and clearSelection only run if the current
workspaceId still matches the one that started the request. Apply this check in
the bulk handlers and any shared completion path they use, so stale completions
from a previous workspace cannot reset or repopulate the new workspace’s
selection state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ae02dfb-3f04-482a-8158-623176cc0dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 1a52ec7 and 3c998c9.

📒 Files selected for processing (6)
  • apps/web/components/task/task-session-sidebar-selection.tsx
  • apps/web/components/task/task-switcher-context-menu.tsx
  • apps/web/components/task/task-switcher.tsx
  • apps/web/hooks/use-sidebar-multi-select.test.ts
  • apps/web/hooks/use-sidebar-multi-select.ts
  • apps/web/hooks/use-task-multi-select.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/components/task/task-session-sidebar-selection.tsx
  • apps/web/hooks/use-task-multi-select.ts
  • apps/web/components/task/task-switcher-context-menu.tsx
  • apps/web/components/task/task-switcher.tsx

@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Correction to my findings above: the kanban-card.tsx mixed-workflow move gap I flagged as a blocker is pre-existing code that this PR does not modify (verified via git diff 4d19fb3...HEAD -- apps/web/components/kanban-card.tsx / kanban-board.tsxuseKanbanCardMenu's moveSelectedToStep/onSendToWorkflow and the toolbar's isMixedWorkflowSelection guard both predate this PR). Per review policy, pre-existing issues on unmodified lines aren't blockers for this PR, so I'm downgrading it to a non-blocking suggestion (worth a follow-up, but out of scope here).

Revised verdict for this PR's actual diff (against 4d19fb3):

Findings

No blockers in the changes introduced by this PR. The latest commit (3c998c9) closes out the prior round's blocker — use-sidebar-multi-select.test.ts now covers workspace reset, full/partial-failure bulk archive, active-task archive routing, and bulkMove success/failure — and fixes the unhandled-rejection risk in bulkMove (now catches so a failed move keeps rows selected instead of losing the unhandled promise from a bare finally), plus wires onClearSelection so deleting a selected sidebar row drops it from the selection.

Suggestion (recommended, doesn't block)

  1. Kanban card context menu doesn't guard mixed-workflow selections for move actions (pre-existing, out of scope for this PR) — apps/web/components/kanban-card.tsx (useKanbanCardMenu). This PR makes a mixed-workflow selection easier to build (Cmd/Ctrl-click now spans swimlanes without first toggling multi-select mode), which makes the pre-existing gap more reachable: the toolbar guards bulk move with canMove={!s.isMixedWorkflowSelection} (kanban-board.tsx:405) and the sidebar guards it too (task-switcher-context-menu.tsx:355), but the per-card context menu's moveSelectedToStep/onSendToWorkflow send the whole selection to the right-clicked card's workflow with no such guard. Worth a follow-up ticket to thread isMixedWorkflowSelection into useKanbanCardMenu.
  2. dispatchSidebarRowClick tests miss the ctrl-click and handler-absent branchesapps/web/components/task/task-switcher-click.test.ts:22-46. Low risk (covered by e2e, already flagged by CodeRabbit) but worth two more cases for parity with kanban-card-click.test.ts.

Summary

Severity Count
Blocker 0
Suggestion 2

Verdict: Ready to merge.

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

OpenCode review complete

OpenCode model: opencode-go/minimax-m3

No suggestions found for commit 5731b9bb7523.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c998c9e77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread apps/web/components/task/task-switcher.tsx
Comment thread apps/web/lib/kanban/view-registry.ts
Comment thread apps/web/hooks/use-task-multi-select.ts
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 2m 45s —— View job


Claude Code Review

  • Gather context (diff vs origin/main, prior review history, latest commit eacb93d)
  • Review tests and verification
  • Review implementation for issues (security, architecture, logic, performance, complexity, quality)
  • Post findings report and inline comments

Posted a findings report and one inline comment.

Summary: The latest push (eacb93d) closes out the prior round's last open item — pruneToVisible drops selected ids that scroll out of view (collapsed group / filter change) so plain clicks stop being misread as selection-mode, and traces correctly through realignAnchor so the Shift-click anchor realigns instead of going stale. It also fixes the backward-range bulk-move ordering bug Codex flagged, sorting ids into rendered order before the sidebar's bulkMove. No blockers in this push. One suggestion: the new pruneToVisible test only asserts selectedIds, not that the anchor realigns to a surviving id — worth extending to catch a regression in that realignment.

Verdict: Ready to merge.

Comment thread apps/web/components/task/task-session-sidebar-selection.tsx
@claude

claude Bot commented Jun 28, 2026

Copy link
Copy Markdown

Findings (review of latest push, commit eacb93d)

This commit closes out the last open item from the prior round: pruneToVisible (apps/web/hooks/use-sidebar-multi-select.ts:56-63) drops selected ids that scroll out of view (collapsed group / filter change), wired via an effect in useSidebarSelection (task-session-sidebar-selection.tsx:89-91) so isSelecting no longer stays latched on hidden rows. It also fixes the backward-range bulk-move ordering bug Codex flagged: onBulkMove (task-session-sidebar-selection.tsx:102-109) now sorts ids into rendered top-to-bottom order before calling the hook's bulkMove, so a Shift-click anchored below the target no longer lands scrambled at the destination.

I traced both changes through the reducer:

  • pruneToVisible's dispatch({ type: "set_selected", ids: next }) goes through realignAnchor (use-task-multi-select.ts:204-208, :265), so if the pruned-out id was the anchor, the anchor correctly realigns to a remaining selected id (or clears when the selection empties) instead of stranding a later Shift+click.
  • The effect's dependency on pruneToVisible (which itself depends on selectedIds) causes the effect to re-fire on every selection change, but since the function is idempotent and only dispatches when next.size !== selectedIds.size, this settles after one extra no-op pass — no infinite loop.
  • Since pruneToVisible now keeps selectedIds constrained to visible ids, onBulkMove's order.get(a) ?? 0 fallback (for an id missing from visibleTaskIds) is effectively unreachable in normal use, since ids is sourced from the already-pruned selection.

No blockers found in this push's diff.

Suggestion (recommended, doesn't block)

  1. pruneToVisible test doesn't assert anchor realignmentapps/web/hooks/use-sidebar-multi-select.test.ts:53-63
    • Issue: The new test only asserts selectedIds after pruning b (which was the anchor, since toggle_select anchors to the just-added id). It doesn't assert result.current anchor behavior (there's no direct anchor getter, but a follow-up selectRange call would expose it).
    • Why: realignAnchor is exactly the mechanism that prevents a stale-anchor bug here; a regression that stopped realigning the anchor on prune (e.g. reverting to keep state.anchorId unconditionally) would not be caught by this suite.
    • Fix: extend the test to call selectRange after pruning and assert the range starts from the realigned anchor ('a'), not the pruned-out 'b'.

Summary

Severity Count
Blocker 0
Suggestion 1

Verdict: Ready to merge.

Comment thread apps/web/hooks/use-sidebar-multi-select.test.ts
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 0s —— View job


I'll analyze this and get back to you.

@zeval zeval force-pushed the feature/cmd-click-support-si-iwd branch from 3bfff53 to 5f09827 Compare July 6, 2026 15:11
@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 6m 6s —— View job


Code Review

  • Gather context (PR diff, prior review history, changed files)
  • Read code review skill
  • Review implementation for issues (security, architecture, logic, performance, complexity, quality)
  • Post findings report and inline comments

Reviewed the latest commits since the last "Ready to merge" verdict (c8253bc), focusing on the new bulk pin/delete sidebar feature (f938825, 4b615fa, 5f09827) and the hardening fixes.

All prior-round blockers are resolved (visible-order bulk moves, anchor realignment, unit tests, keyboard parity, currentStepId for selection moves, mixed-workflow/workflow-less guards, sortByDisplayOrder lazy evaluation, aria-selected, singular/plural helpers, transitive comparator fix, and more). Two new blockers from this round:

Findings

Blocker (must fix before merge)

  1. task-switcher-context-menu.tsx exceeds the 600-line limit (627 lines) — this PR pushed it from ~406 lines. Extract the helper item components into a sibling task-switcher-context-menu-items.tsx. Inline comment posted.

  2. task-switcher.tsx exceeds the 600-line limit (619 lines) — this PR pushed it from ~485 lines via multi-select prop threading. Extract the tree-rendering internals (TaskRow, GroupSection, etc.) into a sibling task-switcher-group.tsx. Inline comment posted.

Suggestion (recommended, doesn't block)

  1. console.error spy not protected by try/finally in task-session-sidebar-selection.test.ts:44-53 — spy leaks if any assertion before mockRestore() throws. Use try/finally or beforeEach/afterEach.

Summary

Severity Count
Blocker 2
Suggestion 1

Verdict: Blocked — fix the two file-size violations before merge. The feature logic, prior-round fixes, and test coverage are all solid.

@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Findings

This is a re-review of the latest commits since the last "Ready to merge" verdict, specifically the new bulk pin/delete sidebar feature (commits f938825, 4b615fa, 5f09827) and the kanban/sidebar hardening fixes.

The prior round's open items (visible-order bulk moves, anchor realignment, missing unit tests, keyboard parity, currentStepId for selection moves, mixed-workflow/workflow-less guards) are all resolved and covered by tests.

Blocker (must fix before merge)

  1. task-switcher-context-menu.tsx exceeds the 600-line file limitapps/web/components/task/task-switcher-context-menu.tsx

    • Issue: File is now 627 lines. This PR added 232 lines (the BulkSelectionMenuItems component, bulk archive/pin/delete paths, and related helpers), pushing it from ~406 lines to 627.
    • Why: The 600-line limit is enforced by apps/web/eslint.config.mjs and the PR's validation runs lint with --max-warnings 0, so this is a CI failure. Tracked in root CLAUDE.md and apps/web/AGENTS.md.
    • Fix: Extract the helper item components (BulkSelectionMenuItems, TaskArchiveItem, TaskMoveItems, TaskDeleteItem, and the color/link/pin/rename atoms) into a sibling file such as task-switcher-context-menu-items.tsx. The public exports (TaskItemWithContextMenu, StepDef) stay in the current file.
  2. task-switcher.tsx exceeds the 600-line file limitapps/web/components/task/task-switcher.tsx

    • Issue: File is now 619 lines. This PR added 135 lines of multi-select prop threading through GroupSectionProps/GroupSection, pushing it from ~485 to 619.
    • Why: Same lint/CI constraint.
    • Fix: Extract TaskRowProps, TaskRow, TaskTreeContext, TaskTreeNode, TaskTreeLevel, GroupSectionProps, and GroupSection into a sibling file (e.g. task-switcher-group.tsx). The main file keeps the public TaskSwitcher export and TaskSwitcherProps type.

Suggestion (recommended, doesn't block)

  1. console.error spy not protected by try/finallyapps/web/components/task/task-session-sidebar-selection.test.ts:44-53
    • Issue: The spy is installed as a bare variable and restored at the end of the test body. If the act() call or any assertion throws before line 52, the spy leaks and suppresses console.error output in all subsequent tests.
    • Why: OpenCode flagged this in the prior round; it wasn't addressed.
    • Fix: Wrap the spy in try/finally or move setup/teardown to beforeEach/afterEach.

Summary

Severity Count
Blocker 2
Suggestion 1

Verdict: Blocked — fix the two file-size limit violations before merge. The feature logic, tests, and prior-round fixes are all solid.

Comment thread apps/web/components/task/task-switcher-context-menu.tsx
Comment thread apps/web/components/task/task-switcher.tsx
@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 7m 29s —— View job


Code Review — feat: cmd+click and shift+click multi-select for kanban and sidebar

Checklist

  • Correctness — logic is sound, no off-by-one or edge-case gaps
  • Security — no data leakage, no XSS/injection vectors introduced
  • Tests — new behaviour is covered at unit and E2E level
  • File/function size limits — all files ≤600 lines, functions ≤100 lines
  • Accessibility — aria-selected wired for multi-selected rows
  • Error handling — partial failures keep failed ids selected + toast; bulk-move swallows rejection correctly
  • No dead code / stale imports

Findings

Suggestion (non-blocking) — console.error spy leaks on assertion failure

task-session-sidebar-selection.test.ts, the "clears the dialog even when archiving rejects" test restores the console.error spy at the end of the test body. If the expect(result.current.state).toBeNull() assertion throws, mockRestore() is never called and the spy leaks into subsequent tests.

Wrap in try/finally (see inline comment).


What was reviewed

Area Files
Shared reducer use-task-multi-select.ts
Sidebar multi-select use-sidebar-multi-select.ts, .test.ts
Sidebar selection wiring task-session-sidebar-selection.tsx, .test.ts
Context menu bulk path task-switcher-context-menu.tsx
Sidebar row rendering task-switcher.tsx, task-item.tsx
Kanban card click/menu kanban-card.tsx
Pure helpers apply-view.ts (flattenVisibleTaskIds, sortIdsByVisibleOrder, computeMixedWorkflowSelection), .test.ts
Kanban order helper task-order.ts (sortIdsByCreatedDesc)
Bulk pin/unpin actions sidebar-task-prefs-actions.ts
E2E page object e2e/pages/sidebar-tasks-page.ts
Extracted components task-switcher-group.tsx, task-switcher-color-menu.tsx, task-session-sidebar-archived-item.ts

Previously resolved blockers (all fixed)

  • File size violationstask-switcher-context-menu.tsx (was 627 lines) and task-switcher.tsx (was 619 lines) are now 564 and 571 lines respectively after extracting TaskColorMenu and GroupHeader/TaskSwitcherSkeleton in 7532efe.
  • Partial archive/delete failure — failed IDs kept selected, error toast fired.
  • bulkMove rejection — swallowed with selection preserved for retry.
  • Anchor realignment after pruneset_selected realigns anchor to last visible selected id.
  • Visible-order bulk movessortIdsByVisibleOrder (sidebar) and sortIdsByCreatedDesc (kanban) correct ordering.
  • Mixed-workflow/workflow-less guardcomputeMixedWorkflowSelection gates move-to-step on kanban and sidebar.
  • Keyboard parityhandleTaskItemKeyDown routes through onSelect when wired.
  • All-pinned bulk unpin5f09827 correctly calls unpinTasks when all selected tasks are already pinned.
  • Kanban card mixed-workflow menu gatedb6c18e sets onMoveToStep: undefined for mixed selections.

Verdict

Ready to merge. One non-blocking suggestion on test hygiene (spy cleanup). All correctness, security, size-limit, and test-coverage requirements are satisfied.

Comment thread apps/web/components/task/task-session-sidebar-selection.test.ts
@claude

claude Bot commented Jul 6, 2026

Copy link
Copy Markdown

Claude finished @zeval's task in 2m 28s —— View job


Code Review

  • Read review skill and gather context
  • Review latest commits since previous verdict
  • Check prior blocker resolutions
  • Scan for new issues
  • Post findings

Findings

This is a re-review of the single commit since the last "Ready to merge" verdict (7532efe): 06869aa — "test: restore sidebar selection console spy safely".

The previous round (2026-07-06T15:23) found two blockers (file size violations) and one suggestion (spy leak). The 7532efe refactor committed in the same push extracted TaskColorMenutask-switcher-color-menu.tsx and GroupSection/GroupHeadertask-switcher-group.tsx, bringing both files within the 600-line limit:

File Before After
task-switcher-context-menu.tsx 627 lines 564 lines
task-switcher.tsx 619 lines 571 lines

06869aa addresses the one remaining suggestion — the console.error spy in task-session-sidebar-selection.test.ts now correctly restores in a finally block, so a failing assertion can no longer leak the spy into subsequent tests.

No new issues introduced by either commit.

Summary

Severity Count
Blocker 0
Suggestion 0

Verdict: Ready to merge.

@zeval zeval merged commit 5105ac2 into main Jul 6, 2026
59 checks passed
@zeval zeval deleted the feature/cmd-click-support-si-iwd branch July 6, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant