import type { Draw, Rect } from '../tui/draw.js'; export interface ViewManifest { id: string; title: string; subtitle?: string; description: string; refreshMs?: number; } /** Severity of a {@link HostSignals.setBanner} banner — drives glyph + hue and * the derived state chip (error→blocked, action→attention, info→neutral). */ export type BannerLevel = 'info' | 'action' | 'error'; export interface Banner { msg: string; level: BannerLevel; } /** Optional host context passed to the text presenter's dump on the piped path. */ export interface DumpContext { banner: Banner | null; } /** Semantic chrome signals the core raises; each target renders them in its own * idiom (TUI footer/banner/title-chip vs web status line/alert bar/pill). */ export interface HostSignals { setStatus(msg: string | null): void; setBanner(msg: string, level: BannerLevel): void; clearBanner(): void; setSubtitle(s: string | null): void; /** Interaction-mode chip override (compose/react); null returns to derived. */ setMode(mode: string | null): void; /** TUI: leave the pane; web: no-op/close tab. */ quit(): void; } /** The host-tracked chrome record both targets render. */ export interface ChromeState { status: string | null; banner: Banner | null; subtitle: string | null; mode: string | null; busy: boolean; loaded: boolean; lastRefresh: number; } /** A transport-agnostic request descriptor the host fulfills. The core never * executes anything itself — it describes WHAT to run/read/fetch and the * host's Transport decides HOW (local exec today, a cloud endpoint later). */ export type SourceRequest = { kind: 'exec'; bin: string; args: string[]; cwd?: string; stdin?: string; } | { kind: 'file'; path: string; } | { kind: 'http'; method: 'GET' | 'POST' | 'PUT' | 'DELETE'; url: string; headers?: Record; body?: string; }; export type StreamRole = 'controller' | 'observer'; export interface StreamRequest { kind: 'stream'; target: string; role?: StreamRole; } export interface StreamChannel { readonly id: string; send(frame: unknown): void; close(): void; } export interface StreamSource { id: string; request(args: A): StreamRequest; onFrame: string; onOpen?: string; onClose?: string; } export interface RawResponse { ok: boolean; exitCode?: number; status?: number; stdout: string; stderr: string; } /** Typed error with a render-ready display. Presenters render `display` * VERBATIM and never branch on `kind` (the view-internal taxonomy). */ export interface SourceError { kind: string; display: { headline: string; explanation: string; nextStep: string; level: BannerLevel; blocking: boolean; }; } export type Result = { ok: true; data: T; } | { ok: false; error: SourceError; }; /** A READ: declarative descriptor + pure parse + optional cadence. Resolved by * the host through its Transport via `ctx.resolve(source, args)`. */ export interface Source { id: string; request(args: A): SourceRequest; parse(raw: RawResponse): Result; refreshMs?: number; } /** A WRITE: same {request, parse} pair, no cadence — invoked by an intent via * `ctx.execute(command, args)`. */ export interface Command { id: string; request(args: A): SourceRequest; parse(raw: RawResponse): Result; } /** A semantic action. State updates are immutable via `ctx.set`; async effects * (transport calls) live in the same handler — a thunk, not a pure reducer. * Sync intents call `ctx.set` once; async intents `await ctx.resolve/execute` * between `set`s. The host serializes async intents in its single-flight lane. */ export type Intent = (ctx: IntentCtx, payload: P) => void | Promise; export interface IntentCtx { /** Snapshot of state at read time. */ readonly state: S; /** Immutable update → triggers a re-render. Value or (prev)=>next fn. */ set(next: S | ((prev: S) => S)): void; /** Run a READ source through the host's transport → typed Result. */ resolve(source: Source, args?: A): Promise>; /** Run a WRITE command through the host's transport → typed Result. */ execute(command: Command, args?: A): Promise>; connect(source: StreamSource, args?: A): StreamChannel; /** Semantic chrome signals (status/banner/subtitle/mode/quit). */ signal: HostSignals; /** Chain another intent by name. */ dispatch(intent: string, payload?: unknown): Promise; } export interface ViewCore { manifest: ViewManifest; /** Cheap, synchronous initial state. No fetch, no screen. The host mounts, * paints a loading frame, then dispatches the first 'refresh'. */ init(opts: Readonly>): S; /** Declarative READ dependencies the host resolves through the transport. */ sources?: Record>; streams?: Record>; /** WRITE descriptors invoked from intents. */ commands?: Record>; /** Semantic actions. Both presenters emit these. */ intents: Record>; } export interface KeyHint { keys: string; label: string; } /** A catalog binding supplies only its semantic hint label: the host derives * the rendered key label from the effective user binding snapshot. */ export interface CatalogKeyHint { label: string; } export type KeyBinding = { /** Closed crouter catalog action. Shipped presenters use this form. */ bindingId: import('../keybindings/index.js').BindingId; intent: string; payload?: (state: S) => unknown; when?: (state: S) => boolean; hint?: CatalogKeyHint; } | { /** Literal keys remain owned by custom/plugin views, outside crouter's catalog. */ keys: string[]; intent: string; payload?: (state: S) => unknown; when?: (state: S) => boolean; hint?: KeyHint; } | { /** Text-capture binding: while `when(state)` is true the host runs a * built-in line-edit buffer over raw printable keys and dispatches * `capture` with the next draft value on each edit. Deletion is the * host catalog action `crtr.view.host.editor.backspace`. */ capture: string; when: (state: S) => boolean; hint?: KeyHint | CatalogKeyHint; }; export interface TuiPresenter { /** Pure read of state, paints via draw.*; never ANSI. */ render(state: S, draw: Draw, content: Rect): void; /** Pure input→intent mapping. */ keymap: KeyBinding[]; } /** Props handed to web.jsx's default-export React component. The component is a * pure function of state; DOM events call `dispatch`. (Typed loosely here so * the core contract carries no React type dependency.) */ export interface ViewProps { state: S; dispatch: (intent: string, payload?: unknown) => void; chrome: ChromeState; } export type WebPresenter = (props: ViewProps) => unknown; export interface TextPresenter { /** Static text for the non-TTY / piped path. Snapshot of current state. */ dump(state: S, ctx?: DumpContext): string; } export declare function ok(data: T): Result; export declare function fail(error: SourceError): Result;