summaryrefslogtreecommitdiff
path: root/docs/tui-refactor-plan.md
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-09 10:13:57 -0600
committert <t@tjp.lol>2026-06-09 10:14:06 -0600
commit1cef19b12d59147ec32e4c0cd6a4828384b16417 (patch)
tree2d83fb1e1ed1527b8c7298a70a13c748f8178351 /docs/tui-refactor-plan.md
parent3d535f8360f35132faaeef093d61cf377be5e9cc (diff)
archive the tui refactor doc
Diffstat (limited to 'docs/tui-refactor-plan.md')
-rw-r--r--docs/tui-refactor-plan.md527
1 files changed, 0 insertions, 527 deletions
diff --git a/docs/tui-refactor-plan.md b/docs/tui-refactor-plan.md
deleted file mode 100644
index 9f18ff0..0000000
--- a/docs/tui-refactor-plan.md
+++ /dev/null
@@ -1,527 +0,0 @@
-# `panto` CLI/TUI Refactor Plan
-
-Status: **draft, in review**. A near-full rewrite of panto's terminal
-interface, heavily influenced by [pi](https://github.com/earendil-works/pi-mono)'s
-`pi-tui`. This document is the plan of record; iterate here before writing code.
-
-## 1. Goal & guiding principle
-
-Replace today's print-based `CLIRenderer` (`src/main.zig`) with a real terminal
-UI that *feels native* while supporting rich, dynamic, extension-controlled
-components.
-
-The defining principle, borrowed from pi: **a scrollback-preserving differential
-line renderer, not a full-screen TUI.**
-
-- We never switch to the terminal's alternate screen buffer. We render into the
- normal buffer. Consequence: the user's shell scrollback before launching
- `panto` survives, and scrolling up in tmux/Ghostty is just the terminal's own
- native scroll — not something panto implements.
-- Each frame, components produce lines of text; the engine diffs against the
- previously rendered lines and rewrites only what changed, moving the cursor up
- with ANSI control codes and reprinting the minimal region. A spinner reprints
- one line; collapsing all tool calls reprints the visible region.
-- Content that grows past the top of the viewport scrolls up into real native
- scrollback. The engine only ever manipulates the visible terminal height.
-
-This is what produces the blend the design is chasing: native, print-style feel
-with full raw-mode dynamism layered on top.
-
-### Confidence note
-
-The mechanics below were derived by reading pi's compiled `pi-tui`
-(`tui.js`, `terminal.js`) directly — high confidence on *what pi does*. Claims
-about terminal/PTY behavior and performance ("uncapped redraw can be slower",
-"IMEs anchor to the hardware cursor") are reasoning + pi's own code comments,
-not independently benchmarked; flagged inline as **[verify]** where it matters.
-
-## 2. Architecture overview
-
-```
- ┌───────────────────────────────────────────────┐
- │ Streaming agent events (libpanto pull Stream) │
- └─────────────────────────┬─────────────────────┘
- │ deltas mutate component state
- ▼
- ┌───────────────┐ render(width) ┌──────────────────┐ ANSI bytes ┌───────────┐
- │ Components │ ───────────────► │ TUI engine │ ───────────► │ Terminal │
- │ (built-in & │ ◄─────────────── │ (diff, viewport, │ │ (raw mode │
- │ extension) │ firstLineChanged │ cursor, write) │ ◄─ raw input │ stdin) │
- └───────────────┘ └──────────────────┘ keys/paste └───────────┘
- ▲
- │ input routing, focus, overlays
- ┌─────┴───────┐
- │ Input layer │ Kitty kbd protocol +
- │ │ modifyOtherKeys fallback
- └─────────────┘
-```
-
-Layers, bottom-up:
-
-1. **Terminal** — raw mode, stdin decoding, ANSI output, capability negotiation.
-2. **Input layer** — Kitty keyboard protocol negotiation with `modifyOtherKeys`
- fallback, bracketed paste, `Key` decoding, keybinding lookup.
-3. **TUI engine** — owns the component list, runs the differential render,
- tracks viewport/scrollback, composites overlays, positions the hardware
- cursor for IME, routes input to the focused component.
-4. **Components** — built-in and extension-supplied. Produce lines from
- structured state; receive input when focused.
-5. **App / chat loop** — wires libpanto's streaming events to component state
- and `requestRender`.
-
-## 3. Rendering model (the core)
-
-### 3.1 Output discipline
-
-- **No alternate screen.** Never emit `?1049h`/`?47h`.
-- **Synchronized output.** Wrap every frame in `\x1b[?2026h` … `\x1b[?2026l`
- so the terminal composites the whole frame atomically (no tearing/flicker).
-- **Full redraw only when forced:** first paint, terminal width change (wrapping
- changes), terminal height change (viewport realignment). A full redraw clears
- scrollback (`\x1b[2J\x1b[H\x1b[3J`); ordinary frames never do.
-- **Width contract.** Every line a component returns must have visible width
- ≤ render width. The engine validates and treats overflow as a hard error
- (caught, terminal restored, diagnostic written) — components must truncate.
-
-### 3.2 Viewport & scrollback tracking
-
-The engine tracks, across renders:
-
-- `previous_lines` — the last rendered line array (also the diff baseline and
- the cache reused for clean-below-cut reprinting; see §3.3).
-- per-component **line offset and line count** (so a length change in one
- component shifts everything below it, and offsets below are recomputed).
-- `viewport_top` / `max_lines_rendered` — the working area. Lines above
- `viewport_top` have scrolled into native scrollback and are off-limits to
- differential updates. If a change lands above `viewport_top`, the engine must
- fall back to a full redraw.
-
-### 3.3 The dirty model (`firstLineChanged`) — primary fast path
-
-This is the component→engine contract that makes streaming cheap. It exists at
-**two layers** that optimize two different costs:
-
-- **Producing lines** (component CPU: markdown, highlight, wrap) — the expensive
- one during streaming.
-- **Writing lines** (engine: diff + cursor + write) — cheap regardless.
-
-**Component-side signal:** each component exposes
-
-```
-firstLineChanged: ?usize // lowest line index (component-local) whose
- // rendered output differs from the last render,
- // or null if nothing changed.
-```
-
-Defined as *lowest differing output line*, **not** "where text was appended."
-A markdown delta that restyles earlier lines (e.g. a closing code fence) makes
-those lines differ, so an honest implementation already reports the earlier
-index — no special reflow case is needed.
-
-**Engine pass (top-to-bottom, single walk):**
-
-1. Accumulate each component's global offset from its (possibly cached) line
- count.
-2. Compute `cut = min(offset_i + firstLineChanged_i)` across **all** components
- (not just the first non-null — a footer/spinner ticking mid-stream is the
- common counter-example to "stop at first").
-3. Reprint from `cut` downward:
- - components **above** the cut: untouched.
- - the component **owning** the cut and any dirty component below it:
- re-render from its local `firstLineChanged`.
- - clean components **below** the cut (`firstLineChanged == null`): reuse their
- **cached** lines verbatim — reprinted because they're below the rolled-back
- point, but **not re-rendered** (no component CPU). The engine already holds
- these lines for the diff baseline, so no new state.
-4. Handle length changes: if a component now returns fewer lines (e.g. ctrl+o
- collapse), clear orphaned trailing lines and recompute offsets below.
-5. **Line-diff backstop.** From `cut` downward, still run the old-vs-new line
- diff. `firstLineChanged` decides *where re-rendering starts*; the diff is the
- *correctness floor* (handles length deltas, defends against an inaccurate
- signal). The signal is the fast-path input; the diff is the guarantee.
-
-**Lifecycle (no desync):** `firstLineChanged` is *derived from the component's
-render cache*, mirroring pi's `invalidate()` model — state mutation invalidates
-the cache (dirties), a successful render re-populates it (cleans). It is not a
-separately hand-managed field that can drift.
-
-For streaming specifically, the cut is almost always deep in the last
-component, so per-frame work is: re-render one short tail + memcpy cached lines
-below + write from the cut. This is the "outperform the print CLI" path.
-
-### 3.4 Coalescing / frame rate
-
-- `requestRender()` schedules a frame; multiple requests within a window
- coalesce into one render.
-- **Target ~120fps (≈8ms) adaptive:** render immediately when idle; coalesce
- only under burst. Coalescing is a *ceiling* to avoid redrawing faster than the
- terminal drains (which queues deltas and *grows* latency) — **[verify]** with
- a P1 benchmark rather than removing the cap outright.
-
-### 3.5 Virtual cursor + hardware-cursor sync (IME) — kitchen sink
-
-The cursor is drawn by the component as part of its rendered output (a styled
-reverse-video block), not the terminal's hardware cursor. This keeps the cursor
-correct under differential rendering, lets it live inside wrapped text/overlays,
-and lets us style it.
-
-Because IMEs (CJK input, accent/emoji composition) anchor their candidate popup
-to the *hardware* cursor **[verify]**, we keep the hidden hardware cursor in sync
-with the virtual one:
-
-- A focused component emits an invisible marker (APC escape, zero-width) at the
- virtual cursor's location.
-- After rendering, the engine scans the visible region for the marker, strips
- it, computes its row/col, and moves the (normally hidden) hardware cursor
- there.
-- `Focusable` is the interface a component implements to participate.
-
-Full scope from day one: composite the virtual cursor, keep hardware cursor
-synced. (If schedule slips, the safe deferral is to leave the hardware cursor at
-end-of-content and add marker-positioning later — but the interface ships now.)
-
-## 4. Core interfaces
-
-Idiomatic Zig interfaces (vtable-over-`*anyopaque` or tagged dispatch — TBD in
-§4.5). Designed to bridge cleanly to Lua now, and to a C ABI *later, elsewhere*
-(out of scope here, but the shapes stay coherent: lines-out as arrays, structured
-data in).
-
-### 4.1 `Component`
-
-```
-Component = struct {
- // Produce lines for the given width. Each line's visible width <= width.
- render: fn (self, width: usize, alloc) []const []const u8,
-
- // Lowest component-local line index whose output changed since last render,
- // or null. Derived from the render cache. See §3.3.
- firstLineChanged: fn (self) ?usize,
-
- // Drop cached render state (e.g. on theme change). Re-dirties.
- invalidate: fn (self) void,
-
- // Optional: receive input when focused.
- handleInput: ?fn (self, data: []const u8) void,
-
- // Optional: opt into key-release events (Kitty protocol).
- wantsKeyRelease: bool = false,
-};
-```
-
-### 4.2 `Focusable` (virtual cursor / IME)
-
-```
-Focusable = struct {
- focused: bool, // set by engine on focus change
- // The component emits CURSOR_MARKER at the cursor position in render()
- // output when focused; the engine handles the rest (§3.5).
-};
-```
-
-Container components that embed an input must propagate `focused` to the child.
-
-### 4.3 Input types
-
-```
-Key = struct {
- code: KeyCode, // enum: char, enter, escape, tab, arrows, fn-keys…
- mods: Mods, // packed struct { ctrl, alt, shift, super, hyper… }
- event: KeyEvent, // press | repeat | release
- text: ?[]const u8, // resolved text for printable keys
-};
-```
-
-- Rich model from day one (modifiers + press/repeat/release), even though legacy
- fallback can only populate a subset.
-- `Keybinding` storage: a lookup from `Key` pattern → action, owned by the app,
- passed to components that need app-level bindings.
-
-### 4.4 Theme
-
-A **fresh, dedicated theme module** collecting all colors in one place (today's
-CLI just emits raw ANSI dim/cyan inline). Deep configurable theming is out of
-scope for this project — the goal is centralization, not a theming system. The
-module exposes a theme object of named foreground/background style functions
-(mirrors pi's `fg(name, text)` / `bg(name, text)`), passed to components at
-render. Markdown theming hooks reserved for the deferred markdown work (§8).
-
-### 4.5 Dispatch mechanism — decided: vtable
-
-**Vtable-of-fn-pointers over `*anyopaque`.** A tagged union is rejected: it
-requires knowing all component types up front, but extensions must be able to
-**define their own** components, not just replace built-ins (§7). The vtable is
-also the natural shape for the Lua bridge (and a future C ABI). Built-in and
-extension components are indistinguishable to the engine.
-
-## 5. Input layer
-
-- **Raw mode** via termios; restore on exit (and on crash — install handlers).
-- **Kitty keyboard protocol** negotiation at startup: request desired flags
- (disambiguate, report-events, report-alternates), query, use if supported.
- This is what enables unambiguous modifier chords (`Ctrl+I` ≠ `Tab`, lone
- `Esc`, key releases).
-- **`modifyOtherKeys` fallback** (`\x1b[>4;2m`) for terminals without Kitty.
-- **Bracketed paste** (`\x1b[?2004h`): wrap pastes so the editor treats them as
- literal text, not a stream of keypresses.
-- **Stdin buffering/splitting:** batched input must be split into individual
- key sequences so `matchesKey`/release detection work (pi's `StdinBuffer`).
-- Disable/restore all of the above cleanly on stop and on suspend/resume.
-
-Platform note: panto targets Unix first (matches current build). Windows VT
-input is explicitly out of scope for this refactor.
-
-## 6. Built-in components
-
-All are **public** and **replaceable via events** (§7), and their render-to-lines
-logic is itself public so a handler can construct the default and wrap/filter its
-output. Each takes **structured data in** and produces **lines out**.
-
-> **Invariant — no "active component."** The engine holds a *list* of live
-> components; there is never a notion of "the current component." Multiple tool
-> calls (and future subagents) render in parallel, each bound to its own
-> instance/call-id. No code may stash `active_component` or reach for "the"
-> component of a kind — every event and delta carries its own instance identity.
-> Violating this corrupts parallel tool/subagent rendering.
-
-| Component | State in | Notes |
-|---|---|---|
-| **welcome** | version, cwd, model info | shown at session start |
-| **thinking text** | accumulating thinking deltas | dimmed; streams |
-| **assistant text** | accumulating assistant deltas | streams; markdown **deferred** (§8) but hosts it |
-| **user text** | submitted user message | |
-| **tool use** | tool name, args, status, result | **one component owns call + result**; collapsible (ctrl+o, **global toggle**) — collapse is a length change (§3.3). Render progression: `tool (?)…` at start → `tool (<name>) <input json>` once args finish streaming → `tool (<name>) <input json>\n\n<output>` once the result lands. Args JSON is rendered **verbatim** (no pretty-print), terminal-wrapped. **Collapsed (the default)** shows only the **last 5 output lines**; expanded shows the full output |
-| **compaction summary** | compaction event data | shown when context is compacted |
-| **input box** | editor buffer, cursor | `Focusable`; single-row by default, **shift+enter inserts a newline** (enter submits), grows one row per line. **P1:** unbounded growth (no cap). **P2:** configurable cap (default 8) then scrolls within that window showing the last 8 contiguous lines; plus standard editing bindings (word-nav via alt+←/→, **ctrl+u** delete-to-line-start, etc.) and **ctrl+g** to punt the buffer to `$EDITOR` as a markdown tempfile. **No kill-ring / undo** (dropped). Raw-key editing |
-| **footer** | model, context-window token count | persistent bottom line(s); shows the **latest context-window size in tokens** = `usage.input + usage.cache_read + usage.cache_write` from the most recent `message_complete`. **No git branch** (panto uses jj, not git; dropped). During P1 also renders a **frame-timing element** (last frame's render time shown inverted as a theoretical-max "fps"), used to validate perf then removed |
-
-The chat transcript is a `Container` of these in order; the input box and footer
-are pinned below it.
-
-## 7. Extension UI surface — one event mechanism
-
-There is **one** mechanism for all extension UI: string-keyed events with
-handlers that can set/replace the component for that event. It subsumes slot
-replacement, custom tool rendering, and imperative component spawning.
-
-### 7.1 Events are open strings, not an enum
-
-Event types are **strings**. Extensions register handlers for, and **fire**,
-their own event types. Built-in events are simply event strings panto emits
-itself; extension events are mechanically identical.
-
-```
-panto.on("tool", handler) // subscribe (built-in or custom event)
-panto.emit("my-event", data) // fire — from a custom tool, a keybinding,
- // or inside another event's handler
-```
-
-This is also the **only** way a component gets on screen: pick an event string,
-register a handler that sets your component, then emit the event. There is no
-separate `addComponent` API — component additions are *always* tied to an event
-firing. (Imperative "spawn a status panel on a timer" = define an event type and
-emit it.)
-
-### 7.2 The event object's UI API
-
-Handlers receive an `event` carrying structured data plus:
-
-```
-event.getComponent() -> ?Component // the component currently chosen for this
- // event: the built-in default at first,
- // or whatever a prior handler set.
-event.setComponent(c: Component) // set/replace it.
-// + structured data, e.g. event.tool_name, event.args, ...
-```
-
-`getComponent()` is named to make clear it returns *whatever is current*, not a
-frozen "default" — after the first handler runs it is no longer the default.
-
-**Wrapping is the documented, expected pattern:**
-
-```
-panto.on("tool", (e) => {
- if (e.tool_name !== "skill") return;
- const inner = e.getComponent(); // the default tool-use component
- e.setComponent(wrapSkill(inner)); // decorate / replace
-});
-```
-
-### 7.3 Handler ordering & precedence
-
-Handlers for an event run in **registration order**. Precedence is
-**last-wins-blind**: the final `setComponent` is used. This is acceptable
-*because* the chain-friendly path (`getComponent` → wrap → `setComponent`) is the
-documented norm — a handler that clobbers without reading the current component
-is at fault, not the framework. No magic merge.
-
-### 7.4 Timing & lifecycle
-
-- **Streaming LIFECYCLE events (as built).** Rather than a single fire-once
- boundary, each streaming block emits events across its whole lifecycle, so a
- handler can act (and re-choose the component) at any meaningful point:
- - thinking: `thinking` (start) → `thinking_delta` (per chunk) →
- `thinking_complete`.
- - assistant text: `assistant_text` → `assistant_text_delta` →
- `assistant_text_complete`.
- - tool: `tool` (block-start; name **unknown**, component shows `tool (?)`) →
- `tool_details` (name resolved) → `tool_delta` (args chunk) →
- `tool_call_complete` (full args) → `tool_result` (atomic result block).
- - `user_message` / `session_start` / `compaction` fire once.
-- **Mid-stream swap.** `setComponent` may be called at **any** lifecycle event,
- not only creation. Swapping replaces the slot's component at the **same key**;
- the incoming component fully takes over the region (renders from line 0, the
- predecessor's orphaned lines cleared). The superseded component is released by
- its owner. This is what makes `tool` honest: it fires at block-start as
- `tool (?)`, and a handler **claims the call by name at `tool_details`** by
- swapping in its own component over the default that was already rendering.
-- **panto keeps driving.** Across any swap, panto continues feeding the
- structured deltas/details/result into the slot's typed default box; the engine
- renders whatever component currently owns the slot (default, wrapper, or
- swapped-in). Drive-by-default-box, render-by-current-component.
-- **No "active component" (§6 invariant).** Each `tool`/subagent block yields its
- own component instance keyed by call-id/block-index. Parallel calls each get
- their own; a swap replaces the component *at* a key, never a global current.
-
-### 7.5 The skills example (worked)
-
-A skills extension:
-1. Registers the `skill` agent tool (existing tool machinery).
-2. Registers a `skill` component type.
-3. `panto.on("tool", ...)` and, when `event.tool_name === "skill"`, calls
- `event.setComponent(skillComponent)` (optionally wrapping `getComponent()`).
-
-panto needs no concept of "skills." It emits `tool` at tool-use start; the
-handler claims the call by name and swaps the component; panto drives that
-instance with the tool's deltas. A subagents extension works identically.
-
-### 7.6 Zig → Lua bridge
-
-Implement the event system + component vtable in Zig (matching `libpanto`'s
-"shape the internal type to *be* the public API, then allowlist in
-`public.zig`" convention), then bridge to Lua via the existing
-`lua_bridge`/`lua_runtime` machinery. A Lua handler receives a bridged `event`
-object; a Lua-defined component implements the same
-`render`/`firstLineChanged`/`handleInput` vtable contract across the bridge.
-Native `.so`/C-ABI loading is **explicitly out of scope** for this project, but
-the vtable stays C-ABI-friendly so it can be added elsewhere later.
-
-## 8. Streaming integration
-
-- **Stateful component + `requestRender`** (no per-delta "append" method on the
- interface). A delta arrives → append to the owning component's internal buffer
- → mark dirty → `requestRender()`. The dirty model (§3.3) makes the resulting
- frame minimal.
-- The app loop consumes libpanto's pull `Stream` (as `CLIRenderer.driveTurn`
- does today) but, instead of writing to stdout, routes each event to component
- state:
- - thinking delta → thinking-text component
- - text delta → assistant-text component
- - tool-use start/args/result → tool-use component (one **per call**, keyed by
- call-id; never "the" tool component — see §6 invariant)
- - compaction/retry events → compaction component / status
-- Each component-creation boundary emits the corresponding built-in event (§7)
- *before* first paint, so handlers can replace/wrap before anything renders.
-- **Append fast path** is handled entirely by `firstLineChanged` + cached
- lines (§3.3): the streaming component re-renders only its dirty tail; the
- engine reprints from the cut. No separate API surface.
-- **Markdown caveat (deferred):** when markdown lands, the assistant-text
- component must cache finished blocks and only re-render the last open block, so
- `firstLineChanged` stays near the tail instead of jumping to line 0 each token.
- The component interface already accommodates this; only the markdown renderer
- is deferred.
-
-## 9. Phasing
-
-**P1 — Engine + input + chat skeleton (the core bet) ✅ COMPLETE.**
-- Terminal raw mode, ANSI output, synchronized output, capability detect.
-- Differential renderer: viewport/scrollback tracking, `firstLineChanged` dirty
- model + cached-line reuse + line-diff backstop, full-redraw triggers.
-- Input layer: Kitty negotiation + `modifyOtherKeys` fallback + bracketed paste
- + `Key` decoding.
-- Minimal components: assistant text, user text, input box (single-row +
- shift+enter growth, **unbounded in P1** — no cap yet), footer.
-- Footer renders the frame-timing element (theoretical-max "fps" from the last
- frame's render time) for perf validation; expected to sit well north of 120
- always, after which the element is removed.
-- Wire libpanto streaming into component state.
-- **Benchmark streaming feel** vs. current print CLI; validate the 120fps /
- coalescing target and the append fast path. **[verify]** the perf assumptions.
- Methodology is left to the implementation agent's discretion — assumptions are
- acceptable for now; we expect to refine once the streaming TUI is exercised in
- practice.
-
-**P2 — Full built-in set + editor. ✅ COMPLETE.**
-- Remaining built-ins: welcome, thinking, tool-use (collapsible), compaction.
-- ~~Overlay system~~ **TABLED** (not deferred — likely never picked up). The
- design prefers the native terminal-scrollback style over composited modals,
- which is also far simpler. Removed from scope.
-- Full editor: configurable line cap (default 8) with scroll-window past it
- (show last 8 contiguous lines), word-nav (alt+←/→), standard editing
- bindings (ctrl+u delete-to-line-start, etc.), and **ctrl+g** to edit the
- buffer in `$EDITOR` (markdown tempfile). **No kill-ring / undo** (dropped —
- keep the editor simple).
-- Footer with **context-window token count** (`usage.input + cache_read +
- cache_write`, latest from `message_complete`). **No git branch** (panto uses
- jj, not git).
-
-**P3 — Extension components + cursor/IME polish. ✅ COMPLETE.**
-- Event system (`on`/`emit`, `event.get/setComponent`); a full **streaming
- lifecycle** taxonomy wired at the real libpanto boundaries (start / `_delta` /
- `_complete` for thinking & assistant; `tool` / `tool_details` / `tool_delta` /
- `tool_call_complete` / `tool_result` for tools; `user_message` /
- `session_start` / `compaction` once — see §7.4); public default components.
- **Mid-stream component swaps** are supported: a handler may `setComponent` at
- any lifecycle event, replacing the slot's component (full-region takeover,
- superseded component released) while panto keeps driving deltas into the
- default box. `tool` fires at block-start as `tool (?)`; handlers claim by name
- at `tool_details` (§7.5).
-- Zig event + component API → Lua bridge. Extension-authoring APIs moved under
- `panto.ext` (`register_tool`/`register_command`/`on`/`emit`); the bare
- `panto` namespace is reserved for the future full libpanto API. Lua-defined
- components bridge the `Component` vtable via **synchronous** `lua_pcallk`
- render (not the libuv scheduler) with a native cache-derived
- `firstLineChanged`.
-- Virtual cursor compositing + hardware-cursor sync (IME marker positioning).
- Relative-move positioning inside the sync block; `differential` reconciles
- from the actual left-on row so repaints stay correct. **IME anchoring is
- implemented but unverified against a real IME** (the plan's [verify] flag
- stands).
-
-**Deferred (post-refactor):**
-- Markdown + syntax highlighting renderer (hosting components designed for it
- now; see §8 caveat).
-- Image rendering (Kitty/iTerm protocols).
-- Windows / native `.so` C-ABI extension loading.
-
-## 10. Open questions / decisions still pending
-
-All P1-blocking questions resolved. Remaining item (5) is non-blocking and left
-for the implementation agent to take a first swing at.
-
-5. **Built-in event taxonomy**: confirm the exact set/names of events panto
- emits and their precise creation-boundary timing (draft in §8: `session_start`,
- `user_message`, `thinking`, `assistant_text`, `tool`, `compaction`). The
- implementation agent should take a first swing; we expect to correct it later
- as the TUI streaming is put through its paces.
-
-*Resolved:*
-- Dispatch mechanism = vtable (§4.5); extension UI = one string-event
- mechanism (§7); no "active component" (§6).
-- **(1) Editor depth**: single-row default; **shift+enter** inserts a newline
- (enter submits); grows one row per line. **P1** ships height-1 + shift+enter
- with *unbounded* growth (no cap — the cap and its scroll-window are one
- feature, deferred together). **P2** adds the configurable cap (default 8) and
- scroll-window showing the last 8 contiguous lines (§6 input box).
-- **(2) Theme source**: fresh dedicated theme module centralizing all colors;
- no configurable theming system this project (§4.4).
-- **(3) App loop location**: new module; `main.zig` shrinks to wiring (§2/§9).
-- **(4) Benchmark methodology**: left to the implementation agent's discretion;
- assumptions acceptable for now. Footer surfaces a frame-timing/"fps" element
- during P1 to validate, then it's removed (§6 footer, §9 P1).
-
-## 11. Non-goals
-
-- Full-screen TUI / alternate screen.
-- Implementing our own scrollback or scroll handling (native terminal does it).
-- Windows support, image rendering, markdown — all explicitly later/deferred.