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%
48.8 KB · 952 lines markdown
Rendered Raw Blame History
1<!--2KHAELOR3File: docs/TUI_DESIGN.md4Author: Simon-Pierre Boucher5Contact: contact@spboucher.ai6-->78# KHAELOR — TUI Design (V1, definitive)910> Phase 1 deliverable. The terminal IS the product (Absolute Rule #2). This document fixes the11> layout architecture, every visual element, every interaction, the rendering techniques, and the12> Phase 2 framework spike that selects the renderer. Techniques here are framework-independent and13> are adopted regardless of the spike outcome (ADR-14).14>15> Aesthetic contract (CLAUDE.md §14): minimal · calm · dense when needed · keyboard-native ·16> hierarchy from spacing, typography, subtle color, indentation, and status — never from noise.17> No rainbow colors, no giant banners, no border mazes, no constant animation, no emoji spam.1819---2021## 0. Framework decision: the two candidates, and the recommendation2223### 0.1 Empirical facts (established, not speculative)24251. **`@opentui/core` is eliminated.** OpenCode's engine depends on `bun-ffi-structs` — it is26   Bun-FFI-bound and not viable for KHAELOR's Node-only `npm install -g` distribution (ADR-1).27   ADR-14's candidate (b) is dead; its *techniques* (16 ms coalescing, delta-driven repaints,28   width-responsive chrome) survive and are adopted below.292. **Ink 7 requires Node ≥ 22.** KHAELOR targets Node ≥ 22, so Ink 7 is compatible.303. **Stock Ink's full-rerender model is a demonstrated risk.** Hermes had to vendor a ~100-file31   Ink fork (ScrollBox, alternate screen, mouse, selection, virtualization) to get acceptable32   streaming behavior (HERMES §8.1).3334### 0.2 The two remaining candidates3536- **(a) Ink 7, strictly bounded**: all settled content rendered through `<Static>` (written once37  to stdout, never reconciled again); the live tree is *only* the bounded live region (§1). Ink's38  full-rerender cost is then proportional to ~20 lines, not the session.39- **(b) Minimal custom ANSI renderer**: a purpose-built renderer for KHAELOR's fixed layout —40  append-only scrollback writes + a repaintable live region, synchronized-output frames, a41  hand-written line editor. No reconciler, no flexbox, no framework.4243Both are wired behind the same thin `RendererAdapter` (§15) so the Phase 2 spike is cheap and the44loser is discarded without touching the rest of the system.4546### 0.3 Recommendation: **(b) the minimal custom ANSI renderer**, with Ink 7 as the control4748Rationale:4950- **The layout architecture removes the need for a framework.** KHAELOR's design (§1) commits to51  terminal-native scrollback with print-once immutable settled content. The only thing that ever52  repaints is a bounded live region of ≤ ~24 lines. A reconciler + Yoga layout engine managing a53  fixed 24-line strip is machinery without a problem. The same commitment is what makes a custom54  renderer *tractable* — ADR-14 predicted exactly this ("option (c) is viable precisely because55  the adopted techniques are what make a custom renderer tractable").56- **Both mature reference teams ended up owning their renderer.** OpenCode built `@opentui` from57  scratch; Hermes forked Ink into ~100 vendored files. The pattern across the two products whose58  terminal feel we respect is: at this quality bar you own the paint path eventually. Owning ~1.5–2K59  purpose-built lines from day one is cheaper than owning a fork of someone else's reconciler.60- **Exact control of the properties the spec makes non-negotiable**: DEC 2026 synchronized-output61  frames (flicker), cursor parking (stable input), zero framework overhead between keypress and62  echo (<16 ms), no retained render tree growing with the session (memory), plain stdout writes63  for settled content (terminal-native selection/copy).64- **Honest cost**: the line editor, wrapping, and overlay painting are real work (Hermes' composer65  is a 47 KB hand-written editor). This is the one axis where Ink wins, which is why Ink 7 is66  built as the spike's control candidate and remains fully acceptable if it passes the criteria67  in §15.4 and the custom editor cost blows the Phase 2 budget.6869The spike (§15) decides on measurements, not taste. If Ink 7 (a) passes every criterion and (b)70ships materially sooner, Ink wins — the recommendation is a prior, not a verdict.7172---7374## 1. Layout architecture7576### 1.1 Position: terminal-native scrollback, no alternate screen for conversation7778KHAELOR renders the conversation into the **main screen buffer**, like a well-behaved CLI — not an79alternate-screen full-screen app.8081Justification:8283- **Selection/copy is a hard criterion.** Native terminal selection, copy, and search work on84  printed scrollback for free. Alternate-screen apps must reimplement selection (Hermes' fork did;85  OpenCode disables Escape-dismissal during mouse selection just to protect it — OPENCODE §3.6).86- **Scrollback is free and unbounded.** No virtualization, no Hermes `useVirtualHistory`, no87  OpenCode 100-message UX cliff (explicitly rejected, OPENCODE NOT-COPY #6). The terminal already88  ships a better scrollback than we can write.89- **Crash-safe transcript.** If KHAELOR dies, the conversation remains on screen.90- **Memory.** Settled content leaves the process; the live data structures are O(live region), not91  O(session).9293**The settled/live split** (the load-bearing rule of the whole design):9495- **Settled content** — completed message blocks, finished tool rows, committed user messages — is96  printed to scrollback **exactly once** and never touched again. There is no retroactive mutation97  of printed lines, ever.98- **Live region** — a bounded strip pinned to the bottom (≤ ~24 rows) that is the *only* thing99  repainted: the streaming tail of the current response, the agent status line, the composer, and100  the status bar. Every repaint is wrapped in a DEC 2026 synchronized-output frame.101102**One exception:** explicitly-entered full-screen viewers (`/diff` viewer §7.2, and nothing else103in V1) use the alternate screen like `less` does — enter, navigate, `q`, return with the104conversation untouched. A pager is the one UI shape the main buffer genuinely cannot host.105106### 1.2 Screen anatomy107108```109┌─ terminal scrollback (native, unbounded, selectable) ─────────────────┐110│  … earlier conversation, printed once, immutable …                    │111│                                                                       │112│  ❯ add retry logic to the session store                               │113│                                                                       │114│  ▸ Read src/session/store.ts                                          │115│  ▸ Search "retry" · 6 matches                                         │116│                                                                       │117│  I found the failure point in SessionStore.append — writes            │118│  are not retried on transient EAGAIN. Fixing that first.              │119├─ live region (bounded, repainted as one synchronized frame) ──────────┤120│  ▸ Edit src/session/store.ts                          ← streaming tail│121│                                                                       │122│  ● Editing src/session/store.ts · 3.1s                ← agent status  │123│                                                                       │124│  ╭─────────────────────────────────────────────────╮                  │125│  │ ❯ _                                             │   ← composer box │126│  ╰─────────────────────────────────────────────────╯                  │127│  main +2 −0  │  claude-sonnet  │  context 24%  │  $0.31   ← status bar│128└───────────────────────────────────────────────────────────────────────┘129```130131(The frame above is illustrative; KHAELOR draws no boxes around these regions.)132133Live-region rules:134135- **Hard caps** (Hermes issue #34095 — unbounded live trails OOM-killed Node): live streaming tail136  ≤ 16,000 chars / ≤ `min(240, rows − 8)` lines. When the tail exceeds the cap, its settled head is137  flushed to scrollback (§8 defines "settled") and the region shrinks back.138- The composer is always the last editable element; the status bar is always the last row.139- **Overlays** (palettes, pickers, permission panel, config) render *inside* the live region,140  replacing the streaming-tail slot, above the composer/status bar. They are bounded panels, not141  floating windows — there is no compositor on the main screen buffer.142- On resize (`SIGWINCH`): scrollback is left alone (the terminal reflows or doesn't — its143  business); the live region is fully repainted at the new width within one frame.144145### 1.3 Repaint discipline146147- One repaint pass per animation frame; input events, stream deltas, and process output are148  coalesced in a **16 ms window** into a single frame (ADR-4; OpenCode `batch()`, OPENCODE §3.3;149  Hermes converged at 33 ms — we take the stricter figure).150- Every frame: `CSI ?2026h` … move cursor to live-region origin … repaint changed rows only151  (per-row damage tracking) … park the cursor at the composer caret … `CSI ?2026l`. Terminals152  without 2026 support get cursor-hide/show bracketing as fallback.153- Settled content is emitted *between* frames as plain writes above the live region (erase live154  region → print settled block → repaint live region), so scrollback stays clean for155  selection/copy.156157---158159## 2. Startup experience160161`khaelor` reaches an editable prompt in < 150 ms (§14). Nothing blocks on the network; git status162and index warmup fill in asynchronously (status bar segments appear when real data exists —163Absolute Rule #4).164165```166 KHAELOR167 ~/dev/my-project · main168 claude-sonnet-4-5 · thinking adaptive169────────────────────────────────────────────────────170 ╭──────────────────────────────────────────────────╮171 │ ❯ What do you want to build?                     │172 ╰──────────────────────────────────────────────────╯173```174175- Three lines of identity (the wordmark carries the brand gradient), one rule, the composer box.176  The question lives inside the box as a dim placeholder on first run — after the first message177  the box shows just the prompt glyph. No ASCII logo, no version spam, no log lines, no animation.178- The rule line spans the terminal width (dim). The model line shows the *configured* model id or179  alias — never a hard-coded name.180- If `ANTHROPIC_API_KEY` is missing, the question line is replaced by a single calm instruction and181  the composer accepts `/config`:182183```184 KHAELOR185 ~/dev/my-project · main186────────────────────────────────────────────────────187 No Anthropic API key found.188 Set ANTHROPIC_API_KEY or run /config to add one.189 ╭──────────────────────────────────────────────────╮190 │ ❯ _                                              │191 ╰──────────────────────────────────────────────────╯192```193194- Resuming (`khaelor` in a project with an interrupted session) adds exactly one line:195  `Interrupted session from 12 min ago · /resume to continue` — no auto-resume, no modal.196197---198199## 3. The composer200201The highest-priority component (CLAUDE.md §14). Reference standard: OpenCode's prompt202(OPENCODE §3.5) — structured parts, not a flat string.203204### 3.1 Anatomy205206The composer is a rounded bordered box, full terminal width minus a one-column margin:207208```209 ╭────────────────────────────────────────────────────────────────╮210 │ ❯ refactor @src/session/store.ts to journal events atomically, │211 │   then run the tests_                                          │212 ╰────────────────────────────────────────────────────────────────╯213```214215- Prompt glyph `❯` (accent color; `>` in ASCII fallback) on the first content row; continuation216  rows indent to align. The hardware cursor parks at the real text position inside the box.217- Content grows from 1 row to `min(8, rows/3)` rows, then scrolls internally; the box height is218  content rows + 2 border rows.219- Border state: brand gradient (violet→cyan) while the agent is idle — the box is the focus; dim220  while the agent works. The glyph dims with it; queued input is always allowed (§3.6), and a221  `⋯ n queued` indicator rides the bottom border.222- On first run the empty box shows a dim placeholder (`What do you want to build?`); afterwards223  just the prompt glyph.224- Below 40 columns the box degrades to a plain ` ❯ ` prompt line (no borders).225- Palettes (§4), the permission panel (§9), and other overlays render *above* the box; the box and226  status bar are never displaced.227228### 3.2 Model: structured parts over a text buffer229230The buffer is text + **spans** (the extmark idea, OPENCODE §3.5): file mentions and collapsed231pastes are spans with display text, style, and structured payload. Span offsets are maintained232through every edit. Submission produces structured parts — `text | fileRef{path, range?} |233pastedBlock{content}` — so the Context Engine receives references, not flattened strings.234235- **Paste intelligence**: bracketed paste; ≥ 3 lines or > 150 chars collapses to a236  `⧉ pasted 47 lines` span (expanded only at submit); a path-looking paste becomes a file mention.237- **History**: `~/.khaelor/prompt-history.jsonl`, 50 entries per project, deduplicated; Up at238  buffer start / Down at buffer end navigate it (cursor-position-aware, so multiline editing is239  never hijacked).240- **Undo/redo**: a bounded edit-op stack (`Ctrl+_` undo; best-effort, in-composer only).241- **External editor**: `Ctrl+G` round-trips the buffer through `$EDITOR`, re-locating spans by242  placeholder tokens on return.243244### 3.3 Slash commands245246`/` at offset 0 opens the slash palette (§4.1) anchored above the composer. Typing filters;247Enter completes or executes.248249### 3.4 `@` file mentions250251`@` opens fuzzy repository search over the repository index (respecting `.gitignore` /252`.khaelorignore`), ranked by match score × frecency (`frequency / (1 + ageDays)` — OpenCode's253formula, stored in `~/.khaelor/frecency.jsonl`):254255```256 ❯ refactor @agent257            ┌────────────────────────────────────────┐258             src/kernel/agent.ts                 ★259             src/agents/agent-runtime.ts260             tests/agent.test.ts261            └────────────────────────────────────────┘262```263264(`★` = frecency-boosted; dim, not loud.) Selection inserts a **file-reference span**265`@src/kernel/agent.ts` — a structured reference the Context Engine resolves, not file contents.266Range syntax `@src/kernel/agent.ts:40-90` parses in V1; the picker UI for ranges is post-V1.267268### 3.5 Shell mode269270`!` at offset 0 switches the composer into shell mode for one submission:271272```273 ! git status --short274```275276The glyph changes to `!` (warning tint). Output prints to scrollback as a settled block, marked277`$ git status --short` with head/tail truncation for long output; a one-key follow-up hint278(`a — add output to context`) lets it become agent context explicitly. Shell mode runs under the279same permission rules as agent `bash`.280281### 3.6 Message queueing while the agent works (steering)282283The composer never locks. Text typed mid-run is submitted normally and queued (ADR-11: injected284only at safe tool-result boundaries, never breaking role alternation):285286```287 ▸ Run npm test · running 8s288289 ⋯ Queued — use the smaller fixture instead290   Esc cancel run · Ctrl+U discard queued291```292293Queued instructions render in the live region with the `⋯` marker until injected, at which point294they settle into scrollback as a normal user message. Multiple queued messages stack in order.295296### 3.7 Composer key bindings297298| Key | Action |299|---|---|300| `Enter` | Submit (or queue, while agent runs) |301| `Shift+Enter` / `Ctrl+J` | Insert newline (`Shift+Enter` via kitty-keyboard / modifyOtherKeys when detected; `Ctrl+J` always works; trailing `\` + `Enter` also continues) |302| `←` `→`, `Ctrl+B` `Ctrl+F` | Move by character |303| `Alt+←/→`, `Alt+B` `Alt+F` | Move by word |304| `Ctrl+A` / `Ctrl+E` | Line start / line end |305| `↑` / `↓` | Line up/down in multiline; history at buffer edges |306| `Ctrl+R` | Incremental history search |307| `Backspace` / `Ctrl+H` | Delete char back |308| `Ctrl+W` / `Alt+Backspace` | Delete word back |309| `Alt+D` | Delete word forward |310| `Ctrl+U` | Delete to line start (or discard queued message when composer empty) |311| `Ctrl+_` | Undo |312| `Tab` | Accept selected completion |313| `Ctrl+G` | Edit buffer in `$EDITOR` |314| `/` (at offset 0) | Slash palette |315| `@` | File-mention search |316| `!` (at offset 0) | Shell mode |317318---319320## 4. Palettes321322Both palettes are the same component (one generic filter-list, as OpenCode's `dialog-select`323proves out — OPENCODE §3.6) with different sources. They render in-live-region, anchored above the324composer, max height `min(12, rows − 6)`.325326### 4.1 Slash palette327328```329 ❯ /se330   ┌──────────────────────────────────────────────────────┐331    /sessions   browse and resume sessions332    /new        start a new session333    /resume     resume the most recent session334   └──────────────────────────────────────────────────────┘335    ↑↓ navigate · Enter run · Esc close336```337338- Fuzzy filtering (fuzzysort-style scoring with exact-prefix bonus), selected row inverted, match339  characters underlined (not colored-only — §12).340- Full V1 set: `/model /config /permissions /context /sessions /resume /new /rename /clear341  /compact /cost /status /diff /processes /help /quit`.342343### 4.2 Universal command palette — `Ctrl+K`344345Same panel, sourced from the **command registry** (one registry powers keys, slash commands, and346the palette — OpenCode's proven unification, OPENCODE §3.6), showing live bindings:347348```349   ┌──────────────────────────────────────────────────────┐350    ❯ diff_351    ──────────────────────────────────────────────────────352    View diff                    /diff        d353    Compact context              /compact354    Show processes               /processes355   └──────────────────────────────────────────────────────┘356    ↑↓ navigate · Enter run · Esc close357```358359Users never need to memorize commands: everything reachable is listed with its key and slash name.360361---362363## 5. Agent status line364365**One** compact dynamic line in the live region, directly above the composer. States:366`thinking · reading · searching · editing · running · waiting · verifying · idle`.367368```369 ● Searching repository · 2.3s370 ● Editing src/kernel/agent.ts371 ● Running npm test · 41/148372 ● Waiting for permission373 ● Verifying · npm run typecheck374```375376Rules (Absolute Rule #4 and §18 — no spinner-driven UX):377378- Every element is **real data**: the state derives from actual bus events (`ToolStarted`,379  `ModelRequestStarted`, …); elapsed time is a real timer; counts like `41/148` appear **only**380  when a tool parser actually extracted them. Never a fabricated percentage, never `⠋ Thinking...`381  as a substitute for information we have.382- `●` pulses between two shades at ~2 Hz while active — the only animation in the product — and is383  `○` when idle. In monochrome, `●`/`○` still carry the distinction.384- Width is pre-reserved so the line never jitters as text changes (Hermes' spinner-width trick,385  HERMES §8.2). One line, always; transient states are never printed into the conversation.386- When idle the line collapses to nothing (the composer moves up a row).387388---389390## 6. Tool call presentation391392### 6.1 Collapsed one-liners (default)393394Each tool call settles into scrollback as exactly one line:395396```397 ▸ Read src/kernel/agent.ts · 212 lines398 ▸ Search "ContextEngine" · 14 matches399 ▸ Edit src/context/engine.ts · +31 −12400 ▸ Run npm test · passed · 4.2s401 ▸ Run npm test · 2 failed · 6.8s402 ▸ Start process 3121 · npm run dev403```404405- `▸` dim; tool verb normal; argument bright; result annotation dim. Failures swap the annotation406  to the error color **and** the word `failed` (never color alone). Counts (`+31 −12`,407  `14 matches`, exit codes, durations) come from real tool results.408- While running, the row lives in the live region with the elapsed timer409  (`▸ Run npm test · 8s`) and a rolling tail of output when useful; it settles to its final410  one-liner when the tool completes.411412### 6.2 Expansion413414Settled scrollback is immutable (§1.1), so expansion **prints** detail rather than mutating rows:415416- **Live turn**: `Ctrl+T` cycles the detail level of the current turn's live tool row417  (`collapsed → tail (12 lines) → collapsed`), Hermes' three-state `DetailsMode` reduced to two.418- **After settling**: every tool call gets a turn-local index shown on demand. `d` (empty composer)419  prints the most recent edit's diff (§7.1); `/tool` lists this turn's calls; `/tool 3` prints420  call 3's full detail as a new settled block:421422```423 ▸ Run npm test · 2 failed · 6.8s                      [3]424425 ── tool 3 · npm test ────────────────────────────────────426  FAIL src/context/engine.test.ts427    ✕ compaction preserves running processes428  … 214 lines omitted · full output: ~/.khaelor/tool-out/8f3a.txt429 ─────────────────────────────────────────────────────────430```431432### 6.3 Long output433434Head/tail truncation with explicit omission markers; the full output is spilled to435`~/.khaelor/tool-out/<id>.txt` and the path is shown (and given to the model — ADR-8). The436conversation layout is never destroyed by a 40,000-line test log: what settles is bounded437(≤ 12 lines per tool by default, matching Hermes' persisted-trail cap).438439---440441## 7. Diff presentation442443### 7.1 Inline (after every edit)444445Never bare "Edited file":446447```448 ✓ src/context/engine.ts  +31 −12        d expand diff449```450451`d` (composer empty) prints the unified diff of the most recent edit as a settled block, syntax452highlighted, `+` lines in the added color, `−` in the removed color, with `+`/`−` glyphs453preserved for monochrome:454455```456 ── diff · src/context/engine.ts · +31 −12 ───────────────457  @@ -84,7 +84,9 @@ export class ContextEngine {458  -  const budget = this.window - used;459  +  const reserve = this.config.compactionReserve;460  +  const budget = this.window - used - reserve;461 ─────────────────────────────────────────────────────────462```463464### 7.2 `/diff` — the full viewer (alternate screen)465466The one full-screen surface in V1 (§1.1). Enter → alternate screen; `q`/`Esc` → back, conversation467untouched. Shows the session's cumulative changes (baseline attribution per ADR-15 — only468KHAELOR's changes, never pre-existing user diff).469470Side-by-side when `width > 120` (OpenCode's threshold), unified below:471472```473 /diff · 3 files · +64 −21                                    2/3474 ─────────────────────────────────────────────────────────────────475  src/session/store.ts                                    +18 −6476 ▸src/context/engine.ts                                   +31 −12477  src/tools/edit.ts                                       +15 −3478 ─────────────────────────────────────────────────────────────────479  84  const budget =            │ 84  const reserve = this.config.480  85    this.window - used;     │ 85  const budget = this.window -481      ─                         │ 86    used - reserve;           +482 ─────────────────────────────────────────────────────────────────483  ↑↓/jk scroll · ]/[ next/prev file · Tab file list · u unified · q close484```485486Syntax highlighting per §8.2; hunk navigation `]`/`[` (OpenCode's diff-viewer bindings);487added/removed counts per file and total; accept/revert hooks are post-V1 (no shadow git in V1,488ADR-15).489490---491492## 8. Markdown rendering pipeline493494**Settled-block incremental streaming** — Hermes' `StreamScanState` technique (HERMES §8.2),495adopted as-is:4964971. Stream deltas append to a raw tail rendered as lightly-styled plain text (inline code and bold498   get cheap regex styling; nothing structural).4992. A scanner advances only over newline-terminated input, detecting **settled top-level blocks**500   (boundary: blank line outside a code fence; a fence settles at its closing fence).5013. A settled block is rendered **once** through the full markdown renderer and flushed to502   scrollback (immutable). Only the live tail is ever re-scanned — never O(blocks²)503   re-tokenization, and settled text never reflows or flickers.5044. On `ModelFinished`, the remaining tail settles.505506Renderer scope (V1): headings (spacing + weight, no banner rules), bold/italic/strikethrough,507inline code (subtle background tint), fenced code blocks, ordered/unordered/nested lists, tables,508blockquotes, links (OSC 8 hyperlinks when supported; `text (url)` otherwise), horizontal rules.509510- **Code blocks**: syntax highlighting via a lightweight token highlighter (Hermes-style511  hand-rolled per-language rules with an LRU cache, or `highlight.js` grammars re-emitted as ANSI512  — spike decides by startup cost; Shiki/WASM is excluded from the hot path for cold-start513  reasons). Long lines wrap with a dim `↪` continuation marker; content is copy-friendly plain514  text in scrollback — **no** background-color fills that poison copied text, a thin dim gutter515  `│` marks the block instead.516- **Tables** render with box-drawing only when they fit the width; otherwise degrade to aligned517  plain columns.518- A block that would exceed the live cap mid-stream flushes early at the last safe line boundary519  (§1.2 caps).520521---522523## 9. Panels524525All panels are live-region overlays (§1.2): bounded, keyboard-driven, `Esc` closes, opening takes526one keypress or one slash command. None of them clears the conversation.527528### 9.1 Permission panel (CLAUDE.md §13 — verbatim contract)529530```531╭─ KHAELOR requests permission ─────────────────────╮532│  Run                                              │533│  npm install                                      │534│                                                   │535│  Working directory                                │536│  ~/dev/project                                    │537│                                                   │538│  [ Enter ] Allow once                             │539│  [ A ]     Always allow in this project           │540│  [ Esc ]   Deny                                   │541╰───────────────────────────────────────────────────╯542```543544- The interaction takes milliseconds: it appears already focused; three keys, no typing.545- `A` shows the **generalized pattern** it will persist (`always allow: npm install *`) derived546  from conservative shell-word parsing; commands containing shell operators get exact-command547  approval only (ADR-9). Grants persist to project config.548- For edits, the body shows the target path and a ≤ 8-line diff preview instead of a command.549- Denial is recorded and fed to the model as steering (ADR-9); the panel closes instantly either550  way. While the panel is open the agent status line reads `● Waiting for permission`.551552### 9.2 Model selector — `/model`553554```555 ┌─ model ────────────────────────────────────────────┐556   current   claude-sonnet-4-5        thinking adaptive557   ─────────────────────────────────────────────────558  ❯ claude-sonnet-4-5      default · fast559    claude-opus-4-5        deepest reasoning560    claude-haiku-4-5       cheapest · aux model561   ─────────────────────────────────────────────────562   t thinking: adaptive · o output budget: 16000563 └────────────────────────────────────────────────────┘564   ↑↓ select · Enter apply · t/o cycle · Esc close565```566567Entries come from configuration/aliases (no hard-coded permanent list — CLAUDE.md §6); `t` cycles568thinking mode, `o` cycles output budget. Applying updates the status bar immediately.569570### 9.3 `/config`571572Keyboard-navigable panel over the same config the file exposes (file remains editable directly):573574```575 ┌─ config ── ~/.khaelor/config.json · .khaelor/config.json ─┐576  ❯ Model            claude-sonnet-4-5577    Thinking         adaptive578    Max output       16000579    Permissions      12 rules →580    Theme            khaelor-dark581    API key          set via environment ✓582 └────────────────────────────────────────────────────────────┘583   ↑↓ navigate · Enter edit · p project scope · Esc close584```585586Secrets are never displayed (`set via environment ✓` / `stored in keychain ✓`). `p` toggles587whether an edit writes user (`~/.khaelor/config.json`) or project (`.khaelor/config.json`) scope,588with the target shown before writing.589590### 9.4 `/sessions` picker591592```593 ┌─ sessions · ~/dev/my-project ───────────────────────────┐594  ❯ retry logic in session store      12m ago   $0.42  main595    context compaction checkpoint      2h ago   $1.13  main596    initial TUI scaffolding           1d ago    $2.87  tui/shell597 └──────────────────────────────────────────────────────────┘598   ↑↓ select · Enter resume · n new · r rename · x delete · / filter · Esc599```600601Titles, ages, costs, branches from real session metadata (§8, CLAUDE.md). Resuming replays the602event log; the transcript reprints into scrollback as settled content.603604### 9.5 `/context` inspector605606```607 ┌─ context · 41,382 / 200,000 tokens · 21% ───────────────┐608   system prompt              3,120   ██609   project instructions       1,240   █610   conversation              24,988   ████████████611   tool observations          9,414   █████612   repository context         1,620   █613   checkpoint                   —     (no compaction yet)614   reserved output           16,000   ████████615   ─────────────────────────────────────────────────────616   files in context617    src/session/store.ts      read · turn 3618    src/context/engine.ts     edited · turn 5619 └──────────────────────────────────────────────────────────┘620   Enter file detail · c compact now · Esc close621```622623Every number from real usage/accounting (Absolute Rule #4). Bars are supplementary — numbers624carry the information (§12). After compaction, the checkpoint row expands to show the structured625YAML checkpoint on Enter.626627### 9.6 `/cost`628629```630 ┌─ cost · this session ───────────────────────────────────┐631   input tokens           182,455        $0.27632   output tokens           24,110        $0.18633   cache write             41,020        $0.08634   cache read             512,300        $0.08   (90% hit)635   ─────────────────────────────────────────────636   total                                 $0.61637 └──────────────────────────────────────────────────────────┘638```639640Exclusively from API usage metadata (ADR-10); cache health made visible (ADR-7). If pricing for641the configured model is unknown, token counts show and the dollar column reads `n/a` — never an642invented estimate.643644### 9.7 `/processes`645646```647 PROCESSES648 ● 3121  npm run dev   running   04:32649 ● 3198  pytest        running   00:18650 ○ 3012  npm test      exited    code 0651   ↑↓ select · Enter tail output · s stop · Esc close652```653654`●` running / `○` exited (shape carries state, color reinforces). `Enter` prints the selected655process's recent output as a settled block.656657---658659## 10. Streaming UX rules660661Binding rules for any renderer:6626631. **16 ms coalescing.** All deltas (text, thinking, tool output) batch into per-frame updates;664   one repaint per frame regardless of event rate (ADR-4).6652. **No scroll jumps.** Settled content is appended above the live region; the live region is666   repainted in place. The viewport never leaps. If the user scrolls up in native scrollback,667   nothing repositions them — new content accumulates below, exactly like `tail -f`.6683. **Stable input.** The composer and its caret are unconditionally repainted last and the cursor669   parks at the caret every frame. Typing latency is independent of stream rate (input events are670   processed before render batching; echo goes in the next frame, < 16 ms).6714. **No flicker.** Synchronized-output frames (§1.3); per-row damage tracking; never672   clear-screen-and-redraw.6735. **Thinking display** is compact: while the model emits permitted thinking, the status line674   reads `● Thinking · 4s`; a dim, cap-limited preview line may show beneath it. Thinking is never675   the centerpiece (CLAUDE.md §14).6766. **Interruption — `Esc`.** Acknowledgment is *immediate* (same frame): status line flips to677   `◌ Stopping…` before any teardown completes. Then cancellation propagates678   (AbortSignal tree: model stream → in-flight tools → loop, ADR-11), dangling `tool_use` closed679   with synthetic cancelled results, and the partial output settles into scrollback marked:680681```682 ▸ Run npm test · cancelled · 3.2s683 ◌ Interrupted — partial response kept684```685686   Background `process` entries are untouched (ADR-11). `Esc` with an overlay open closes the687   overlay instead; `Esc` when idle with text in the composer clears selection/completion first688   (§13 precedence).689690---691692## 11. Status bar693694One row, bottom of screen, dim by default — subtle, persistent, never animated:695696```697 main +4 −1  │  claude-sonnet-4-5  │  context 31%  │  $0.42  │  ● 2698```699700Segments (all real data; a segment with no data is absent, not zeroed):701702| Segment | Source | Example |703|---|---|---|704| git branch + dirty counts | repo watcher | `main +4 −1` |705| model | session config | `claude-sonnet-4-5` |706| context utilization | real usage vs usable window (ADR-6) | `context 31%` |707| session cost | API usage metadata | `$0.42` |708| background processes | process registry | `● 2` |709| queued steering (when any) | queue | `⋯ 1 queued` |710711**Width-responsive degradation** (Hermes' progressive disclosure, HERMES §8.2 — thresholds fixed712now, tuned in Phase 8). Segments have priorities; lower-priority segments drop whole, never713truncate mid-token:714715| Width | Shown |716|---|---|717| ≥ 100 | all segments |718| 80–99 | model shortens to alias (`sonnet`); processes drop if none running |719| 60–79 | cost drops → `main +4 −1 │ sonnet │ 31%` |720| < 60 | `main │ 31%` |721722Context ≥ 80% tints the percentage warning and appends `· /compact` as a nudge; ≥ 90% error tint.723Percentages, not bars, at this size.724725---726727## 12. Color and theming728729**One exceptional default theme** (`khaelor-dark`), `khaelor-light` variant, `/theme` deferred730(CLAUDE.md §19).731732Palette philosophy — a vibrant, saturated system (per explicit product direction): every surface733carries its own hue, hierarchy still comes from brightness and spacing, and monochrome loses zero734meaning because state is never color-alone.735736| Role | Use | Dark value (truecolor) |737|---|---|---|738| text | body | `#d8dee9` |739| dim | chrome, annotations, rules | `#6b7280` |740| accent | `❯`, selection, active state | `#7aa2f7` |741| violet | thinking state, model segment, gradient start | `#bb9af7` |742| cyan | reading state, branch segment, gradient end | `#7dcfff` |743| teal | searching state, context segment, process tools | `#73daca` |744| magenta | verifying state | `#ff79c6` |745| orange | cost segment, waiting state, numbers | `#ff9e64` |746| success | `✓`, passed, added `+`, running state | `#9ece6a` |747| warning | context pressure, shell mode `!`, editing state | `#e0af68` |748| error | failed, removed `−` | `#f7768e` |749750Color deployment:751752- **Brand gradient** (violet→cyan, per-character truecolor interpolation): the startup wordmark,753  H1 headings, and the composer-box border when the agent is idle. Gradients only ever run over754  plain text; ANSI-256/16 degrade to solid violet, monochrome to identity.755- **Agent states** each have a saturated color (status-line dot + label): thinking violet ·756  reading cyan · searching teal · editing amber · running green · verifying magenta · waiting757  orange. The word always accompanies the color.758- **Tool one-liners**: the `▸` glyph is colored by tool family (read cyan · search violet · edit759  amber · exec green · process teal).760- **Diffs**: `+`/`−` in success/error plus a subtle truecolor background tint per line; glyphs761  preserved for monochrome.762- **Status bar**: each segment has its own accent (branch cyan · model violet · context teal →763  warning → error under pressure · cost orange · queued amber); separators stay dim.764- **Panels** (permission, palettes): accent-dim borders instead of gray.765- **Syntax highlighting**: keywords violet · strings green · numbers orange · functions cyan ·766  types teal-cyan · comments dim — a real palette derived from the theme.767768Rules:769770- **Capability ladder**: truecolor → ANSI-256 (quantized palette) → ANSI-16 (role-mapped) →771  monochrome. Detected via `COLORTERM`/terminfo.772- **`NO_COLOR`** (and `TERM=dumb`) honored absolutely: full monochrome, everything still legible773  because **state is never color-alone** — every state pairs a symbol or word:774  `✓` done · `▸` tool · `●`/`○` running/stopped · `⋯` queued · `◌` interrupted · `+`/`−` diff ·775  `failed`/`passed` spelled out.776- **Light/dark**: background detected via OSC 11 query with a 100 ms timeout (fallback:777  `COLORFGBG`, else assume dark); the matching variant is selected automatically. Both variants778  pass a contrast check (≥ 4.5:1 for text roles) in tests.779- Syntax highlighting derives from the same palette, so code blocks belong to the theme instead780  of shouting over it. Vibrant ≠ noisy: saturation lives in small marks (glyphs, borders, labels,781  segments), never in body text.782783---784785## 13. Keyboard reference (complete, V1)786787Precedence: overlay bindings (modal) > single-key actions (only when composer empty) > composer788bindings > global. A pushed overlay suppresses lower layers automatically (OpenCode's mode stack,789OPENCODE §3.4) — no manual focus bookkeeping.790791### Global792793| Key | Action |794|---|---|795| `Esc` | Interrupt agent run · else close overlay · else clear completion/selection |796| `Ctrl+K` | Universal command palette |797| `Ctrl+T` | Cycle live tool detail (current turn) |798| `Ctrl+C` | Clear composer; twice within 1 s quits |799| `Ctrl+D` | Quit (empty composer only) |800| `Ctrl+L` | Repaint live region (recover from external corruption) |801| `Ctrl+Z` | Suspend (proper `renderer.suspend()` + restore on `SIGCONT`) |802803### Composer804805See §3.7 (character/word/line navigation, history, kill, undo, `$EDITOR`, `/`, `@`, `!`).806807### Single-key actions (composer empty; hinted inline where relevant)808809| Key | Action |810|---|---|811| `d` | Print diff of most recent edit |812| `a` | (after shell mode output) add output to context |813814### Overlays / pickers (uniform)815816| Key | Action |817|---|---|818| `↑`/`↓` (`Ctrl+P`/`Ctrl+N`) | Navigate |819| typing | Fuzzy filter |820| `Enter` | Select / apply |821| `Tab` | Complete without executing (slash palette) |822| `Esc` | Close |823824### Permission panel825826`Enter` allow once · `A` always allow (project) · `Esc` deny (§9.1).827828### `/diff` viewer (alternate screen)829830`↑↓`/`j k` scroll · `PgUp/PgDn`/`Ctrl+U/Ctrl+D` half-page · `]`/`[` next/prev file · `Tab` file831list · `u` unified/split toggle · `g`/`G` top/bottom · `q`/`Esc` close.832833All bindings are declared in the single command registry (§4.2) with name/title/category/key, so834the palette lists them and a future rebinding config gets them for free. Kitty keyboard protocol835and modifyOtherKeys are negotiated at startup for `Shift+Enter` and key-release fidelity;836capability absence degrades to documented fallbacks, never broken keys.837838---839840## 14. Performance budget and measurement plan841842Budgets are release gates, measured — never asserted (CLAUDE.md §18; Absolute Rule #4).843844| Metric | Budget | Method |845|---|---|---|846| Cold start → editable prompt | < 150 ms p95 | `hyperfine 'khaelor --benchmark-startup'` (flag prints ready-timestamp and exits); CI-tracked |847| Input echo latency | < 16 ms p95 | instrumented: keypress-read timestamp → frame-flush timestamp, recorded in-process under `--debug`; synthetic typing at 30 cps during a full-rate stream replay |848| Live-region repaint | < 8 ms p95 (frame budget headroom) | per-frame timing histogram in debug log |849| Stream throughput | no dropped frames at ≥ 2,000 tokens/s replay | recorded-event replay harness (§15.2) |850| Memory, long session | < 150 MB RSS after 4 h / 500-message synthetic session; **flat slope** after settling | replay harness + periodic `process.memoryUsage()` samples |851| Session resume (1,000-event log) | < 500 ms to interactive | benchmark on fixture logs |852| Repository search (`@` mention, 50K-file repo) | first results < 100 ms | fixture repo benchmark |853854Instrumentation ships in the product behind `--debug` (histograms to `~/.khaelor/logs/perf.jsonl`)855so dogfooding sessions produce real latency data continuously. When a budget is missed: attribute856the layer first (model / network / fs / index / render / architecture), then optimize that layer —857never guess (§18 speed rule).858859---860861## 15. Phase 2 framework spike862863### 15.1 The `RendererAdapter` seam864865The spike builds **one** UI core twice behind a deliberately thin adapter; everything above it866(event bus consumption, markdown scanner, layout logic, key decoding policy) is shared:867868```ts869interface RendererAdapter {870  mount(opts: { stdin: NodeJS.ReadStream; stdout: NodeJS.WriteStream }): void;871  /** Append an immutable, pre-rendered block to terminal scrollback. Never repainted. */872  printSettled(block: RenderedBlock): void;873  /** Repaint the bounded live region (streaming tail, status line, composer, status bar, overlay). */874  updateLive(state: LiveRegionState): void;875  onKey(handler: (key: KeyEvent) => void): void;876  onResize(handler: (size: { rows: number; cols: number }) => void): void;877  metrics(): RendererMetrics;   // frame timings, bytes written, dropped frames878  unmount(): void;879}880```881882- **Candidate A — Ink 7**: `printSettled``<Static>` items; `updateLive` → the live component883  tree (status line, composer `<TextInput>`-equivalent, status bar); measure whether Ink's884  reconciler + Yoga stays inside budget for a ≤ 24-row tree and whether `<Static>` output remains885  byte-clean for selection/copy.886- **Candidate B — custom ANSI**: `printSettled` → direct write above the live region;887  `updateLive` → damage-tracked row repaint inside DEC 2026 frames; the line editor is the main888  build cost — the spike implements the §3.7 navigation set only (spans/paste/`$EDITOR` deferred889  to Phase 2 proper).890891Spike scope cap: ~3 days per candidate. Shared harness first, then A, then B.892893### 15.2 The scenario (identical for both, replayed from a recorded event fixture)8948951. Stream a 10K-token markdown response (headings, tables, 3 fenced code blocks) at recorded-real896   and 4×-accelerated rates, through the settled-block scanner.8972. 30 tool rows: start → live timer → settle; two overlapping with the text stream.8983. Synthetic typing at 30 cps into the composer *during* the full-rate stream (echo latency899   measured per §14).9004. Open/close the palette during streaming; show one permission panel.9015. Resize 120→60→200 columns mid-stream.9026. Long-session soak: 500 messages replayed, memory sampled.9037. Manual flicker/selection pass on: Terminal.app, iTerm2, kitty, Alacritty, VS Code terminal, and904   inside tmux. Flicker check: eyeball + `asciinema` recording scrubbed frame-by-frame; selection905   check: select and copy a settled code block mid-stream, paste, diff against source.906907### 15.3 Decision criteria (pass/fail; all must pass to be eligible)908909| # | Criterion | Test |910|---|---|---|911| 1 | Flicker-free streaming | no visible tearing in scrubbed recordings on all 6 terminals |912| 2 | Stable input line | caret never moves or blinks away during scenario 3 |913| 3 | Input latency | < 16 ms p95 during full-rate stream (instrumented) |914| 4 | Long-session memory | flat RSS slope after soak; < 150 MB |915| 5 | Terminal-native selection/copy | copied settled block byte-identical (modulo trailing WS) |916| 6 | Node-only | `npm i -g` on clean Node 22, no Bun, no native build step |917| 7 | Resize integrity | no corrupted rows after scenario 5 |918919Tie-breakers if both pass: (1) measured margins on criteria 3–4, (2) lines of owned code the team920must maintain, (3) estimated distance to Phase 2 composer completion, (4) startup cost added.921922### 15.4 Outcome handling923924- Both pass → recommendation (§0.3) applies its tie-breakers; expected outcome is **B**, accepted925  outcome may be **A**.926- Only one passes → it wins, recorded as an ADR-14 addendum with the measurement tables.927- Neither passes → the shared harness *is* the beginning of candidate B done more carefully; fix928  the failing criterion in B (it is the only candidate whose paint path we fully control).929- The losing candidate's adapter is deleted, not kept "just in case". The `RendererAdapter` seam930  remains — it is also the seam the golden/snapshot tests (§21 of CLAUDE.md) render through.931932---933934## 16. Summary of positions taken935936| Question | Position |937|---|---|938| Conversation surface | Main-buffer terminal-native scrollback; settled content printed once, immutable |939| Repaint surface | Bounded live region (≤ ~24 rows), DEC 2026 synchronized frames, 16 ms coalescing |940| Alternate screen | Only for the `/diff` full viewer (pager semantics) |941| Framework | Spike between Ink 7 (Static + bounded live tree) and custom ANSI renderer; **custom renderer recommended** |942| Markdown streaming | Hermes settled-block scanning: raw tail live, blocks rendered once when settled |943| Tool calls | Collapsed one-liners; live-turn toggle `Ctrl+T`; post-hoc expansion prints blocks (`d`, `/tool n`) |944| Overlays | In-live-region bounded panels; one generic filter-list powers all pickers; one command registry powers keys + palette + slash |945| Status | One agent status line (real data only) + one width-degrading status bar |946| Honesty | Every number from real events/usage; segments absent rather than faked; no spinner theater |947| Accessibility | Symbols + words always accompany color; NO_COLOR fully supported; keyboard-complete |948949---950951*Author: Simon-Pierre Boucher · contact@spboucher.ai*952