feat(dashboard): cancel queued tasks from the UI + extend /cancel to drop queued entries #302

Closed
opened 2026-04-23 23:40:16 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator watching the queue, I want to cancel a queued task (one that has not started running yet) directly from the dashboard, so that I don't have to wait for it to promote into "running" just to hit Cancel — and so that mis-dispatched work never burns a worker slot.

Current behaviour

apps/server/src/main.ts:685 (handleCancel) filters over busyWorkers — workers with a currentAbort + currentTask. A task sitting in worker.queued[] has no abort handle and is invisible to /cancel. The UI mirrors this: apps/web/src/components/task-detail.tsx:109 only renders the Cancel button when status === "running". Queued rows have no affordance.

Acceptance criteria

Backend

  • Extend /cancel to accept a queued-task identifier:
    • POST /cancel { "task_id": "<uuid>" } — when task_id matches a queued entry (scan each worker's queued[]), drop it from the queue, mark the persisted TaskRecord as cancelled, return { status: "dropped-from-queue", task_id, agent }.
    • Existing running-task behaviour unchanged when task_id matches a running task.
    • 404 when task_id matches neither running nor any queued.
  • Worker exposes a narrow dropQueuedById(taskId: string): TaskRecord | null method. Shared with the unassign-handler sibling ticket — land both against the same primitive.
  • SSE: emit task_cancelled with { task_id, agent, reason: "operator-dropped" | "operator-aborted" } so the dashboard can pop the row without a full refetch.

UI

  • task-detail.tsx: render the Cancel button when status === "queued" as well, with copy "Drop from queue" vs. "Cancel" for running. Same confirmation flow; on success, refetch the task detail.
  • Queue / Monitor list views: add a kebab / trailing action per queued row with "Drop from queue" (guarded by the existing operator-auth). Disabled when the row has already promoted to running.
  • Toast copy matches the backend reason: "Dropped abc12345 from boss-2 queue" vs. "Cancelled abc12345 on boss-2".

Tests

  • main.test.tsPOST /cancel unit tests: new cases for task_id pointing at a queued entry (drops + persists cancellation), at an unknown id (404), and at a running task (unchanged behaviour).
  • worker.test.ts: dropQueuedById removes the entry, persists the record, returns the dropped row; no-op for unknown id.
  • task-detail.test.tsx: Cancel button renders for queued status; click posts /cancel with the right body.

Out of scope

  • Bulk drop (select N queued rows + drop). Could add later if needed.
  • Cancelling all queued entries for a worker (that's what /cancel { agent } should eventually do — scope it to a follow-up, current handler semantics are per-running-task).
  • Rate-limiting operator cancel spam. The route is already authed; no observed abuse yet.

References

  • apps/server/src/main.ts:685handleCancel to extend
  • apps/server/src/background/worker.ts — queue internals, shared with sibling
  • apps/web/src/components/task-detail.tsx:109 — UI gate to widen
  • Sibling ticket: webhook issues.unassigned handler (reuses the same dropQueued* primitive)
## User story As an operator watching the queue, I want to cancel a queued task (one that has not started running yet) directly from the dashboard, so that I don't have to wait for it to promote into "running" just to hit Cancel — and so that mis-dispatched work never burns a worker slot. ## Current behaviour `apps/server/src/main.ts:685` (`handleCancel`) filters over `busyWorkers` — workers with a `currentAbort` + `currentTask`. A task sitting in `worker.queued[]` has no abort handle and is invisible to `/cancel`. The UI mirrors this: `apps/web/src/components/task-detail.tsx:109` only renders the Cancel button when `status === "running"`. Queued rows have no affordance. ## Acceptance criteria ### Backend - [ ] Extend `/cancel` to accept a queued-task identifier: - `POST /cancel { "task_id": "<uuid>" }` — when `task_id` matches a queued entry (scan each worker's `queued[]`), drop it from the queue, mark the persisted `TaskRecord` as `cancelled`, return `{ status: "dropped-from-queue", task_id, agent }`. - Existing running-task behaviour unchanged when `task_id` matches a running task. - 404 when `task_id` matches neither running nor any queued. - [ ] Worker exposes a narrow `dropQueuedById(taskId: string): TaskRecord | null` method. Shared with the unassign-handler sibling ticket — land both against the same primitive. - [ ] SSE: emit `task_cancelled` with `{ task_id, agent, reason: "operator-dropped" | "operator-aborted" }` so the dashboard can pop the row without a full refetch. ### UI - [ ] `task-detail.tsx`: render the Cancel button when `status === "queued"` as well, with copy "Drop from queue" vs. "Cancel" for running. Same confirmation flow; on success, refetch the task detail. - [ ] Queue / Monitor list views: add a kebab / trailing action per queued row with "Drop from queue" (guarded by the existing operator-auth). Disabled when the row has already promoted to running. - [ ] Toast copy matches the backend reason: "Dropped `abc12345` from boss-2 queue" vs. "Cancelled `abc12345` on boss-2". ### Tests - [ ] `main.test.ts` → `POST /cancel` unit tests: new cases for `task_id` pointing at a queued entry (drops + persists cancellation), at an unknown id (404), and at a running task (unchanged behaviour). - [ ] `worker.test.ts`: `dropQueuedById` removes the entry, persists the record, returns the dropped row; no-op for unknown id. - [ ] `task-detail.test.tsx`: Cancel button renders for `queued` status; click posts `/cancel` with the right body. ## Out of scope - Bulk drop (select N queued rows + drop). Could add later if needed. - Cancelling *all* queued entries for a worker (that's what `/cancel { agent }` should eventually do — scope it to a follow-up, current handler semantics are per-running-task). - Rate-limiting operator cancel spam. The route is already authed; no observed abuse yet. ## References - `apps/server/src/main.ts:685` — `handleCancel` to extend - `apps/server/src/background/worker.ts` — queue internals, shared with sibling - `apps/web/src/components/task-detail.tsx:109` — UI gate to widen - Sibling ticket: webhook `issues.unassigned` handler (reuses the same `dropQueued*` primitive)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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/claude-hooks#302
No description provided.