From 54be094e65ffafe69e3490cbe9b5ae37d1fa927d Mon Sep 17 00:00:00 2001 From: Claudomator Agent Date: Mon, 6 Jul 2026 01:29:45 +0000 Subject: feat(web): add Tasks board column model (TASK_COLUMNS, columnForTaskState, groupTasksByColumn) Pure logic layer for the new Tasks tab Kanban board: a 5-column partition of all 10 task states (Queue/Running/Ready/Interrupted/Done) with per-column sort directions (Queue+Ready oldest-first, Running+Interrupted+Done newest-first). Also fixes 2 pre-existing stale assertions in tab-persistence.test.mjs (default tab is 'stories', not 'queue', since cc6b323). Co-Authored-By: Claude Sonnet 4.6 --- web/app.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) (limited to 'web/app.js') diff --git a/web/app.js b/web/app.js index 2f5aacf..9cd3100 100644 --- a/web/app.js +++ b/web/app.js @@ -3350,6 +3350,60 @@ export function groupStoriesByColumn(stories) { return groups; } +// --------------------------------------------------------------------------- +// Tasks board — column model +// --------------------------------------------------------------------------- + +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'] }, +]; + +// columnForTaskState returns the column key for a given task state, falling +// back to 'queue' for an empty/unrecognized state so a task is never dropped +// off the board entirely. +export function columnForTaskState(state) { + const col = TASK_COLUMNS.find(c => c.states.includes(state)); + return col ? col.key : 'queue'; +} + +// Sort directions are per-column, matching the semantics the old (removed) +// panels used: +// queue — oldest-first (FIFO) +// ready — oldest-first (longest-waiting surfaces first) +// interrupted — newest-first (most recent failure is most urgent) +// done — newest-first (most recently completed is most relevant) +// running — newest-first (new-but-inconsequential default) +const TASK_COLUMN_SORT = { + queue: (a, b) => new Date(a.created_at || 0) - new Date(b.created_at || 0), + running: (a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0), + ready: (a, b) => new Date(a.created_at || 0) - new Date(b.created_at || 0), + interrupted: (a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0), + done: (a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0), +}; + +// groupTasksByColumn returns { [columnKey]: task[] }, each sub-array sorted +// per the per-column sort direction above. +export function groupTasksByColumn(tasks) { + const groups = {}; + for (const col of TASK_COLUMNS) groups[col.key] = []; + for (const t of tasks || []) { + const key = columnForTaskState(t.state); + if (!groups[key]) groups[key] = []; + groups[key].push(t); + } + for (const col of TASK_COLUMNS) { + const cmp = TASK_COLUMN_SORT[col.key]; + if (cmp) groups[col.key].sort(cmp); + } + return groups; +} + +// --------------------------------------------------------------------------- + // A story "has reached validation" once evaluator verdicts could plausibly // exist for it — VALIDATING or later in the lifecycle (see StoryOrchestrator // stage 2 in CLAUDE.md). Used to gate the eval-verdict indicator so we don't -- cgit v1.2.3