summaryrefslogtreecommitdiff
path: root/docs/superpowers/specs/2026-07-06-tasks-board-design.md
blob: 50b6af82b5085a5321814fa9732e6c76c39777ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# Unified Tasks Board — Design Spec

**Date:** 2026-07-06
**Status:** Approved

---

## Goal

The web UI's "Board" (Stories tab) only shows Stories — a Kanban of the `stories` table. Plain top-level tasks (e.g. anything submitted directly via the chatbot MCP `submit_task` tool, with no story association) have no visibility anywhere in the current nav: the old Queue/Interrupted/Ready/Running tabs that used to show them were removed in commit `cc6b323` (2026-07-05), in favor of a 4-tab nav (Stories/Stats/Drops/Settings).

Add a single new "Tasks" tab: a Kanban board of all tasks (story-linked or not), grouped by state into columns, so a task's whole lifecycle — created → queued → running → ready/interrupted → done — is visible at a glance, with live/replayed execution logs inline on every card without needing to click in.

---

## Context

Investigation while root-causing an unrelated bug (chatbot-submitted tasks being auto-cancelled — since fixed) surfaced that a large amount of UI infrastructure for this already exists in `web/app.js`, just disconnected from the current nav after `cc6b323`:

- `createTaskCard(task)` — full-featured task card: name, state badge, priority, created-at, project, description, error message (for failed states), checker-report flag, changestats/deployment badges, and state-appropriate action buttons (Resume/Restart/Cancel/Accept/Reject).
- `openTaskPanel(taskId)` → `renderTaskPanel(task, executions)` — side-panel task detail view: summary, Q&A history, overview, agent config, and (implicitly, via execution rows) access to full log history.
- `renderRunningView(tasks)` / `startRunningLogStream(taskId, logArea)` — the old Running tab's per-card live log tailing via SSE, with elapsed timer, exec ID, and a Cancel button.
- `openLogViewer(execId, containerEl)` — a fuller log viewer, same SSE mechanism.
- `STORY_COLUMNS` / `columnForStatus` / `groupStoriesByColumn` — the exact column-grouping pattern to mirror for tasks (see `renderStoriesBoard`).

None of this was deleted; only the nav buttons, panel containers (`index.html`), and the `renderActiveTab` switch cases dispatching to them were removed. CSS (`.stories-board`, `.stories-column*`) is untouched and generic enough to reuse for the new board without duplication.

**Also discovered:** `web/api/executions/{id}/logs/stream` (`internal/api/logs.go`, `handleStreamLogs`) already handles both live and terminal executions transparently — for a `RUNNING` execution it live-tails (polling every 250ms); for any terminal execution it replays the whole stored log once, then immediately emits `done`. This means the exact same log-streaming code path works, unmodified, for a currently-running task's card and for a long-finished one's card — no separate "static tail" fetch mechanism is needed.

---

## Design

### 1. Nav & entry point

One new top-level tab, added to the existing 4 (Stories/Stats/Drops/Settings) — 5 total:

```html
<button class="tab" data-tab="tasks" title="Tasks">📝</button>
...
<div data-panel="tasks" hidden>
  <div class="tasks-board"></div>
</div>
```

Default landing tab remains Stories (unchanged from current behavior — confirmed with user, not reverting to the old `'queue'` default).

### 2. Column model

New pure functions in `app.js`, structurally identical to `STORY_COLUMNS`/`columnForStatus`/`groupStoriesByColumn`:

```js
export const TASK_COLUMNS = [
  { key: 'queue',       label: 'Queue',       states: ['PENDING', 'QUEUED'] },
  { key: 'running',     label: 'Running',     states: ['RUNNING', 'BLOCKED'] },
  { key: 'ready',       label: 'Ready',       states: ['READY'] },
  { key: 'interrupted', label: 'Interrupted', states: ['FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED'] },
  { key: 'done',        label: 'Done',        states: ['COMPLETED'] },
];

export function columnForTaskState(state) { /* find matching column, fallback 'queue' */ }
export function groupTasksByColumn(tasks) { /* { [columnKey]: task[] }, sorted per-column (see below) */ }
```

Sort direction is per-column, matching the semantics the old (removed) panels already used — not a uniform order:
- **Queue**: oldest-first (ascending) — FIFO; matches old `renderQueuePanel`.
- **Ready**: oldest-first (ascending) — the longest-waiting-for-review item surfaces first, so nothing sits forgotten; matches old `renderReadyPanel`.
- **Interrupted**: newest-first (descending) — the most recent failure is the most urgent to look at; matches old `renderInterruptedPanel`.
- **Done**: newest-first (descending) — the most recently completed task is most relevant; matches old `renderReadyPanel`'s completed-history sub-section.
- **Running**: newest-first (descending) — the old `renderRunningView` applied no explicit sort (rarely more than a couple of cards, bounded by `max_concurrent`), so this is a new-but-inconsequential default rather than a faithful restoration.

This is a clean, mutually-exclusive partition of all 10 task states (`internal/task` state machine). Design decisions, confirmed with user:
- **BLOCKED → Running**: not an error state — the task is actively in-flight, just paused on a subtask or an `ask_user` answer. Grouping it with Running keeps "things currently in motion" together.
- **TIMED_OUT / BUDGET_EXCEEDED → Interrupted**: both are abnormal stops needing a human decision (resume, bump budget), same bucket as FAILED/CANCELLED — all four already have Resume/Restart buttons on their cards via `createTaskCard`.

Unlike the Stories board, no drag-and-drop: a task's state is driven entirely by the executor/state machine, not manually reassignable by dragging a card to a new column.

### 3. Rendering

`renderTasksBoard(tasks, container)` mirrors `renderStoriesBoard` (same `.tasks-board`/`.tasks-column`/`.tasks-column-header`/`.tasks-column-count`/`.tasks-column-list` CSS classes as `.stories-board` etc. — visually identical column chrome, no CSS duplication needed since the existing rules are generic flex-column layout, not story-specific).

Each column's task list renders via the existing `renderTasksIntoContainer()` helper (non-destructive: reuses/updates existing card DOM nodes rather than a full teardown-and-rebuild on every poll tick, exactly as the old Queue/Ready/Interrupted panels already did).

### 4. Inline log tail on every card (no click required)

Every card, in every column, gets an inline log area wired to `/api/executions/{id}/logs/stream` for that task's most recent execution (fetched via the existing `/api/executions?task_id=X&limit=1` call already used by `startRunningLogStream`):

- **Running column**: reuses the existing larger, prominent log panel from the old `renderRunningView` card design (elapsed timer, agent/model, exec ID, live-tailing log, Cancel button) — this is a true live stream, since the execution is actively producing output.
- **Queue column**: no execution exists yet for PENDING/QUEUED tasks (no `executions` row until dispatch) — shows a plain "Waiting to start…" placeholder instead of an empty log box. This is a natural consequence of the data model, not a gap to build around.
- **Ready / Interrupted / Done columns**: a smaller, compact log excerpt (a handful of lines, fixed small max-height, scrollable) using the *same* SSE mechanism — since the execution is terminal, the endpoint replays the full stored log once and closes immediately, so this reads as "last lines of what happened," not a live stream, with no separate fetch path needed.

**Rerender/lifecycle trade-off (the one real engineering wrinkle):** `renderTasksIntoContainer`'s existing HTML-diff-and-replace strategy would constantly detect a log-populated card as "changed" (since log lines keep appending) and repeatedly tear down/rebuild it, closing and reopening the stream every poll tick. Fix: the log area is a stable child element *excluded* from the HTML-diff comparison — built once per task ID and left alone across re-renders — with stream lifecycle tracked in a small map (`taskId → EventSource`), generalizing the existing `runningViewLogSources` map (currently Running-tab-only) to cover every column. A stream is opened once per visible task needing one, left open/replaying, and closed only when that task's card is no longer on the board or state changes require a fresh execution's log.

Total concurrent open SSE connections is naturally bounded: Queue opens none, Ready/Interrupted/Done connections close almost immediately after replay, and truly-live connections are capped by `max_concurrent` in `claudomator.toml` (currently 3) since that's the ceiling on simultaneously-RUNNING tasks regardless of board size.

### 5. Out of scope

- Budget/Roles stay exactly where they are (merged into Settings, per `cc6b323`) — not reviving those as separate tabs.
- No drag-and-drop / manual state reassignment on the Tasks board.
- No changes to the Stories board, its columns, or its drag-and-drop.

---

## Testing

- New `web/test/tasks-board.test.mjs`, mirroring `web/test/stories-board.test.mjs`'s structure: column-mapping completeness (every task state maps to exactly one column), fallback-state behavior, grouping/sorting correctness.
- Fix 2 pre-existing failing tests in `web/test/tab-persistence.test.mjs` (stale assertions that the default tab is `'queue'`, left broken since `cc6b323` changed the default to `'stories'` without updating this test) — update assertions to `'stories'`, matching actual current behavior. Drive-by fix since it's directly adjacent to the nav change this spec makes.
- No Go-side changes are anticipated (both REST endpoints this design relies on — `/api/tasks`, `/api/executions?task_id=`, `/api/executions/{id}/logs/stream` — already exist and are already exercised by existing Go tests); confirm with `go test ./...` after implementation regardless, as a safety check.