Merge pull request #28829 from NousResearch/bb/tui-no-history-truncation

fix(tui): render full assistant text in scrollback (no history truncation)
This commit is contained in:
brooklyn!
2026-05-19 12:17:35 -05:00
committed by GitHub
parent 5a3317693c
commit b0af1d0931
8 changed files with 57 additions and 42 deletions

View File

@@ -1,6 +1,4 @@
import {
HISTORY_RENDER_MAX_CHARS,
HISTORY_RENDER_MAX_LINES,
LIVE_RENDER_MAX_CHARS,
LIVE_RENDER_MAX_LINES,
THINKING_COT_MAX
@@ -129,11 +127,6 @@ export const boundedLiveRenderText = (
{ maxChars = LIVE_RENDER_MAX_CHARS, maxLines = LIVE_RENDER_MAX_LINES } = {}
) => boundedRenderText(text, 'showing live tail', { maxChars, maxLines })
export const boundedHistoryRenderText = (
text: string,
{ maxChars = HISTORY_RENDER_MAX_CHARS, maxLines = HISTORY_RENDER_MAX_LINES } = {}
) => boundedRenderText(text, 'showing tail', { maxChars, maxLines })
const boundedRenderText = (
text: string,
labelPrefix: string,

View File

@@ -1,7 +1,6 @@
import type { Msg } from '../types.js'
import { transcriptBodyWidth } from './inputMetrics.js'
import { boundedHistoryRenderText } from './text.js'
const hashText = (text: string) => {
let h = 5381
@@ -30,10 +29,40 @@ export const messageHeightKey = (msg: Msg) => {
].join(':')
}
export const wrappedLines = (text: string, width: number) => {
const w = Math.max(1, width)
// Hard cap on rows the estimator will count. Each row above this is
// invisible to the estimator (gets clipped to MAX_ESTIMATE_LINES), but
// post-mount Yoga measurement converges to the real height on first
// render. Without this, a long assistant turn (10k+ chars) costs O(text)
// per offset rebuild × every uncached item — cold-mounting a 1000-row
// transcript becomes a multi-million-char wrap walk that blocks the UI.
//
// 800 covers any realistic assistant message (the prior history-clip
// ceiling was 16 lines, then full text — this is the sane middle).
const MAX_ESTIMATE_LINES = 800
return text.split('\n').reduce((n, line) => n + Math.max(1, Math.ceil(line.length / w)), 0)
export const wrappedLines = (text: string, width: number, maxLines: number = MAX_ESTIMATE_LINES) => {
const w = Math.max(1, width)
// Worst case: every cell is its own row at width=1, plus a small
// slack for the trailing partial line. Walking past this byte budget
// cannot increase n any further once n is already past maxLines, so
// bail. Saves O(text) walks on multi-megabyte single-line messages.
const budget = Math.min(text.length, maxLines * w + maxLines)
let n = 0
let start = 0
for (let i = 0; i <= budget; i++) {
if (i === text.length || i === budget || text.charCodeAt(i) === 10) {
const rows = Math.max(1, Math.ceil((i - start) / w))
n += rows >= maxLines - n ? maxLines - n : rows
start = i + 1
if (n >= maxLines) {
return maxLines
}
}
}
return n
}
export const estimatedMsgHeight = (
@@ -42,13 +71,11 @@ export const estimatedMsgHeight = (
{
compact,
details,
limitHistory = false,
userPrompt = '',
withSeparator = false
}: {
compact: boolean
details: boolean
limitHistory?: boolean
userPrompt?: string
withSeparator?: boolean
}
@@ -70,11 +97,16 @@ export const estimatedMsgHeight = (
}
const bodyWidth = transcriptBodyWidth(cols, msg.role, userPrompt)
const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text
const text = msg.text
let h = wrappedLines(text || ' ', bodyWidth)
if (!compact && msg.role === 'assistant') {
h += Math.min(6, (text.match(/\n\s*\n/g) ?? []).length)
// Paragraph gaps add up to 6 extra rows of breathing room. Slice
// first so the regex never walks more than the first ~16k chars of
// a giant assistant message — post-mount Yoga measurement converges
// to the real height regardless of how the estimate undercounts.
const scan = text.length > 16_000 ? text.slice(0, 16_000) : text
h += Math.min(6, (scan.match(/\n\s*\n/g) ?? []).length)
}
if (details) {