feat(tui): enhance model selection and improve UI components

- Added tests for handling key events in model selector, including Enter and ArrowDown functionality.
- Updated TUI layout to include a new header, activity bar, and improved status bar with spinner and uptime.
- Refactored transcript rendering to support markdown and improved message display.
- Introduced overlays for model selection, help, and editor with appropriate rendering logic.
- Implemented smoke tests for various TUI states to ensure UI integrity and functionality.
This commit is contained in:
asepharyana
2026-09-03 16:13:29 +07:00
parent 5912a35bbb
commit c53fdfa7c9
6 changed files with 896 additions and 220 deletions
+37 -22
View File
@@ -58,6 +58,25 @@ function makeTurnRunner(runtime: WiredRuntime, state: AppStateRest): TurnRunner
};
}
/** Append an entry to the bounded live-activity breadcrumb. */
function addActivity(
state: AppStateRest,
entry: { label: string; detail?: string; kind: "tool" | "agent" | "error" },
): void {
const activity = state.misc.activity;
// Dedupe consecutive repeats of the same tool so `read read read` collapses.
const last = activity[activity.length - 1];
const key = `${entry.label}:${entry.kind}`;
if (last && `${last.label}:${last.kind}` === key && last.detail === entry.detail) {
last.count = (last.count ?? 1) + 1;
} else {
activity.push({ ...entry, count: 1 });
}
// Keep the breadcrumb tight (max 12 entries).
while (activity.length > 12) activity.shift();
state.dirty = true;
}
/** Route one live `TurnEvent` into TUI state (transcript, status bar, toasts). */
function handleEvent(state: AppStateRest, ev: TurnEvent): void {
switch (ev.kind) {
@@ -74,38 +93,34 @@ function handleEvent(state: AppStateRest, ev: TurnEvent): void {
);
break;
case "tool_result":
pushTranscript(
state,
makeChatMessage(
Roles.Tool,
`→ ${ev.tool_name}${ev.is_error ? " (error)" : ""}`,
),
);
// A tool result is a breadcrumb, not a full transcript entry — keep the
// transcript clean and fold the tool name into the live activity bar.
addActivity(state, {
label: ev.tool_name,
detail: ev.is_error ? "error" : undefined,
kind: ev.is_error ? "error" : "tool",
});
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}` : ""}`,
),
);
addActivity(state, {
label: ev.progress.agent_name,
detail: ev.progress.error ?? ev.progress.current_tool ?? undefined,
kind: ev.progress.error ? "error" : "agent",
});
break;
case "workflow_agent_update":
pushTranscript(
state,
makeChatMessage(
Roles.Tool,
`[${ev.agent_name}] ${ev.status}${ev.error ? ` — ${ev.error}` : ""}`,
),
);
addActivity(state, {
label: ev.agent_name,
detail: ev.error ?? undefined,
kind: ev.error ? "error" : "agent",
});
break;
case "error":
pushTranscript(state, makeChatMessage(Roles.System, `error: ${ev.message}`));
addActivity(state, { label: "error", detail: ev.message, kind: "error" });
toastError(state, `Turn error: ${ev.message}`);
break;
case "compacted":