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:
zesdex
2026-09-03 12:28:56 +07:00
committed by asepharyana
parent 30cc579773
commit d86d6aeef2
3 changed files with 170 additions and 56 deletions
+27 -11
View File
@@ -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>
);
}