fix(desktop): persist elapsed-timer across chat navigation - #62300
fix(desktop): persist elapsed-timer across chat navigation#62300DavidMetcalfe wants to merge 1 commit into
Conversation
…nning assistant message id)
The 'time elapsed' counter on the loading/stall indicators reset to ~0s when
navigating away/back from the chat view because ResponseLoadingIndicator and
StreamStallIndicator called useElapsedSeconds() without a key, so each remount
restarted the timer from Date.now().
Key both indicators on the running assistant message id (run:${id}) via the
shared keyed useElapsedSeconds hook. The module-level registry persists the
start time across unmount/remount. A new prompt (new message id) starts a fresh
counter.
runningAssistantMessageIdFromThread requires the last thread message to be an
assistant message that is currently running, so no stale timestamp is reused
during the pre-append transition (last message = user prompt or a prior
completed turn). activity-timer.ts gets a 1000-entry LRU cap on the registry.
Verified: vitest 7/7 (status.test.tsx + activity-timer.test.tsx), tsc clean.
Cross-vendor Antigravity review (Flash + GPT-OSS) -> APPROVE-WITH-NITS.
Fixes NousResearch#62158
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the remount behavior and covering both anonymous chat-status timer call sites. The underlying issue remains on current main: ResponseLoadingIndicator and StreamStallIndicator still call useElapsedSeconds without a key at apps/desktop/src/components/assistant-ui/thread/status.tsx:62,150.
Problems
apps/desktop/src/components/assistant-ui/thread/status.tsx:70introducesrunningAssistantMessageIdFromThread(), which callsuseAuiStateat line 71 but is not named as a custom hook. Desktop ESLint enablesreact-hooks/rules-of-hooksas an error atapps/desktop/eslint.config.mjs:99-100, so this should be renamed touseRunningAssistantMessageIdFromThread(or inlined into the component) before merge.
Suggested changes
- Rename the helper as a custom hook and retain the current selector/guard behavior; the existing regression coverage can stay unchanged.
Automated hermes-sweeper review.
| // turn count continuously, and a new prompt (new message id) resets the timer. | ||
| const RUN_KEY_PREFIX = 'run:' | ||
|
|
||
| function runningAssistantMessageIdFromThread(): string | undefined { |
There was a problem hiding this comment.
useAuiState is called inside this function (line 71), but this lowercase name is neither a component nor a custom hook. apps/desktop/eslint.config.mjs:99-100 enables react-hooks/rules-of-hooks as an error. Rename this to useRunningAssistantMessageIdFromThread or move the selector into ResponseLoadingIndicator.
|
Closing as superseded — the fix landed on main through a different implementation of the same approach, and the issue this PR targets (#62158) was closed as fixed on 2026-08-15. Same fix shape, different key sourceThis PR keyed
Supersession timeline
The PR now conflicts with main after the Outstanding review item is mootThe hermes-sweeper keep_open review (@teknium1) asked to rename What wasn't adopted from this PR
Residual edge for anyone picking this up
|
Summary
Fixes #62158 — the "time elapsed" counter on the chat loading/stall indicators resets to ~0s when the user navigates away from and back to the chat view, instead of continuing to count the in-flight turn.
Root cause:
ResponseLoadingIndicatorandStreamStallIndicatorcalleduseElapsedSeconds()with no key. The hook starts fromDate.now()whenever no registry key is supplied, so each component remount (chat switch, Settings open/close, etc.) restarted the timer from zero.Fix: Key both indicators on the running assistant message id (
run:${id}) via the existing shared keyeduseElapsedSeconds(timerKey)hook. The module-levelstartedAtByKeyregistry persists the start timestamp across unmount/remount, so the counter continues seamlessly. A new prompt produces a new message id → fresh counter.runningAssistantMessageIdFromThreadrequires the last thread message to be an assistant message that is currently running (status.type === 'running'), so no stale timestamp is reused during the pre-append transition (last message = user prompt, or a prior completed turn's assistant message).activity-timer.tsgets a 1000-entry LRU cap on the registry to bound memory across sessions/runs.Notes
useThreadRuntime().runIddoes not exist in the installed@assistant-ui/react(^0.12.28) — verified againstnode_modules.d.ts. The running assistant message has a stable id ins.thread.messagesfrom run start through streaming + stall (verified in@assistant-ui/corebase-thread-runtime-core.jsstategetter readingmostRecentAssistantMessage).useAuiState(s => s.message.id)is the established pattern in the codebase.status.type === 'running'guard is defensive against any gap where the loading indicator would otherwise key off a stale prior-turn id. If the runtime guarantees the running message is always last+running beforeloading === 'response', the guard is redundant but harmless.runningguard above and locked by a regression test.Test plan
apps/desktop/src/components/assistant-ui/thread/status.test.tsx(new, 4 tests): timer persists across remount of the same run (9s → 13s); resets on a new run id; does not reuse a stale timestamp when the last message is a user prompt; does not reuse a stale timestamp when the last message is a completed prior-turn assistant.apps/desktop/src/components/chat/activity-timer.test.tsx(+2 tests): key-change reset; navigation away/back persistence.vitest7/7 pass;tsc -p . --noEmitclean on changed files.