repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/ultra/ui.ts
Raw// ultra — live progress sinks (Task 7). TWO surface-specific views over the
// shared `progress.ts` reducer, render + input only, NO orchestration:
//
// • TOOL sink (Layer 7a) — a `renderResult(result, opts, theme, ctx)` that
// folds NOTHING itself: it reads the reducer state off `result.details`
// (pushed through the SDK `onUpdate` channel by the engine) and draws a
// collapsed/expanded board. Read-only — no `controls`, no steering, and it
// NEVER renders per-agent bodies/transcripts. Matches Pi tool rendering.
//
// • COMMAND overlay (Layers 7b/7c) — a `ctx.ui.custom` `Component` that folds
// the `onProgress` stream itself, renders the board + a focused-agent
// drill-in transcript (follow-mode + throttle), and is the ONLY surface
// with per-agent steering/abort. Modeled on `devil/ui.ts`.
//
// Both sinks consume only what `progress.ts` defines; this module never
// constructs an SDK session and never holds orchestration logic.
import type {
AgentToolResult,
Theme,
ToolDefinition,
ToolRenderResultOptions,
} from "@earendil-works/pi-coding-agent";
import {
createBashToolDefinition,
createEditToolDefinition,
createFindToolDefinition,
createGrepToolDefinition,
createLsToolDefinition,
createPowerShellToolDefinition,
createReadToolDefinition,
createWriteToolDefinition,
getMarkdownTheme,
keyHint,
ToolExecutionComponent,
} from "@earendil-works/pi-coding-agent";
import {
type Component,
Markdown,
matchesKey,
Text,
type TUI,
truncateToWidth,
} from "@earendil-works/pi-tui";
import {
formatStructuredText,
formatUsage,
formatValueMarkdown,
summarizeValue,
} from "./display.ts";
import {
type AgentControls,
type AgentStatus,
initialReducerState,
type PlannedPhase,
type ReducerState,
reduceProgress,
type TranscriptItem,
type UltraProgressEvent,
} from "./progress.ts";
import { formatUltraResultMarkdown } from "./result.ts";
import type { TokenUsage } from "./runner.ts";
// ---------------------------------------------------------------------------
// Shared board helpers (used by both sinks)
// ---------------------------------------------------------------------------
const STATUS_ICON: Record<AgentStatus, string> = {
queued: "",
running: "",
done: "",
dropped: "",
};
const PHASE_ICON = {
"agents-pending": "",
"condition-pending": "",
skipped: "−",
} as const;
const RESET = /\x1b\[0m/g;
function fitLine(text: string, width: number): string {
const w = Math.max(1, width);
const out = truncateToWidth(text, w);
return text.includes("\x1b") ? out : out.replace(RESET, "");
}
/** `phase N/len` from the reducer's ordered phase plan. */
function phaseCounter(state: ReducerState): string {
if (state.phases.length === 0) return "";
const idx = state.phase
? state.phases.findIndex((phase) => phase.phase === state.phase) + 1
: 0;
return `phase ${idx}/${state.phases.length}`;
}
/** One-line aggregate with terminal, active, queued, and later work counts. */
function summaryLine(state: ReducerState): string {
const parts: string[] = [];
const pc = phaseCounter(state);
if (pc) parts.push(pc);
parts.push(`${state.done}/${state.total} done`);
if (state.running > 0) parts.push(`${state.running} active`);
if (state.queued > 0) parts.push(`${state.queued} queued`);
if (state.later > 0)
parts.push(`${state.later} later phase${state.later === 1 ? "" : "s"}`);
if (state.dropped > 0) parts.push(`${state.dropped}✗`);
return parts.join(" · ");
}
function headerLine(state: ReducerState, brand = "ultra"): string {
const wallTime =
state.startedAt === undefined
? undefined
: `${elapsed(state.startedAt, state.endedAt)} elapsed`;
return [brand, state.workflowName, wallTime].filter(Boolean).join(" · ");
}
function descriptionLine(state: ReducerState): string | undefined {
return state.workflowDescription;
}
function elapsed(startedAt: number, endedAt = Date.now()): string {
const seconds = Math.max(0, Math.floor((endedAt - startedAt) / 1000));
if (seconds < 60) return `${seconds}s`;
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
}
type AgentRow = ReducerState["rows"][number];
type AgentBoardLine = {
text: string;
color: "accent" | "error" | "dim";
bold?: boolean;
};
function agentModel(row: AgentRow): string | undefined {
const model = row.tier
? `${row.tier}${row.model ? ` → ${row.model}` : ""}`
: (row.model ?? row.failure?.model);
const thinkingLevel = row.thinkingLevel ?? row.failure?.thinkingLevel;
return [model, thinkingLevel].filter(Boolean).join(" · ") || undefined;
}
function agentMetadata(row: AgentRow, state: ReducerState): string {
const task = row.summary?.trim() || row.agentId;
const parts = [task === row.agentId ? undefined : row.agentId, row.status];
if (row.usage) parts.push(formatUsage(row.usage).summary);
if (row.failure)
parts.push(
`${row.failure.retryable ? "retryable" : "terminal"} · ${row.failure.attempts} attempt${row.failure.attempts === 1 ? "" : "s"}`,
);
const model = agentModel(row);
if (model) parts.push(model);
if (row.startedAt !== undefined)
parts.push(elapsed(row.startedAt, row.endedAt));
if (row.cached) parts.push("checkpoint reused");
if (row.status === "queued") parts.push(queuedDetail(state, row.phase));
return parts.filter(Boolean).join(" · ");
}
function agentBoardLines(
row: AgentRow,
state: ReducerState,
prefix: string,
): AgentBoardLine[] {
const task = row.summary?.trim() || row.agentId;
const color =
row.status === "running"
? "accent"
: row.status === "dropped"
? "error"
: "dim";
const primary = `${prefix}${STATUS_ICON[row.status]} ${task}`;
const metadata = agentMetadata(row, state);
if (row.status === "done" || row.status === "queued") {
return [
{ text: primary, color },
{
text: `${" ".repeat(prefix.length + 2)}${metadata}`,
color: "dim",
},
];
}
const activity = row.failure
? `${row.failure.code}: ${row.failure.message}`
: row.action?.trim();
const secondary = [activity, metadata].filter(Boolean);
return [
{ text: primary, color, bold: true },
...secondary.map(
(text): AgentBoardLine => ({
text: `${" ".repeat(prefix.length + 2)}${text}`,
color: "dim",
}),
),
];
}
type DisplayRow =
| { kind: "agent"; row: ReducerState["rows"][number] }
| { kind: "phase"; phase: PlannedPhase; expanded: boolean };
function rowsInPhase(state: ReducerState, phase: string) {
return state.rows.filter((row) => row.phase === phase);
}
function displayRows(
state: ReducerState,
expanded: (phase: PlannedPhase) => boolean,
): DisplayRow[] {
const out: DisplayRow[] = [];
const seen = new Set<string>();
for (const phase of state.phases) {
const open = expanded(phase);
out.push({ kind: "phase", phase, expanded: open });
if (!open) continue;
for (const row of rowsInPhase(state, phase.phase)) {
seen.add(row.agentId);
out.push({ kind: "agent", row });
}
}
for (const row of state.rows) {
if (
!seen.has(row.agentId) &&
!state.phases.some((p) => p.phase === row.phase)
)
out.push({ kind: "agent", row });
}
return out;
}
function phaseLabel(
state: ReducerState,
phase: PlannedPhase,
expanded: boolean,
): string {
if (phase.status === "agents-pending")
return `${PHASE_ICON[phase.status]} ${phase.phase} · agents not planned yet`;
if (phase.status === "condition-pending")
return `${PHASE_ICON[phase.status]} ${phase.phase} · conditional`;
if (phase.status === "skipped")
return `${PHASE_ICON[phase.status]} ${phase.phase} · skipped`;
const rows = rowsInPhase(state, phase.phase);
const done = rows.filter((row) => row.status === "done").length;
const running = rows.filter((row) => row.status === "running").length;
const dropped = rows.filter((row) => row.status === "dropped").length;
const queued = rows.filter((row) => row.status === "queued").length;
const counts = [
`${done}/${rows.length} done`,
running ? `${running} active` : "",
queued ? `${queued} queued` : "",
dropped ? `${dropped} dropped` : "",
]
.filter(Boolean)
.join(" · ");
if (phase.status === "done")
return `${dropped ? STATUS_ICON.dropped : STATUS_ICON.done} ${phase.phase}${counts ? ` · ${counts}` : " · done"}`;
if (state.phase === phase.phase)
return `${expanded ? "▼" : "▶"} ${phase.phase}${counts ? ` · ${counts}` : ""}`;
return `${STATUS_ICON.queued} ${phase.phase}${rows.length ? ` · ${rows.length} queued` : ""}`;
}
function phaseDetail(phase: PlannedPhase): string | undefined {
if (phase.status === "agents-pending")
return "earlier results determine which agents are needed";
if (phase.status === "condition-pending")
return "earlier results determine whether this phase runs";
return undefined;
}
function queuedDetail(state: ReducerState, phase: string): string {
return state.phase === phase
? "waiting for concurrency slot"
: "starts when phase begins";
}
function automaticPhase(state: ReducerState): string | undefined {
const current = state.phases.find(
(phase) => phase.phase === state.phase && phase.status === "resolved",
);
return (current ?? state.phases.find((phase) => phase.status === "resolved"))
?.phase;
}
function toolPhaseExpanded(state: ReducerState, phase: PlannedPhase): boolean {
if (rowsInPhase(state, phase.phase).some((row) => row.status === "dropped"))
return true;
return phase.phase === automaticPhase(state);
}
function aggregateUsage(state: ReducerState): TokenUsage | undefined {
if (state.workflowResult?.tokenUsage) return state.workflowResult.tokenUsage;
let usage: TokenUsage | undefined;
for (const row of state.rows) {
if (!row.usage) continue;
usage ??= {
input: 0,
output: 0,
total: 0,
cacheRead: 0,
cacheWrite: 0,
cost: 0,
};
usage.input += row.usage.input;
usage.output += row.usage.output;
usage.total += row.usage.total;
usage.cacheRead += row.usage.cacheRead;
usage.cacheWrite += row.usage.cacheWrite;
usage.cost += row.usage.cost;
}
return usage;
}
function metricsLines(state: ReducerState, expanded: boolean): string[] {
if (state.total === 0) return [];
const usage = aggregateUsage(state);
const formatted = usage ? formatUsage(usage) : undefined;
const resumed = state.rows.filter((row) => row.cached).length;
if (!usage && state.done === 0 && resumed === 0) return [];
return [
[
formatted?.summary,
`checkpoint ${state.done}/${state.total}`,
resumed ? `${resumed} resumed` : "",
]
.filter(Boolean)
.join(" · "),
...(expanded && formatted ? [formatted.detail] : []),
];
}
function divider(label: string, width: number): string {
const w = Math.max(1, width);
const text = ` ${label} `;
if (text.length >= w) return fitLine(label, w);
const left = Math.floor((w - text.length) / 2);
const right = w - text.length - left;
return `${"─".repeat(left)}${text}${"─".repeat(right)}`;
}
/** The collapsed expand hint. Guarded: `keyHint` needs an initialised theme. */
function expandHint(): string {
try {
return keyHint("app.tools.expand", "to expand");
} catch {
return "to expand";
}
}
// ---------------------------------------------------------------------------
// Layer 7a — TOOL sink: `renderResult` over the reducer state
// ---------------------------------------------------------------------------
/**
* Pure board renderer for the tool surface. Collapsed = aggregate counts +
* bounded phase and run summaries + the expand hint (never action or transcript
* bodies). Expanded = phase-first summaries with active and failed phases opened,
* plus model/usage metadata and the final outcome. The drill-in transcript is
* the OVERLAY's job, never the tool sink's, so `state.transcripts` is never read here.
*/
function renderToolBoard(
state: ReducerState,
options: ToolRenderResultOptions,
theme: Theme,
width: number,
): string[] {
const w = Math.max(1, width);
const lines: string[] = [];
const header = `${headerLine(state, theme.bold("ultra"))} · ${summaryLine(state)}`;
lines.push(theme.fg("accent", fitLine(header, w)));
const description = descriptionLine(state);
if (description) lines.push(theme.fg("muted", fitLine(description, w)));
for (const metrics of metricsLines(state, options.expanded))
lines.push(theme.fg("muted", fitLine(metrics, w)));
if (!options.expanded) {
const focus = Math.max(
0,
state.phases.findIndex((phase) => phase.phase === state.phase),
);
const first = Math.max(0, Math.min(focus - 1, state.phases.length - 3));
if (first > 0)
lines.push(theme.fg("dim", fitLine(` ${first} earlier phases`, w)));
for (const phase of state.phases.slice(first, first + 3))
lines.push(
theme.fg("muted", fitLine(` ${phaseLabel(state, phase, false)}`, w)),
);
const remaining = state.phases.length - first - 3;
if (remaining > 0)
lines.push(theme.fg("dim", fitLine(` ${remaining} later phases`, w)));
const current =
state.phase ??
state.phases.find((phase) => phase.status === "resolved")?.phase;
const runs = current ? rowsInPhase(state, current) : [];
const ranked = (["running", "dropped", "queued", "done"] as const).flatMap(
(status) => runs.filter((row) => row.status === status),
);
for (const row of ranked.slice(0, 3)) {
const summary = row.summary?.trim();
const label = summary ? `${row.agentId} · ${summary}` : row.agentId;
lines.push(
theme.fg(
row.status === "dropped" ? "error" : "muted",
fitLine(` ${STATUS_ICON[row.status]} ${label}`, w),
),
);
}
if (ranked.length > 3)
lines.push(
theme.fg("dim", fitLine(` ${ranked.length - 3} more runs`, w)),
);
if (state.workflowResult?.fullOutputPath)
lines.push(
theme.fg(
"muted",
fitLine(`Evidence: ${state.workflowResult.fullOutputPath}`, w),
),
);
lines.push(theme.fg("muted", fitLine(expandHint(), w)));
return lines.map((l) => fitLine(l, w));
}
// Expanded: one phase row each, with only active or failed phases opened.
for (const display of displayRows(state, (phase) =>
toolPhaseExpanded(state, phase),
)) {
if (display.kind === "phase") {
const color =
display.phase.status === "done"
? "success"
: state.phase === display.phase.phase
? "accent"
: "dim";
lines.push(
theme.fg(
color,
fitLine(phaseLabel(state, display.phase, display.expanded), w),
),
);
const detail = phaseDetail(display.phase);
if (detail) lines.push(theme.fg("muted", fitLine(` └ ${detail}`, w)));
continue;
}
for (const line of agentBoardLines(display.row, state, " ")) {
const text = line.bold ? theme.bold(line.text) : line.text;
lines.push(theme.fg(line.color, fitLine(text, w)));
}
}
if (state.workflowResult) {
lines.push(theme.fg("border", divider("outcome", w)));
lines.push(
...new Markdown(
formatUltraResultMarkdown(state.workflowResult),
0,
0,
getMarkdownTheme(),
).render(w),
);
}
return lines.map((l) => fitLine(l, w));
}
/**
* A throwaway `Component` that draws the tool-surface board for ONE render.
* It holds no cache: every `render(width)` recomputes from the snapshot it was
* built with, so a fresh `renderResult` call (one per `onUpdate`, P2) with fresh
* `details` always tracks the latest reducer state (no frozen snapshot).
*/
class UltraResultComponent implements Component {
constructor(
private readonly state: ReducerState,
private readonly options: ToolRenderResultOptions,
private readonly theme: Theme,
) {}
render(width: number): string[] {
return renderToolBoard(this.state, this.options, this.theme, width);
}
invalidate(): void {
// No cached state — nothing to drop.
}
}
/**
* Accept `result.details` only when it structurally IS a reducer state. It
* reaches us over an SDK-controlled channel (structured-cloned per `onUpdate`
* tick, and re-hydrated from session persistence on resume), so a truthy-but-
* foreign value can arrive — a resumed/foreign tool result, a payload from
* another producer, a serialization wrapper. The board reads `state.phases`
* and `state.rows` unguarded, so anything lacking them falls back to an empty
* board. This is the single choke point: an uncaught throw in `renderResult`
* escapes tool-error handling and crashes the whole TUI render loop.
*/
function asReducerState(details: unknown): ReducerState {
const d = details as Partial<ReducerState> | null | undefined;
const phaseStatuses: readonly string[] = [
"resolved",
"agents-pending",
"condition-pending",
"done",
"skipped",
];
const agentStatuses: readonly string[] = [
"queued",
"running",
"done",
"dropped",
];
const phasesValid =
Array.isArray(d?.phases) &&
d.phases.every(
(phase) =>
typeof phase === "object" &&
phase !== null &&
typeof phase.phase === "string" &&
phaseStatuses.includes(phase.status) &&
Array.isArray(phase.agentIds) &&
phase.agentIds.every((agentId: unknown) => typeof agentId === "string"),
);
const rowsValid =
Array.isArray(d?.rows) &&
d.rows.every(
(row) =>
typeof row === "object" &&
row !== null &&
typeof row.agentId === "string" &&
typeof row.phase === "string" &&
agentStatuses.includes(row.status),
);
return d && phasesValid && rowsValid
? (d as ReducerState)
: initialReducerState();
}
/**
* Tool-surface `renderResult`: reads the reducer state off `result.details`
* (pushed through `onUpdate`) and returns a board `Component`. Read-only — no
* `controls`, no steering. SDK errors have text content, not reducer details;
* render them before the board fallback so failed calls never look successful.
*/
export function renderUltraResult(
result: AgentToolResult<ReducerState | undefined>,
options: ToolRenderResultOptions,
theme: Theme,
context?: { isError: boolean },
): Component {
if (context?.isError) {
const message =
result.content
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n")
.trim() || "Workflow failed without an error message.";
if (options.expanded)
return new Text(theme.fg("error", `ultra · failed\n${message}`), 0, 0);
return {
render: (width) => [
theme.fg(
"error",
fitLine(
`ultra · failed: ${message.replace(/\s+/g, " ")}`,
Math.min(width, 160),
),
),
theme.fg("muted", fitLine(expandHint(), width)),
],
invalidate() {},
};
}
return new UltraResultComponent(
asReducerState(result?.details),
options,
theme,
);
}
// ---------------------------------------------------------------------------
// Layers 7b/7c — COMMAND overlay: board + drill-in transcript + steer/abort
// ---------------------------------------------------------------------------
/**
* Per-agent controls as the OVERLAY consumes them. Extends the locked
* `AgentControls` (`{steer, abort}`) with an optional `followUp` for re-prompting
* an agent that has already finished. The runner binds `followUp` on a finished
* agent's controls (cross-task contract — see module notes); when absent the
* follow-up affordance is simply inert.
*/
export interface OverlayAgentControls extends AgentControls {
followUp?(text: string): Promise<void> | void;
}
/**
* Minimal host surface the overlay needs. The real `TUI` (passed by
* `ctx.ui.custom`) satisfies this structurally; tests inject a fake with a
* `requestRender` spy + a fixed terminal size.
*/
export interface OverlayHost {
requestRender(): void;
terminal: { rows: number; columns: number };
}
export interface UltraOverlayOptions {
host: OverlayHost;
theme: Theme;
/** Confirmed `Esc Esc` run-level abort through the command path's AbortController. */
onRunAbort: () => void;
/** Injectable clock for the render throttle (default `Date.now`). */
now?: () => number;
/** Render-throttle window in ms (default 80, matching devil). */
throttleMs?: number;
workflowName?: string;
workflowDescription?: string;
/** Optional Pi TUI handle; when present, tool transcript rows use Pi's native tool renderer. */
toolUi?: TUI;
toolsExpanded?: () => boolean;
hideToolPreviews?: boolean;
isToolsExpandKey?: (data: string) => boolean;
onToolsExpand?: () => void;
cwd?: string;
}
const DEFAULT_THROTTLE_MS = 80;
// Use Pi's native views for source/code tools; generic tools get a field/list view.
const nativeToolDefinitions = new Map<
string,
(
cwd: string,
) => NonNullable<ConstructorParameters<typeof ToolExecutionComponent>[4]>
>([
["read", createReadToolDefinition],
["write", createWriteToolDefinition],
["edit", createEditToolDefinition],
["bash", createBashToolDefinition],
["powershell", createPowerShellToolDefinition],
["grep", createGrepToolDefinition],
["find", createFindToolDefinition],
["ls", createLsToolDefinition],
]);
/** Render transcript markdown with Pi's own markdown renderer. */
function transcriptLines(text: string, width: number): string[] {
if (!text) return [];
return new Markdown(text, 0, 0, getMarkdownTheme()).render(
Math.max(1, width),
);
}
function normalizeToolResult(item: TranscriptItem & { kind: "tool" }) {
const r = item.result as
| { content?: unknown; details?: unknown; isError?: boolean }
| undefined;
const content = Array.isArray(r?.content)
? (r.content as Array<{
type: string;
text?: string;
data?: string;
mimeType?: string;
}>)
: item.fallback
? [{ type: "text", text: item.fallback }]
: [];
return {
content,
details: r?.details,
isError: item.isError ?? r?.isError ?? false,
};
}
/**
* The `ctx.ui.custom` overlay (command surface only). Folds the `onProgress`
* stream with the shared reducer, renders the board + a focused-agent drill-in
* transcript (follow-mode), and exposes per-agent steer/abort + global abort.
* Render + input only: it holds NO orchestration — the run is driven elsewhere
* and pushes events in via `handleProgress`.
*/
export class UltraOverlayComponent implements Component {
private state: ReducerState;
private focusKey: string | undefined;
private readonly expandedPhases = new Set<string>();
private readonly collapsedPhases = new Set<string>();
private followMode = true;
private scrollOffset = 0;
private lastMaxScroll = 0;
private lastRenderAt = Number.NEGATIVE_INFINITY;
private steerMode = false;
private steerBuffer = "";
private steeredFlag = false;
private abortArmedAt: number | undefined;
private readonly transcriptTools = new Map<
string,
{
component: ToolExecutionComponent;
args: unknown;
result: unknown;
isError: boolean | undefined;
isPartial: boolean | undefined;
expanded: boolean;
}
>();
private readonly host: OverlayHost;
private readonly theme: Theme;
private readonly onRunAbort: () => void;
private readonly now: () => number;
private readonly throttleMs: number;
private readonly toolUi: TUI | undefined;
private readonly toolsExpanded: () => boolean;
private readonly hideToolPreviews: boolean;
private readonly isToolsExpandKey: (data: string) => boolean;
private readonly onToolsExpand: () => void;
private readonly cwd: string | undefined;
constructor(options: UltraOverlayOptions) {
this.state = initialReducerState({
workflowName: options.workflowName,
workflowDescription: options.workflowDescription,
});
this.host = options.host;
this.theme = options.theme;
this.onRunAbort = options.onRunAbort;
this.now = options.now ?? Date.now;
this.throttleMs = options.throttleMs ?? DEFAULT_THROTTLE_MS;
this.toolUi = options.toolUi;
this.toolsExpanded = options.toolsExpanded ?? (() => false);
this.hideToolPreviews = options.hideToolPreviews ?? false;
this.isToolsExpandKey = options.isToolsExpandKey ?? (() => false);
this.onToolsExpand = options.onToolsExpand ?? (() => {});
this.cwd = options.cwd;
}
/** True once the human has submitted a steer this run (enqueue-time taint). */
get steered(): boolean {
return this.steeredFlag;
}
/** Test/inspection accessor for the folded reducer state. */
get reducerState(): ReducerState {
return this.state;
}
// ---- progress sink: fold + throttled re-render -----------------------
handleProgress(event: UltraProgressEvent): void {
const previousPhase = this.state.phase;
this.state = reduceProgress(this.state, event);
if (event.kind === "start" && event.phase !== previousPhase) {
if (previousPhase) {
this.expandedPhases.delete(previousPhase);
this.collapsedPhases.delete(previousPhase);
}
this.collapsedPhases.delete(event.phase);
this.focusKey = `agent:${event.agentId}`;
this.followMode = true;
this.scrollOffset = 0;
}
this.ensureVisibleFocus();
const now = this.now();
// Terminal events bypass the throttle so the final board is never lost
// behind the render window.
const force = event.kind === "end" || event.kind === "phase";
if (force || now - this.lastRenderAt >= this.throttleMs) {
this.lastRenderAt = now;
this.host.requestRender();
}
}
// ---- input ------------------------------------------------------------
handleInput(data: string): void {
if (this.isToolsExpandKey(data)) {
this.onToolsExpand();
this.host.requestRender();
return;
}
if (this.steerMode) {
this.handleSteerInput(data);
return;
}
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
const now = this.now();
if (this.abortArmedAt !== undefined && now - this.abortArmedAt <= 1500) {
this.abortArmedAt = undefined;
this.onRunAbort();
} else {
this.abortArmedAt = now;
this.host.requestRender();
}
return;
}
this.abortArmedAt = undefined;
if (matchesKey(data, "tab")) {
this.focusBy(1, true);
} else if (matchesKey(data, "down")) {
this.focusBy(1, false);
} else if (matchesKey(data, "up")) {
this.focusBy(-1, false);
} else if (matchesKey(data, "return") || matchesKey(data, "enter")) {
this.toggleFocusedPhase();
} else if (
matchesKey(data, "pageUp") ||
matchesKey(data, "alt+up") ||
matchesKey(data, "ctrl+u")
) {
this.scrollBy(-10);
} else if (
matchesKey(data, "pageDown") ||
matchesKey(data, "alt+down") ||
matchesKey(data, "ctrl+d")
) {
this.scrollBy(10);
} else if (data === "s") {
this.openSteer();
} else if (data === "x") {
this.abortFocused();
}
}
private phaseExpanded(phase: PlannedPhase): boolean {
if (this.collapsedPhases.has(phase.phase)) return false;
if (this.expandedPhases.has(phase.phase)) return true;
return phase.phase === automaticPhase(this.state);
}
private visibleRows(): DisplayRow[] {
return displayRows(this.state, (phase) => this.phaseExpanded(phase));
}
private displayKey(display: DisplayRow): string {
return display.kind === "phase"
? `phase:${display.phase.phase}`
: `agent:${display.row.agentId}`;
}
private ensureVisibleFocus(): void {
const visible = this.visibleRows();
if (visible.length === 0) {
this.focusKey = undefined;
return;
}
if (
this.focusKey &&
visible.some((row) => this.displayKey(row) === this.focusKey)
)
return;
const firstAgent = visible.find((row) => row.kind === "agent");
this.focusKey = this.displayKey(firstAgent ?? visible[0]);
}
private focusBy(delta: number, agentsOnly: boolean): void {
const candidates = this.visibleRows().filter(
(row) => !agentsOnly || row.kind === "agent",
);
if (candidates.length === 0) return;
const current = candidates.findIndex(
(row) => this.displayKey(row) === this.focusKey,
);
const next =
current < 0
? 0
: agentsOnly
? (current + delta + candidates.length) % candidates.length
: Math.min(candidates.length - 1, Math.max(0, current + delta));
this.focusKey = this.displayKey(candidates[next]);
this.followMode = true;
this.scrollOffset = 0;
this.host.requestRender();
}
private toggleFocusedPhase(): void {
const phase = this.focusedPhase();
if (!phase) return;
if (this.phaseExpanded(phase)) {
this.expandedPhases.delete(phase.phase);
this.collapsedPhases.add(phase.phase);
this.focusKey = `phase:${phase.phase}`;
} else {
this.collapsedPhases.delete(phase.phase);
this.expandedPhases.add(phase.phase);
}
this.host.requestRender();
}
private scrollBy(delta: number): void {
if (delta < 0) {
this.followMode = false;
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
} else {
this.scrollOffset += delta;
if (this.scrollOffset >= this.lastMaxScroll) this.followMode = true;
}
this.host.requestRender();
}
private focusedRow() {
if (!this.focusKey?.startsWith("agent:")) return undefined;
const id = this.focusKey.slice("agent:".length);
return this.state.rows.find((row) => row.agentId === id);
}
private focusedPhase(): PlannedPhase | undefined {
if (!this.focusKey) return undefined;
const row = this.focusedRow();
const name = row?.phase ?? this.focusKey.slice("phase:".length);
return this.state.phases.find((phase) => phase.phase === name);
}
private openSteer(): void {
if (!this.focusedRow()) return;
this.steerMode = true;
this.steerBuffer = "";
this.host.requestRender();
}
private abortFocused(): void {
const controls = this.focusedRow()?.controls as
| OverlayAgentControls
| undefined;
if (controls?.abort) void Promise.resolve(controls.abort()).catch(() => {});
}
private handleSteerInput(data: string): void {
if (matchesKey(data, "escape")) {
// Cancel the buffer back to normal mode — does NOT abort the run.
this.steerMode = false;
this.steerBuffer = "";
this.host.requestRender();
return;
}
if (matchesKey(data, "return") || matchesKey(data, "enter")) {
this.submitSteer();
return;
}
if (matchesKey(data, "backspace")) {
this.steerBuffer = this.steerBuffer.slice(0, -1);
this.host.requestRender();
return;
}
// Accept a single printable character (excludes control bytes + DEL).
if (data.length === 1) {
const code = data.charCodeAt(0);
if (code >= 0x20 && code !== 0x7f) {
this.steerBuffer += data;
this.host.requestRender();
}
}
}
private submitSteer(): void {
const text = this.steerBuffer;
const row = this.focusedRow();
this.steerMode = false;
this.steerBuffer = "";
this.host.requestRender();
if (!row || !text) return;
const controls = row.controls as OverlayAgentControls | undefined;
if (row.status === "running") {
// Taint the run synchronously, at enqueue time (before the steer
// promise settles) — this is what the test asserts and what v2
// caching keys off.
this.steeredFlag = true;
if (controls?.steer)
void Promise.resolve(controls.steer(text)).catch(() => {});
} else {
// A finished agent is re-prompted as a follow-up, not steered.
if (controls?.followUp)
void Promise.resolve(controls.followUp(text)).catch(() => {});
}
}
private transcriptTool(
agentId: string,
item: TranscriptItem & { kind: "tool" },
index: number,
): ToolExecutionComponent | undefined {
if (!this.toolUi || !this.cwd) return undefined;
const key = `${agentId}:${item.toolCallId ?? index}`;
const expanded = this.toolsExpanded();
let cached = this.transcriptTools.get(key);
if (!cached) {
const component = new ToolExecutionComponent(
item.toolName,
item.toolCallId ?? key,
item.args ?? {},
undefined,
nativeToolDefinitions.get(item.toolName)?.(this.cwd) ??
({
renderCall: (args, _theme, context) =>
new Markdown(
`### ${item.toolName}\n${context.expanded ? formatValueMarkdown(args) : summarizeValue(args)}`,
0,
0,
getMarkdownTheme(),
),
renderResult: (result, options, theme, context) => {
if (!options.expanded)
return new Text(
theme.fg(
context.isError ? "error" : "dim",
context.isError
? "Failed"
: options.isPartial
? "Running"
: "Finished",
),
0,
0,
);
const parts = result.content.flatMap((part) =>
part.type === "text" ? [formatStructuredText(part.text)] : [],
);
if (result.details !== undefined)
parts.push("### Details", formatValueMarkdown(result.details));
return new Markdown(parts.join("\n\n"), 0, 0, getMarkdownTheme());
},
} satisfies Pick<ToolDefinition, "renderCall" | "renderResult">),
this.toolUi,
this.cwd,
);
component.markExecutionStarted();
component.setArgsComplete();
component.setExpanded(expanded);
cached = {
component,
args: item.args,
result: undefined,
isError: undefined,
isPartial: undefined,
expanded,
};
this.transcriptTools.set(key, cached);
} else if (item.args !== undefined && item.args !== cached.args) {
cached.component.updateArgs(item.args);
cached.args = item.args;
}
const expansionChanged = expanded !== cached.expanded;
if (expansionChanged) {
cached.component.setExpanded(expanded);
cached.expanded = expanded;
}
if (
item.result !== undefined &&
(item.result !== cached.result ||
item.isError !== cached.isError ||
item.isPartial !== cached.isPartial ||
expansionChanged)
) {
const result = normalizeToolResult(item);
cached.component.updateResult(
expanded ? result : { ...result, content: [] },
item.isPartial ?? false,
);
cached.result = item.result;
cached.isError = item.isError;
cached.isPartial = item.isPartial;
}
return cached.component;
}
private transcriptBodyLines(
agentId: string | undefined,
width: number,
): string[] {
if (!agentId) return [];
const items = this.state.transcriptItems[agentId];
if (!items || items.length === 0) {
return transcriptLines(this.state.transcripts[agentId] ?? "", width);
}
const lines: string[] = [];
let text = "";
const flushText = () => {
if (!text) return;
lines.push(...transcriptLines(text, width));
text = "";
};
for (const [index, item] of items.entries()) {
if (item.kind === "text") {
text += item.text;
continue;
}
flushText();
if (item.kind === "user") {
lines.push(this.theme.fg("accent", "user"));
lines.push(...transcriptLines(item.text, width));
continue;
}
if (this.hideToolPreviews && !this.toolsExpanded()) continue;
const tool = this.transcriptTool(agentId, item, index);
if (tool) lines.push(...tool.render(width));
else if (item.fallback)
lines.push(...transcriptLines(`> ${item.fallback}`, width));
}
flushText();
return lines;
}
// ---- render -----------------------------------------------------------
render(width: number): string[] {
const w = Math.max(1, width);
const state = this.state;
const lines: string[] = [];
const header = [headerLine(state), summaryLine(state)]
.filter(Boolean)
.join(" · ");
lines.push(this.theme.fg("accent", fitLine(header, w)));
for (const context of [
descriptionLine(state),
...metricsLines(state, this.toolsExpanded()),
]) {
if (context) lines.push(this.theme.fg("muted", fitLine(context, w)));
}
const maxLines = Math.max(1, this.host.terminal.rows - 3);
const fixedAfterRows = 4; // transcript divider + body allowance + two footer rows
const workBudget = Math.max(0, maxLines - lines.length - fixedAfterRows);
const display = this.visibleRows();
const groups = display.map((item) => {
const focused = this.displayKey(item) === this.focusKey;
if (item.kind === "phase") {
const marker = focused ? "▶" : " ";
const color = focused
? "accent"
: item.phase.status === "done"
? "success"
: "dim";
return [
this.theme.fg(
color,
fitLine(
`${marker} ${phaseLabel(state, item.phase, item.expanded)}`,
w,
),
),
];
}
const marker = focused ? "▶" : " ";
return agentBoardLines(item.row, state, `${marker} `).map((line) => {
const text =
line.bold || focused ? this.theme.bold(line.text) : line.text;
return this.theme.fg(line.color, fitLine(text, w));
});
});
const focusedDisplay = Math.max(
0,
display.findIndex((item) => this.displayKey(item) === this.focusKey),
);
let groupStart = focusedDisplay;
let groupEnd = Math.min(groups.length, focusedDisplay + 1);
let used = groups[focusedDisplay]?.length ?? 0;
if (used <= workBudget) {
while (groupStart > 0 || groupEnd < groups.length) {
let expanded = false;
if (
groupStart > 0 &&
used + groups[groupStart - 1].length <= workBudget
) {
groupStart--;
used += groups[groupStart].length;
expanded = true;
}
if (
groupEnd < groups.length &&
used + groups[groupEnd].length <= workBudget
) {
used += groups[groupEnd].length;
groupEnd++;
expanded = true;
}
if (!expanded) break;
}
}
const workLines = groups.slice(groupStart, groupEnd).flat();
lines.push(...workLines.slice(0, workBudget));
const focusedRow = this.focusedRow();
const focusedPhase = this.focusedPhase();
lines.push(
this.theme.fg(
"border",
divider(
focusedRow
? `transcript: ${focusedRow.agentId}`
: `phase: ${focusedPhase?.phase ?? "—"}`,
w,
),
),
);
const body = this.transcriptBodyLines(focusedRow?.agentId, w - 2);
// Reserve both footer rows and keep a 3-row terminal safety margin.
const maxVisible = Math.max(0, maxLines - lines.length - 2);
const maxScroll = Math.max(0, body.length - maxVisible);
this.lastMaxScroll = maxScroll;
if (this.followMode) this.scrollOffset = maxScroll;
this.scrollOffset = Math.min(Math.max(0, this.scrollOffset), maxScroll);
for (const l of body.slice(
this.scrollOffset,
this.scrollOffset + maxVisible,
)) {
lines.push(this.theme.fg("dim", fitLine(` ${l}`, w)));
}
if (this.steerMode) {
const verb =
focusedRow && focusedRow.status === "running" ? "steer" : "follow-up";
lines.push(
this.theme.fg(
"warning",
fitLine(
`${verb} ${focusedRow?.agentId ?? ""}> ${this.steerBuffer}`,
w,
),
),
);
} else if (
this.abortArmedAt !== undefined &&
this.now() - this.abortArmedAt <= 1500
) {
lines.push(
this.theme.fg(
"warning",
fitLine("Press Esc again to abort all agents", w),
),
);
} else {
const follow = this.followMode ? "follow" : "paused";
lines.push(
this.theme.fg(
"dim",
fitLine(
`↑↓ focus · Tab agents · Enter phase · Alt+↑/↓ transcript (${follow})`,
w,
),
),
this.theme.fg(
"dim",
fitLine(
`s steer · x abort agent · ${keyHint("app.tools.expand", "tools")} · Esc Esc abort all`,
w,
),
),
);
}
return lines.map((l) => fitLine(l, w));
}
invalidate(): void {
for (const { component } of this.transcriptTools.values())
component.invalidate();
}
}