fix(tui): wrap streaming markdown split in column Box

StreamingMd returned <><Md/><Md/></> — a bare Fragment with two <Md>
children. Each <Md> returns a <Box flexDirection="column">, but its
parent in messageLine.tsx (line 169) is `<Box width={...}>` with no
flexDirection, which Ink defaults to 'row'. So during streaming the
two column boxes rendered side-by-side, producing the visible "tokens
jumble into two columns until it fixes itself" bug — the "fix" was
message.complete flipping isStreaming→false, which swaps the
StreamingMd subtree for a single DeferredMd/Md child (no siblings → row
direction is harmless).

Wrap the two <Md> siblings in a flexDirection="column" Box so they
stack. Localized fix so the non-streaming path (single-child, works
fine in a row parent) is untouched.

Reported by user:
> "tokens streaming... going into 2 columns randomly and jumbling
>  together until it fixes itself"

No test changes — findStableBoundary tests still pass (the layout
change is parent-structural, not in the boundary logic). Build clean,
tsc clean, 352 tests pass.
This commit is contained in:
Brooklyn Nicholson
2026-04-26 16:55:56 -05:00
parent cd7a200e6c
commit 7242361a69

View File

@@ -19,11 +19,16 @@
// flips off → message moves to history and renders via <Md> directly), so
// the ref resets naturally.
//
// See src/app/useMainApp.ts for the reasoning on why we don't memoize the
// whole Md text during streaming: that cache never hits because `text` is
// growing. Mirror claude-code's `StreamingMarkdown` approach adapted to
// our line-based tokenizer.
// Layout: the two <Md> subtrees MUST render stacked (column). The parent
// container in messageLine.tsx is a default `flexDirection: 'row'` Box
// (Ink's default), so returning a bare Fragment of two <Md> siblings
// laid them out side-by-side — producing the "two jumbled columns while
// streaming" rendering bug. Wrapping in a flexDirection="column" Box
// here localizes the fix to the streaming path; the non-streaming <Md>
// already returns its own column Box, so its single-child case was never
// affected.
import { Box } from '@hermes/ink'
import { memo, useRef } from 'react'
import type { Theme } from '../theme.js'
@@ -113,10 +118,10 @@ export const StreamingMd = memo(function StreamingMd({ compact, t, text }: Strea
}
return (
<>
<Box flexDirection="column">
<Md compact={compact} t={t} text={stablePrefix} />
<Md compact={compact} t={t} text={unstableSuffix} />
</>
</Box>
)
})