Skip to content

fix(native-chat): show the "Working for" bar while a turn runs - #23537

Merged
brennanb2025 merged 2 commits into
mainfrom
restore-working-bar-indicator
Sep 28, 2026
Merged

brennanb2025 merged 2 commits into
mainfrom
restore-working-bar-indicator

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​206 $\color{#cf222e}{\Huge{\mathbf{−}}}$​145 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​61
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​94 $\color{#cf222e}{\Huge{\mathbf{−}}}$​129 $\color{#cf222e}{\Huge{\mathbf{−}}}$​35

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 8 tool calls.

Mid-turn Settled
Before (base) before, mid-turn before, settled
After (this PR) after, mid-turn after, settled

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:

after, turn detail expanded

Mobile was not exercised live (the shared Android emulator was held by another test run); it is covered by the unit tests below.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

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 tc passes, oxlint and the changed-code quality gate (including React Doctor and design system) pass.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and 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

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

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.
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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 configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: f678cb4a-a1e6-4465-9688-542263877906

📥 Commits

Reviewing files that changed from the base of the PR and between 34d7966 and 7254ff6.

📒 Files selected for processing (5)
  • mobile/src/session/use-mobile-native-chat-turn-disclosure.test.tsx
  • mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx
  • src/shared/native-chat-turn-status.test.ts
  • src/shared/native-chat-turn-status.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

The 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 7254f

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 Review

Security architecture risk: 🔵 Low · up to 7254f

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
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The changed reachability is the displayed status of a native-chat turn on renderer and mobile, rather than a newly exposed action or service endpoint.

Trust Boundaries and Controls

  • observed — An explicit host-null result still prevents an unverified turn from being presented through the new local-timing fallback.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 20 functions across 15 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly describes the main change: restoring the “Working for” duration bar while a native-chat turn runs.
Description check ✅ Passed The description is complete and focused. It explains the user-visible behavior, implementation, rationale, alternatives, edge cases, visual proof, testing, and compatibility impact. It lists no linked…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: b9798fdb-db13-4866-8fda-29733afc0396

📥 Commits

Reviewing files that changed from the base of the PR and between 47b8408 and 34d7966.

📒 Files selected for processing (16)
  • mobile/src/session/MobileNativeChatMessage.tsx
  • mobile/src/session/MobileNativeChatTurnStatus.test.ts
  • mobile/src/session/MobileNativeChatTurnStatus.tsx
  • mobile/src/session/MobileNativeChatView.test.ts
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/use-mobile-native-chat-turn-disclosure.ts
  • src/renderer/src/components/native-chat/NativeChatMessageList.tsx
  • src/renderer/src/components/native-chat/NativeChatMessageList.turn-indicator.test.tsx
  • src/renderer/src/components/native-chat/NativeChatTranscriptRow.tsx
  • src/renderer/src/components/native-chat/NativeChatTurnActivityLine.tsx
  • src/renderer/src/components/native-chat/NativeChatWorkingStatus.tsx
  • src/renderer/src/components/native-chat/native-chat-transcript-slots.test.ts
  • src/renderer/src/components/native-chat/native-chat-transcript-slots.ts
  • src/renderer/src/components/native-chat/native-chat-working-status-shared-clock.test.tsx
  • src/shared/native-chat-turn-status.test.ts
  • src/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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.tsx

Repository: 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — the full diff at 34d7966709 (16 files, one commit) against main:

  • Turn-status derivation split — describeNativeChatTurnStatus no longer takes thinking and only yields the workingFor/workedFor clock; describeNativeChatActiveTurnLabel no longer takes elapsedSeconds and only yields thinking/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.ts drops the workedSeconds != null gate so the running turn's active status renders as the bar under its prompt, settling in place. native-chat-working-status-shared-clock.test.tsx still proves one shared 1s clock.
  • Desktop rows retasked — NativeChatWorkingStatus loses the thinking prop, always counts while unsettled, and always draws its divider; NativeChatTurnActivityLine takes thinking: boolean, keeps the spinner and aria-live, and drops the clock and the data-native-chat-turn-status marker (now owned by the bar).
  • Mobile parity — MobileNativeChatTurnStatus becomes the bar and new MobileNativeChatTurnActivity becomes the footer line; use-mobile-native-chat-turn-disclosure resolves the live active status 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.

Pullfrog  | View workflow run | Using 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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes — the incremental delta since the prior Pullfrog review at 34d7966709: one commit, 7254ff6ace.

  • Settle fallback — selectNativeChatTurnStatuses now 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 host settledByTurn entry, including the null "end never observed" verdict, so host authority is preserved.
  • Mobile guard dropped — use-mobile-native-chat-turn-disclosure resolves turnKey === activeTurnKey ? active : completedByTurn without the old isWorking guard, so that live-on-settle status reaches the mobile row.
  • Tests — a shared test pins the fallback and the host-null suppression; 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.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status: ready for review

Head: 7254ff6ace. CI green (31 passed, 11 skipped, none failing or pending). Mergeable.

Review loops (2):

  • Loop 1 found one real defect and pushed 7254ff6ace: when a turn ended without a duration from the host, the turn status resolved to nothing for one pass before the local duration was written, so the bar unmounted and remounted as "Worked for". On desktop that was a hidden remount; on mobile it could paint a one-frame blink with the list shrinking and growing. The just-ended turn now keeps its running status for that one pass, derived from the timing already held, and mobile reads the active turn's row from the same shared selector as desktop. Three new tests fail without the fix.
  • Loop 2 re-reviewed the whole PR and came back clean. It checked specifically that the held running status cannot outlive one pass (scope change, mobile disable, active-turn switch, late host data, unanchored turns), so a finished turn can never keep a ticking clock.

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):

  • After: "Working for Ns" with its divider sits directly under the prompt from t=2 s. The tail reads "Working…" with no clock. "Working for" appears exactly once. At the end the same bar reads "Worked for 42s ›" at the identical position (166 px in the transcript from start through settle). The toggle expands and collapses the detail.
  • Before (base cdf9f6e938): no bar during the turn and the clock sits on the tail. The "Worked for" bar appears only at the end and pushes the content down.
  • Mobile: not run live. The shared Android emulator was held by another test run. Mobile is covered by the unit tests only (1,826 passing).

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):

  • A message sent while a turn is running moves the live bar to the new message, but the clock counts from the earlier turn's start. The same number used to show on the tail line.
  • A send the host folds into a running turn ends with two "Worked for" bars with the same duration.
  • The tool row can read "Running 2 commands" while only one command runs. This shows the same before and after.
  • The "Responding" accessibility label sits on a plain element with no role, so screen readers may not announce it.

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…".

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