eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
215 lines (214 loc) • 8.69 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 SearchActionOption, type SelectState } from "#setup/cli/select-state.js";
import type { SelectNotice } from "#setup/prompter.js";
import type { ModelSettingsRequest } from "#setup/flows/model.js";
import { type ModelEditorState } from "./model-editor.js";
import type { ProviderPickerPhase } from "./provider-picker.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[];
searchAction?: SearchActionOption;
select: SelectState;
/** Live frame rendered beside a searchable input while it loads replacement rows. */
loadingFrame?: string;
/** A dim-inverse affordance appended to the cursor row, e.g. ` ↵ change `. */
cursorBadge?: string;
}
/**
* A menu row that turns into an inline editor while the cursor rests on it.
* `optionValue` names the row; the `editor` discriminant chooses the widget —
* an in-place rename field, or a masked provider-key field with its own
* validation phases. Rename defaults stay placeholders until typing begins;
* provider keys edit in place. Layout and inline editing are orthogonal, so
* the editor travels as a payload rather than as its own panel `kind`.
*/
interface SetupInlineEditRow {
optionValue: string;
caretVisible: boolean;
editor: {
kind: "rename";
editor: LineState;
defaultValue: string;
formatHint: (value: string) => string;
} | {
kind: "key";
phase: ProviderPickerPhase;
};
}
/**
* Select presentation variants. The discriminant owns the interaction grammar
* so feature combinations are deliberate rather than resolved by conditional
* precedence inside the renderer. Inline editing is the exception: it composes
* with a layout instead of defining one, so `inline-edit` carries both.
*/
type SetupOptionSelectPanelState = (SetupSelectPanelBase & {
kind: "single";
}) | (SetupSelectPanelBase & {
kind: "search";
layout?: "task-list";
placeholder?: string;
}) | (SetupSelectPanelBase & {
kind: "multi";
}) | (SetupSelectPanelBase & {
kind: "searchable-multi";
placeholder?: string;
}) | (SetupSelectPanelBase & {
kind: "stacked";
}) | (SetupSelectPanelBase & {
kind: "task-list";
}) | (SetupSelectPanelBase & {
kind: "inline-edit";
layout: "stacked" | "task-list";
edit: SetupInlineEditRow;
});
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";
}
/** One live flow status after its animation frame and visual intent are resolved. */
export type FlowPanelStatus = {
kind: "progress";
text: string;
indicator: FlowPanelIndicator;
} | {
kind: "external-action";
text: string;
emphasis: string;
indicator: FlowPanelIndicator;
};
export type FlowPanelContent = {
kind: "question";
rows: readonly string[];
/** The install wait keeps its indicator above the concurrent actions. */
status?: FlowPanelStatus;
} | {
kind: "status";
status: FlowPanelStatus;
/** 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[];
/** The composite Change-model screen's inputs: the resolved request plus live state. */
export interface ModelEditorPanelInput {
request: ModelSettingsRequest;
state: ModelEditorState;
}
/**
* A dim, background-free selection badge carrying the Enter affordance, e.g.
* `↵`, `↵ change`, `↵ validate`.
*/
export declare function enterBadge(theme: Theme, label?: string): string;
/**
* A discrete reasoning track: `●` below the current notch, `◉` on it, `○`
* above, joined by `─` connectors. `accent` paints the covered stretch —
* notches and connectors up to the current position — blue. `index` -1 means
* unset: an all-`○` track with no fill.
*/
export declare function reasoningTrack(input: {
count: number;
index: number;
connectorWidth: number;
accent: boolean;
theme: Theme;
}): string;
/**
* Paints the Change-model screen: a value menu whose reasoning and tier rows
* adjust inline with left/right, and whose Model row opens the searchable
* catalog.
*/
export declare function renderModelEditorQuestion(input: ModelEditorPanelInput, 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 {};