/** * @license * Copyright 2026 Steven Roussey * SPDX-License-Identifier: Apache-2.0 */ import type { RunEvent } from "../../run-events/RunEventTypes"; import type { RunState } from "../../ui/model/runRowModel"; import type { WebCommandNode } from "../commandTree"; /** * The console's view of a run, rebuilt from its event stream. * * Pure and DOM-free so it can be tested without a browser, which is where the * behavior that matters lives: what a row shows, what an iteration map knows, * and what survives a reconnect. */ export interface RunRow { readonly id: string; readonly type: string; readonly label: string; readonly depth: number; readonly parent: string | undefined; readonly order: number; readonly status: string; readonly progress: number | undefined; readonly message: string | undefined; readonly streamText: string; readonly messages: unknown; readonly usage: { input: number; output: number; cached: number; } | undefined; readonly startedAt: number | undefined; readonly endedAt: number | undefined; } /** * Above this iteration count the client stops retaining per-index state and * tracks only what is running — the same rule, and the same reason, as the * terminal: a per-index array copied on every event is O(N) per event and * O(N²) over a run. */ export declare const FULL_ITERATION_TRACKING_MAX = 200; export interface IterationMap { readonly count: number; readonly running: ReadonlySet; readonly done: number; readonly slots: ReadonlyMap | undefined; readonly progress: ReadonlyMap; } export interface RunViewState { readonly rows: ReadonlyMap; readonly iterations: ReadonlyMap; readonly logs: readonly { readonly level: string; readonly text: string; }[]; readonly graphProgress: number | undefined; readonly usage: { input: number; output: number; cached: number; } | undefined; readonly state: RunState | "running"; readonly error: string | undefined; readonly output: unknown; /** * `kind` and `data` are carried, not just the schema: a confirm's schema * describes the action and its data IS what the person reads before * deciding, so a view holding only the schema can draw the description as * empty boxes to type in and nothing else. */ readonly humanRequest: { readonly requestId: string; readonly kind: string; readonly message: string; readonly schema: unknown; readonly data: unknown; } | undefined; /** * What the person has sent this session, in order. * * Held rather than derived because it never crosses the event stream: the * console typed it and answered with it, and what comes back up is the run * getting on with the turn. */ readonly chatAsks: readonly string[]; readonly lastSeq: number; readonly nextOrder: number; } /** One side of a conversation, as the console draws it. */ export interface ChatEntry { readonly role: "user" | "assistant"; readonly text: string; /** Whether this turn is still being written. */ readonly pending: boolean; } /** * Whether a run is asking for the next message of a conversation rather than * filling in a form. * * Keyed on the `format` the asking side puts on the field, which is the same * way every other port says what it means. A chat drawn as a one-line text * input is answerable, but it is not a conversation, and the answer it takes * shows up afterwards as a form somebody once filled in. */ export declare function isChatRequest(request: RunViewState["humanRequest"]): boolean; /** * The conversation, zipped from the two halves the console holds: what it sent, * and what each turn wrote back. * * One turn is one {@link CHAT_TURN_TYPE} row, in the order the rows arrived, so * the Nth answer belongs under the Nth message — a turn is only ever started by * a message, so the pairing cannot slip. A turn still running shows what it has * said so far rather than nothing. */ export declare function chatTranscript(state: RunViewState): readonly ChatEntry[]; /** Records a message the console just sent, so the transcript can show it. */ export declare function appendChatAsk(state: RunViewState, text: string): RunViewState; export declare function emptyRunView(): RunViewState; export declare function reduceRunEvent(state: RunViewState, event: RunEvent, at?: number): RunViewState; /** Applies a numbered event, ignoring one a reconnect replayed twice. */ export declare function applyRecord(state: RunViewState, seq: number, event: RunEvent, at?: number): RunViewState; /** * Rows in the order they should be drawn, every row directly beneath the one * that owns it. * * Arrival order cannot do this on its own. A subgraph's children are reported * long after their parent's siblings — the parent has to start running before * it owns anything — so a walk that ends a parent's children at the next * depth-0 row hands a While Loop's subtasks to whatever root was added last. * The rows form a tree, so this walks one. */ export declare function orderedRows(state: RunViewState, sortByStatus: boolean): readonly RunRow[]; /** * What the run screen has to show. * * Most commands in a CLI never build a task graph — `list`, `detail`, `add`, * `remove` all just print — so a screen that only knows how to draw rows spends * those runs promising a task that never arrives, with the output the operator * actually asked for exiled to a panel underneath. When there are no rows, the * command's output IS the run. */ export type ConsoleContent = "tasks" | "output" | "waiting"; export declare function consoleContent(rowCount: number, state: RunViewState): ConsoleContent; /** The command's own stdout/stderr, as printed. */ export declare function runLogText(state: RunViewState): string; /** Filter state for the command rail. */ export declare function filterCommandTree(nodes: readonly WebCommandNode[], query: string): readonly WebCommandNode[]; /** * Every group key in a tree. * * A FILTERED rail is a result list rather than a tree to walk: the groups that * survive are there because something inside them matched, so leaving them shut * would answer a search with a row of closed folders. The rail draws with this * while a filter is on, and with the reader's own open set otherwise — so * clearing the filter returns the tree exactly as they left it rather than * expanded by a search they have finished with. */ export declare function allGroupKeys(nodes: readonly WebCommandNode[]): Set; /** Every ancestor key of a path, which is what the rail has to open. */ export declare function openPathsFor(path: readonly string[]): string[]; /** * On a stacked (narrow) layout the rail and the main pane cannot share the * viewport. Selecting a command — or attaching a run — must show the detail * pane; going back restores the list. */ export type StackedPane = "list" | "detail"; export declare function stackedPane(action: "select" | "back"): StackedPane;