mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
444b5e96fa
commit
2d0f2185cf
6 changed files with 215 additions and 5 deletions
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue