Repository navigation
fix(native-chat): show the "Working for" bar while a turn runs - #23537
Conversation
The turn bar under the user's message only rendered once a turn settled, so a running turn had no bar, only the spinner line at the tail. The running turn now draws the same bar with a live clock, and it settles in place to "Worked for". The tail line keeps its spinner but drops the clock, so the clock is said once: activity text, else "Thinking", else "Working…". Mobile gets the same split.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughThe change separates elapsed turn duration from live activity labels. The renderer and mobile views show duration in a status bar and display thinking or provider activity separately. Active status now appears beneath the user prompt. Priority: ⬇️ Low Merge Risk: 🔵 Low · up to The duration updates visually, but screen-reader users may not hear the final duration unless they navigate to the bar. This is a bounded accessibility issue to address or explicitly accept before merging. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change appears confined to chat timing and activity indicators. No new security exposure was identified, but the available evidence does not establish complete security coverage. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
🚥 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: b9798fdb-db13-4866-8fda-29733afc0396
📒 Files selected for processing (16)
mobile/src/session/MobileNativeChatMessage.tsxmobile/src/session/MobileNativeChatTurnStatus.test.tsmobile/src/session/MobileNativeChatTurnStatus.tsxmobile/src/session/MobileNativeChatView.test.tsmobile/src/session/MobileNativeChatView.tsxmobile/src/session/use-mobile-native-chat-turn-disclosure.tssrc/renderer/src/components/native-chat/NativeChatMessageList.tsxsrc/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsxsrc/renderer/src/components/native-chat/NativeChatTranscriptRow.tsxsrc/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsxsrc/renderer/src/components/native-chat/NativeChatWorkingStatus.tsxsrc/renderer/src/components/native-chat/native-chat-transcript-slots.test.tssrc/renderer/src/components/native-chat/native-chat-transcript-slots.tssrc/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsxsrc/shared/native-chat-turn-status.test.tssrc/shared/native-chat-turn-status.ts
💤 Files with no reviewable changes (2)
- mobile/src/session/MobileNativeChatMessage.tsx
- src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.
| aria-live="polite" | ||
| > | ||
| <span className={thinking ? 'animate-pulse' : undefined}>{label}</span> | ||
| <span>{label}</span> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,115p' src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx
sed -n '1,125p' mobile/src/session/MobileNativeChatTurnStatus.tsx
sed -n '1,100p' src/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsx
rg -n 'accessibilityLiveRegion|aria-live|announceForAccessibility|turnStatus' mobile/src/session/MobileNativeChatMessage.tsx mobile/src/session/MobileNativeChatView.tsx src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsxRepository: stablyai/orca
Length of output: 8546
Announce the settled duration once.
NativeChatWorkingStatus and MobileNativeChatTurnStatus expose the running clock and settled duration without a live region. The existing activity live regions announce only the active status, not the settled duration. Add a polite live region that is enabled only when workedSeconds is non-null. Keep it disabled while counting so elapsed clock ticks are not announced.
Apply this to both status components.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the full diff at 34d7966709 (16 files, one commit) against main:
- Turn-status derivation split —
describeNativeChatTurnStatusno longer takesthinkingand only yields theworkingFor/workedForclock;describeNativeChatActiveTurnLabelno longer takeselapsedSecondsand only yieldsthinking/working. The shared module is now the compile-time guarantee that the bar can never say "Thinking" and the tail line can never carry a clock. - Live bar admitted by the transcript —
native-chat-transcript-slots.tsdrops theworkedSeconds != nullgate so the running turn'sactivestatus renders as the bar under its prompt, settling in place.native-chat-working-status-shared-clock.test.tsxstill proves one shared 1s clock. - Desktop rows retasked —
NativeChatWorkingStatusloses thethinkingprop, always counts while unsettled, and always draws its divider;NativeChatTurnActivityLinetakesthinking: boolean, keeps the spinner andaria-live, and drops the clock and thedata-native-chat-turn-statusmarker (now owned by the bar). - Mobile parity —
MobileNativeChatTurnStatusbecomes the bar and newMobileNativeChatTurnActivitybecomes the footer line;use-mobile-native-chat-turn-disclosureresolves the liveactivestatus for the active user row. - Tests — the remaining diff is adaptation across six suites, including new position/ordering pins (bar under the prompt, tail after the newest content) and a "clock said exactly once" assertion.
I ran the changed suites locally: desktop turn-status, transcript-slots, turn-indicator, and shared-clock (68 passed); mobile turn-status and chat-view (40 passed); and the mobile check:tests-typecheck ratchet. All green.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
When a turn ends without a host-recorded duration (a send folded into a running turn, or a host that stamps no start), the local duration is stamped one pass after the working flag drops. For that pass the turn status resolved to nothing, so the live "Working for" bar unmounted and remounted as "Worked for" - invisible on desktop (layout effect) but a painted blink on mobile, where the stamp runs in a passive effect. The shared selector now keeps the just-ended turn's running status until its duration lands, unless the host says it never saw the end, and mobile reads the active turn's row from that selector the way desktop does.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — the incremental delta since the prior Pullfrog review at 34d7966709: one commit, 7254ff6ace.
- Settle fallback —
selectNativeChatTurnStatusesnow keeps the just-ended turn's status live for the single render before the local duration is stamped, so the bar settles in place instead of blinking out. The fallback (activeTiming?.workedSeconds === null && !settledByTurn?.has(activeTurnKey)) is suppressed by any hostsettledByTurnentry, including thenull"end never observed" verdict, so host authority is preserved. - Mobile guard dropped —
use-mobile-native-chat-turn-disclosureresolvesturnKey === activeTurnKey ? active : completedByTurnwithout the oldisWorkingguard, so that live-on-settle status reaches the mobile row. - Tests — a shared test pins the fallback and the host-
nullsuppression; a mobile test asserts no null turn status on any render across the settle and that it ends settled; the desktop turn-indicator test now asserts the live and settled clocks are the same bar node.
I ran the shared turn-status, unverifiable-turn-status, and transcript-slot suites (52 passed), the desktop turn-indicator and shared-clock suites (23 passed), and pnpm tc:web. A static trace of the settle path confirms the fallback cannot strand the bar: a workedSeconds: null timing entry is only ever created by an isWorking: true pass, and both hooks' reduce effects list isWorking as a dependency, so the follow-up pass always stamps the duration — after which the reduce is a monotonic no-op for that turn.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Review status: ready for reviewHead: Review loops (2):
Architecture: the original duplicate "Working for" came from two rows that both owned the clock; #19977 removed the bar instead of fixing ownership. This PR gives each fact one owner, enforced by the label functions' signatures: the bar owns the running/settled clock, the tail line owns what is happening now and cannot take a duration. One row settles in place rather than swapping row types, so it cannot jump. Live QA (desktop dev build on a separate Mac, structured Claude chat, ~40 s turn, sampled every second; screenshots are in the PR body):
Readiness checklist: passed at the start and at the end, with no blockers. It adds no docs files and makes no wire, storage or host change. Already on main, not caused by this PR (for follow-up, not fixed here):
Behaviour choices, visible but intended: the bar keeps counting while a question or approval card is waiting, because the turn is still running. A turn started by the agent with no user message in view has no bar and shows only "Working…". |

ELI5
While an agent is working in a chat, the "Working for 12s" bar under your message is back. It used to appear only after the agent finished. The spinner line at the bottom of the chat now says what the agent is doing instead of repeating the clock.
What Changed
Before: while a turn ran, the space under your message was empty and the only indicator was the spinner line at the bottom, which showed "Working for 8m 34s". The "Worked for …" bar under your message only appeared once the turn ended.
After: the bar under your message shows "Working for 12s" with its divider from the first second, and turns into "Worked for 1m 12s ›" in place when the turn ends, keeping its fold toggle. The bottom line keeps its spinner and shows the agent's activity text, else "Thinking" while it is reasoning, else a plain "Working…". The clock appears only once, on the bar. The bar never says "Thinking".
Mechanism: the transcript only attached a turn's status row to your message once the turn had a finished duration. That gate now also admits the running turn, so the same row renders live and settles in place. The shared label logic is split to match: the bar's label is only the running or finished clock, and the bottom line's label never carries a clock. Only the bottom line is an announced live region, so screen readers are not read a ticking clock. Mobile makes the same split: the bar under your message, and the activity line in the list footer. When a turn ends without a duration from the host, the local duration is written one pass after the turn stops; for that pass the turn keeps its running status, so the bar changes in place instead of unmounting and remounting (which painted a one-frame blink on mobile). That hand-off is derived from the timing already held and always resolves on the next pass, so a finished turn can never keep a ticking clock.
Why
The bar was removed while a turn ran because the bar and the bottom line were two rows both saying "Working for". That fixed the duplication by dropping the bar, which also dropped the one thing anchored to your message that says the turn is still running. Keeping both rows but giving each one job (the bar owns the clock, the bottom line owns what is happening now) removes the duplication without losing the bar, and the bar no longer jumps into existence when the turn ends.
Alternative considered: keep only the bottom line and add the clock back to it. That leaves nothing under your message during long turns, which is the gap this fixes.
Known edge: a turn with no user message at all (started by the agent) has no message to hang the bar on, so it shows only the bottom line with "Working…" and no clock.
Linked Issue
None.
Visual Proof
Live structured Claude chat, desktop dev build, one ~40 s turn of four
sleep 8tool calls.Before: nothing under the prompt while the turn runs and the clock sits on the bottom line; the "Worked for" bar appears only at the end and pushes the content down. After: "Working for 17s" sits on the divider under the prompt from the first second, the bottom line reads "Working…", and the same bar becomes "Worked for 42s ›" at the identical position (sampled every second: the bar's offset in the transcript stayed at 166 px from t=2 s through the settle, and "Working for" appeared exactly once). The fold toggle expands and collapses the turn's detail:
Mobile was not exercised live (the shared Android emulator was held by another test run); it is covered by the unit tests below.
Testing
Updated the desktop turn-indicator, transcript-slot, clock, and shared label tests, and the mobile turn status and chat view tests, to the new split. Added checks that the live bar sits under the prompt, that it is the same position the settled bar takes, and that "Working for" appears exactly once. Also covered: the bar stays mounted as the same element through the settle on desktop and on every mobile render, and the host's "end never observed" still hides it. Native-chat suites pass (desktop 1,990 tests across the native-chat and shared turn-status suites, mobile 1,826),
pnpm tcpasses, oxlint and the changed-code quality gate (including React Doctor and design system) pass.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
Renderer and mobile only; no wire, host, or storage change, so SSH, mixed-version remotes, and platforms are unaffected. The live bar reuses the existing shared one-second clock, so no new timers.
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)