Skip to content

Comments

[Phase 3] Task Board: bulk stop, reset, and state management actions#389

Merged
frankbria merged 11 commits intomainfrom
feature/issue-340-task-board-bulk-actions
Feb 19, 2026
Merged

[Phase 3] Task Board: bulk stop, reset, and state management actions#389
frankbria merged 11 commits intomainfrom
feature/issue-340-task-board-bulk-actions

Conversation

@frankbria
Copy link
Owner

@frankbria frankbria commented Feb 19, 2026

Summary

Implements #340: Adds comprehensive task state management to the Task Board, including single-task and bulk stop/reset actions with confirmation dialogs.

  • Single-task actions: Stop button on IN_PROGRESS cards, Reset button on FAILED cards
  • Bulk actions: Context-aware batch bar shows Execute/Stop/Reset based on selected task statuses
  • Confirmation dialogs: AlertDialog-based confirmations for destructive bulk operations with partial failure handling
  • Column select-all: Checkbox in column headers for quick selection by status
  • Loading states: Per-task spinners and batch loading indicators
  • Accessibility: ARIA roles on spinners/errors, dismiss button on error banner, keyboard navigation preserved
  • Race condition fix: Task IDs frozen at dialog-open time to prevent SWR revalidation desync

Acceptance Criteria

  • Clicking an IN_PROGRESS task navigates to the Execution Monitor
  • IN_PROGRESS tasks show a Stop action in the task card
  • FAILED tasks show a Reset to Ready action in the task card
  • Users can select multiple tasks via checkboxes
  • Bulk action bar appears with context-appropriate actions
  • Confirmation dialogs for destructive bulk actions (stop, reset)
  • UI updates after actions with error handling (Promise.allSettled for partial failures)
  • All actions work through the existing v2 API (no backend changes needed)

Test Plan

  • TaskCard: Stop/Reset buttons render for correct statuses, loading states show spinner
  • BatchActionsBar: Context-appropriate buttons for READY/IN_PROGRESS/FAILED/mixed selections
  • BulkActionConfirmDialog: Renders correct title/description, loading state, cancel/confirm flows
  • TaskColumn: Select All checkbox with checked/indeterminate states
  • TaskBoardView: Full integration tests for single stop/reset, bulk stop/reset, error handling
  • 302 unit tests passing (pre-existing Playwright E2E infra issue unrelated to changes)

Implementation Notes

  • Used existing tasksApi.stopExecution() and tasksApi.updateStatus() — no API changes required
  • AlertDialog used for destructive confirmations (better accessibility than Dialog)
  • Bulk operations fan out individual calls via Promise.allSettled() for graceful partial failure
  • Shift-click range selection deferred to follow-up (not in acceptance criteria)

Closes #340

Summary by CodeRabbit

  • New Features

    • Per-task Stop/Reset actions with loading spinners and a "View Execution" button for in-progress tasks that navigates to execution details.
    • Bulk operations (execute/stop/reset) with confirmation dialogs, per-batch loading states, and strategy selector when READY tasks are selected.
    • Select/deselect-all checkbox per column for multi-select.
  • Bug Fixes / UX

    • Action buttons show accurate counts and enabled/disabled states; dismissible error banner for failures.
  • Tests

    • Expanded unit tests covering individual and bulk flows, dialogs, selection, loading, and error states.
  • Documentation

    • Added code-review notes for bulk actions rollout.

Test User added 8 commits February 18, 2026 21:30
… FAILED tasks

- Add onStop and onReset optional props to TaskCardProps
- Show Stop button with Cancel01Icon (destructive ghost styling) for IN_PROGRESS tasks
- Show Reset button with ArrowTurnBackwardIcon for FAILED tasks
- Both buttons use e.stopPropagation() to avoid triggering card onClick
- Add 7 new tests covering button visibility, callbacks, and styling
…ierarchy

- Add onStop/onReset props to TaskColumn and TaskBoardContent
- Implement handleStop (calls tasksApi.stopExecution) in TaskBoardView
- Implement handleReset (calls tasksApi.updateStatus READY) in TaskBoardView
- Pass handlers through TaskBoardContent -> TaskColumn -> TaskCard
- Add 5 integration tests for stop/reset actions and error handling
…sk actions

- AlertDialog-based component for execute/stop/reset bulk actions
- Destructive styling for stop action confirm button
- Loading state with spinner on confirm button
- 8 tests covering all action types, callbacks, and states
…statuses

- Add selectedTasks, onStopBatch, onResetBatch, isStoppingBatch, isResettingBatch props
- Show Execute/Stop/Reset buttons with counts based on task status
- Strategy selector only visible when READY tasks are selected
- Destructive variant for Stop, outline for Reset
- Loading spinners for each action type
- 12 new tests covering all states and interactions
- Add isStoppingBatch, isResettingBatch, confirmAction state
- Add selectedTasks useMemo for status-aware batch actions
- Implement handleStopBatch/handleResetBatch with confirmation flow
- handleConfirmAction uses Promise.allSettled for parallel API calls
- Wire BulkActionConfirmDialog and enhanced BatchActionsBar props
- Add 5 integration tests for full bulk action flow
- Add select-all Checkbox to column header in selection mode
- Support checked, unchecked, and indeterminate states
- Add onSelectAll/onDeselectAll props through TaskColumn -> TaskBoardContent -> TaskBoardView
- Implement handleSelectAll/handleDeselectAll in TaskBoardView
- 8 new tests for select-all behavior and edge cases
- Add isLoading prop to TaskCard, show Loading03Icon spinner instead of action buttons
- Add loadingTaskIds Set state to TaskBoardView for per-task tracking
- Wire loadingTaskIds through TaskBoardContent -> TaskColumn -> TaskCard
- handleStop/handleReset add task to loadingTaskIds during API call
- 4 new tests for loading spinner behavior across task statuses
- Add role="status" and aria-label to loading spinner in TaskCard
- Freeze task IDs at dialog-open time to prevent count desync from SWR revalidation
- Add role="alert" and dismiss button to error banner in TaskBoardView
- Use frozen taskIds in handleConfirmAction instead of re-filtering selectedTasks
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

Adds per-task stop/reset actions, bulk selection and status-aware batch actions with confirmation dialog, per-task and batch loading states, aggregated error handling, routing to execution for in-progress tasks, and comprehensive tests across task components and a new BulkActionConfirmDialog.

Changes

Cohort / File(s) Summary
Batch UI & Dialog
web-ui/src/components/tasks/BatchActionsBar.tsx, web-ui/src/components/tasks/BulkActionConfirmDialog.tsx
BatchActionsBar now accepts selectedTasks, onStopBatch/onResetBatch and loading flags; conditionally renders Execute/Stop/Reset and strategy selector by status. New BulkActionConfirmDialog component and exported BulkActionType.
Board orchestration
web-ui/src/components/tasks/TaskBoardView.tsx, web-ui/src/components/tasks/TaskBoardContent.tsx
TaskBoardView implements per-task and batch handlers (stop/reset), confirmAction flow, parallel execution via Promise.allSettled, error aggregation/banner, and loading state management; TaskBoardContent forwards new callbacks/props to TaskColumn.
Column & Card changes
web-ui/src/components/tasks/TaskColumn.tsx, web-ui/src/components/tasks/TaskCard.tsx
TaskColumn adds header select-all checkbox with indeterminate state and onSelectAll/onDeselectAll callbacks; wires per-task onStop/onReset and loadingTaskIds. TaskCard gains onStop/onReset/isLoading props; shows Stop for IN_PROGRESS, Reset for FAILED, spinner when loading.
Tests
web-ui/__tests__/components/tasks/BatchActionsBar.test.tsx, web-ui/__tests__/components/tasks/BulkActionConfirmDialog.test.tsx, web-ui/__tests__/components/tasks/TaskBoardView.test.tsx, web-ui/__tests__/components/tasks/TaskCard.test.tsx, web-ui/__tests__/components/tasks/TaskColumn.test.tsx, web-ui/__tests__/components/tasks/TaskDetailModal.test.tsx
New/extended unit tests covering batch action visibility/counts, confirmation dialog flows, per-task and batch interactions (stop/reset), select-all and indeterminate behavior, loading/disabled states, routing from IN_PROGRESS to execution, and error banner flows.
API mocks & icons
web-ui/__tests__/components/tasks/TaskBoardView.test.tsx (mocks), web-ui/__mocks__/@hugeicons/react.js
Tests add tasksApi.stopExecution mock; icon mock adds ViewIcon.
Docs
docs/code-review/2026-02-19-task-board-bulk-actions-review.md
New code-review document summarizing the bulk stop/reset feature, findings, and action items.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as TaskBoardView
    participant Dialog as BulkActionConfirmDialog
    participant API as Tasks API
    participant State as Component State

    User->>UI: Select multiple tasks (checkboxes)
    UI->>State: update selectedTasks
    State-->>UI: render BatchActionsBar (actions shown based on statuses)

    User->>UI: Click "Stop" in BatchActionsBar
    UI->>State: set confirmAction='stop' and open dialog
    State-->>Dialog: open(actionType='stop', taskCount)

    User->>Dialog: Click Confirm
    Dialog->>UI: onConfirm
    UI->>State: set isStoppingBatch=true

    loop parallel per task
      UI->>API: POST /tasks/{id}/stop
      API-->>UI: response (success/failure)
    end

    UI->>State: aggregate results, close dialog, clear selection, set isStoppingBatch=false
    State-->>UI: updated UI (show error banner if some failed)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰
I nibble checks and count the rows,
Stop or Reset — the carrot shows.
A dialog hums, confirmations bright,
Spinners twirl through day and night.
Rabbity cheers for batch control!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: Phase 3 implementation of bulk stop, reset, and state management actions for the Task Board.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #340: single/bulk stop and reset actions, BatchActionsBar with status-aware buttons, confirmation dialogs, select-all checkboxes, loading states, and error handling using existing v2 API endpoints.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #340 requirements: task action components, state management, test coverage, and documentation. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-340-task-board-bulk-actions

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

@macroscopeapp
Copy link
Contributor

macroscopeapp bot commented Feb 19, 2026

Add bulk stop/reset with confirmation, per-task stop/reset, and column select-all to the Task Board in TaskBoardView.tsx

Introduce bulk stop/reset flows with a confirmation dialog, add per-task stop/reset actions with loading states, derive selection from the full dataset, and add column-level select-all controls. Wire UI via BatchActionsBar, TaskColumn, and TaskCard, and surface errors with a dismissible banner. Tests cover selection mode, dialogs, button states, API calls, and navigation.

📍Where to Start

Start with the bulk action orchestration and state in TaskBoardView.tsx, then review the confirmation UI in BulkActionConfirmDialog.tsx and the action surface in BatchActionsBar.tsx.


Macroscope summarized a2fe539.

@claude
Copy link

claude bot commented Feb 19, 2026

PR #389 Review — Task Board: Bulk Stop, Reset & State Management

This is a well-structured addition. The component decomposition is clean, the race-condition fix (freezing task IDs at dialog-open time) is explicitly noted and correctly implemented, and test coverage is thorough. A few issues worth addressing before merge:


Bug: Loading state cleared before SWR refresh in handleConfirmAction

In TaskBoardView.tsx, the isStoppingBatch/isResettingBatch flags are set to false before the trailing await mutate(), so the dialog dismisses and the loading spinner disappears before the SWR cache is refreshed. Users will see a flash of stale data.

// Current — loading cleared before mutate()
setIsStoppingBatch(false);      // spinner gone
...
setConfirmAction(null);
handleClearSelection();
await mutate();                  // data refresh happens here
// Fix — keep loading until refresh is done
try {
  const results = await Promise.allSettled(
    confirmAction.taskIds.map((id) => tasksApi.stopExecution(workspacePath, id))
  );
  const failures = results.filter((r) => r.status === 'rejected');
  if (failures.length > 0) setActionError('Failed to stop N task(s)');
} finally {
  setIsStoppingBatch(false);
  setConfirmAction(null);
  handleClearSelection();
  await mutate();
}

Same pattern applies to the reset branch.


Bug: Missing try/finally means dialog can get stuck open

If mutate() at the bottom of handleConfirmAction throws (network error, SWR error), setConfirmAction(null) and handleClearSelection() may never be called, leaving the dialog open and selection state dirty. Wrapping the body in try/finally resolves both this and the item above in one pass.


Dead code: execute branch in handleConfirmAction

The confirmAction.type === 'execute' branch is never reachable because no code path calls setConfirmAction({ type: 'execute', ... }). The existing batch execute flow goes directly through handleExecuteBatch without a confirmation dialog. This branch should either be removed or the execute confirmation should be wired in.


Minor: unused variable in test

In TaskBoardView.test.tsx ('shows batch Stop button after selecting IN_PROGRESS tasks'):

const checkboxes = screen.getAllByRole('checkbox');  // collected but never used
const buildApiCheckbox = screen.getByRole('checkbox', { name: /select build api/i });

The checkboxes constant can be removed.


Observation: optional onStop/onReset silently suppress actions

Making the handlers optional in TaskCard is fine for flexibility, but the guard task.status === 'IN_PROGRESS' && onStop && means an IN_PROGRESS card silently shows no action button if the prop is omitted. Since TaskBoardView always passes these, this is not a functional bug, but the optional typing understates the expected contract. Worth a comment or a prop-type note for future callers.


Positive notes

  • Promise.allSettled for partial-failure handling is exactly right for bulk operations.
  • Freezing taskIds in confirmAction state at dialog-open time correctly sidesteps the SWR revalidation desync described in the PR.
  • AlertDialog over Dialog for destructive confirmations is the correct Radix UI primitive choice.
  • Select-all indeterminate checkbox state is handled properly, including 'click indeterminate -> select all' behavior.
  • Accessibility additions (role=alert on error banner, role=status on spinner, dismiss button, aria-labels) are solid.
  • Test coverage is comprehensive -- per-component unit tests plus integration tests in TaskBoardView.test.tsx covering error paths and confirmation flows.

The try/finally fix is the most important item; the rest are low-severity. Happy to see this land once those are addressed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
web-ui/src/components/tasks/TaskBoardView.tsx (1)

56-85: ⚠️ Potential issue | 🟠 Major

Batch operations have inconsistent scope when filters are applied.

selectedCount shows selectedTaskIds.size (all selected items), but handleStopBatch and handleResetBatch filter selections to only visible tasks via selectedTasks. Meanwhile, handleExecuteBatch uses the full selectedTaskIds. No selection clearing occurs when filters change, so users can end up with "5 selected" displayed but only 2 visible tasks affected by stop/reset—while execute would process all 5 hidden selections.

🛠️ Suggested fix (make all operations consistent)
-  const selectedTasks = useMemo(
-    () => filteredTasks.filter((t) => selectedTaskIds.has(t.id)),
-    [filteredTasks, selectedTaskIds]
-  );
+  const selectedTasks = useMemo(
+    () => (data?.tasks ?? []).filter((t) => selectedTaskIds.has(t.id)),
+    [data?.tasks, selectedTaskIds]
+  );

This makes stop/reset operations consistent with execute, which already operates on the full selectedTaskIds set.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/src/components/tasks/TaskBoardView.tsx` around lines 56 - 85, The
batch-action consistency bug stems from mixed use of selectedTasks (visible,
from filteredTasks) and selectedTaskIds (global) across handlers:
handleStopBatch and handleResetBatch currently act on selectedTasks while
handleExecuteBatch uses selectedTaskIds, and no selections are cleared when
filters change. Update handleStopBatch and handleResetBatch to operate on the
full selectedTaskIds set (same as handleExecuteBatch) and/or centralize a
utility that returns Array.from(selectedTaskIds) for batch operations;
additionally, add logic in the filter/search effect to clear or reconcile
selectedTaskIds when filters change so the displayed selectedCount and
actionable items remain in sync (use selectedTaskIds, selectedTasks,
handleStopBatch, handleResetBatch, handleExecuteBatch, and the filter effect to
locate code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web-ui/__tests__/components/tasks/TaskCard.test.tsx`:
- Around line 207-229: The tests that assert the loading spinner (in the specs
"shows loading spinner instead of action button when isLoading", "shows loading
spinner for READY task when isLoading", and "shows loading spinner for FAILED
task when isLoading") rely on getByTestId('icon-Loading03Icon') from the jest
SVG mock; replace those assertions to query the accessible spinner instead (use
getByRole or queryByRole with role 'status' and name/label matching /loading/i)
so renderCard(...) is validated against the real component behavior rather than
mock-only data-testid attributes, and remove any reliance on the
__mocks__/@hugeicons/react.js testid in these tests.

---

Outside diff comments:
In `@web-ui/src/components/tasks/TaskBoardView.tsx`:
- Around line 56-85: The batch-action consistency bug stems from mixed use of
selectedTasks (visible, from filteredTasks) and selectedTaskIds (global) across
handlers: handleStopBatch and handleResetBatch currently act on selectedTasks
while handleExecuteBatch uses selectedTaskIds, and no selections are cleared
when filters change. Update handleStopBatch and handleResetBatch to operate on
the full selectedTaskIds set (same as handleExecuteBatch) and/or centralize a
utility that returns Array.from(selectedTaskIds) for batch operations;
additionally, add logic in the filter/search effect to clear or reconcile
selectedTaskIds when filters change so the displayed selectedCount and
actionable items remain in sync (use selectedTaskIds, selectedTasks,
handleStopBatch, handleResetBatch, handleExecuteBatch, and the filter effect to
locate code).

- Wrap handleConfirmAction in try/finally so loading states and dialog
  cleanup always execute, even if mutate() throws
- Move loading flag reset into finally block to keep spinners visible
  until SWR refresh completes (prevents stale data flash)
- Remove unreachable 'execute' branch from handleConfirmAction
- Derive selectedTasks from full task list instead of filteredTasks
  so bulk actions include all selected tasks regardless of active filters
- Replace getByTestId('icon-Loading03Icon') with getByRole('status')
  to test actual accessibility behavior, not jest mock internals
- Remove unused checkboxes variable in TaskBoardView test
- Add JSDoc on optional onStop/onReset props in TaskCard
@claude
Copy link

claude bot commented Feb 19, 2026

PR Review: Task Board Bulk Stop/Reset Actions

This is a well-scoped implementation. The feature works within the existing API surface, the race-condition mitigation (freezing taskIds at dialog-open time in confirmAction) is the right call, and the test coverage is genuinely thorough. Notes below are ordered by severity.


Potential Bugs

1. handleConfirmAction has no execute branch, but BulkActionType exports it

BulkActionType = 'execute' | 'stop' | 'reset', and the dialog renders actionType={confirmAction?.type ?? 'execute'}. The execute case is never set into confirmAction today, so this is currently safe. But the exported type implies it's a valid input to handleConfirmAction, which silently does nothing for execute. Either:

  • Remove 'execute' from BulkActionType and use a separate type in the dialog, or
  • Add an execute branch to handleConfirmAction (or an else that logs an error).

The ambiguity will confuse the next person wiring up a confirm-before-execute flow.

2. Error cast is unsafe

In handleStop / handleReset:

const apiErr = err as ApiError;
setActionError(apiErr.detail || 'Failed to stop task');

err as ApiError doesn't guarantee err has a .detail property — any thrown value (network timeout, JS error, etc.) goes through this branch. A safer pattern:

const detail = err \!= null && typeof err === 'object' && 'detail' in err
  ? String((err as ApiError).detail)
  : undefined;
setActionError(detail || 'Failed to stop task');

Minor Code Quality

3. TaskCard outer condition can be simplified

Before:

{(task.status === 'READY' || task.status === 'BACKLOG' || task.status === 'IN_PROGRESS' || task.status === 'FAILED') && (

This now covers every status that has an action. Since DONE, BLOCKED, etc. won't render any buttons (all branches are conditional), the wrapper div is harmless but the long condition is misleading. Consider an early-exit check or naming a constant:

const hasAction =
  (task.status === 'READY') ||
  (task.status === 'BACKLOG') ||
  (task.status === 'IN_PROGRESS' && \!\!onStop) ||
  (task.status === 'FAILED' && \!\!onReset);

This also documents that the button only appears when the handler is wired up.

4. handleClearSelection always fires after bulk stop/reset

In the finally block of handleConfirmAction, handleClearSelection() deselects everything regardless of partial failure. When 3 of 5 tasks fail to stop, the user loses visibility into which ones succeeded. Consider skipping clear-selection when there were failures, so the user can see the affected tasks and retry or investigate.

5. BatchActionsBar scans selectedTasks three times

Three separate .filter() calls on the same array is fine for small lists but easy to combine:

const { readyCount, inProgressCount, failedCount } = useMemo(() => {
  return selectedTasks.reduce(
    (acc, t) => {
      if (t.status === 'READY') acc.readyCount++;
      else if (t.status === 'IN_PROGRESS') acc.inProgressCount++;
      else if (t.status === 'FAILED') acc.failedCount++;
      return acc;
    },
    { readyCount: 0, inProgressCount: 0, failedCount: 0 }
  );
}, [selectedTasks]);

This is a style note, not a blocker.


Test Coverage Gaps

6. Batch partial-failure error is not tested

The handleConfirmAction path where some but not all tasks fail (failures.length > 0) sets an error message like 'Failed to stop 1 task(s)'. This branch has no test. Given it's an important failure state (the "partial failure handling" mentioned in the PR summary), it's worth adding a test in TaskBoardView.test.tsx that mocks one stopExecution call to reject.

7. handleDeselectAll is not directly tested in TaskBoardView

The TaskColumn tests cover onDeselectAll at the unit level, but the integration test in TaskBoardView doesn't exercise the deselect-all path. Minor, but worth noting.


What's Done Well

  • Race condition mitigation is correct: capturing taskIds into confirmAction state at dialog-open time isolates the batch from SWR revalidation mid-flight.
  • selectedTasks uses data?.tasks (not filteredTasks) — bulk actions won't silently skip tasks that are hidden by the filter.
  • Promise.allSettled for graceful partial failure in batch operations is the right choice.
  • AlertDialog over Dialog for destructive actions is appropriate for accessibility.
  • e.preventDefault() in AlertDialogAction correctly prevents auto-close before the async confirm finishes.
  • Loading state per-task (loadingTaskIds) prevents double-clicking and gives clear visual feedback.
  • Test coverage for TaskCard, BatchActionsBar, BulkActionConfirmDialog, and TaskColumn is solid.

Overall: Approve after addressing bug #1 (type ambiguity) and bug #2 (unsafe error cast). The rest are improvements worth picking up but not blockers.

- Remove 'execute' from BulkActionType (unreachable after earlier fix)
- Remove dead execute config from ACTION_CONFIG
- Add test for batch stop partial failure error message
- Update BulkActionConfirmDialog tests for narrowed type
- Add code review report documenting all resolved issues
@claude
Copy link

claude bot commented Feb 19, 2026

Review: Task Board Bulk Stop, Reset & State Management

Overall: The implementation is clean and well-structured. All issues flagged in the previous CodeRabbit review have been resolved — the selectedTasks scope fix (data?.tasks instead of filteredTasks) and the getByRole('status') test assertion are both confirmed in the diff. The try/finally restructure for loading state cleanup is correct. No blocking issues.


Observations

1. Single-task vs batch confirmation asymmetry

handleStop (card-level) fires immediately — no dialog. handleStopBatch (batch) shows BulkActionConfirmDialog. For a user who selects one IN_PROGRESS task and uses the batch Stop, they get a dialog. If they click the Stop button directly on the card, it runs without one. This is a defensible trade-off (single actions are low-stakes, batch is more dangerous), but it may surprise users. Worth a brief comment or a future UX note in the backlog.

2. setIsStoppingBatch(true) is inside try, not before it

try {
  if (confirmAction.type === 'stop') {
    setIsStoppingBatch(true);   // ← set after entering try
    const results = await Promise.allSettled(...)

There's a render cycle between handleConfirmAction being called and setIsStoppingBatch(true) taking effect. In practice React batches the two setState calls (setActionError(null) + setIsStoppingBatch(true)) in the same flush, so the Confirm button won't visually re-enable between clicks. Not a real bug, but it would be clearer to move both loading setters before the try block.

3. docs/code-review/ artifact committed to the repo

docs/code-review/2026-02-19-task-board-bulk-actions-review.md is a review artifact committed as source. Review history belongs in PR comments, not the tree — it won't be useful to future contributors and adds noise to git log. Consider removing it or moving it to an untracked location before merge.

4. Batch partial-failure message lacks task identity

setActionError(\Failed to stop ${failures.length} task(s)`)` tells the user how many failed but not which ones. For 1–2 tasks this is fine; for larger batches it makes recovery harder. Not blocking for v1, but aligns with the existing backlog item about retaining selection on partial failure.

5. BatchActionsBar action button counts vs "X selected" display

selectedCount={selectedTaskIds.size} (total selected IDs) is displayed as "X selected," while the Execute/Stop/Reset button counts come from selectedTasks (filtered to data?.tasks). These can briefly diverge during an SWR revalidation if a task was deleted mid-session. The user might see "3 selected" but "Execute 1, Stop 1" (2 total). No action needed — it self-corrects on next SWR poll — just worth being aware of.


What's working well

  • Promise.allSettled for partial-failure tolerance is the right call.
  • Race condition fix (freezing taskIds into confirmAction at dialog-open time) is solid.
  • Per-task loadingTaskIds Set prevents double-clicks without a global lock.
  • e.preventDefault() on AlertDialogAction correctly prevents auto-close during async confirm.
  • role="alert" / role="status" ARIA attributes and the dismiss button on the error banner are good a11y choices.
  • 302 tests with happy-path, error-path, partial-failure, and loading-state coverage.

Recommendation: Ready to merge after removing (or justifying) the docs/code-review/ file. The other observations are minor and appropriate for the backlog.

Acceptance criterion #1 from issue #340 requires clicking an IN_PROGRESS
task to navigate to the Execution Monitor. Adds a "View Execution" button
in the TaskDetailModal footer that closes the modal and navigates to
/execution/{taskId}.
@frankbria frankbria merged commit f871452 into main Feb 19, 2026
7 of 8 checks passed
@claude
Copy link

claude bot commented Feb 19, 2026

Post-Merge Review — PR #389: Task Board Bulk State Management

The PR is already merged and the committed review report (docs/code-review/2026-02-19-task-board-bulk-actions-review.md) captures the full history across three review rounds. This comment adds two small observations for the backlog.


Implementation Quality: Strong

The final state of the code is clean:

  • Race condition fix (confirmAction freezing task IDs at dialog-open time) is the right approach — prevents SWR revalidation from changing the batch mid-flight.
  • try/finally in handleConfirmAction correctly ensures loading states are always reset, even on error.
  • selectedTasks derived from data?.tasks (not filteredTasks) is the correct scoping — bulk actions work on all selected tasks regardless of active filter.
  • Promise.allSettled + partial failure reporting is the right pattern for fan-out API calls.
  • e.preventDefault() on AlertDialogAction prevents the Radix auto-close before the async confirm resolves.
  • ARIA attributes (role="alert", role="status", aria-label throughout) are applied consistently.

Minor Observations (backlog only, not blocking)

1. isLoading on BulkActionConfirmDialog includes isExecuting

// TaskBoardView.tsx
isLoading={isStoppingBatch || isResettingBatch || isExecuting}

The dialog is only reachable via the stop/reset path, so isExecuting will never be true when this dialog is open. The || isExecuting is dead code in practice. Not harmful, but could cause confusion when reading the component.

2. Both loading flags reset in finally even when only one was set

finally {
  setIsStoppingBatch(false);  // reset unconditionally
  setIsResettingBatch(false); // reset unconditionally
  ...
}

This is correct and safe — resetting a flag that was already false is a no-op in React. But if the action types ever diverge (e.g., a third BulkActionType is added), this pattern could mask a bug where the wrong flag is reset. A small refactor to setIsStoppingBatch(confirmAction.type === 'stop' ? false : prev) is not worth doing now, but worth noting if the type union expands.


Both issues are documented trade-offs, not defects. The implementation is solid and follows existing codebase patterns throughout.

@frankbria frankbria deleted the feature/issue-340-task-board-bulk-actions branch February 19, 2026 20:01
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.

[Phase 3] Task Board: bulk stop, reset, and state management actions

1 participant