SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
3.7 KB · 114 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/tui/renderer/frame.ts4 * Description: Pure frame composition — settle blocks to scrollback, damage-tracked live-region repaint, cursor parking.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import {11  CSI,12  ERASE_DOWN,13  ERASE_LINE,14  HIDE_CURSOR,15  SHOW_CURSOR,16  SYNC_OFF,17  SYNC_ON,18  truncateAnsi,19} from "./ansi.js";2021/** One live-region snapshot: the bounded bottom strip plus the composer caret. */22export interface LiveFrame {23  lines: string[];24  /** Row index (into `lines`) where the hardware cursor parks. */25  caretRow: number;26  /** Column (0-based, visible columns) where the hardware cursor parks. */27  caretCol: number;28}2930export interface ComposeFrameInput {31  /** Live-region lines currently on screen (empty for the first paint). */32  prevLines: readonly string[];33  /** Caret row of the previous paint (cursor is parked there right now). */34  prevCaretRow: number;35  /** The next live-region state. */36  next: LiveFrame;37  /** Settled blocks to flush to scrollback above the live region, in order. */38  settled: readonly string[][];39  /** Terminal width in columns; every emitted line is truncated to width − 1. */40  width: number;41  /** Wrap the paint in DEC 2026 synchronized-output markers. */42  sync: boolean;43}4445export interface ComposedFrame {46  /** The single write for this frame. */47  data: string;48  /** Live-region lines now on screen — feed back as `prevLines` next frame. */49  lines: string[];50  caretRow: number;51  /** Rows repainted this frame (damage-tracking observability). */52  repaintedRows: number;53}5455/**56 * Compose one paint as a single ANSI string (TUI_DESIGN §1.3):57 *58 * 1. Move from the parked caret to the live-region origin.59 * 2. If settled blocks are pending: erase the live region, print the settled60 *    lines as plain scrollback writes (immutable — never repainted again).61 * 3. Repaint only the live rows that changed (per-row damage tracking); erase62 *    below when the region shrank.63 * 4. Park the hardware cursor at the composer caret.64 *65 * Wrapped in SYNC_ON/SYNC_OFF when supported, cursor hide/show bracketing66 * otherwise. Pure: no I/O, fully deterministic — the terminal writer is a67 * thin sink around this.68 */69export function composeFrame(input: ComposeFrameInput): ComposedFrame {70  const { prevLines, prevCaretRow, next, settled, width, sync } = input;71  const lineWidth = Math.max(1, width - 1);72  const lines = next.lines.map((l) => truncateAnsi(l, lineWidth));73  const caretRow = Math.min(Math.max(0, next.caretRow), Math.max(0, lines.length - 1));7475  let b = sync ? SYNC_ON + HIDE_CURSOR : HIDE_CURSOR;7677  // Move to the live-region origin, column 1.78  b += prevCaretRow > 0 ? `${CSI}${prevCaretRow}F` : "\r";7980  let effectivePrev: readonly string[] = prevLines;81  if (settled.length > 0) {82    // Erase the live region, then print settled lines; they scroll away and83    // are never touched again.84    b += ERASE_DOWN;85    for (const block of settled) {86      for (const line of block) b += truncateAnsi(line, lineWidth) + "\r\n";87    }88    effectivePrev = [];89  }9091  const full = effectivePrev.length !== lines.length;92  const shrunk = effectivePrev.length > lines.length;93  let repaintedRows = 0;9495  for (let i = 0; i < lines.length; i++) {96    const line = lines[i] as string;97    if (full || line !== effectivePrev[i]) {98      b += ERASE_LINE + line;99      repaintedRows += 1;100    }101    if (i < lines.length - 1) b += "\r\n";102  }103  if (shrunk) b += ERASE_DOWN;104105  // Park the cursor at the composer caret — unconditionally, every frame.106  const up = lines.length - 1 - caretRow;107  if (up > 0) b += `${CSI}${up}A`;108  b += `${CSI}${Math.min(next.caretCol, lineWidth) + 1}G`;109110  b += sync ? SHOW_CURSOR + SYNC_OFF : SHOW_CURSOR;111112  return { data: b, lines, caretRow, repaintedRows };113}114