Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions src/dashboard/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 => [
Expand Down Expand Up @@ -379,10 +376,40 @@ 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) {
// 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: nextId, type: 'agent' as const, content: text, timestamp: now }].slice(-MAX_MESSAGES);
});
Comment on lines +388 to +392

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

React state updaters should be pure functions. Performing side effects like incrementing a global counter ('++messageIdCounter') or creating a 'new Date()' inside the 'setMessages' updater can lead to unpredictable behavior, especially in React's Strict Mode or with concurrent rendering where the updater might be called multiple times. It is better to generate these values before calling the state setter.

            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: nextId, type: 'agent' as const, content: text, timestamp: now }].slice(-MAX_MESSAGES);
            });

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in b02cb86. Hoisted nextId and now out of the updater so the setMessages callback is a pure function. Thanks!

}
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);
}
Expand Down