/** * InkPromptController — the single renderer for the interactive shell. * The legacy PromptController and the env-gated migration adapter were * removed 2026-05-09; createPromptController() always returns an * InkPromptController instance. * * Goal: satisfy every method that src/headless/interactiveShell.ts calls * so the rest of the codebase doesn't notice the swap. Live methods * (status, history, secret mode, menu) drive an Ink reconciler commit; * decorative methods that don't yet have an Ink implementation are * marked as TODOs and behave as no-ops, with the call recorded so * future hardening can fill the gaps with real behaviour rather than * silent drift. * * Renderer shim: interactiveShell.ts also calls a handful of methods * directly via promptController.getRenderer() — addEvent, addOutputTap, * captureInput, clearBuffer, setSecretMode, forceRender. We expose a * facade implementing exactly those. */ import { EventEmitter } from 'node:events'; import type { Writable, Readable } from 'node:stream'; import type { ChatItem } from './ChatStatic.js'; export type EditGuardMode = 'display-edits' | 'require-approval' | 'block-writes' | 'ask-permission' | 'plan'; export interface PromptCallbacks { onSubmit: (text: string) => void; onQueue: (text: string) => void; onInterrupt: () => void; onExit?: () => void; onCtrlC?: (info: { hadBuffer: boolean; }) => void; onResume?: () => void; onChange?: (event: { text: string; cursor: number; }) => void; onEditModeChange?: (mode: EditGuardMode) => void; onToggleAutoContinue?: () => void; onClearContext?: () => void; onExpandToolResult?: () => void; onToggleHITL?: () => void; } export interface CommandSuggestion { command: string; description: string; category?: string; } export interface MenuItem { id: string; label: string; description?: string; category?: string; disabled?: boolean; isActive?: boolean; } type Mode = 'idle' | 'streaming'; interface ModeToggleState { autoMode: 'off' | 'on'; autoContinueHotkey?: string; debugEnabled?: boolean; hitlMode: 'off' | 'on'; hitlHotkey?: string; } export interface IPromptController extends EventEmitter { start(): void; stop(): void; setStreaming(s: boolean): void; getMode(): Mode; setContextUsage(p: number | null): void; setModeToggles(opts: Partial): void; setDebugMode(enabled: boolean): void; toggleAutoContinue(): void; getAutoMode(): 'off' | 'on'; setAutoMode(m: 'off' | 'on'): void; toggleHITL(): void; getHITLMode(): 'off' | 'on'; getModeToggleState(): Readonly; setStatusMessage(message: string | null): void; setOverrideStatus(message: string | null): void; setStreamingLabel(label: string | null): void; setStatusLine(s: { main?: string | null; override?: string | null; streaming?: string | null; }): void; setMetaStatus(meta: { elapsedSeconds?: number | null; tokensUsed?: number | null; tokenLimit?: number | null; thinkingMs?: number | null; thinkingHasContent?: boolean; }): void; clearAllStatus(): void; setModelContext(opts: { model?: string | null; provider?: string | null; }): void; setChromeMeta(meta: { workspace?: string; directory?: string; writes?: string; sessionLabel?: string; thinkingLabel?: string; autosave?: boolean; version?: string; }): void; setAvailableCommands(commands: CommandSuggestion[]): void; setInlinePanel(lines: string[]): void; clearInlinePanel(): void; supportsInlinePanel(): boolean; setPinnedPrompt(text: string | null): void; getPinnedPrompt(): string | null; clearPinnedPrompt(): void; setMenu(items: MenuItem[], options: { title?: string; initialIndex?: number; }, callback: (item: MenuItem | null) => void): void; closeMenu(): void; isMenuActive(): boolean; setActivityMessage(message: string | null): void; /** Commit any pending streaming text to history. No-op if empty. */ flushStreaming(): void; setEditMode(mode: EditGuardMode): void; applyEditMode(mode: EditGuardMode): void; getEditMode(): EditGuardMode; getBuffer(): string; getCursor(): number; setBuffer(text: string, cursorPos?: number): void; setSecretMode(enabled: boolean): void; clear(): void; render(): void; forceRender(): void; handleResize(): void; dispose(): void; getRenderer(): any; } export declare class InkPromptController extends EventEmitter implements IPromptController { private readonly callbacks; private inkRender; private React; private AppComponent; private inst; private readonly shim; private readonly stdin; private readonly stdout; private streamingText; private static readonly STREAMING_RERENDER_MS; private _streamingTimer; private statusMain; private statusOverride; private statusStreaming; private activityMessage; private mode; private metaInfo; private vigilContext; private history; private static readonly MAX_HISTORY_ITEMS; private suggestions; private allCommands; private inlinePanel; private secretMode; private editMode; private pinnedPrompt; private modeToggleState; private buffer; private captureSubmit; private menuCallback; private menuItems; private menuOpen; private hitlOpen; private _pendingHitlRequest; private _pendingHitlResolve; private _HitlDecisionComponent; private hitlListeners; private started; private disposed; constructor(stdin: Readable, stdout: Writable, callbacks: PromptCallbacks); /** * Async start so the Ink + React modules can be loaded via dynamic * import (the dist build emits ESM under `module: NodeNext`, where * `require()` mixed with top-level await is a hard error in Node 22+). * Call sites that go through createPromptController() should await * .start(); the legacy controller's start() is sync, so the IPrompt * interface defines start() as `void` and we coerce here. */ start(): void; private startAsync; stop(): void; dispose(): void; /** Manual repaint hook — Ink owns its tick, so render/forceRender are coalesced into a rerender(). */ render(): void; forceRender(): void; handleResize(): void; setStatusMessage(message: string | null): void; setOverrideStatus(message: string | null): void; setStreamingLabel(label: string | null): void; setStatusLine(status: { main?: string | null; override?: string | null; streaming?: string | null; }): void; clearAllStatus(): void; setActivityMessage(message: string | null): void; flushStreaming(): void; setStreaming(streaming: boolean): void; getMode(): Mode; setContextUsage(percentage: number | null): void; setMetaStatus(meta: { elapsedSeconds?: number | null; tokensUsed?: number | null; tokenLimit?: number | null; }): void; setModelContext(options: { model?: string | null; provider?: string | null; }): void; setChromeMeta(meta: { workspace?: string; directory?: string; }): void; setModeToggles(options: Partial): void; setDebugMode(enabled: boolean): void; toggleAutoContinue(): void; getAutoMode(): 'off' | 'on'; setAutoMode(mode: 'off' | 'on'): void; toggleHITL(): void; getHITLMode(): 'off' | 'on'; getModeToggleState(): Readonly; setAvailableCommands(commands: CommandSuggestion[]): void; setInlinePanel(lines: string[]): void; clearInlinePanel(): void; supportsInlinePanel(): boolean; setPinnedPrompt(text: string | null): void; getPinnedPrompt(): string | null; clearPinnedPrompt(): void; setMenu(items: MenuItem[], _options: { title?: string; initialIndex?: number; }, callback: (item: MenuItem | null) => void): void; closeMenu(): void; isMenuActive(): boolean; /** Test/observability hook — true while the raw-mode HITL menu is on screen. */ isHitlSuspended(): boolean; setSecretMode(enabled: boolean): void; setEditMode(mode: EditGuardMode): void; applyEditMode(mode: EditGuardMode): void; getEditMode(): EditGuardMode; /** * The input buffer in the Ink path is owned by the Prompt component, * not by this controller. We keep `this.buffer` as a snapshot for * legacy callers asking for getBuffer/getCursor; updates flow when the * Prompt's onSubmit / clear actions fire. setBuffer drives a new * `initial` prop on the next mount. */ getBuffer(): string; getCursor(): number; setBuffer(text: string, _cursorPos?: number): void; clear(): void; getRenderer(): any; /** Append history (used by the renderer shim's addEvent). */ _appendHistoryEntry(item: ChatItem): void; /** * Streaming-message accumulators. We buffer deltas in memory and * commit ONLY the canonical 'response' text into history. Earlier * versions rendered the in-progress text as a live region between * and the prompt; Ink's log-update couldn't reliably * clear that region when more Static items appended below it, so * the still-streaming partial text would pile up in scrollback * AND the finalised version would render — duplicate text bug. * * UX impact of buffer-only: the user sees a "Streaming…" status * indicator while waiting, then the full assistant message * appears as one bubble. No flicker, no duplication, no live * token-by-token render. For the fast models the CLI uses * (DeepSeek-V4-Pro / -Flash) the wait is sub-second. */ _appendStreamingDelta(delta: string): void; _commitStreaming(finalText: string): void; _finalizeStreamingIfAny(): void; _setBuffer(text: string): void; _setSecretMode(enabled: boolean): void; _installCaptureHandler(handler: (text: string) => void): void; _restoreSubmitHandler(): void; /** Called from the shim to suspend/resume rendering for HITL. */ _suspendForHITL(suspend: boolean): void; /** Force a re-render after HITL resumes. */ _forceRerender(): void; private rerender; private buildTree; /** Update the target/findings badge shown in the footer status line. */ setVigilContext(ctx: { targets?: number; findings?: number; critHigh?: number; }): void; private formatModeChips; /** Build keyboard shortcut + auth tier + context status string for the footer. */ private formatKeyboardShortcuts; } /** * Factory — returns the Ink-backed controller. Ink is now the only * renderer; the legacy UnifiedUIRenderer + PromptController have been * removed. interactiveShell.ts exits early on non-TTY so Ink's * raw-mode requirement is always satisfied here. Plain mode * (NO_COLOR / TERM=dumb) still flows through Ink — Ink itself * down-styles when colours are disabled. */ export declare function createPromptController(stdin: Readable, stdout: Writable, callbacks: PromptCallbacks): Promise; export {}; //# sourceMappingURL=InkPromptController.d.ts.map