/** * Entry height cache for the scrollable history viewport. * * Associates each history entry's stable `id` with its measured row height * and maintains a prefix-sum array so `accumulatedHeight(index)` is O(1) — * essential for the binary-search-based window computation in the virtual * scroll viewport. * * This module is pure TypeScript — no React, no Ink, no DOM dependencies. * It can be unit-tested without mounting components. */ /** * Default height (in terminal rows) assumed for an entry before its first * measurement. 3 rows is a reasonable floor for a single-line entry * (border + content + margin) and prevents a flash of zero-height spacers * during the first render pass. */ export declare const DEFAULT_ENTRY_HEIGHT = 3; /** * Prefix-sum-based height cache for history entries. * * Stores measured heights keyed by entry `id` and keeps a parallel prefix-sum * array for O(1) range-height queries and O(log n) scroll-offset-to-entry-index * resolution. * * Threading note: this class is designed for single-threaded use within one * React render cycle. It is NOT thread-safe. */ export declare class EntryHeightCache { /** id → measured height in terminal rows */ private readonly heights; /** * Prefix-sum of heights in the order last returned by `sync()` (transcript * order), with later `record()` / `recordMany()` calls appended. * `prefix[i]` = sum of heights for entries with index < i. `prefix[0]` is * always 0. * Invariant: `prefix.length === this.ids.length + 1` — but only once * `prefixDirty` is false. Mutators mark the prefix dirty instead of * rebuilding eagerly, so a burst of `record()`/`sync()`/`recordMany()` * calls in one render cycle costs a single O(n) rebuild at the first read * instead of one O(n) rebuild per mutation. After any mutator, `prefix` * may lag until the next read; `size` and `ids` are always authoritative. */ private prefix; /** Ordered entry ids matching the prefix-sum positions. */ private ids; /** True when `prefix` no longer reflects `heights`/`ids` and must be rebuilt. */ private prefixDirty; /** * Set to `true` after the first call to `sync()`, so `record()` knows the * id order is governed by transcript membership and must reject unknown ids. * Reset by `clear()`. */ private synced; /** * Record or update the measured height for an entry. O(1) — marks the * prefix-sum dirty; the rebuild is deferred to the first read so a burst * of records in one render cycle pays for a single rebuild. * * After the first `sync()`, the id MUST be a member of the synced set or * have been previously recorded — unknown ids are rejected with a * `RangeError` because appending them at the end would place them at the * wrong transcript position, corrupting the virtual scroll viewport. * Call `sync()` first if you need to register new ids. * * Before any `sync()` call (fresh cache), unknown ids are appended in * insertion order, which is the only reasonable behaviour when no * transcript order exists yet. * * Returns `true` when the height changed (new or different from last record). */ record(id: number, height: number): boolean; /** * Synchronize cache membership/order with the retained transcript in one * O(n) rebuild. Missing entries receive a bounded estimate immediately, so * the first render of a resumed/long session can be virtualized instead of * mounting the entire history tree just to discover its height. */ sync(entryIds: readonly number[], estimatedHeight?: number): boolean; /** * Update several measured estimates with a single deferred prefix rebuild. * * Rows MUST reference ids already registered via `sync()`. Unknown ids are * rejected with a `RangeError` because appending them at the end would place * them at the wrong transcript position until the next `sync()`, corrupting * the virtual scroll viewport. Call `sync()` first if you need to register * new ids. Duplicate ids within one batch are last-write-wins (each row is * applied in order). */ recordMany(rows: Iterable): boolean; /** * Total height in rows of all measured entries. * Amortized O(1) — returns the last prefix-sum entry, rebuilding once if dirty. */ totalHeight(): number; /** * Accumulated row height of entries with index < `entryIndex`. * `accumulatedHeight(0)` = 0. Clamps to total height when * `entryIndex > ids.length`. O(1) via prefix-sum lookup. */ accumulatedHeight(entryIndex: number): number; /** * Look up the measured height for a single entry id. * Returns `undefined` when the entry has never been measured. */ getHeight(id: number): number | undefined; /** * Number of entries currently tracked. */ get size(): number; /** * Find the entry index whose accumulated range contains the given * vertical `rowOffset` from the top of all content. * * Binary search on the prefix-sum array. O(log n). * Returns `0` for offsets at or before the first entry. * Returns `ids.length` for offsets past the last entry. */ entryIndexAtOffset(rowOffset: number): number; /** * Remove all cached heights and reset to empty state. */ clear(): void; /** * Drop cached rows for entries no longer present in the bounded TUI history * and restore prefix ordering to match the current entry array. * * Unlike `sync()`, retained ids that are not yet measured are NOT seeded * with a placeholder — call `sync()` if you need bounded estimates for ids * that have never been recorded. */ retain(entryIds: readonly number[]): void; /** Rebuild the prefix-sum once if any mutation since the last read dirtied it. */ private ensurePrefix; /** Rebuild the prefix-sum array from current heights in insertion order. */ private rebuild; } /** * Compute the visible window slice for a virtual-scrolled entry list. * * Given the total measured content, viewport size, scroll offset, and entry * array, determines which entries to render and how tall the top/bottom spacer * elements should be. * * All measurements are in terminal rows. * * Returns a result with `startIdx`, `endIdx`, `spacerAbove`, and `spacerBelow` * that the caller uses to render only the visible window. */ export interface ComputeWindowResult { /** Index of the first entry to render (inclusive). */ startIdx: number; /** Index of the last entry to render (exclusive, like Array.slice end). */ endIdx: number; /** Row height of the spacer above the visible window. 0 when at top. */ spacerAbove: number; /** Row height of the spacer below the visible window. 0 when at bottom. */ spacerBelow: number; /** Total content height measured. */ totalHeight: number; /** Whether the computed window differs from a flat render of all entries. */ windowed: boolean; } /** * Compute which entries should be rendered in a virtual-scrolled viewport. * * Pure function — no side effects, no state mutations. Exported for testing. * * @param totalHeight - Total measured height of all entries in rows. * @param viewportRows - Number of visible rows in the viewport. * @param scrollOffset - Rows scrolled up from the bottom (0 = pinned to newest). * @param entryCount - Total number of entries. * @param cache - EntryHeightCache with measured heights. * @returns A {@link ComputeWindowResult} describing the visible slice. */ export declare function computeWindow(totalHeight: number, viewportRows: number, scrollOffset: number, entryCount: number, cache: EntryHeightCache): ComputeWindowResult; //# sourceMappingURL=height-cache.d.ts.map