hermes-agent-features/ui-tui/src/__tests__/virtualHeights.test.ts
brooklyn! 50aaf0c4ad
fix(tui): delineate assistant responses from details (#31087)
* fix(tui): delineate assistant responses from details

Add a muted Response marker before assistant text when thinking/tool details are visible so reasoning and final output do not visually run together.

* fix(tui): account for response separator height

Keep virtual transcript estimates aligned with the new response separator and avoid allocating trimmed copies of long assistant text.

* fix(tui): gate response separator estimate on details

Only add response-separator height when assistant details actually render, and use a non-allocating body-text check.

* fix(tui): skip empty detail height estimates

Do not add virtual transcript height for assistant details when no thinking or tool detail UI will render.

* fix(tui): estimate details by section visibility

Pass resolved thinking/tool visibility into virtual height estimates so hidden detail sections do not reserve response-separator rows.
2026-05-25 10:23:03 -05:00

97 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from 'vitest'
import { estimatedMsgHeight, messageHeightKey, wrappedLines } from '../lib/virtualHeights.js'
import type { Msg } from '../types.js'
describe('virtual height estimates', () => {
it('uses stable content keys across resumed message objects', () => {
const msg: Msg = { role: 'assistant', text: 'same text', tools: ['Search Files [long message]'] }
expect(messageHeightKey(msg)).toBe(messageHeightKey({ ...msg }))
})
it('accounts for wrapping and preserved blank-block rhythm', () => {
const msg: Msg = { role: 'assistant', text: `one\n\n${'x'.repeat(90)}` }
expect(wrappedLines(msg.text, 30)).toBe(5)
expect(estimatedMsgHeight(msg, 35, { compact: false, details: false })).toBeGreaterThan(5)
})
it('uses compound user prompt width when estimating user message wrapping', () => {
const msg: Msg = { role: 'user', text: 'x'.repeat(21) }
expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: '' })).toBe(3)
expect(estimatedMsgHeight(msg, 26, { compact: false, details: false, userPrompt: 'Ψ >' })).toBe(4)
})
it('includes detail sections when visible', () => {
const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'line 1\nline 2', tools: ['Tool A', 'Tool B'] }
expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBeGreaterThan(
estimatedMsgHeight(msg, 80, { compact: false, details: false })
)
})
it('accounts for the response separator when assistant details are visible', () => {
const msg: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
estimatedMsgHeight(msg, 80, { compact: false, details: false }) + 3
)
})
it('does not account for a response separator without visible details', () => {
const msg: Msg = { role: 'assistant', text: 'ok' }
expect(estimatedMsgHeight(msg, 80, { compact: false, details: true })).toBe(
estimatedMsgHeight(msg, 80, { compact: false, details: false })
)
})
it('honors per-section visibility when estimating response separators', () => {
const thinkingOnly: Msg = { role: 'assistant', text: 'ok', thinking: 'plan' }
const toolsOnly: Msg = { role: 'assistant', text: 'ok', tools: ['Tool A'] }
expect(
estimatedMsgHeight(thinkingOnly, 80, {
compact: false,
details: true,
thinkingVisible: false,
toolsVisible: true
})
).toBe(estimatedMsgHeight(thinkingOnly, 80, { compact: false, details: false }))
expect(
estimatedMsgHeight(toolsOnly, 80, {
compact: false,
details: true,
thinkingVisible: true,
toolsVisible: false
})
).toBe(estimatedMsgHeight(toolsOnly, 80, { compact: false, details: false }))
})
it('reserves two extra rows for the inter-turn separator on non-first user messages', () => {
const msg: Msg = { role: 'user', text: 'follow-up question' }
const base = estimatedMsgHeight(msg, 80, { compact: false, details: false })
const withSep = estimatedMsgHeight(msg, 80, { compact: false, details: false, withSeparator: true })
expect(withSep).toBe(base + 2)
})
it('caps wrapped-line counting so giant assistant turns do not block offset rebuilds', () => {
// wrappedLines is invoked once per uncached message during
// useVirtualHistory's offset rebuild. Unbounded counting on a long
// assistant response (10k+ chars × every row × every rebuild) blocks
// the UI on cold mount. Cap is ~800 rows; post-mount Yoga
// measurement converges to the true height regardless.
const giant = 'x'.repeat(1_000_000)
const t0 = performance.now()
const rows = wrappedLines(giant, 80)
const elapsed = performance.now() - t0
expect(rows).toBeLessThanOrEqual(800)
expect(elapsed).toBeLessThan(50)
})
})