Skip to content

feat(dashboard): wire TOOL_CALL SSE events to timeline (#85)#87

Merged
Abernaughty merged 3 commits intomainfrom
feat/85-dashboard-tool-call-wiring
Apr 1, 2026
Merged

feat(dashboard): wire TOOL_CALL SSE events to timeline (#85)#87
Abernaughty merged 3 commits intomainfrom
feat/85-dashboard-tool-call-wiring

Conversation

@Abernaughty
Copy link
Copy Markdown
Owner

@Abernaughty Abernaughty commented Apr 1, 2026

Summary

Wires the backend's TOOL_CALL SSE 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)

  • Added 'tool_call' to SSEEventType union
  • New ToolCallEvent interface matching the backend payload shape

SSE Client (sse.ts)

  • Added 'tool_call' to the event listener array
  • New dispatch case routing tool_call events to tasksStore.handleToolCall()

Tasks Store (tasks.svelte.ts)

  • New handleToolCall() method that appends tool calls as timeline events
  • Maps backend agent names to dashboard IDs (developerdev, qaqa)
  • Formats action string with success/fail icon (✓/✗), tool name, and truncated result preview
  • New nowTime() helper for consistent HH:MM timestamps

Timeline View (TimelineView.svelte)

  • New tool_call entry in eventStyles — blue TOOL badge
  • Compact rendering for tool call events:
    • Smaller agent avatar (20px vs 28px)
    • Thinner left border with lower opacity
    • Slightly reduced overall opacity (0.85)
    • No agent name label (just the avatar)
    • Smaller text (10px vs 11px)
  • Session debrief "Events" metric now shows tool call count in subtitle

Design 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

  • All imports/exports verified consistent across files
  • No new dependencies added
  • Backward compatible — existing event types and rendering unchanged

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

  • New Features
    • Tool calls now appear in the timeline with a compact card, status (success/failure) indicator, and truncated result preview.
    • Session metrics show a dedicated tool-call count alongside the total timeline entries.
  • Bug Fixes
    • Improved reliability for receiving and displaying tool-call events so they appear consistently after reconnects.

- 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
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Walkthrough

Dashboard wiring for backend TOOL_CALL SSE events: new ToolCallEvent type, SSE listener/dispatch for tool_call, store buffering and handleToolCall to append timeline entries, and compact timeline UI rendering for tool-call events.

Changes

Cohort / File(s) Summary
Type Definitions
dashboard/src/lib/types/api.ts
Added 'tool_call' to SSEEventType and new exported ToolCallEvent interface (task_id, agent, tool, success, result_preview).
SSE Integration
dashboard/src/lib/sse.ts
Registered 'tool_call' in eventTypes and added case 'tool_call' dispatch that validates payload and forwards to tasksStore.handleToolCall(...).
Store Logic
dashboard/src/lib/stores/tasks.svelte.ts
Added tasksStore.handleToolCall(data) and pendingToolCalls buffer; converts ToolCallEvent → TimelineEvent, appends immutably or buffers when task missing, replays buffered entries on refresh(), and clears buffer on reset().
Timeline UI
dashboard/src/lib/components/views/TimelineView.svelte
Introduced isToolCall(event) helper and conditional rendering for type: 'tool_call' entries: compact visuals (smaller avatar, suppressed agent name, reduced padding/typography), adjusted border/opactiy, and Events subtitle updated to show ${#tool_call} tool calls.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

🐰 Tip-tap goes my tiny paw,

Tool calls hop from near and far.
Timelines glow with each small deed,
Agents, tools — a sprightly feed.
Hooray — the dashboard hears them all!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: wiring TOOL_CALL SSE events to the dashboard timeline.
Linked Issues check ✅ Passed All coding requirements from issue #85 are met: types extended with ToolCallEvent, SSE dispatch routes tool_call events, store implements handleToolCall(), and timeline view renders tool calls with appropriate visual treatment.
Out of Scope Changes check ✅ Passed All changes directly support wiring TOOL_CALL events end-to-end; no unrelated modifications detected outside the scope of issue #85.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/85-dashboard-tool-call-wiring

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
dashboard/src/lib/sse.ts (1)

45-84: Keep $lib/sse.ts on the window-event boundary.

This new branch adds another direct store dependency to the transport layer. Mirroring the existing log_line pattern for tool_call would keep SSE routing decoupled from store implementations and follow the dashboard convention. As per coding guidelines dashboard/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

📥 Commits

Reviewing files that changed from the base of the PR and between 9900c33 and 7ef4ff2.

📒 Files selected for processing (4)
  • dashboard/src/lib/components/views/TimelineView.svelte
  • dashboard/src/lib/sse.ts
  • dashboard/src/lib/stores/tasks.svelte.ts
  • dashboard/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Replay buffered tool calls when create() inserts the new task.

If the backend emits a tool_call before the POST response is applied, handleToolCall() buffers it because the task is not in memory yet. Lines 112-129 then add the task with timeline: [], but that buffer is only drained from refresh(), 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 a tool_call arrives for a task that is already in tasks while refresh() 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 call tasksStore.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 into fetched before 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7ef4ff2 and 0971188.

📒 Files selected for processing (2)
  • dashboard/src/lib/sse.ts
  • dashboard/src/lib/stores/tasks.svelte.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • dashboard/src/lib/sse.ts

@Abernaughty Abernaughty merged commit a4f1ecc into main Apr 1, 2026
3 checks passed
@Abernaughty Abernaughty deleted the feat/85-dashboard-tool-call-wiring branch April 1, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Dashboard TOOL_CALL SSE event wiring

1 participant