From 92f306bb49f345fd6ec4735f21377f120e4c861c Mon Sep 17 00:00:00 2001 From: Joseph Fung Date: Tue, 24 Mar 2026 09:51:48 -0400 Subject: [PATCH 1/2] fix: dashboard submits to wrong endpoint and doesn't handle completion events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the dashboard frontend: 1. Chat messages were sent to POST /api/steer (mid-flight steering for running tasks) instead of POST /api/task (submits to orchestrator). Since no task was running with the default jobId, messages went to a file on disk that nothing read. 2. The SSE handler only cleared "Working..." on job_update events, which the backend never sends. Added handlers for done, task.end, and job_failed — the event types the orchestrator actually emits. Also suppress task.start, turn.start, turn.end lifecycle events from creating noise in the chat as system messages. --- src/dashboard/frontend/src/App.tsx | 39 ++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/dashboard/frontend/src/App.tsx b/src/dashboard/frontend/src/App.tsx index a2f9e6a..4a0943f 100644 --- a/src/dashboard/frontend/src/App.tsx +++ b/src/dashboard/frontend/src/App.tsx @@ -229,12 +229,9 @@ const App: React.FC = () => { setTaskSteps([]); try { - await axios.post('/api/steer', { - jobId: selectedJob, - message: prompt, - author: 'operator', - source: 'dashboard', - }); + // Submit as a new task to the orchestrator (not /api/steer, which is + // for injecting mid-flight corrections into already-running tasks) + await axios.post('/api/task', { prompt }); } catch (err) { console.error('Task submission failed', err); setMessages(prev => [ @@ -379,10 +376,36 @@ const App: React.FC = () => { let msgType: Message['type'] = 'agent'; let content = ''; - if (data.type === 'job_update') { + if (data.type === 'done') { + // Task completed — the result text was already streamed via 'text' events, + // so we only add a message if the agent bubble is missing. + const text = data.data?.text ?? ''; + if (text) { + setMessages(prev => { + const last = prev[prev.length - 1]; + if (last?.type === 'agent') return prev; // already shown via text streaming + return [...prev, { id: ++messageIdCounter, type: 'agent' as const, content: text, timestamp: new Date() }].slice(-MAX_MESSAGES); + }); + } + setTaskRunning(false); + return; + } else if (data.type === 'task.end') { + // Lifecycle marker — clear progress in case 'done' was missed + setTaskRunning(false); + return; + } else if (data.type === 'job_failed') { + msgType = 'system'; + content = `Task failed: ${data.data?.error ?? 'Unknown error'}`; + setTaskRunning(false); + } else if (data.type === 'task.start') { + // Task accepted — already tracking via taskRunning, no visible message needed + return; + } else if (data.type === 'turn.start' || data.type === 'turn.end') { + // Turn lifecycle markers — no visible action needed + return; + } else if (data.type === 'job_update') { msgType = 'system'; content = `Task ${data.data?.status === 'completed' ? 'completed' : data.data?.status ?? 'update'}`; - // Track task completion if (data.data?.status === 'completed' || data.data?.status === 'failed') { setTaskRunning(false); } From b02cb86526bf4331b8299bde7fe8f462991b655f Mon Sep 17 00:00:00 2001 From: Joseph Fung Date: Thu, 23 Apr 2026 19:14:38 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20keep=20setMessages=20updater=20pure?= =?UTF-8?q?=20=E2=80=94=20hoist=20id/timestamp=20out=20of=20callback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dashboard/frontend/src/App.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dashboard/frontend/src/App.tsx b/src/dashboard/frontend/src/App.tsx index 4a0943f..722cade 100644 --- a/src/dashboard/frontend/src/App.tsx +++ b/src/dashboard/frontend/src/App.tsx @@ -381,10 +381,14 @@ const App: React.FC = () => { // so we only add a message if the agent bubble is missing. const text = data.data?.text ?? ''; if (text) { + // Capture id and timestamp outside the updater so the function stays pure + // (React may call updaters multiple times in Strict Mode / concurrent rendering) + const nextId = ++messageIdCounter; + const now = new Date(); setMessages(prev => { const last = prev[prev.length - 1]; if (last?.type === 'agent') return prev; // already shown via text streaming - return [...prev, { id: ++messageIdCounter, type: 'agent' as const, content: text, timestamp: new Date() }].slice(-MAX_MESSAGES); + return [...prev, { id: nextId, type: 'agent' as const, content: text, timestamp: now }].slice(-MAX_MESSAGES); }); } setTaskRunning(false);