feat(dashboard): wire TOOL_CALL SSE events to timeline (#85)#87
feat(dashboard): wire TOOL_CALL SSE events to timeline (#85)#87Abernaughty merged 3 commits intomainfrom
Conversation
- Add 'tool_call' to SSEEventType union and ToolCallEvent interface (api.ts) - Add 'tool_call' listener and dispatch case in SSE client (sse.ts) - Add handleToolCall() to tasksStore — appends to task timeline (tasks.svelte.ts) - Add tool_call visual treatment in TimelineView — compact style, TOOL badge, success/fail indicator, truncated result preview - Session debrief now shows tool call count in Events metric Closes #85
📝 WalkthroughWalkthroughDashboard wiring for backend TOOL_CALL SSE events: new Changes
Sequence DiagramsequenceDiagram
participant Backend as Backend/Runner
participant SSE as SSE Connection
participant Router as SSE Router
participant Store as Tasks Store
participant Timeline as Timeline View
Backend->>SSE: Emit TOOL_CALL {task_id, agent, tool, success, result_preview}
SSE->>Router: Stream 'tool_call' event
Router->>Router: Parse & validate payload
Router->>Store: tasksStore.handleToolCall(data)
Store->>Store: Convert to TimelineEvent or buffer if task missing
Store->>Store: Append to task.timeline (immutable) or store pending
Store->>Timeline: Trigger reactive update
Timeline->>Timeline: Render compact tool_call entry and update metrics
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
dashboard/src/lib/sse.ts (1)
45-84: Keep$lib/sse.tson the window-event boundary.This new branch adds another direct store dependency to the transport layer. Mirroring the existing
log_linepattern fortool_callwould keep SSE routing decoupled from store implementations and follow the dashboard convention. As per coding guidelinesdashboard/src/**/*.{ts,svelte}: Use SSE client with window event dispatch for real-time dashboard updates from backend.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/lib/sse.ts` around lines 45 - 84, The SSE dispatch currently calls tasksStore.handleToolCall directly for the 'tool_call' SSEEventType, adding a transport-layer dependency on the tasks store; change this to mirror the 'log_line' approach by dispatching a window CustomEvent (e.g. 'sse:tool_call') with the payload so consumers can listen and update stores independently. Update the dispatch function's 'tool_call' case (and any reference to tasksStore.handleToolCall in that switch) to emit the CustomEvent when window is defined, leaving store wiring to the dashboard components that listen for 'sse:tool_call'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@dashboard/src/lib/sse.ts`:
- Around line 71-73: The code is unsafely casting payload to ToolCallEvent
before calling tasksStore.handleToolCall; create a type guard like
isToolCallEvent(payload: any): payload is ToolCallEvent that checks required
fields/types of a ToolCallEvent, then replace the cast with an if
(isToolCallEvent(payload)) { tasksStore.handleToolCall(payload); } else { /*
optionally log/ignore invalid payload */ } so you no longer bypass type checking
and only call handleToolCall with a validated ToolCallEvent.
In `@dashboard/src/lib/stores/tasks.svelte.ts`:
- Around line 160-185: The handleToolCall currently drops events if the task
isn't present and refresh() can overwrite in-flight appended tool calls; add a
per-task buffer (e.g., pendingToolCalls: Map<task_id, ToolCallEvent[]>) and
modify handleToolCall to push events into that buffer when tasks.findIndex(...)
< 0 (or when refresh in progress), returning early; after refresh() completes
(or inside refresh() when merging server tasks) replay/merge buffered events
into the fetched task timelines (or call a helper like
processBufferedToolCalls(task, bufferedEvents)) so timelines are preserved;
ensure refresh() merges timelines rather than blindly replacing them by checking
task.id and appending any buffered tool-call actions before assigning tasks.
---
Nitpick comments:
In `@dashboard/src/lib/sse.ts`:
- Around line 45-84: The SSE dispatch currently calls tasksStore.handleToolCall
directly for the 'tool_call' SSEEventType, adding a transport-layer dependency
on the tasks store; change this to mirror the 'log_line' approach by dispatching
a window CustomEvent (e.g. 'sse:tool_call') with the payload so consumers can
listen and update stores independently. Update the dispatch function's
'tool_call' case (and any reference to tasksStore.handleToolCall in that switch)
to emit the CustomEvent when window is defined, leaving store wiring to the
dashboard components that listen for 'sse:tool_call'.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7e992863-8c8b-40d1-9323-5ead0726c8df
📒 Files selected for processing (4)
dashboard/src/lib/components/views/TimelineView.sveltedashboard/src/lib/sse.tsdashboard/src/lib/stores/tasks.svelte.tsdashboard/src/lib/types/api.ts
Replace unsafe `as ToolCallEvent` cast with `isToolCallEvent()` type guard that validates all required fields before passing to store.
…(CodeRabbit #2) - Add pendingToolCalls Map to buffer events when task isn't in memory - refresh() now replays buffered events after fetching from backend - Extract toolCallToTimeline() and replayBuffered() helpers - reset() clears the buffer - Import TimelineEvent type for buffer typing
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
dashboard/src/lib/stores/tasks.svelte.ts (1)
101-131:⚠️ Potential issue | 🟠 MajorReplay buffered tool calls when
create()inserts the new task.If the backend emits a
tool_callbefore the POST response is applied,handleToolCall()buffers it because the task is not in memory yet. Lines 112-129 then add the task withtimeline: [], but that buffer is only drained fromrefresh(), so the first tool calls for a freshly created task can stay invisible for the whole session.💡 Minimal fix
if (res.ok && body.data) { const created = body.data as CreateTaskResponse; - tasks = [ - ...tasks, - { - id: created.task_id, - description, - status: created.status, - created_at: new Date().toISOString(), - completed_at: null, - budget: { - tokens_used: 0, - token_budget: 50000, - retries_used: 0, - max_retries: 3, - cost_used: 0, - cost_budget: 1.0 - }, - timeline: [] - } - ]; + const newTask: TaskSummary = { + id: created.task_id, + description, + status: created.status, + created_at: new Date().toISOString(), + completed_at: null, + budget: { + tokens_used: 0, + token_budget: 50000, + retries_used: 0, + max_retries: 3, + cost_used: 0, + cost_budget: 1.0 + }, + timeline: [] + }; + tasks = [...tasks, { ...newTask, timeline: replayBuffered(newTask) }]; return created.task_id; }Also applies to: 202-211
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/lib/stores/tasks.svelte.ts` around lines 101 - 131, When create() inserts the new task it sets timeline: [] but buffered tool_call events (received earlier in handleToolCall()) are only applied by refresh(), so initial tool calls can remain invisible; after adding the created task in create(), immediately replay/drain any buffered tool_call events for that task (the same logic refresh() uses) — either call the existing refresh() helper for that single task or extract the buffer-drain logic from refresh() into a helper (e.g., drainBufferedToolCallsForTask(task_id)) and invoke it with created.task_id so buffered events are applied to the new task's timeline.
♻️ Duplicate comments (1)
dashboard/src/lib/stores/tasks.svelte.ts (1)
78-90:⚠️ Potential issue | 🟠 Major
refresh()can still erase tool calls received during the fetch.
handleToolCall()only buffers when the task is missing. If atool_callarrives for a task that is already intaskswhilerefresh()is awaiting/api/tasks, Lines 214-219 append it to the old array and Lines 85-90 then replace that array with the fetched snapshot.dashboard/src/lib/sse.ts, Lines 139-147 calltasksStore.refresh()on reconnect, so this race still exists in the normal reconnect path. Buffer while refresh is in flight, or merge the pre-refresh local timeline intofetchedbefore reassigning.Also applies to: 202-220
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@dashboard/src/lib/stores/tasks.svelte.ts` around lines 78 - 90, The refresh() call can overwrite in-flight tool_call events; mark when a refresh is in-flight (e.g., add a boolean refreshInFlight) and have handleToolCall() buffer events while that flag is true, or—preferably—when refresh() receives the fetched TaskSummary[] merge any pre-refresh local timeline into the fetched tasks before assigning to tasks: for each fetched task call replayBuffered(t) and also merge the existing tasks' timeline (from the current tasks array) into that result so buffered/arrived tool calls are preserved; use the existing function names replayBuffered, handleToolCall, refresh, and the tasks variable to locate where to add the flag and merging logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@dashboard/src/lib/stores/tasks.svelte.ts`:
- Around line 101-131: When create() inserts the new task it sets timeline: []
but buffered tool_call events (received earlier in handleToolCall()) are only
applied by refresh(), so initial tool calls can remain invisible; after adding
the created task in create(), immediately replay/drain any buffered tool_call
events for that task (the same logic refresh() uses) — either call the existing
refresh() helper for that single task or extract the buffer-drain logic from
refresh() into a helper (e.g., drainBufferedToolCallsForTask(task_id)) and
invoke it with created.task_id so buffered events are applied to the new task's
timeline.
---
Duplicate comments:
In `@dashboard/src/lib/stores/tasks.svelte.ts`:
- Around line 78-90: The refresh() call can overwrite in-flight tool_call
events; mark when a refresh is in-flight (e.g., add a boolean refreshInFlight)
and have handleToolCall() buffer events while that flag is true,
or—preferably—when refresh() receives the fetched TaskSummary[] merge any
pre-refresh local timeline into the fetched tasks before assigning to tasks: for
each fetched task call replayBuffered(t) and also merge the existing tasks'
timeline (from the current tasks array) into that result so buffered/arrived
tool calls are preserved; use the existing function names replayBuffered,
handleToolCall, refresh, and the tasks variable to locate where to add the flag
and merging logic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 864bd668-5f41-4745-81e7-07308009e1d8
📒 Files selected for processing (2)
dashboard/src/lib/sse.tsdashboard/src/lib/stores/tasks.svelte.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- dashboard/src/lib/sse.ts
Summary
Wires the backend's
TOOL_CALLSSE events (added in PR #83 / Issue #80) through to the SvelteKit dashboard so users can see agent tool usage in real-time on the task timeline.What Changed
Types (
api.ts)'tool_call'toSSEEventTypeunionToolCallEventinterface matching the backend payload shapeSSE Client (
sse.ts)'tool_call'to the event listener arraytool_callevents totasksStore.handleToolCall()Tasks Store (
tasks.svelte.ts)handleToolCall()method that appends tool calls as timeline eventsdeveloper→dev,qa→qa)nowTime()helper for consistent HH:MM timestampsTimeline View (
TimelineView.svelte)tool_callentry ineventStyles— blueTOOLbadgeDesign Decision
Tool calls are appended as timeline events (with
type: 'tool_call') rather than stored in a separate array. This keeps them in chronological order with other events and means the existing timeline rendering picks them up automatically — only the visual treatment is new.Testing
Screenshots
Tool call events appear inline on the timeline as compact entries between the major events (PLAN, CODE, EXEC, etc.), giving visibility into what tools agents are using without cluttering the view.
Closes #85
Summary by CodeRabbit