/** * Pure layout helpers extracted from App.ts. * * These functions take a *snapshot* of the App's UI state (which panels are * open, how many items they hold) and return geometric values (heights, * offsets) without touching `this`. Keeping them pure means they can be * unit-tested directly — the layout math is the part of the renderer most * prone to off-by-one regressions, and it was previously untestable because * it was inlined in `renderChat` with `this.*` access on every line. * * Convention: every field on `LayoutSnapshot` is `readonly` so callers * cannot mutate the App's real state through the snapshot. */ /** Read-only snapshot of the fields `bottomPanelHeight` consults. */ export interface LayoutSnapshot { readonly height: number; readonly pasteInfoOpen: boolean; readonly pasteInfoPreviewLines: number; readonly isAgentRunning: boolean; readonly confirmOpen: boolean; readonly permissionOpen: boolean; readonly sessionPickerOpen: boolean; readonly sessionPickerItemCount: number; readonly confirmMessageCount: number; readonly statusOpen: boolean; readonly helpOpen: boolean; readonly searchOpen: boolean; readonly searchResultCount: number; readonly exportOpen: boolean; readonly logoutOpen: boolean; readonly logoutProviderCount: number; readonly loginOpen: boolean; readonly loginStep: 'provider' | 'apikey'; readonly loginProviderCount: number; readonly menuOpen: boolean; readonly menuItemCount: number; readonly settingsOpen: boolean; readonly settingsCount: number; readonly showAutocomplete: boolean; readonly autocompleteItemCount: number; readonly hunkPickerOpen: boolean; readonly mentionPickerOpen: boolean; readonly mentionItemCount: number; } /** * Compute how many terminal rows the bottom panel (paste info, agent box, * permission/session/confirm/search/export/login/logout/menu/settings * dialogs, autocomplete) occupies in the current frame. * * Mirrors the if/else chain that used to live inline in `renderChat`. * Returns 0 when no panel is open. */ export declare function bottomPanelHeight(s: LayoutSnapshot): number; /** * Split the available terminal height into the main chat area and the * bottom panel. Returns the y-coordinates the renderer paints into. * * - `messagesStart` is always 0 (top of the screen). * - `messagesEnd` is the last row the message list may use. * - `separatorLine`, `inputLine`, `statusLine` are the three reserved * rows at the bottom of the main area, in order. */ export interface ChatLayout { messagesStart: number; messagesEnd: number; separatorLine: number; inputLine: number; statusLine: number; mainHeight: number; } export declare function chatLayout(height: number, panelHeight: number): ChatLayout; /** * Count how many terminal rows a single chat message will occupy once * word-wrapped to `maxWidth` columns. Used by `scrollToMessage` to find * the right scroll offset. * * Every message renders as: 1 header row + 1 blank row + one or more * wrapped content rows, followed by 1 blank spacing row. */ export declare function messageLineCount(content: string, maxWidth: number): number; /** * Sum `messageLineCount` across a list of messages and return both the * running total and the line offset where `targetIndex` begins. This is * the pure core of the old `scrollToMessage` method. */ export declare function messageOffsets(contents: string[], maxWidth: number, targetIndex: number): { totalLines: number; targetStartLine: number; }; /** * Compute the scroll offset that places the target message roughly in * the middle of the visible window. */ export declare function scrollOffsetForTarget(totalLines: number, targetStartLine: number, visibleLines: number): number; /** * Compute the visible window of a chat transcript given the current scroll * offset. Returns the [startIndex, endIndex) slice into the all-lines array * and, as a side-effect contract, the clamped scroll offset the caller * should store (the renderer overwrites `this.scrollOffset` with this). * * Extracted from `getVisibleMessages` so the off-by-one-prone scroll math * has direct unit tests. */ export declare function scrollWindow(args: { totalLines: number; height: number; scrollOffset: number; }): { startIndex: number; endIndex: number; clampedScrollOffset: number; }; /** Render a gradient progress bar of the given width for the given ratio. */ export declare function agentProgressBar(iteration: number, maxIterations: number, barWidth: number): string; /** Truncate `text` to `maxLen` columns, appending an ellipsis if it doesn’t fit. */ export declare function truncateNotification(text: string, maxLen: number): string; export interface PasteInfo { chars: number; lines: number; preview: string; fullText: string; } /** Threshold above which a paste is considered "large" and shows a dialog. */ export declare const PASTE_DIALOG_THRESHOLD: { chars: number; lines: number; }; /** True when the paste is large enough to warrant the confirm dialog. */ export declare function shouldShowPasteDialog(text: string): boolean; /** Build the PasteInfo struct for a large paste (preview truncated to 200 chars). */ export declare function buildPasteInfo(text: string): PasteInfo; /** * Compact a raw token count into the short string shown in the status bar * ("123", "1.2K", "12.3K"). Returns an empty string when tokens is 0 so * the caller can omit the segment entirely. */ export declare function formatTokenCount(tokens: number): string; /** * Pick the context-sensitive hint shown at the right edge of the status * bar. The "new messages below" badge takes priority when the user has * scrolled up — otherwise the hint depends on whether work is in flight. */ export declare function statusBarRightHint(args: { scrollOffset: number; unseenWhileScrolled: number; isStreaming: boolean; isLoading: boolean; /** * False while something else owns the whole screen — the agent timeline * takes it over and returns before the transcript is drawn at all, so * scrolling changes an offset nothing reads. Offering PgDn there asks for a * keypress that does nothing and leaves the reader hunting for the key. */ canScroll: boolean; }): string; /** The panel that currently owns keyboard focus, in priority order. */ export type ActivePanel = 'pasteInfo' | 'permission' | 'sessionPicker' | 'confirm' | 'status' | 'help' | 'settings' | 'search' | 'export' | 'logout' | 'login' | 'menu' | 'autocomplete' | 'hunkPicker' | 'chat'; export interface PanelState { readonly pasteInfoOpen: boolean; readonly permissionOpen: boolean; readonly sessionPickerOpen: boolean; readonly confirmOpen: boolean; readonly statusOpen: boolean; readonly helpOpen: boolean; readonly settingsOpen: boolean; readonly searchOpen: boolean; readonly exportOpen: boolean; readonly logoutOpen: boolean; readonly loginOpen: boolean; readonly menuOpen: boolean; readonly showAutocomplete: boolean; readonly hunkPickerOpen: boolean; } /** * Return the highest-priority open panel. `chat` is the fallback when no * panel is open. The order matches the if/else chain that used to live in * `handleChatKey`. */ export declare function activePanel(s: PanelState): ActivePanel; export interface InputDisplayOptions { /** Full editor value (may contain newlines). */ value: string; /** Character offset of the cursor within `value`. */ cursorPos: number; /** Available width (terminal columns). */ width: number; /** Whether multi-line (❯❯) mode is active. */ isMultilineMode: boolean; } export interface InputDisplay { /** Prompt symbol shown before the text ("❯ ", "❯❯ ", "[3] ❯ "). */ promptSymbol: string; /** Visible slice of the input (already truncated / ellipsised). */ displayValue: string; /** Column position for the cursor (absolute, 0-based from screen left). */ cursorX: number; /** Placeholder text to show when the editor is empty. */ placeholder: string; /** True when the editor value is empty. */ isEmpty: boolean; } /** * Compute the prompt symbol for the current state. Multi-line content * shows a `[n] ❯ ` prefix with the line count; otherwise `❯❯ ` in * multi-line mode, or the plain `❯ `. */ export declare function inputPromptSymbol(value: string, isMultilineMode: boolean): string; /** * Compute the visible slice of a long input line, plus the cursor column * it maps to. Mirrors the inline logic that used to live in `renderInput`: * when the line fits, show it whole; otherwise anchor the cursor at 70% * of the available width and slide the viewport. */ export declare function inputViewport(args: { line: string; cursorInLine: number; maxInputWidth: number; }): { displayValue: string; cursorOffset: number; }; /** * Top-level entry point used by `renderInput`. Produces the prompt symbol, * the visible text, the absolute cursor X, and the placeholder. */ export declare function computeInputDisplay(opts: InputDisplayOptions): InputDisplay;