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/markdown/scanner.ts4 * Description: Settled-block incremental markdown scanner — blocks settle at blank lines outside fences (TUI_DESIGN §8).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910const FENCE_RE = /^ {0,3}(```+|~~~+)/;1112/**13 * The Hermes StreamScanState technique, adopted as-is: stream deltas append14 * to a raw tail; the scanner advances only over newline-terminated input,15 * detecting settled top-level blocks. A block settles at a blank line outside16 * a code fence; a fence settles at its closing fence. Settled blocks are17 * rendered exactly once and flushed to scrollback; only the tail is ever18 * re-scanned — never O(blocks²), and settled text never reflows.19 */20export class MarkdownStreamScanner {21 /** Complete (newline-terminated) lines not yet settled. */22 private lines: string[] = [];23 /** Trailing input without its newline yet. */24 private partial = "";25 private inFence = false;26 private fenceMarker = "";2728 /** Append streamed text; returns raw source of any blocks that settled. */29 append(text: string): string[] {30 const combined = this.partial + text;31 const parts = combined.split("\n");32 this.partial = parts.pop() ?? "";33 for (const line of parts) this.lines.push(line);34 return this.extractSettled();35 }3637 /** End of stream: everything pending settles (including the partial line). */38 finish(): string[] {39 const settled = this.extractSettled();40 const rest: string[] = [...this.lines];41 if (this.partial !== "") rest.push(this.partial);42 this.lines = [];43 this.partial = "";44 this.inFence = false;45 this.fenceMarker = "";46 const restBlock = trimBlock(rest);47 if (restBlock !== null) settled.push(restBlock);48 return settled;49 }5051 /** The live raw tail (unsettled complete lines + the partial line). */52 tail(): string {53 if (this.lines.length === 0) return this.partial;54 return this.lines.join("\n") + "\n" + this.partial;55 }5657 /** True when the tail sits inside an open code fence. */58 insideFence(): boolean {59 return this.inFence;60 }6162 /**63 * Cap-pressure early flush (TUI_DESIGN §1.2/§8): settle the first64 * `lineCount` complete lines at the last safe boundary even without a65 * block boundary. Returns the flushed source or null when nothing settled.66 * Never splits a fence: inside a fence the flush is refused (the caller67 * caps display instead — fence integrity is load-bearing).68 */69 settleHead(lineCount: number): string | null {70 if (this.inFence) return null;71 const n = Math.min(lineCount, this.lines.length);72 if (n <= 0) return null;73 // Do not cut through a fence that opens within the head.74 let fence = false;75 let marker = "";76 for (let i = 0; i < n; i++) {77 const line = this.lines[i] as string;78 const m = FENCE_RE.exec(line);79 if (m) {80 if (!fence) {81 fence = true;82 marker = (m[1] as string)[0] as string;83 } else if ((line.trimStart()[0] ?? "") === marker) {84 fence = false;85 }86 }87 }88 if (fence) return null;89 const head = this.lines.splice(0, n);90 return trimBlock(head);91 }9293 private extractSettled(): string[] {94 const settled: string[] = [];95 let scanFrom = 0;96 let current: string[] = [];9798 // Re-walk pending lines with fence state; settle at boundaries.99 this.inFence = false;100 this.fenceMarker = "";101 for (let i = 0; i < this.lines.length; i++) {102 const line = this.lines[i] as string;103 const fenceMatch = FENCE_RE.exec(line);104105 if (this.inFence) {106 current.push(line);107 if (fenceMatch && (line.trimStart()[0] ?? "") === this.fenceMarker) {108 // Closing fence settles the whole fenced block immediately.109 this.inFence = false;110 const block = trimBlock(current);111 if (block !== null) settled.push(block);112 current = [];113 scanFrom = i + 1;114 }115 continue;116 }117118 if (fenceMatch) {119 this.inFence = true;120 this.fenceMarker = (fenceMatch[1] as string)[0] as string;121 current.push(line);122 continue;123 }124125 if (line.trim() === "") {126 // Blank line outside a fence: boundary — settle what precedes it.127 const block = trimBlock(current);128 if (block !== null) settled.push(block);129 current = [];130 scanFrom = i + 1;131 continue;132 }133134 current.push(line);135 }136137 this.lines = this.lines.slice(scanFrom);138 return settled;139 }140}141142/** Drop leading/trailing blank lines; null when nothing remains. */143function trimBlock(lines: string[]): string | null {144 let start = 0;145 let end = lines.length;146 while (start < end && (lines[start] as string).trim() === "") start++;147 while (end > start && (lines[end - 1] as string).trim() === "") end--;148 if (start >= end) return null;149 return lines.slice(start, end).join("\n");150}151