Skip to content

fix: stop the busy indicator looping forever after a mid-turn model switch - #125

Merged
saucam merged 2 commits into
mainfrom
fix/status-idle-indicator
Jul 8, 2026
Merged

fix: stop the busy indicator looping forever after a mid-turn model switch#125
saucam merged 2 commits into
mainfrom
fix/status-idle-indicator

Conversation

@saucam

@saucam saucam commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

The bug

The web "thinking…/drafting…" indicator sometimes loops forever after the model's response has finished. Root cause is daemon-side and deterministic: it happens whenever you run /model or /rotate while a turn is in flight.

#teardownProvider (src/daemon/session.ts) nulls #activeRun before draining the event consumer, so the consumer's finally skips its idle reset (#activeRun === run is now false). #setStatus is the only thing that broadcasts session.status_change, so #status stays thinking/tool_running forever and setModel even re-broadcasts the stale status via #broadcastInfoUpdate. The indicator (WorkerIndicator.tsx, gated purely on status) loops indefinitely.

Fix

  • Daemon (primary): #teardownProvider emits the idle transition itself when it tore down an active run and nothing took over — guarded against the onRecoveryNeeded recovery-run path and the terminal error state, so it's a no-op for destroy() and safe for all callers.
  • Web (defensive): WorkerIndicator adds a staleness cap — if a session claims thinking but produces no activity for 90s and no tool is executing, the indicator clears. The daemon fix is the real fix; this guarantees the UI can never loop forever again even if a status update is ever missed.

Test

src/tests/session-integration.test.ts — a mid-turn setModel now broadcasts status_change: idle and the session lands at idle. Without the daemon fix, waitForIdle times out.

Full daemon suite green (905), web typecheck + lint clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved session status handling during model switches so the UI returns to idle correctly after an interrupted turn.
    • Prevented the worker/activity indicator from staying visible indefinitely when session activity has stalled.
  • Tests
    • Added integration coverage for mid-turn provider shutdowns to verify the idle status is broadcast as expected.

…witch

`#teardownProvider` nulls `#activeRun` before draining the event consumer, so
the consumer's `finally` skips its idle reset (its `#activeRun === run`
ownership guard is now false). Nothing else re-asserts idle, and `#setStatus`
is the only path that broadcasts `session.status_change` — so tearing a provider
down mid-turn (setModel / rotate) strands `#status` at thinking/tool_running
forever and the web "thinking…/drafting…" indicator loops indefinitely.

- Daemon: `#teardownProvider` now emits the idle transition itself when it tore
  down an active run and nothing took over (guards against the onRecoveryNeeded
  path installing a fresh run, and against the terminal error state).
- Web: `WorkerIndicator` gains a defensive staleness cap — if a session claims
  "thinking" but produces no message/delta/status activity for 90s and no tool
  is executing, the indicator clears instead of looping, so a missed idle can
  never strand the UI again.

Test: a mid-turn `setModel` now broadcasts `status_change: idle` and lands the
session at idle (previously `waitForIdle` timed out).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@saucam, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a0746f65-729c-4ddb-b9d7-10a76a150cd9

📥 Commits

Reviewing files that changed from the base of the PR and between 8a3b560 and bf52451.

📒 Files selected for processing (2)
  • web/src/components/transcript/WorkerIndicator.tsx
  • web/src/state/messages.ts
📝 Walkthrough

Walkthrough

Session teardown now explicitly transitions status to idle when a mid-turn provider teardown leaves no active run and the session isn't in error, replacing implicit finally-path handling. A new integration test verifies this. Separately, WorkerIndicator visibility now hides after a stall timeout unless a tool is actively running.

Changes

Session Teardown Idle Status

Layer / File(s) Summary
Teardown sequencing and idle emission
src/daemon/session.ts
#teardownProvider records prior active-run state, clears run/consumer task before awaiting provider.teardown(), then explicitly calls #setStatus("idle") when no replacement run exists and status isn't error.
Integration test for teardown idle broadcast
src/tests/session-integration.test.ts
New test starts an approval-blocked tool turn, calls setModel mid-turn to tear down the provider, and asserts the session becomes idle with an "idle" status_change broadcast.

WorkerIndicator Visibility Update

Layer / File(s) Summary
Stall threshold and staleness-aware visibility
web/src/components/transcript/WorkerIndicator.tsx
Adds STALL_MS constant, tracks lastActiveAt via createEffect on focusedSessionId/status, and rewrites visible to hide stale "thinking/tool_running" states unless liveTool() is active.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test as Integration Test
  participant Session
  participant Provider

  Test->>Session: setModel(...) mid-turn
  Session->>Provider: teardown()
  Session->>Session: clear activeRun / eventConsumerTask
  Provider-->>Session: teardown complete
  Session->>Session: setStatus("idle") if no active run & no error
  Session-->>Test: status_change broadcast ("idle")
Loading

Possibly related PRs

  • saucam/codeoid#105: Both PRs modify WorkerIndicator.tsx visibility gating logic based on session status.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main user-facing fix: preventing the busy indicator from getting stuck after a mid-turn model switch.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/status-idle-indicator

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.83%. Comparing base (7855889) to head (bf52451).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #125      +/-   ##
==========================================
+ Coverage   77.10%   77.83%   +0.73%     
==========================================
  Files          89       89              
  Lines       14412    14385      -27     
==========================================
+ Hits        11112    11197      +85     
+ Misses       3300     3188     -112     
Flag Coverage Δ
daemon 77.83% <100.00%> (+0.73%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/daemon/session.ts 82.62% <100.00%> (+6.50%) ⬆️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/src/components/transcript/WorkerIndicator.tsx`:
- Around line 39-44: The fallback timing in WorkerIndicator is using local
component time, which makes stale sessions look fresh again on mount or refocus.
Replace the Date.now()-based lastActiveAt state and the createEffect reset with
a per-session activity timestamp sourced from the store/session model, and make
the indicator derive its stale-window from that persisted value instead of
reinitializing on focusedSessionId() or status() changes. Ensure the logic in
WorkerIndicator keeps the existing stale state across navigation/reload rather
than restarting the 90s timer.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ad56bbc6-eb92-4714-ade2-6139817f59c7

📥 Commits

Reviewing files that changed from the base of the PR and between 7855889 and 8a3b560.

📒 Files selected for processing (3)
  • src/daemon/session.ts
  • src/tests/session-integration.test.ts
  • web/src/components/transcript/WorkerIndicator.tsx

Comment thread web/src/components/transcript/WorkerIndicator.tsx Outdated
…component clock

Addresses a CodeRabbit review point: the staleness fallback seeded lastActiveAt
from Date.now() and reset it on every focusedSessionId() change, so refocusing
(or remounting after navigation) a session that was already stuck restarted the
full 90s window — making a stale session look fresh again.

Track last live activity in a per-session side map in the messages store
(bumped where the epoch bumps, on applyMessage/applyDelta; cleared on
clearSessionMessages). The indicator now reads that store-derived timestamp, so
it reflects REAL last activity and a finished-but-stranded session clears
immediately on refocus instead of showing for another 90s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@saucam
saucam merged commit 8eac4b8 into main Jul 8, 2026
5 checks passed
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