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:
@@ -8,18 +8,35 @@
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import { newStore, userMessage } from "@zesdex/domain";
|
||||
import type { TurnEventSink } from "@zesdex/agent";
|
||||
import type { TurnEvent, TurnEventSink } from "@zesdex/agent";
|
||||
import { runSingleProcess, type WiredRuntime } from "../cli/compose.ts";
|
||||
import { createTuiState, type AppStateRest, type TurnRunner } from "./state.ts";
|
||||
import {
|
||||
createTuiState,
|
||||
type AppStateRest,
|
||||
type TurnRunner,
|
||||
pushTranscript,
|
||||
makeChatMessage,
|
||||
appendToLastTranscript,
|
||||
toastInfo,
|
||||
toastError,
|
||||
} from "./state.ts";
|
||||
import { Roles } from "@zesdex/domain";
|
||||
import { bootTui } from "./ui.tsx";
|
||||
|
||||
/** Build the real turn runner from the wired runtime. */
|
||||
function makeTurnRunner(runtime: WiredRuntime): TurnRunner {
|
||||
/**
|
||||
* Build the real turn runner from the wired runtime.
|
||||
*
|
||||
* Streams every `TurnEvent` into the transcript and updates live UI state
|
||||
* (status bar token usage, reasoning block, tool call markers, toasts).
|
||||
*/
|
||||
function makeTurnRunner(runtime: WiredRuntime, state: AppStateRest): TurnRunner {
|
||||
return async (text, onEvent) => {
|
||||
const abort = new AbortController();
|
||||
state.turnAbort = abort;
|
||||
// Live sink: forward every event to the UI immediately as it happens.
|
||||
const sink: TurnEventSink = {
|
||||
push(ev) {
|
||||
push(ev: TurnEvent) {
|
||||
handleEvent(state, ev);
|
||||
onEvent(ev);
|
||||
},
|
||||
drain() {
|
||||
@@ -41,6 +58,70 @@ function makeTurnRunner(runtime: WiredRuntime): TurnRunner {
|
||||
};
|
||||
}
|
||||
|
||||
/** Route one live `TurnEvent` into TUI state (transcript, status bar, toasts). */
|
||||
function handleEvent(state: AppStateRest, ev: TurnEvent): void {
|
||||
switch (ev.kind) {
|
||||
case "stream_token":
|
||||
appendToLastTranscript(state, ev.content, false);
|
||||
break;
|
||||
case "stream_reasoning":
|
||||
appendToLastTranscript(state, ev.content, true);
|
||||
break;
|
||||
case "system_note":
|
||||
pushTranscript(
|
||||
state,
|
||||
makeChatMessage(Roles.System, `[${ev.systemKind}] ${ev.message}`),
|
||||
);
|
||||
break;
|
||||
case "tool_result":
|
||||
pushTranscript(
|
||||
state,
|
||||
makeChatMessage(
|
||||
Roles.Tool,
|
||||
`→ ${ev.tool_name}${ev.is_error ? " (error)" : ""}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "usage":
|
||||
state.lastUsage = { tokensIn: ev.tokens_in, tokensOut: ev.tokens_out };
|
||||
state.dirty = true;
|
||||
break;
|
||||
case "agent_progress":
|
||||
pushTranscript(
|
||||
state,
|
||||
makeChatMessage(
|
||||
Roles.Tool,
|
||||
`[${ev.progress.agent_name}] ${ev.progress.status}${ev.progress.error ? ` — ${ev.progress.error}` : ev.progress.current_tool ? ` · ${ev.progress.current_tool}` : ""}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "workflow_agent_update":
|
||||
pushTranscript(
|
||||
state,
|
||||
makeChatMessage(
|
||||
Roles.Tool,
|
||||
`[${ev.agent_name}] ${ev.status}${ev.error ? ` — ${ev.error}` : ""}`,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case "error":
|
||||
pushTranscript(state, makeChatMessage(Roles.System, `error: ${ev.message}`));
|
||||
toastError(state, `Turn error: ${ev.message}`);
|
||||
break;
|
||||
case "compacted":
|
||||
pushTranscript(state, makeChatMessage(Roles.System, "Conversation compacted."));
|
||||
break;
|
||||
case "todo_update":
|
||||
toastInfo(state, `TODO updated: ${ev.content.slice(0, 80)}`);
|
||||
break;
|
||||
case "plan_update":
|
||||
toastInfo(state, `Plan updated: ${ev.content.slice(0, 80)}`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the interactive OpenTUI with the real agent wired in. */
|
||||
export async function runTui(): Promise<void> {
|
||||
const cwd = process.cwd();
|
||||
@@ -57,10 +138,17 @@ export async function runTui(): Promise<void> {
|
||||
const memoryDir = `${dataDir}/memory`;
|
||||
|
||||
const state: AppStateRest = createTuiState([cwd], sessionDir, memoryDir);
|
||||
state.turnRunner = makeTurnRunner(runtime);
|
||||
state.turnRunner = makeTurnRunner(runtime, state);
|
||||
state.misc.apiConnected = true;
|
||||
// Surface the real effective model/provider in the header.
|
||||
state.settings = { provider: runtime.model } as Record<string, unknown>;
|
||||
|
||||
// Surface the real effective model / provider / session in the header.
|
||||
const provider = (state.settings.provider as string) || "claude";
|
||||
state.settings = {
|
||||
provider,
|
||||
model: runtime.model,
|
||||
apiBase: runtime.apiBase ?? "",
|
||||
sessionId: state.sessionId,
|
||||
} as Record<string, unknown>;
|
||||
|
||||
await bootTui(state);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user