Repository navigation
fix(native-chat): a long reply keeps its Working bar and Stop after a reload, from the host's turn - #25751
Conversation
…ot the loaded history page
…the host's turn when its rows are not loaded
…running turn Drops the conversation-wide phone Stop: the phone shows Stop only once a turn is running, so a Stop naming no turn could not be reached and only changed the ordinary Stop's request. Stop and card cancel still name the turn the host says is running.
… a half-loaded turn shows no edit totals - A batch carrying rows restates the host's newest turn record, so one without it (an older host after a downgrade) drops back to the loaded rows instead of holding the last answer; the event coalescer merges by the same rule. - A turn whose record is above the loaded rows is grouped only while it runs, and its loaded tail gets no "N changed files" total, which would undercount. - "Thinking" reads the host's running turn too, so it shows when the turn's record is not loaded. - The wire doc for observedAt says what the journal stores: creation time.
…turn-state # Conflicts: # src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
…ing the transcript list under its line limit Also drops the chat zoom prop main removed from the unloaded live turn test.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (33)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe host now supplies the latest turn record independently of the loaded journal page. The client carries this value through page reads, live batches, and session state. Turn membership, timing, thinking status, and stop or cancellation checks use it when the turn record is not loaded. Chat rendering also identifies partial turns and excludes them from turn-diff summaries. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The latest-turn state is carried through live updates, and no remaining issue is established that would prevent merging after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |




ELI5
If you opened, reloaded, or reconnected a structured chat while the agent was deep in a long reply, the chat could forget that the reply was still running. The sidebar and tab still said Working, but inside the chat:
Now the computer running the agent (the "host") tells the chat directly whether a reply is running. The chat no longer has to work that out from the part of the history it happens to have loaded.
What Changed
The problem. Every chat client decided "is a reply running" by scanning the messages it had loaded for the newest "turn" record (one record per agent reply, updated as the reply runs and ends). Two things break that:
When that happens, the chat decides nothing is running. The sidebar never had this problem, because the host computes it from the whole history. A new test proves this end to end on the path a phone open or reconnect (and a desktop snapshot or reset) takes: a running reply with 250 rows after its start → host says running, chat said idle.
What you see now, on desktop and phone, however long the reply:
The mechanism.
latestTurn, the newest turn record taken from the whole history (its id, state, start time and so on). It is computed once per history snapshot.runningStructuredAgentSessionTurnIdanswers "which reply is running" fromlatestTurn. Desktop and mobile both use it for Working, Stop, card cancel, the conversation-command gate, the elapsed-time clock and the "Thinking" label. The old scan of loaded rows remains only for hosts too old to sendlatestTurn.latestTurnacross updates that carry no rows (nothing about a reply changed). An update that carries rows always restates it, so one that arrives without it came from an older host (for example a remote Orca server that was rolled back while the chat stayed open), and the client drops back to the loaded rows instead of holding the last answer. The step that merges rapid live updates applies the same rule.latestTurn. That is what lets the Working bar appear when neither the reply's turn record nor your message that started it is loaded. Such a reply shows no "N changed files" total, because only its newest rows are loaded and the total would undercount; once the reply ends, its loaded rows are left ungrouped, as they were before this change.Why
The common pattern — and Orca's own sidebar — computes "a reply is running", when it started, and what Stop acts on from server-side state that travels with the chat, while only the timeline itself is paged. Following it removes the whole class of bug: whether a reply is running no longer depends on how much history happens to be loaded or how tall it draws on screen. Live QA has not yet reproduced the bug on desktop with a real agent; that QA follows in the review loop.
Alternatives considered:
latestTurnonly when it changes. Rejected: that needs extra bookkeeping per client, to save a few hundred bytes on frames that already carry whole messages.Differences from the common pattern
latestTurnrides the chat's own live stream rather than a shared thread record. It is computed by the same host from the same history as the sidebar, and it arrives in order with the rows it governs.latestTurn. Remove that once such hosts are no longer supported./compact, still read the loaded rows. Both sit at the newest end of the history while they matter, so no case was found where they fall off the page. Follow-up: deliver pending prompts beside the timeline, as the reply state now is.Compatibility (remote-wire rules):
latestTurnis a new optional field. Older clients ignore it.nullvalue means "no reply recorded".Overlap: #25670 and #25675 (drafts) change how a finished or cut-off reply is worded. This PR changes no wording; expect small merge conflicts in
use-structured-agent-session.tsand the mobile session hook.Linked Issue
N/A — maintainer change from a native chat audit
Visual Proof
Live Electron QA (before = main, after = this PR; reload partway through a long reply): #25751 (comment)
Testing
New:
structured-agent-session-live-turn-window.test.ts(real host history → live subscriber → shared client state). Covers:New:
NativeChatMessageList.unloaded-live-turn.test.tsx(desktop render): "Working for 1m 3s" draws with neither the turn record nor the opening message loaded, with no partial "changed files" total; without the host record, no bar draws.Added cases to:
use-structured-agent-session-stop.test.tsx;use-mobile-structured-agent-session-prompt-cancel.test.tsx;use-mobile-native-chat-turn-disclosure.test.tsx;structured-agent-session-reducer.test.ts(kept across row-less updates; an older host's rows drop it) andstructured-agent-session-coalescer.test.ts(merging follows the same rule);native-chat-turn-membership.test.ts(grouped only while running, whole once the record loads);structured-agent-session-live-turn.test.ts.Existing suites around every touched module: 55 files / 581 tests green (desktop, host and shared), 15 mobile session files / 138 tests green.
Typecheck and lint:
tc:node,tc:web,tc:cli, mobile typecheck and its test-typecheck ratchet, oxlint, and the changed-code quality and React Doctor gates.The cross-version agent-session wire test could not start locally, because its extracted older-release checkout needs a package this checkout no longer installs. CI covers it.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)