fix(tui): repair layout overflow + streaming dup; polish UI/state
Two real bugs squashed:
- Layout: giving the header box and scrollbox an explicit width in a
column-flex root made OpenTUI overflow vertically, pushing the input
row and status bar entirely off-screen (only header+one message showed).
Root cause confirmed in a minimal probe; fix = drop the redundant
width so column children size off the root box.
- Streaming: 9router's OpenAI-compatible layer emits FULL-TEXT chunks
(the whole transcript so far) instead of incremental deltas, so naive
content+=token duplicated text ("Hello! 👋 How canHello! 👋 How can...").
Added reconcileStream() that accepts both incremental and full-text
modes (shorter-replay → ignore, growing-authoritative → adopt, else
append), unit-tested for both.
UI/state polish:
- Rich transcript: per-role colour + labelled blocks (YOU/AI/SYS/TOOL),
word-wrapped to terminal width, reasoning dimmed, no misleading
"(empty)" on tool-calling assistant messages.
- Header: app · real model · session, spread across full width.
- Status bar: ● running/idle, live token usage (in/out), autocomplete
hint, active toast.
- Bordered autocomplete dropdown + full-width help/overlay panels.
- Turn runner streams all TurnEvents into state (tool markers, agent
progress, workflow updates, toasts, usage) — ui.tsx is now display-only.
This commit is contained in:
@@ -357,15 +357,46 @@ export class TranscriptCache {
|
||||
const msg = this.messages[this.messages.length - 1];
|
||||
if (msg && msg.role === Roles.Assistant) {
|
||||
if (isReasoning) {
|
||||
msg.reasoning += text;
|
||||
msg.reasoning = reconcileStream(msg.reasoning, text);
|
||||
} else {
|
||||
msg.content += text;
|
||||
msg.content = reconcileStream(msg.content, text);
|
||||
}
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconcile an incoming stream chunk against the accumulated text.
|
||||
*
|
||||
* Streaming backends disagree on chunk semantics:
|
||||
* - Native OpenAI/Anthropic send small *incremental* deltas.
|
||||
* - Some gateways/proxies (e.g. 9router's OpenAI-compatible layer) emit
|
||||
* *full-text* chunks, where each chunk is the complete transcript so far.
|
||||
*
|
||||
* Blindly concatenating full-text chunks duplicates content (partial + full),
|
||||
* which manifests as garbled messages. This helper accepts both modes:
|
||||
* - a chunk that is a strict prefix of the tail → a shorter replay → ignore;
|
||||
* - a chunk that extends/replays the whole accumulated text → take it as the
|
||||
* new authoritative tail (full-text mode);
|
||||
* - otherwise → treat as an incremental delta and append.
|
||||
*/
|
||||
export function reconcileStream(accumulated: string, chunk: string): string {
|
||||
if (chunk.length === 0) return accumulated;
|
||||
if (accumulated.length === 0) return chunk;
|
||||
if (accumulated.length >= chunk.length && accumulated.startsWith(chunk)) {
|
||||
// Shorter (or equal) replay of what we already hold — ignore.
|
||||
return accumulated;
|
||||
}
|
||||
if (chunk.length >= accumulated.length && chunk.startsWith(accumulated)) {
|
||||
// Full-text mode: the chunk is the whole transcript so far (matching our
|
||||
// accumulated text plus more). Adopt it wholesale.
|
||||
return chunk;
|
||||
}
|
||||
// Incremental delta mode: a new suffix. Append.
|
||||
return accumulated + chunk;
|
||||
}
|
||||
|
||||
/* ── AppStateRest ─────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
@@ -403,6 +434,8 @@ export interface AppStateRest {
|
||||
turnRunner: TurnRunner | null;
|
||||
/** Abort controller for the in-flight turn (driven by AbortTurn). */
|
||||
turnAbort: AbortController | null;
|
||||
/** Token usage from the most recent turn (surfaced in the status bar). */
|
||||
lastUsage: { tokensIn: number; tokensOut: number } | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_HELP_TEXT = ` Zesdex TUI — Keyboard Shortcuts
|
||||
@@ -474,6 +507,7 @@ export function createTuiState(
|
||||
pendingSubmit: null,
|
||||
turnRunner: null,
|
||||
turnAbort: null,
|
||||
lastUsage: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user