Skip to content

fix(dashboard): tool cards no longer stuck spinning forever#1676

Merged
esengine merged 1 commit into
mainfrom
fix/tool-card-stuck-running
May 24, 2026
Merged

fix(dashboard): tool cards no longer stuck spinning forever#1676
esengine merged 1 commit into
mainfrom
fix/tool-card-stuck-running

Conversation

@esengine

Copy link
Copy Markdown
Owner

Summary

  • Bug: in reasonix code + browser dashboard, every tool card's spinner stayed visible forever even after the underlying tool completed (or was aborted). The card showed running indefinitely until session-reload, even though the loop had long since produced the result.
  • Root cause: src/cli/ui/effects/loop-to-dashboard.ts:8 minted dashboard event ids as `${assistantId}-${role}-${Date.now()}` for every event. tool_start and tool carry different role strings and fire at different timestamps → completely different ids. The SSE bridge (dashboard/src/lib/tauri-bridge.ts) passes that id through as the segment callId, and the dashboard reducer keys segments by it (dashboard/src/App.tsx:931-951) — so tool.result never matched the segment created by tool.intent, segment.result stayed undefined, and ToolCard/ShellCard's running = result === undefined (dashboard/src/ui/cards.tsx:345, thread.tsx:137-141) kept the spinner spinning forever.
  • Fix: LoopEvent.callId already exists for exactly this purpose — src/loop/types.ts:40-41 says "UI uses this as the card id" — and both tool_start (loop.ts:1024) and tool (loop.ts:1078) emit it from this.inflightIdFor(call). Use ev.callId ?? id in loopEventToDashboard for both events so they share a stable id and the dashboard reducer can pair them.

How it was found

SSE event log captured via temporary ?debug-sse instrumentation showed the full event sequence flowing correctly (busy-change{busy:true} → deltas → assistant_finaltool_start → user abort → tool (with kill output) → assistant_final(synthetic) → busy-change{busy:false}), with $turn_complete correctly synthesized on the final busy-change. So state.busy was actually flipping false on the dashboard (confirmed by the jobs_list poll firing right after, which is gated on !state.busy). The visible "spinner" was the per-card spinner, not the composer's stop/send affordance — pointing at the segment-update path.

Test plan

  • npx vitest run tests/loop-to-dashboard.test.ts — 8 passed (added a regression case asserting tool_start + tool share the loop's callId)
  • npx tsc --noEmit
  • Manual: reasonix code → browser dashboard → ask AI to do anything that uses a tool → confirm card transitions running → done within a few hundred ms of the tool finishing (and not only on session reload)

`loopEventToDashboard` was generating dashboard event ids as
`${assistantId}-${role}-${Date.now()}` for both tool_start and tool
events. Different role string and different timestamp → completely
different ids. The SSE bridge passes that id through as the segment
callId, and the dashboard reducer keys segments by it — so tool.result
never matched the segment created by tool.intent, segment.result
stayed undefined, and the card sat in `running` state forever
(its only "done" trigger is `result !== undefined`).

LoopEvent already carries a stable `callId` for this exact purpose
(see `src/loop/types.ts:40-41`: "UI uses this as the card id"). Use
it for both tool_start and tool so they share an id and the reducer
can pair them.

Regression-tested in `tests/loop-to-dashboard.test.ts`.
@esengine esengine merged commit 8686c91 into main May 24, 2026
3 checks passed
@esengine esengine deleted the fix/tool-card-stuck-running branch May 24, 2026 10:26
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.

1 participant