mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-10 03:22:05 +00:00
Multi-turn transcripts ran together visually because every user message got the same vertical rhythm regardless of position. Adds a short ─── in the border colour above every user message after the first, so each turn reads as its own block. Height estimator gains a `withSeparator` flag so virtual scrolling pre-allocates the extra two rows (rule + top margin) and avoids a jump on first measurement. While in the area: the busy-indicator duration was padded with `padStart(7)`, leaving five visible spaces between `·` and the digits (`⠋ · 2s`) — especially loud under the verb-less `unicode` style. Drop the padding entirely (`⠋ · 2s`); the model label now shifts a few columns as the duration grows, which is the right trade-off for the minimal indicator styles. The verb-padding test stays; the duration-padding test is removed alongside the function it covered.
98 lines
2.5 KiB
TypeScript
98 lines
2.5 KiB
TypeScript
import type { Msg } from '../types.js'
|
|
|
|
import { transcriptBodyWidth } from './inputMetrics.js'
|
|
import { boundedHistoryRenderText } from './text.js'
|
|
|
|
const hashText = (text: string) => {
|
|
let h = 5381
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
h = ((h << 5) + h) ^ text.charCodeAt(i)
|
|
}
|
|
|
|
return (h >>> 0).toString(36)
|
|
}
|
|
|
|
export const messageHeightKey = (msg: Msg) => {
|
|
const todoSig = msg.todos?.map(t => `${t.status}:${t.content}`).join('\u0001') ?? ''
|
|
|
|
const panelSig =
|
|
msg.panelData?.sections
|
|
.map(s => `${s.title ?? ''}:${s.text?.length ?? 0}:${s.items?.length ?? 0}:${s.rows?.length ?? 0}`)
|
|
.join('\u0001') ?? ''
|
|
|
|
const introSig = msg.kind === 'intro' ? (msg.info?.version ?? '') : ''
|
|
|
|
return [
|
|
msg.role,
|
|
msg.kind ?? '',
|
|
hashText([msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? '', todoSig, panelSig, introSig].join('\0'))
|
|
].join(':')
|
|
}
|
|
|
|
export const wrappedLines = (text: string, width: number) => {
|
|
const w = Math.max(1, width)
|
|
|
|
return text.split('\n').reduce((n, line) => n + Math.max(1, Math.ceil(line.length / w)), 0)
|
|
}
|
|
|
|
export const estimatedMsgHeight = (
|
|
msg: Msg,
|
|
cols: number,
|
|
{
|
|
compact,
|
|
details,
|
|
limitHistory = false,
|
|
userPrompt = '',
|
|
withSeparator = false
|
|
}: {
|
|
compact: boolean
|
|
details: boolean
|
|
limitHistory?: boolean
|
|
userPrompt?: string
|
|
withSeparator?: boolean
|
|
}
|
|
) => {
|
|
if (msg.kind === 'intro') {
|
|
return msg.info?.version ? 9 : 5
|
|
}
|
|
|
|
if (msg.kind === 'panel') {
|
|
return Math.max(3, (msg.panelData?.sections.length ?? 1) * 2 + 1)
|
|
}
|
|
|
|
if (msg.kind === 'trail' && msg.todos?.length) {
|
|
if (msg.todoCollapsedByDefault) {
|
|
return 2
|
|
}
|
|
|
|
return Math.max(2, msg.todos.length + 2)
|
|
}
|
|
|
|
const bodyWidth = transcriptBodyWidth(cols, msg.role, userPrompt)
|
|
const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text
|
|
let h = wrappedLines(text || ' ', bodyWidth)
|
|
|
|
if (!compact && msg.role === 'assistant') {
|
|
h += Math.min(6, (text.match(/\n\s*\n/g) ?? []).length)
|
|
}
|
|
|
|
if (details) {
|
|
h += (msg.tools?.length ?? 0) + wrappedLines(msg.thinking ?? '', bodyWidth)
|
|
}
|
|
|
|
if (msg.role === 'user' || msg.kind === 'diff') {
|
|
h += 2
|
|
} else if (msg.kind === 'slash') {
|
|
h++
|
|
}
|
|
|
|
// Inter-turn separator above non-first user messages (1 rule row + 1
|
|
// top-margin row). The render-side gate is in appLayout.tsx; we trust
|
|
// the caller to pass `withSeparator` only when it matches that gate.
|
|
if (withSeparator) {
|
|
h += 2
|
|
}
|
|
|
|
return Math.max(1, h)
|
|
}
|