mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-01 07:01:41 +00:00
fix(tui): ignore late thinking deltas after completion (#31055)
* fix(tui): ignore late thinking deltas after completion Prevent stale reasoning events from repainting the TUI status after a turn has already completed and the UI is idle. * test(tui): restore timers after thinking delta assertion Keep fake timer cleanup in a finally block so assertion failures cannot leak timer mode into later tests.
This commit is contained in:
parent
e6ca730a22
commit
874c2b1fe6
2 changed files with 44 additions and 8 deletions
|
|
@ -139,6 +139,7 @@ describe('createGatewayEventHandler', () => {
|
|||
const verdict = '✓ Goal achieved: long judge reason goes only in transcript, not merged with cwd label.'
|
||||
|
||||
vi.useFakeTimers()
|
||||
|
||||
try {
|
||||
onEvent({
|
||||
payload: { kind: 'goal', text: verdict },
|
||||
|
|
@ -303,14 +304,40 @@ describe('createGatewayEventHandler', () => {
|
|||
vi.useFakeTimers()
|
||||
const appended: Msg[] = []
|
||||
const streamed = 'short streamed reasoning'
|
||||
const onEvent = createGatewayEventHandler(buildCtx(appended))
|
||||
|
||||
createGatewayEventHandler(buildCtx(appended))({ payload: { text: streamed }, type: 'thinking.delta' } as any)
|
||||
vi.runOnlyPendingTimers()
|
||||
try {
|
||||
onEvent({ payload: {}, type: 'message.start' } as any)
|
||||
onEvent({ payload: { text: streamed }, type: 'thinking.delta' } as any)
|
||||
vi.runOnlyPendingTimers()
|
||||
|
||||
expect(getTurnState().reasoning).toBe(streamed)
|
||||
expect(getTurnState().reasoningActive).toBe(true)
|
||||
expect(getTurnState().reasoningTokens).toBe(estimateTokensRough(streamed))
|
||||
vi.useRealTimers()
|
||||
expect(getTurnState().reasoning).toBe(streamed)
|
||||
expect(getTurnState().reasoningActive).toBe(true)
|
||||
expect(getTurnState().reasoningTokens).toBe(estimateTokensRough(streamed))
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('ignores late thinking.delta after the turn has already completed', () => {
|
||||
vi.useFakeTimers()
|
||||
const appended: Msg[] = []
|
||||
const onEvent = createGatewayEventHandler(buildCtx(appended))
|
||||
|
||||
try {
|
||||
onEvent({ payload: {}, type: 'message.start' } as any)
|
||||
onEvent({ payload: { text: 'final answer' }, type: 'message.complete' } as any)
|
||||
expect(getUiState().busy).toBe(false)
|
||||
expect(getUiState().status).toBe('ready')
|
||||
|
||||
onEvent({ payload: { text: 'thinking...' }, type: 'thinking.delta' } as any)
|
||||
vi.runOnlyPendingTimers()
|
||||
|
||||
expect(getUiState().status).toBe('ready')
|
||||
expect(getTurnState().reasoning).toBe('')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves streamed reasoning as one completed thinking panel after segment flushes', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { STARTUP_IMAGE, STARTUP_QUERY } from '../config/env.js'
|
||||
import { STREAM_BATCH_MS } from '../config/timing.js'
|
||||
import { SETUP_REQUIRED_TITLE, buildSetupRequiredSections } from '../content/setup.js'
|
||||
import { buildSetupRequiredSections, SETUP_REQUIRED_TITLE } from '../content/setup.js'
|
||||
import type {
|
||||
CommandsCatalogResponse,
|
||||
ConfigFullResponse,
|
||||
|
|
@ -313,6 +313,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
}
|
||||
|
||||
case 'thinking.delta': {
|
||||
if (!getUiState().busy) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = ev.payload?.text
|
||||
|
||||
if (text !== undefined) {
|
||||
|
|
@ -340,6 +344,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
|
||||
if (p.kind === 'goal') {
|
||||
sys(p.text)
|
||||
|
||||
const brief = p.text.startsWith('✓')
|
||||
? '✓ goal complete'
|
||||
: p.text.startsWith('↻')
|
||||
|
|
@ -347,8 +352,10 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
: p.text.startsWith('⏸')
|
||||
? '⏸ goal paused'
|
||||
: 'ready'
|
||||
|
||||
setStatus(brief)
|
||||
restoreStatusAfter(6000)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -356,6 +363,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
|
||||
if (p.kind === 'compressing') {
|
||||
sys(p.text)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -528,6 +536,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
case 'tool.complete': {
|
||||
const inlineDiffText =
|
||||
ev.payload.inline_diff && getUiState().inlineDiffs ? stripAnsi(String(ev.payload.inline_diff)).trim() : ''
|
||||
|
||||
const resultText = ev.payload.result_text ? stripAnsi(String(ev.payload.result_text)) : undefined
|
||||
|
||||
if (inlineDiffText) {
|
||||
|
|
@ -589,7 +598,6 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
sys(`[bg ${ev.payload.task_id}] ${ev.payload.text}`)
|
||||
|
||||
return
|
||||
|
||||
case 'review.summary': {
|
||||
// Self-improvement background review emitted a persistent summary
|
||||
// of what it saved to memory/skills. Surface it as a system line
|
||||
|
|
@ -597,6 +605,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||
// flash. Python-side already formats it as "💾 Self-improvement
|
||||
// review: …".
|
||||
const text = String(ev.payload?.text ?? '').trim()
|
||||
|
||||
if (text) {
|
||||
sys(text)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue