fix(desktop): clear stale compaction status across session switches (#64127)

* fix(desktop): clear stale compaction status

Clear the compaction phase when a turn resumes with model or tool activity, and key response timers by session and turn so switching chats preserves elapsed time.\n\nSupersedes #48115 by porting its resumed-content approach to the current split stream hook and covering tool-first resumptions.\n\nCo-authored-by: liuhao1024 <sunsky.lau@gmail.com>

* fix(desktop): resume after thinking activity

* fix(desktop): clear turn timer on stop
This commit is contained in:
Gille 2026-07-14 06:48:48 -06:00 committed by GitHub
parent 444b5e96fa
commit 2d0f2185cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 215 additions and 5 deletions

View file

@ -0,0 +1,56 @@
import { act, cleanup, render, screen } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { __resetElapsedTimerRegistryForTests } from '@/components/chat/activity-timer'
import { I18nProvider } from '@/i18n'
import { $activeSessionId, $turnStartedAt } from '@/store/session'
import { ResponseLoadingIndicator } from './status'
function renderIndicator() {
return render(
<I18nProvider configClient={null} initialLocale="en">
<ResponseLoadingIndicator />
</I18nProvider>
)
}
describe('ResponseLoadingIndicator timer', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
__resetElapsedTimerRegistryForTests()
})
afterEach(() => {
cleanup()
$activeSessionId.set(null)
$turnStartedAt.set(null)
__resetElapsedTimerRegistryForTests()
vi.useRealTimers()
})
it('preserves each running session timer while switching between sessions', () => {
$activeSessionId.set('session-a')
$turnStartedAt.set(Date.now())
const sessionA = renderIndicator()
act(() => vi.advanceTimersByTime(5_000))
expect(screen.getByText('5s')).toBeTruthy()
sessionA.unmount()
$activeSessionId.set('session-b')
$turnStartedAt.set(Date.now())
const sessionB = renderIndicator()
act(() => vi.advanceTimersByTime(3_000))
expect(screen.getByText('3s')).toBeTruthy()
sessionB.unmount()
$activeSessionId.set('session-a')
$turnStartedAt.set(new Date('2026-01-01T00:00:00.000Z').getTime())
renderIndicator()
expect(screen.getByText('8s')).toBeTruthy()
})
})

View file

@ -11,6 +11,7 @@ import { cn } from '@/lib/utils'
import { $backgroundResume } from '@/store/background-delegation'
import { $compactionActive } from '@/store/compaction'
import { $activeSessionAwaitingInput } from '@/store/prompts'
import { $activeSessionId, $turnStartedAt } from '@/store/session'
const StatusRow: FC<{ children: ReactNode; label: string } & React.ComponentPropsWithoutRef<'div'>> = ({
children,
@ -36,6 +37,13 @@ const CompactionHint: FC = () => (
<span className="shimmer min-w-0 truncate text-muted-foreground/55">{COMPACTION_LABEL}</span>
)
function useActiveTurnTimerKey(): string | undefined {
const activeSessionId = useStore($activeSessionId)
const turnStartedAt = useStore($turnStartedAt)
return activeSessionId && turnStartedAt ? `turn:${activeSessionId}:${turnStartedAt}` : undefined
}
export const CenteredThreadSpinner: FC = () => {
const { t } = useI18n()
@ -59,7 +67,8 @@ export const CenteredThreadSpinner: FC = () => {
export const ResponseLoadingIndicator: FC = () => {
const { t } = useI18n()
const elapsed = useElapsedSeconds()
const timerKey = useActiveTurnTimerKey()
const elapsed = useElapsedSeconds(true, timerKey)
const compacting = useStore($compactionActive)
return (
@ -134,6 +143,7 @@ export const StreamStallIndicator: FC = () => {
const [stalled, setStalled] = useState(false)
const compacting = useStore($compactionActive)
const turnTimerKey = useActiveTurnTimerKey()
// A pending clarify / approval / sudo / secret means the turn is paused on the
// user, not working — so don't resurrect the "thinking" timer while they
// decide (matches the pet's awaitingInput pose taking priority over busy).
@ -147,7 +157,7 @@ export const StreamStallIndicator: FC = () => {
}, [activity])
const active = (stalled || compacting) && !awaitingInput
const elapsed = useElapsedSeconds(active)
const elapsed = useElapsedSeconds(active, compacting ? turnTimerKey : undefined)
if (!active) {
return null