mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-04 02:21:47 +00:00
fix(tui): stabilize live todo progress
This commit is contained in:
parent
1566f1eecc
commit
f5552f92e2
14 changed files with 256 additions and 86 deletions
|
|
@ -1 +1 @@
|
|||
export const liveTailOrder = () => ['todo', 'scroll-history', 'assistant'] as const
|
||||
export const liveTailOrder = () => ['scroll-history', 'assistant', 'live-todo'] as const
|
||||
|
|
|
|||
48
ui-tui/src/lib/liveProgress.test.ts
Normal file
48
ui-tui/src/lib/liveProgress.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { appendToolShelfMessage, isTodoDone } from './liveProgress.js'
|
||||
|
||||
describe('isTodoDone', () => {
|
||||
it('only treats non-empty all-completed/cancelled lists as done', () => {
|
||||
expect(isTodoDone([])).toBe(false)
|
||||
expect(isTodoDone([{ content: 'x', id: 'x', status: 'completed' }])).toBe(true)
|
||||
expect(isTodoDone([{ content: 'x', id: 'x', status: 'in_progress' }])).toBe(false)
|
||||
expect(
|
||||
isTodoDone([
|
||||
{ content: 'x', id: 'x', status: 'completed' },
|
||||
{ content: 'y', id: 'y', status: 'cancelled' }
|
||||
])
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('appendToolShelfMessage', () => {
|
||||
it('merges adjacent tool shelves into one contextual shelf', () => {
|
||||
const merged = appendToolShelfMessage([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] }], {
|
||||
kind: 'trail',
|
||||
role: 'system',
|
||||
text: '',
|
||||
tools: ['two ✓']
|
||||
})
|
||||
|
||||
expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓', 'two ✓'] }])
|
||||
})
|
||||
|
||||
it('adds tools to the nearest contextual thinking shelf', () => {
|
||||
const merged = appendToolShelfMessage(
|
||||
[{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] }],
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
|
||||
)
|
||||
|
||||
expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] }])
|
||||
})
|
||||
|
||||
it('starts a new shelf across assistant text boundaries', () => {
|
||||
const merged = appendToolShelfMessage(
|
||||
[{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] }, { role: 'assistant', text: 'done' }],
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
|
||||
)
|
||||
|
||||
expect(merged).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
34
ui-tui/src/lib/liveProgress.ts
Normal file
34
ui-tui/src/lib/liveProgress.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Msg, TodoItem } from '../types.js'
|
||||
|
||||
export const isTodoDone = (todos: readonly TodoItem[]) =>
|
||||
todos.length > 0 && todos.every(todo => todo.status === 'completed' || todo.status === 'cancelled')
|
||||
|
||||
export const isToolShelfMessage = (msg: Msg | undefined) =>
|
||||
Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length)
|
||||
|
||||
const canHoldToolShelf = (msg: Msg | undefined) =>
|
||||
Boolean(msg?.kind === 'trail' && !msg.text && (msg.thinking?.trim() || msg.tools?.length))
|
||||
|
||||
export const appendToolShelfMessage = (prev: readonly Msg[], msg: Msg): Msg[] => {
|
||||
if (!isToolShelfMessage(msg)) {
|
||||
return [...prev, msg]
|
||||
}
|
||||
|
||||
for (let index = prev.length - 1; index >= 0; index--) {
|
||||
const candidate = prev[index]
|
||||
|
||||
if (canHoldToolShelf(candidate)) {
|
||||
const next = [...prev]
|
||||
|
||||
next[index] = { ...candidate!, tools: [...(candidate!.tools ?? []), ...(msg.tools ?? [])] }
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
if (candidate?.kind !== 'trail' || candidate.text) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return [...prev, msg]
|
||||
}
|
||||
|
|
@ -4,20 +4,26 @@ import { appendTranscriptMessage } from './messages.js'
|
|||
|
||||
describe('appendTranscriptMessage', () => {
|
||||
it('merges adjacent tool-only shelves into one transcript row', () => {
|
||||
const out = appendTranscriptMessage(
|
||||
[{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓'] }],
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("two") ✓'] }
|
||||
)
|
||||
const out = appendTranscriptMessage([{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓'] }], {
|
||||
kind: 'trail',
|
||||
role: 'system',
|
||||
text: '',
|
||||
tools: ['Terminal("two") ✓']
|
||||
})
|
||||
|
||||
expect(out).toEqual([{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓', 'Terminal("two") ✓'] }])
|
||||
expect(out).toEqual([
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("one") ✓', 'Terminal("two") ✓'] }
|
||||
])
|
||||
})
|
||||
|
||||
it('does not merge tool shelves across thinking text', () => {
|
||||
it('merges tool shelves into the nearest thinking shelf', () => {
|
||||
const out = appendTranscriptMessage(
|
||||
[{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['Terminal("one") ✓'] }],
|
||||
{ kind: 'trail', role: 'system', text: '', tools: ['Terminal("two") ✓'] }
|
||||
)
|
||||
|
||||
expect(out).toHaveLength(2)
|
||||
expect(out).toEqual([
|
||||
{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['Terminal("one") ✓', 'Terminal("two") ✓'] }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,8 @@
|
|||
import type { Msg, Role } from '../types.js'
|
||||
|
||||
const isToolShelf = (msg: Msg | undefined) =>
|
||||
Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length)
|
||||
import { appendToolShelfMessage } from './liveProgress.js'
|
||||
|
||||
export const appendTranscriptMessage = (prev: Msg[], msg: Msg): Msg[] => {
|
||||
if (isToolShelf(msg) && isToolShelf(prev.at(-1))) {
|
||||
const last = prev.at(-1)!
|
||||
|
||||
return [...prev.slice(0, -1), { ...last, tools: [...(last.tools ?? []), ...(msg.tools ?? [])] }]
|
||||
}
|
||||
|
||||
return [...prev, msg]
|
||||
}
|
||||
export const appendTranscriptMessage = (prev: Msg[], msg: Msg): Msg[] => appendToolShelfMessage(prev, msg)
|
||||
|
||||
export const upsert = (prev: Msg[], role: Role, text: string): Msg[] =>
|
||||
prev.at(-1)?.role === role ? [...prev.slice(0, -1), { role, text }] : [...prev, { role, text }]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue