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%
4.0 KB · 115 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: src/tui/renderer/capabilities.ts4 * Description: Terminal capability detection — color depth ladder, NO_COLOR, DEC 2026 probe, OSC 11 background query.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910export type ColorDepth = "truecolor" | "ansi256" | "ansi16" | "mono";11export type Background = "dark" | "light";1213export interface TerminalCapabilities {14  colorDepth: ColorDepth;15  /** DEC 2026 synchronized-output supported (probed; false when unknown). */16  syncUpdates: boolean;17  background: Background;18}1920/**21 * Color capability ladder (TUI_DESIGN §12): truecolor → ANSI-256 → ANSI-16 →22 * monochrome. `NO_COLOR` and `TERM=dumb` are honored absolutely; non-TTY23 * output is monochrome.24 */25export function detectColorDepth(env: NodeJS.ProcessEnv, isTty: boolean): ColorDepth {26  if (!isTty) return "mono";27  if (env["NO_COLOR"] !== undefined && env["NO_COLOR"] !== "") return "mono";28  const term = env["TERM"] ?? "";29  if (term === "dumb" || term === "") return "mono";30  const colorterm = env["COLORTERM"] ?? "";31  if (colorterm === "truecolor" || colorterm === "24bit") return "truecolor";32  if (term.includes("256color")) return "ansi256";33  return "ansi16";34}3536/**37 * Parse a DECRQM reply for mode 2026: `CSI ? 2026 ; Ps $ y`.38 * Ps 1 (set) / 2 (reset) / 3 (permanently set) mean the mode is recognized →39 * synchronized updates are supported. Ps 0 (unrecognized) / 4 (permanently40 * reset) mean unsupported. Returns null when no complete reply is present.41 */42export function parseDecrqmReply(data: string): boolean | null {43  const m = /\x1b\[\?2026;(\d+)\$y/.exec(data);44  if (!m) return null;45  const ps = Number(m[1]);46  return ps === 1 || ps === 2 || ps === 3;47}4849/**50 * Parse an OSC 11 background-color reply (`OSC 11 ; rgb:RRRR/GGGG/BBBB ST|BEL`)51 * into light/dark via relative luma. Returns null when no complete reply is present.52 */53export function parseOsc11Reply(data: string): Background | null {54  const m = /\x1b\]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)(?:\x07|\x1b\\)/.exec(data);55  if (!m) return null;56  const channel = (hex: string): number => {57    const max = Math.pow(16, hex.length) - 1;58    return parseInt(hex, 16) / max;59  };60  const r = channel(m[1] as string);61  const g = channel(m[2] as string);62  const b = channel(m[3] as string);63  const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;64  return luma > 0.5 ? "light" : "dark";65}6667export interface ProbeStreams {68  stdin: NodeJS.ReadStream;69  stdout: NodeJS.WriteStream;70}7172/**73 * Probe the terminal for DEC 2026 support and background color. Requires raw74 * mode to already be active (replies arrive on stdin without a newline).75 * Resolves within `timeoutMs` (default 100 ms — TUI_DESIGN §12) with whatever76 * was learned; absence of a reply degrades to `{ syncUpdates: false,77 * background: null }` — never a hang, never a guess presented as fact.78 */79export function probeTerminal(80  streams: ProbeStreams,81  timeoutMs = 100,82): Promise<{ syncUpdates: boolean; background: Background | null }> {83  const { stdin, stdout } = streams;84  if (!stdin.isTTY || !stdout.isTTY) {85    return Promise.resolve({ syncUpdates: false, background: null });86  }87  return new Promise((resolve) => {88    let buffer = "";89    let sync: boolean | null = null;90    let background: Background | null = null;91    let done = false;9293    const finish = (): void => {94      if (done) return;95      done = true;96      stdin.off("data", onData);97      clearTimeout(timer);98      resolve({ syncUpdates: sync ?? false, background });99    };100101    const onData = (chunk: Buffer | string): void => {102      buffer += chunk.toString("utf8");103      if (sync === null) sync = parseDecrqmReply(buffer);104      if (background === null) background = parseOsc11Reply(buffer);105      if (sync !== null && background !== null) finish();106    };107108    const timer = setTimeout(finish, timeoutMs);109    timer.unref();110    stdin.on("data", onData);111    // DECRQM for mode 2026, then OSC 11 background query.112    stdout.write("\x1b[?2026$p\x1b]11;?\x07");113  });114}115