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%
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