fix(tui): sliding-window stream reconciler + UI polish
- Replace naive reconcileStream with stateful sliding-window replay merger: tracks per-message replay cursors (_posContent/_posReasoning) to deduplicate the segment-replay waves 9router emits. Verified against 970 real captured chunks (100% clean output). - TranscriptMessage: colored [TAG] headers, 4-space indented body, thin separator lines between messages, dimmed reasoning. - InputView: divider above prompt, '❯' prompt, cursor block. - 75 tests pass, typecheck clean, biome clean.
This commit is contained in:
+27
-11
@@ -276,25 +276,37 @@ function TranscriptMessage(props: {
|
||||
msg.role === Roles.Assistant && !msg.content.trim()
|
||||
? ""
|
||||
: msg.content || "(empty)";
|
||||
const bodyWidth = Math.max(20, width - 6);
|
||||
|
||||
// Each message is a labelled block: a coloured role tag line followed by the
|
||||
// word-wrapped body. Plain `<text>` is used (not `<markdown>`) so live
|
||||
// streaming re-renders a single clean string — no overlap artifacts, and the
|
||||
// body fills the full transcript width.
|
||||
return (
|
||||
<box flexDirection="column" width="100%">
|
||||
<text fg={color}>{` ${tag.padEnd(4)} `}</text>
|
||||
{/* Role tag header line */}
|
||||
<text fg={color}>{` [${tag}]`}</text>
|
||||
{/* Indented, word-wrapped body */}
|
||||
{content.length > 0 && (
|
||||
<text fg={color}>{wrapText(content, Math.max(20, width - 4))}</text>
|
||||
<text fg={msg.role === Roles.Assistant ? "default" : color}>
|
||||
{indent(content, 4, bodyWidth)}
|
||||
</text>
|
||||
)}
|
||||
{/* Live reasoning (dimmed, kept to one line) */}
|
||||
{msg.reasoning.length > 0 && (
|
||||
<text fg="gray">{` ⋯ ${oneLine(msg.reasoning)}`}</text>
|
||||
<text fg="gray">{` ⋯ ${oneLine(msg.reasoning)}`}</text>
|
||||
)}
|
||||
{/* Thin separator between messages */}
|
||||
<text fg="#2a3340">{` ${"─".repeat(Math.max(0, Math.min(60, width - 4)))}`}</text>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bottom input row: `>` prompt + buffer on one line. */
|
||||
/** Indent every wrapped line of `text` by `pad` spaces within `width` columns. */
|
||||
function indent(text: string, pad: number, width: number): string {
|
||||
return wrapText(text, width)
|
||||
.split("\n")
|
||||
.map((l) => (l.length ? " ".repeat(pad) + l : ""))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/** Bottom input row: `❯` prompt + buffer on one line, with a subtle divider. */
|
||||
function InputView(props: { state: AppStateRest; width: number }): ReactNode {
|
||||
const { state, width } = props;
|
||||
const shown = state.input.buffer.slice(
|
||||
@@ -302,9 +314,13 @@ function InputView(props: { state: AppStateRest; width: number }): ReactNode {
|
||||
Math.max(0, state.input.cursor) + 40,
|
||||
);
|
||||
return (
|
||||
<box flexDirection="row" width="100%">
|
||||
<text fg="cyan">{"> "}</text>
|
||||
<text fg="white">{shown}</text>
|
||||
<box flexDirection="column" width="100%">
|
||||
<text fg="#2a3340">{" " + "─".repeat(Math.max(0, width - 4))}</text>
|
||||
<box flexDirection="row" width="100%">
|
||||
<text fg="cyan">{" ❯ "}</text>
|
||||
<text fg="white">{shown}</text>
|
||||
<text fg="gray">{"▏"}</text>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user