eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
161 lines (160 loc) • 6.34 kB
TypeScript
/**
* Pure rendering for the bordered setup flow panel — the input-region variant
* a setup command runs inside for its whole duration (the Claude Code
* `/model`-panel grammar): a full-width rule, the command as a blue title,
* the flow's recent progress lines, and the active question (numbered option
* rows or a text field) or the ephemeral status spinner. Behavior state comes
* from the shared select reducer (`#setup/cli/select-state.js`); this module
* only paints rows, so the renderer hosts lifecycle and keys while tests
* assert on strings.
*
* Column grammar: the panel adds a one-space left margin to every row under
* the rule, while each section contributes two more spaces. Titles, spinners,
* notices, filter prompts, and option-state glyphs therefore all begin at
* column 3.
*/
import type { ChannelSetupAction, PromptOption } from "#setup/cli/index.js";
import { type SelectState } from "#setup/cli/select-state.js";
import type { SelectNotice } from "#setup/prompter.js";
import { type LineState } from "./line-editor.js";
import type { Theme } from "./theme.js";
/** One row of a setup select panel; the shared prompt-option shape. */
export type SetupPanelOption = PromptOption<string>;
interface SetupQuestionPanelBase {
message: string;
error?: string;
/** Outcome lines from earlier menu laps, shown beneath the options. */
notices?: readonly SelectNotice[];
}
interface SetupSelectPanelBase extends SetupQuestionPanelBase {
options: readonly SetupPanelOption[];
select: SelectState;
}
interface SetupEditableRow {
/**
* The row whose hint is a live rename field. Hovering it (cursor on the row)
* makes it editable directly — typing and backspace edit the name in place —
* so the editor's text and a blinking caret render only on that row.
*/
optionValue: string;
editor: LineState;
defaultValue: string;
formatHint: (value: string) => string;
caretVisible: boolean;
}
/**
* Select presentation variants. The discriminant owns the interaction grammar
* so feature combinations are deliberate rather than resolved by conditional
* precedence inside the renderer.
*/
type SetupOptionSelectPanelState = (SetupSelectPanelBase & {
kind: "single";
}) | (SetupSelectPanelBase & {
kind: "search";
placeholder?: string;
}) | (SetupSelectPanelBase & {
kind: "multi";
}) | (SetupSelectPanelBase & {
kind: "searchable-multi";
placeholder?: string;
}) | (SetupSelectPanelBase & {
kind: "stacked";
}) | (SetupSelectPanelBase & {
kind: "task-list";
}) | (SetupSelectPanelBase & {
kind: "editable";
edit: SetupEditableRow;
});
interface SetupActionsPanelState {
kind: "actions";
/** Inert explanation rendered above, and separately from, the action group. */
context: string;
actions: readonly ChannelSetupAction[];
/** No action is focused until the user moves into the action group. */
cursor: number | undefined;
}
export type SetupSelectPanelState = SetupOptionSelectPanelState | SetupActionsPanelState;
export interface SetupTextPanelState {
message: string;
editor: LineState;
placeholder?: string;
mask: boolean;
error?: string;
/** Context lines shown above the message; gone once the question settles. */
notices?: readonly SelectNotice[];
}
export interface SetupAcknowledgePanelState {
message: string;
lines: readonly string[];
}
/** One progress line shown inside the flow panel while it runs. */
export interface FlowPanelLine {
text: string;
tone: "info" | "success" | "warning" | "error";
/**
* Subprocess output a warning/error settle pulled in as its evidence.
* Renders like any info line in the panel, but survives the panel close
* alongside the diagnostic it explains (a plain info line does not).
*/
evidence?: boolean;
}
/** One already-resolved animation frame and its active color. */
export interface FlowPanelIndicator {
glyph: string;
color: "green" | "yellow";
}
export type FlowPanelContent = {
kind: "question";
rows: readonly string[];
/** The install wait keeps its indicator above the concurrent actions. */
status?: {
text: string;
indicator: FlowPanelIndicator;
};
} | {
kind: "status";
status: {
text: string;
indicator: FlowPanelIndicator;
};
/** Latest child-process output shown transiently beneath the status. */
preview?: string;
} | {
kind: "preview";
text: string;
indicator: FlowPanelIndicator;
} | {
kind: "idle";
indicator: FlowPanelIndicator;
};
/** The whole bordered section: title, recent progress, and one explicit mode. */
export interface FlowPanelState {
/** The invoked command, e.g. "/deploy". Empty renders no title row. */
title: string;
lines: readonly FlowPanelLine[];
content: FlowPanelContent;
}
/**
* Paints the bordered flow panel. Everything a running command produces lives
* here — progress, questions, the status indicator — and the panel vanishes
* wholesale when the command resolves; only the command echo and the elbow
* outcome persist in the transcript.
*/
export declare function renderFlowPanel(state: FlowPanelState, theme: Theme, width: number): string[];
/**
* Paints a selection section for the flow panel. Ordinary selects use the
* shared option reducer; concurrent actions render an explicit context row and
* independent action group. A searchable select windows the option list around
* the cursor and advertises the rest with a count footer.
*/
export declare function renderSelectQuestion(state: SetupSelectPanelState, theme: Theme, width: number): string[];
/** Paints a text question section: message, a block-cursor input line, hints. */
export declare function renderTextQuestion(state: SetupTextPanelState, theme: Theme, width: number, caretVisible: boolean): string[];
/**
* Paints a static acknowledgement section (for the flow panel): a heading and
* dim body lines where option rows normally sit, held until the user
* dismisses it. There is nothing to cancel — the text is the point — so the
* footer advertises only enter.
*/
export declare function renderAcknowledgeQuestion(state: SetupAcknowledgePanelState, theme: Theme, width: number): string[];
export {};