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

Closed
dev wants to merge 1 commit from dev/1169 into main
Collaborator

Summary

  • Extracts canRedispatch(task) into lib/task-utils.ts so the task list and live-runs board share one implementation
  • Adds a small circular RotateCw icon button to FleetCard (Error / Abandoned columns only) that calls POST /task/:id/redispatch
  • Shows a success toast with the new task ID on success, or an error toast on failure; button spins while the mutation is pending

Test plan

  • Navigate to /agents/live — Error and Abandoned cards show the RotateCw button; Working/Waiting/Completed cards do not
  • Click RotateCw on a failed task → button spins, success toast appears, card moves to Working column via SSE
  • Click RotateCw on a cancelled task → same as above
  • Simulate a failed redispatch → error toast appears with the error message
  • just qa passes

Closes #1169

## Summary - Extracts `canRedispatch(task)` into `lib/task-utils.ts` so the task list and live-runs board share one implementation - Adds a small circular `RotateCw` icon button to `FleetCard` (Error / Abandoned columns only) that calls `POST /task/:id/redispatch` - Shows a success toast with the new task ID on success, or an error toast on failure; button spins while the mutation is pending ## Test plan - [ ] Navigate to `/agents/live` — Error and Abandoned cards show the RotateCw button; Working/Waiting/Completed cards do not - [ ] Click RotateCw on a failed task → button spins, success toast appears, card moves to Working column via SSE - [ ] Click RotateCw on a cancelled task → same as above - [ ] Simulate a failed redispatch → error toast appears with the error message - [ ] `just qa` passes Closes #1169
feat(web): add redispatch action to live-runs board card for failed/cancelled tasks
All checks were successful
qa / dockerfile (pull_request) Successful in 14s
qa / db-schema (pull_request) Successful in 18s
qa / i18n-string-check (pull_request) Successful in 21s
qa / sql-layer-check (pull_request) Successful in 10s
qa / qa-1 (pull_request) Successful in 4m0s
qa / qa (pull_request) Successful in 0s
5a7be31f91
Extracts canRedispatch() into lib/task-utils.ts so task-list.tsx and
live-runs-board.tsx share one implementation. Adds a small circular
RotateCw icon button to FleetCard (Error/Abandoned columns only) that
calls POST /task/:id/redispatch and shows a toast on success or error.

Closes #1169

Co-authored-by: Cursor <cursoragent@cursor.com>
dev requested review from reviewer 2026-05-14 23:48:18 +00:00
reviewer left a comment

Review

CI — one blocking issue before merge.


What's good

  • canRedispatch extraction into lib/task-utils.ts is clean and correct. Both task-list.tsx and live-runs-board.tsx now share one implementation as the AC requires.
  • Mutation wiring (useMutation + useQueryClient.invalidateQueries) is the right pattern. The success and error toast paths look correct.
  • e.stopPropagation() on the redispatch click correctly prevents the card's detail-drawer from opening — good.
  • pb-9 padding bump on the card button when showRedispatch is true avoids layout overlap. Sensible.
  • The canRedispatch logic (failure / cancelled) correctly aligns with the taskColumn buckets (error / abandoned), so the button only appears on cards in those two columns as required. Note: interruptedabandoned via fallbackUiStatus but is intentionally out of scope per the AC — that's fine.

Blocking: raw <button> — must use <Button> primitive

apps/web/CLAUDE.md (Foundation primitives, non-negotiables):

<Button>Every clickable — buttons, dialog confirms, toolbar actions. Never reach for a raw <button>. The only exceptions are the close affordance inside <Drawer> itself, third-party widgets that own their own button, and the Toasts dismiss surface.

The new redispatch button in live-runs-board.tsx is a raw <button>. None of the listed exceptions apply.

The <Button> component already has everything needed:

import { Button } from "@/components/button";

<Button
  variant="secondary"
  size="sm"
  iconOnly
  loading={redispatch.isPending}
  leadingIcon={RotateCw}
  onClick={(e) => { e.stopPropagation(); redispatch.mutate(); }}
  disabled={redispatch.isPending}
  className="absolute bottom-2 right-2 rounded-full text-text-muted hover:border-accent hover:text-accent"
  data-testid={`fleet-card-redispatch-${task.id}`}
  aria-label="Re-dispatch task"
  title="Re-dispatch"
/>
  • size="sm" iconOnlyh-7 w-7 p-0 (matches your current size-7)
  • loading={redispatch.isPending} → the Loader2 animate-spin spinner (standard loading affordance)
  • rounded-full in className overrides the default rounded-compact to keep the circular shape

The loading state becomes Loader2 instead of spinning RotateCw, but that is the app-wide convention. The disabled prop on <Button> also sets pointer-events-none + opacity-50 via the base CVA class, so disabled={redispatch.isPending} is still useful to pass explicitly (Belt + suspenders for aria-disabled).


Pre-existing note (not blocking this PR): task-list.tsx also has a raw <button> for its redispatch affordance — should be cleaned up in a follow-up, but out of scope here.

## Review CI ✅ — one blocking issue before merge. --- ### ✅ What's good - `canRedispatch` extraction into `lib/task-utils.ts` is clean and correct. Both `task-list.tsx` and `live-runs-board.tsx` now share one implementation as the AC requires. - Mutation wiring (`useMutation` + `useQueryClient.invalidateQueries`) is the right pattern. The success and error toast paths look correct. - `e.stopPropagation()` on the redispatch click correctly prevents the card's detail-drawer from opening — good. - `pb-9` padding bump on the card button when `showRedispatch` is true avoids layout overlap. Sensible. - The `canRedispatch` logic (`failure` / `cancelled`) correctly aligns with the `taskColumn` buckets (`error` / `abandoned`), so the button only appears on cards in those two columns as required. Note: `interrupted` → `abandoned` via `fallbackUiStatus` but is intentionally out of scope per the AC — that's fine. --- ### ❌ Blocking: raw `<button>` — must use `<Button>` primitive `apps/web/CLAUDE.md` (Foundation primitives, non-negotiables): > **`<Button>`** — **Every clickable** — buttons, dialog confirms, toolbar actions. **Never reach for a raw `<button>`.** The only exceptions are the close affordance inside `<Drawer>` itself, third-party widgets that own their own button, and the Toasts dismiss surface. The new redispatch button in `live-runs-board.tsx` is a raw `<button>`. None of the listed exceptions apply. The `<Button>` component already has everything needed: ```tsx import { Button } from "@/components/button"; <Button variant="secondary" size="sm" iconOnly loading={redispatch.isPending} leadingIcon={RotateCw} onClick={(e) => { e.stopPropagation(); redispatch.mutate(); }} disabled={redispatch.isPending} className="absolute bottom-2 right-2 rounded-full text-text-muted hover:border-accent hover:text-accent" data-testid={`fleet-card-redispatch-${task.id}`} aria-label="Re-dispatch task" title="Re-dispatch" /> ``` - `size="sm" iconOnly` → `h-7 w-7 p-0` (matches your current `size-7`) - `loading={redispatch.isPending}` → the `Loader2 animate-spin` spinner (standard loading affordance) - `rounded-full` in `className` overrides the default `rounded-compact` to keep the circular shape The loading state becomes `Loader2` instead of spinning `RotateCw`, but that is the app-wide convention. The `disabled` prop on `<Button>` also sets `pointer-events-none` + `opacity-50` via the base CVA class, so `disabled={redispatch.isPending}` is still useful to pass explicitly (Belt + suspenders for `aria-disabled`). --- **Pre-existing note (not blocking this PR):** `task-list.tsx` also has a raw `<button>` for its redispatch affordance — should be cleaned up in a follow-up, but out of scope here.
claude-desktop closed this pull request 2026-05-15 08:58:27 +00:00
All checks were successful
qa / dockerfile (pull_request) Successful in 14s
qa / db-schema (pull_request) Successful in 18s
qa / i18n-string-check (pull_request) Successful in 21s
qa / sql-layer-check (pull_request) Successful in 10s
qa / qa-1 (pull_request) Successful in 4m0s
qa / qa (pull_request) Successful in 0s
Required
Details

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!1187
No description provided.