From b11da780977427c861128b0543eb3aff9aa43515 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 5 Jul 2026 23:16:46 +0000 Subject: [PATCH] Wire up autonomous agent dispatch for Kanban tasks Assigning a task to opencode, hermes, or gemini now actually runs it: creating or PATCHing a task with one of those assignees moves it to in_progress and hands the title+body to execute_agent() in a background thread. On completion the agent's response is appended as a task comment and the task is marked done (success) or blocked (timeout/error), matching the existing block/complete state machine. Adds POST /api/kanban/tasks/{id}/dispatch for manual dispatch/retry, and makes the previously-stubbed POST /api/kanban/dispatch actually scan todo/ready tasks with an agent assignee and dispatch each one. Dashboard: the assignee field is now a select of the three agents, the task detail modal shows an activity log of past agent runs and a Dispatch button, and polls every 3s while a task is in_progress. Also fixed a pre-existing bug where the detail modal read a nonexistent kanbanData.tasks field (the board API only returns columns), so clicking a card silently did nothing before this fix. --- dashboard/api.js | 1 + dashboard/pages/kanban.js | 76 ++++++++++++++++++++++++++++++++++----- server.py | 75 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 141 insertions(+), 11 deletions(-) diff --git a/dashboard/api.js b/dashboard/api.js index 886e71f..b2bf437 100644 --- a/dashboard/api.js +++ b/dashboard/api.js @@ -67,6 +67,7 @@ const api = { linkKanbanTasks: (parentId, childId) => api.post('/api/kanban/links', { parent_id: parentId, child_id: childId }), unlinkKanbanTasks: (parentId, childId) => api.del(`/api/kanban/links?parent_id=${encodeURIComponent(parentId)}&child_id=${encodeURIComponent(childId)}`), dispatchKanban: () => api.post('/api/kanban/dispatch', {}), + dispatchKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/dispatch`, {}), specifyKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/specify`, {}), decomposeKanbanTask: (id) => api.post(`/api/kanban/tasks/${encodeURIComponent(id)}/decompose`, {}), // Goals diff --git a/dashboard/pages/kanban.js b/dashboard/pages/kanban.js index fee2bb3..333e437 100644 --- a/dashboard/pages/kanban.js +++ b/dashboard/pages/kanban.js @@ -1,5 +1,9 @@ let kanbanData = null; +function getAllKanbanTasks() { + return Object.values(kanbanData?.columns || {}).flat(); +} + async function renderKanban() { const content = document.getElementById('pageContent'); content.innerHTML = ` @@ -53,7 +57,7 @@ function renderKanbanBoard() { if (!board || !kanbanData) return; const columnsObj = kanbanData.columns || {}; const columns = Object.keys(columnsObj); - const allTasks = kanbanData.tasks || Object.values(columnsObj).flat(); + const allTasks = getAllKanbanTasks(); const filterText = (document.getElementById('kanbanFilterInput')?.value || '').toLowerCase(); const filterPriority = document.getElementById('kanbanFilterPriority')?.value || 'all'; const filterCategory = document.getElementById('kanbanFilterCategory')?.value || 'all'; @@ -147,7 +151,12 @@ function showAddKanbanTask() {
- +
@@ -206,31 +215,68 @@ async function onKanbanDrop(e, status) { } } -function showKanbanDetail(id) { - const task = kanbanData?.tasks?.find(t => t.id === id); +const KANBAN_AGENTS = ['opencode', 'hermes', 'gemini']; + +function closeKanbanDetailModal() { + if (window._kanbanDetailPoll) { clearInterval(window._kanbanDetailPoll); window._kanbanDetailPoll = null; } + closeModal(); +} + +async function showKanbanDetail(id) { + const task = getAllKanbanTasks().find(t => t.id === id); if (!task) return; + renderKanbanDetailModal(task); + + if (window._kanbanDetailPoll) clearInterval(window._kanbanDetailPoll); + if (task.status === 'in_progress') { + window._kanbanDetailPoll = setInterval(async () => { + try { + const fresh = await api.getKanbanTask(id); + if (fresh.status !== 'in_progress') { + clearInterval(window._kanbanDetailPoll); + window._kanbanDetailPoll = null; + renderKanban(); + } + if (document.getElementById('kanbanDetailModal')) renderKanbanDetailModal(fresh); + } catch {} + }, 3000); + } +} + +function renderKanbanDetailModal(task) { const modal = document.getElementById('modalContainer'); + const canDispatch = KANBAN_AGENTS.includes(task.assignee) && task.status !== 'in_progress' && task.status !== 'done'; + const comments = task.comments || []; modal.innerHTML = ` -