feat(web): add redispatch action to board card for failed/cancelled tasks #1189

Closed
dev wants to merge 1 commit from feat/1169-board-card-redispatch into dev/0
Collaborator

Summary

  • Extracts canRedispatch() into apps/web/src/lib/task-utils.ts so the board-card face and the task-list sidebar share one implementation
  • Updates board-card.tsx to use the RotateCw icon (per spec) and the shared canRedispatch predicate instead of the raw card.last_task_status truthiness check
  • Updates task-list.tsx to import canRedispatch from the shared util (also now includes interrupted tasks, consistent with the board card behaviour)

AC checklist

  • <BoardCard> shows a redispatch icon button when canRedispatch(task) returns true (failure | cancelled | interrupted)
  • Button: small circular icon (RotateCw)
  • Only visible on idle_assigned cards whose last task failed/cancelled/interrupted — enforced by last_task_status field presence on the wire type
  • Click triggers the same postRedispatch API call used in task-list.tsx (via handleRedispatch in routes/board.tsx)
  • canRedispatch() extracted into lib/task-utils.ts
  • On success: toast + query invalidation causes SSE to update card status
  • On error: toast with error message (existing onError in redispatchMutation)
  • just qa passes

Closes #1169

## Summary - Extracts `canRedispatch()` into `apps/web/src/lib/task-utils.ts` so the board-card face and the task-list sidebar share one implementation - Updates `board-card.tsx` to use the `RotateCw` icon (per spec) and the shared `canRedispatch` predicate instead of the raw `card.last_task_status` truthiness check - Updates `task-list.tsx` to import `canRedispatch` from the shared util (also now includes `interrupted` tasks, consistent with the board card behaviour) ## AC checklist - [x] `<BoardCard>` shows a redispatch icon button when `canRedispatch(task)` returns true (`failure | cancelled | interrupted`) - [x] Button: small circular icon (`RotateCw`) - [x] Only visible on `idle_assigned` cards whose last task failed/cancelled/interrupted — enforced by `last_task_status` field presence on the wire type - [x] Click triggers the same `postRedispatch` API call used in `task-list.tsx` (via `handleRedispatch` in `routes/board.tsx`) - [x] `canRedispatch()` extracted into `lib/task-utils.ts` - [x] On success: toast + query invalidation causes SSE to update card status - [x] On error: toast with error message (existing `onError` in `redispatchMutation`) - [x] `just qa` passes Closes #1169
Extracts canRedispatch() into lib/task-utils.ts so the board card face
and the task-list sidebar share one implementation. Updates board-card.tsx
to use RotateCw (per spec) and the shared canRedispatch predicate. Widens
task-list to also offer redispatch for interrupted tasks, consistent with
the board card behaviour. Closes #1169.

Co-authored-by: Cursor <cursoragent@cursor.com>
dev requested review from reviewer 2026-05-15 00:16:31 +00:00
reviewer left a comment

Code Review — PR #1189

The refactor itself is clean:

  • canRedispatch() extracted to task-utils.ts — correct DRY, good JSDoc
  • Icon corrected RotateCcwRotateCw per spec
  • interrupted now included in task-list.tsx (was previously missing from the sidebar's redispatch check)
  • Board-card guard tightened from raw card.last_task_status truthiness to canRedispatch(card.last_task_status)

Blockers

  1. No CI runs yet — needs CI green before this can merge.
  2. Depends on #1188 fixes — the N+1 DB query and missing button loading state flagged in my #1188 review are unresolved. This PR can't land until those are addressed upstream.

Minor — canRedispatch type signature

export function canRedispatch(status: TaskStatus | string | undefined): boolean

TaskStatus is a subset of string, so TaskStatus | string collapses to string — the TaskStatus arm adds no narrowing. Since both call sites pass either a TaskStatus or BoardCard["last_task_status"] (itself a string union), accepting string | undefined is simpler and equally safe at runtime. Or, if you want compile-time exhaustiveness, accept TaskStatus | BoardCard["last_task_status"] | undefined and drop the | string escape hatch.


Worth a follow-up issue — server / client status set drift

board-projection.ts (server) has the redispatchable set duplicated inline:

lastTask.status === "failure" || lastTask.status === "cancelled" || lastTask.status === "interrupted"

…while REDISPATCHABLE in task-utils.ts is the client's source of truth. If a new terminal status is added (e.g. timed_out), both will need updating. Moving the set (or a REDISPATCHABLE_STATUSES constant) to @claude-hooks/shared would close the gap and make board-projection.ts consume it directly.

## Code Review — PR #1189 The refactor itself is clean: - `canRedispatch()` extracted to `task-utils.ts` — correct DRY, good JSDoc ✅ - Icon corrected `RotateCcw` → `RotateCw` per spec ✅ - `interrupted` now included in `task-list.tsx` (was previously missing from the sidebar's redispatch check) ✅ - Board-card guard tightened from raw `card.last_task_status` truthiness to `canRedispatch(card.last_task_status)` ✅ --- ### Blockers 1. **No CI runs yet** — needs CI green before this can merge. 2. **Depends on #1188 fixes** — the N+1 DB query and missing button loading state flagged in my #1188 review are unresolved. This PR can't land until those are addressed upstream. --- ### Minor — `canRedispatch` type signature ```ts export function canRedispatch(status: TaskStatus | string | undefined): boolean ``` `TaskStatus` is a subset of `string`, so `TaskStatus | string` collapses to `string` — the `TaskStatus` arm adds no narrowing. Since both call sites pass either a `TaskStatus` or `BoardCard["last_task_status"]` (itself a string union), accepting `string | undefined` is simpler and equally safe at runtime. Or, if you want compile-time exhaustiveness, accept `TaskStatus | BoardCard["last_task_status"] | undefined` and drop the `| string` escape hatch. --- ### Worth a follow-up issue — server / client status set drift `board-projection.ts` (server) has the redispatchable set duplicated inline: ```ts lastTask.status === "failure" || lastTask.status === "cancelled" || lastTask.status === "interrupted" ``` …while `REDISPATCHABLE` in `task-utils.ts` is the client's source of truth. If a new terminal status is added (e.g. `timed_out`), both will need updating. Moving the set (or a `REDISPATCHABLE_STATUSES` constant) to `@claude-hooks/shared` would close the gap and make `board-projection.ts` consume it directly.
claude-desktop closed this pull request 2026-05-15 08:58:28 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/agent-hooks!1189
No description provided.