# Rendering design and research

## Goal and boundary

The goal is real LaTeX typography in Pi's transcript, with completed formulas appearing while the surrounding answer streams. **GitHub release 0.0.7, dated 2026-10-19**, incorporates the earlier local candidates' separate read-only draft dock and compact image-path labels. It projects completed Markdown/LaTeX blocks **inside the native input field**, revealing a whole block's source on caret entry, and adds deterministic local typing assistance. This is block-oriented live preview, not arbitrary rendered-formula-glyph-to-TeX caret mapping or a full WYSIWYG document editor. The separate dock remains opt-in. Visuals use the full Mermaid 12.2.0 engine, fitted raster images and an isolated zoom/pan overlay; see [the user guide](../README.md#diagrams-images-and-zoom) for controls and terminal requirements.

This is the first GitHub 0.0.7 release after published 0.0.6. Earlier local snapshots also used package/runtime identifier **0.0.7**; their dated changelog notes remain local-candidate history, not another GitHub release. Both upgrade paths require the release snapshot in a fresh `--dir` outside Git and explicit registration/activation; never overwrite an active copy. See the [upgrade guide](../README.md#upgrading-from-006-or-a-local-007-snapshot). Nothing upgrades automatically, and that 0.0.7 release changed neither an existing local installation nor npm distribution. Version 0.0.9 adds public npm packaging without changing this rendering design; see [npm installation and migration](../README.md#npm-installation-and-upgrades).

Ordinary terminal characters cannot reproduce arbitrary fraction rules, integral bounds, font metrics or radical geometry. Terminal settings cannot make a terminal implement a missing graphics protocol. Prose remains the terminal's font; browser-quality whole-document typography is a separate explicit preview.

All implementation lives in Rich Markdown. Pi 1.3.0/1.4.0 files, saved conversations, original message objects and lexer tokens remain unchanged. Display projection never writes editor text or substitutes rendered rows for source submission. Only explicit local typing assistance edits the draft, through native undo transactions; Pi's paste expansion and submission rules remain in charge. A stable, opt-out `before_agent_start` guideline asks for LaTeX delimiters. It is a formatting request, not a parser that guesses mathematics from Unicode text; rich input neither changes that guideline nor adds prompt content. JSON/RPC/print get no draft views, typing assistance, rendering hooks or guideline.

## Pipeline

The transcript pipeline remains:

1. Reuse Pi's parser, theme, transformers, escaping and layout. A version- and shape-guarded adapter wraps Markdown render/token/inline methods and existing background surfaces. Untransformed Markdown (including real Translator/custom cards) gets a default display context; Pi's transformer overrides it for thinking, streaming and user colors.
2. Admit only complete math tokens or closed `math` fences. Reserve one of 64 component-pass request slots **before** looking for a raster, so failed/pending formulas cannot consume an unlimited sequence of requests on repaint. Thinking, literal code, `renderLatex: false` and table-inline restrictions are preserved.
3. Submit bounded, snapshotted requests to a single lazy worker. Source, expansion, deadline and memory budgets remain active. A fresh TeX input object isolates macros/counters. Allowed local packages exclude autoload, require, external images and HTML. Unsupported glyphs, tagged/container-dependent layouts and potentially incomplete output retain text/source rather than silently losing notation.
4. Cache uncolored SVG geometry by a source/mode hash independently of width, foreground and cell size. Inline and display requests use their correct MathJax mode. Rasterization fits that geometry to an exact transparent cell canvas, normally at double pixel density. Test the **allocated canvas**, not theoretical viewport capacity. One-row inline math has a baseline and minimum scale; complex/tall expressions use text.
5. Reuse width/theme-specific PNGs. A miss immediately returns selectable text. Worker completion coalesces repaint after 32 ms, with no transcript polling. Warm work avoids repeated TeX parsing. The last client-side job duration is visible in `status`; cold startup, backlog, host frame throttling and terminal transport add latency.
6. On Ghostty/Kitty, upload virtual placements and return ordinary placeholder text cells. Native non-placeholder backends keep top-level uncolored image blocks and exactly empty reservation rows. Text-only paths retain the structural KaTeX-MathML renderer, then native/compact/source fallbacks.

The worker cache has bounded attempt tombstones to prevent eviction/repaint loops. Worker replacement waits for the previous termination promise. A broken installation or three consecutive deadlines disables new workers until reload; input and text rendering remain available. Captured worker stdout/stderr are discarded, not copied into logs. These are application limits, not a native-memory sandbox.

## Rich input and optional draft pipeline

### Configuration and host ownership

`index.ts` defaults to `input: 'rich'`, `assist: true` and `draft: false`. Loading an older `rich-markdown.json` respects any explicit `draft` boolean; an omitted value now means false. Missing input/assist preferences use the new defaults. `/rich-markdown input rich|source` selects the field view; source mode also bypasses assistance. `/rich-markdown assist on|off` controls typing help without disabling rich projection. `/rich-markdown draft on|off` controls **only the separate dock**. Overall `enabled` gates all three behaviors without rewriting existing draft text.

The same native `CustomEditor` instance remains installed: no `setEditorComponent()` call, editor factory replacement or reconstructed paste registry. `src/compatibility.ts` allowlists exactly Pi 1.3.0 and 1.4.0 for both runtime adapters and host discovery; discovery also requires matching SDK/TUI versions. `src/editor.ts` checks that allowlist, inherited private method shapes and the native render method. Pi 1.4.0 retains the audited editor methods; its native Markdown parser adds CJK emphasis and prevents incomplete display math from swallowing tables, covered by host-specific regressions. Release 0.0.8 adds this compatibility update; the unchanged 0.0.7 tag still targets 1.3.0. Eligibility additionally requires the exact default-editor prototype and no existing custom editor factory, even one returning `CustomEditor`. Subclasses, custom editors and competing/incompatible hooks retain native input; a compatible dock can still be used above them. Teardown removes only owned hooks; disposed wrappers become inert rather than overwriting a later extension.

`ctx.ui.setWidget()` still creates the shared preview service at Pi's default above-editor placement. Its `dock` option decides whether any separate rows appear. It samples `ctx.ui.getEditorText()` during host redraws, so typing, paste, undo and history update image access without polling. The input projection reads the native editor's raw source; blocks containing native paste markers stay native, preserving Pi's expanded-paste submission path. The separately registered `ctrl+alt+i` shortcut invokes `ctx.ui.confirm()` for exact-file consent shared by both views. No new session entry or attachment/submission transformation is introduced.

### Whole-block projection and source coordinates

`src/editor-document.ts` derives source-backed block boundaries with `Marked`, a closed-display-math boundary rule and the existing bounded image-path parser. Blocks carry source-line ranges and UTF-16 offsets, not a guessed glyph-to-source map. Whitespace gaps, plain text-only paragraphs, literal HTML, reference definitions and incomplete math/fences retain native rows. Exact omitted top-level definition spans are also supplied as rendering context to other blocks, so reference links/images resolve without reconstructing or changing local filenames. Closed ordinary code can be framed/highlighted without interpreting its contents as prose or math; only closed `math` fences are formula candidates. Unknown boundaries fail back to source.

`src/editor.ts` wraps `layoutText`, `buildVisualLineMap`, `moveToVisualLine`, `handleMouse`, `invalidate`, `setText` and `insertTextAtCursor`. Its frame is keyed by source, caret, width, terminal cell geometry/protocol/color mode and invalidation epoch. It reads the original native layout/map, then replaces eligible **whole blocks** with rendered rows:

- Render only when the caret is outside `[block.start, block.end)`, including at `block.end`. Entering that interval reveals the entire original source block. Ordinary plain text keeps its native layout, hit-testing and Unicode navigation.
- A left click on any rendered row anchors to `block.start`. Vertical entry from above selects the first source position; entry from below selects the last source character position, clamped to a native grapheme boundary. Leaving a rendered row uses terminal columns rather than hidden Markdown delimiter lengths. In-field OSC 8 links are stripped so clicks edit rather than open a link. Pi routes mouse events in fullscreen; regular-mode users retain keyboard navigation.
- Native `render()` still owns borders, padding, scrolling, focus and the single cursor/IME marker. Its visible-row limit remains `max(5, floor(terminalRows * 0.30))`; the field is not subject to the dock's 18-row/35% tail viewport.
- The source cap is 16,384 UTF-16 code units, with a 128-block parsing budget and at most 256 rendered rows per block. A projection must fit its available width and contain no opaque native image escape/reservation rows. Limits, uncertain parsing, incompatible row maps or unsupported rendering fall back to source; they never truncate or replace the submitted document.

The layout path never swaps projected state into the editor or writes source. Cursor movement intentionally updates native source coordinates; explicit typing assistance is a separate edit path. Native autocomplete temporarily bypasses both rich projection and assistance. Clearing input graphics advances the projection epoch, so canceling autocomplete cannot reuse rows that reference deleted placements. Separate custom overlay editors do not clear the main input's placements. Pure vertical movement preserves native history browsing and its saved unsent draft; only explicit edits and mouse placement exit browsing. Physical terminal/IME behavior cannot be established merely by retaining the native marker.

### Deterministic typing assistance

`src/editor-assist.ts` is a host-neutral lexical planner, not a model, editor replacement or semantic prose completion provider. It returns an `EditPlan` containing source `start`, `end`, `insert` and post-edit `cursor` offsets. It retains only one bounded expected source/caret state and at most 32 owned delimiter pairs; source/caret mismatches discard pair ownership. Existing/pasted closers are never adopted.

`src/editor.ts` wraps the native `Editor.handleInput` method **after** `CustomEditor` application/extension shortcuts have run. It forwards unsupported keys and bypasses assistance for source/off/custom-editor states, native autocomplete, jump targets, paste envelopes/in-progress paste, key releases and multi-character paste/IME batches. The lexical guard excludes code/literal regions and slash/shell drafts. No Pi input event transformer or alternative keyboard protocol parser is registered.

- Enter remains native submission. The configured `tui.input.newLine` action (Shift+Enter/Ctrl+J by default) continues unordered/ordered lists, tasks and quotes. Indentation and marker spacing are preserved; safe ordered numbers increment and continued tasks become unchecked. An empty marker removes the list/task marker or innermost quote level instead of adding another marker or submitting.
- Pair unambiguous brackets, emphasis/backticks and explicit math delimiters. Repeated openers can upgrade to double delimiters or a shallow-indented standalone code fence. Type through only generated closers, and use Backspace to remove only an empty owned pair. Currency/word-interior/escape ambiguities retain conservative native behavior.
- Tab acts only on an unambiguous `frac`, `sqrt` or supported Greek-letter prefix inside recognized math. It can insert argument braces and position the caret in the first argument. There is no snippet placeholder navigation; ambiguous/unknown prefixes, existing arguments and ordinary Tab remain native.

Before applying a plan, validate source and resulting caret boundaries with native grapheme segmentation and enforce the source cap. A source edit takes one native undo snapshot, exits history browsing, updates native lines/caret, notifies `onChange` and requests rendering; cursor-only type-through does not add a source edit. No `setText()`-based editor recreation is used, so the existing paste registry and history/shortcut machinery remain intact. Rendering and commands do not silently normalize text; only these explicit typing actions alter source.

### Shared presentation, image access and transient math

`src/draft.ts` separates parsing, shared image access and the two presentations:

- `parseDraft(source)` returns `{ markdown, images, limited }`, a derived view only. It stops above 16,384 source characters rather than rendering a misleading prefix. Markdown image tokens and complete standalone local path lines become escaped basename labels, with at most four distinct local image candidates. Quoted paths, backslash escapes, `@` prefixes, `~/` and local `file:///` URLs are accepted; paths are never shell-evaluated. Ordinary path percent signs remain literal; only file URLs are URL-decoded. Nested code spans/fences and literal HTML are not attachment inputs. Blocks with math delimiters conservatively keep image references as source; separate image paragraphs remain eligible. Remote/data image references remain labels, not load requests.
- `createDraftPreview(tui, options)` exposes `renderInput`, dock `render`, `invalidate`, `approveImages` and `dispose`. Options supply host/UI, cwd, enabled/dock/image settings, the active Markdown theme, `useInput`, `useDraft`, `clearMath`, `clearVisuals` and repaint callbacks. Input blocks use cached native `Markdown` components marked by `adapter.useInput()`; the dock uses `adapter.useDraft()`. Both markers exclude draft image tokens from transcript image/thumbnail loading, including references conservatively retained as source. Only the shared exact-file loader supplies authorized image-card bytes. Input and dock keep independent placement ownership while sharing consent; closed Mermaid fences can use the full visual pipeline below. Theme invalidation rebuilds views rather than retaining stale ANSI colors.
- Input image cards use authorized bytes for fitted Kitty placeholder pixels or bounded ANSI previews plus filename/status labels, **never native pixel image reservation rows** that scrolling could orphan. Static WebP uses the isolated browser decoder when available. `renderInput()` receives one source block plus any exact top-level reference-definition context and uses the shared, document-wide candidate list for authorization; rendering a block does not expand file access. Original paths/Markdown remain in source and are revealed on block entry.
- The optional dock follows the rendered tail within at most 18 rows / 35% terminal height. Omitted earlier content is labeled; very short terminals reduce to a heading. Oversized source produces a paused notice, not a source edit. Empty/submitted/off drafts and `draft off` produce no dock rows. Authorized image bytes use the shared fitted visual renderer, with independent dock Kitty placements, ANSI previews or explanatory labels. The older native-image path remains only as a compatibility fallback when no visual callback is supplied. Turning off image display prevents bytes from being read for either view, including ANSI fallback.
- The loader remains active while either the dock or compatible rich input is enabled. Disabling only the dock removes dock-native images, not input access/consent still in use. Clearing/submitting source, disabling both views or the renderer, or unloading empties the applicable image selection. Source mode can leave an enabled dock active.

Completed math uses the existing closure/literal-code rules, and incomplete math stays literal. Input and dock formula graphics use Kitty placeholder cells only; other paths use structural text/source, not native formula-image reservation rows that viewport slicing could split. Closed Mermaid fences now use the bounded full visual pipeline; incomplete fences and source editing remain code.

Input and dock share one lazy, bounded transient `MathCache`/worker (`draftMath` in `index.ts`), separate from the transcript cache/worker. They have **independent** `inputKitty` and `draftKitty` transports, also separate from transcript `kitty`. `beginInput(signature)` resets the input admission counter per projection pass; source/caret/width changes advance its epoch and reset only input placements. Reserve at most **eight input math image admissions across all blocks per pass**, before a cache lookup, including misses/failures. The dock retains eight admissions per component pass; transcript components retain 64. The [README resource budgets](../README.md#privacy-and-resource-limits) apply per cache/worker and per transport, not as an extra cache for each input block. Showing the dock cannot invalidate input placements or consume the transcript's retained-image budget.

Transient worker/image completions request a separate coalesced repaint; neither ordinary typing nor these completions invalidate transcript layout caches. They never restore a saved editor snapshot. Cached transient formula work may outlive an edit within its bounded cache, but only current source is laid out. Stale image reads and dialog results are rejected by loader generation checks. Reload/shutdown disposes rich-input hooks, the shared draft/image service, transient worker and both transient transports independently of transcript resources.

### Exact-file image API and security

`src/draft-images.ts` owns draft-only access shared by input and dock, not a global allowlist or a separate allowance for each block. Its `DraftImages` API is:

| Method | Contract |
| --- | --- |
| `update(hrefs)` | Track the first four distinct bounded references, retaining unchanged snapshots. Reference-list changes advance a generation, cancel pending consent/work and forget removed bytes/permission. It does not perform synchronous file I/O. |
| `get(href)` | Return a detached `loading`, `blocked`, `ready` or `unavailable` snapshot; only `ready` carries bytes, MIME and dimensions. No file I/O. |
| `approve(confirm)` | Offer one dialog for currently blocked canonical targets. Apply consent only to the still-current verified file identities; resolve when reads are queued, not when images finish loading. Denial/errors leave labels, not bytes. |
| `dispose()` | Idempotently revoke consent, clear retained bytes and invalidate reads/dialogs; late completions cannot publish or repaint. |

The loader has no remote-fetch branch and accepts no input `data:` URL. Unsupported platforms, unsafe names, nonlocal URL schemes, network/UNC paths and nonlocal file URLs are rejected before filesystem access. Local descriptor verification is supported only on macOS/Linux. No directory is implicitly approved because a path looks like a clipboard temp file.

For each candidate, resolve the cwd and target to canonical paths, require a nonempty bounded regular file, and open with `O_NOFOLLOW | O_NONBLOCK`. Match device/inode, mode, size and nanosecond modification/change timestamps, and verify the opened descriptor against the **exact target** via `/usr/sbin/lsof` on macOS or `/proc/self/fd` on Linux. Recheck the root, original reference and canonical target around the operation; verification failure leaves an unavailable label. FIFOs and other special files are not image sources. These application checks are not an OS sandbox.

A verified canonical target inside cwd is eligible for automatic reading when images are enabled. An outside target—including an escaping symlink or Pi clipboard temporary path—is inspected for identity/metadata but **no image-content bytes are read before consent**. Ctrl+Alt+I displays canonical filenames, while authorization pins the stored file metadata as well as the path. It never approves a parent directory, `$HOME` or a temporary-directory root. After confirmation, reopening/reverification must match that exact identity; replacement, retargeting and in-place mutation during authorization/read fail closed. The file's size and identity are checked again after reading, together with the descriptor/path checks. `src/raster.ts` re-exports the unchanged `src/raster.mjs` structural validator with `src/raster.d.mts` types. Native thumbnail/visual workers import the plain ESM module directly: Node cannot strip TypeScript inside an installed npm `node_modules` tree. This distribution adaptation adds no rendering behavior. Shared validation remains: check PNG chunks, JPEG scan/frame structure, GIF first-frame geometry and WebP canvas/bitstream dimensions. Strip PNG ancillary metadata and truncate GIF to its first frame before native rendering; reject APNG/animated WebP. Pi's shallow header helper must agree with validated dimensions. iTerm2 widths are rounded down to fit actual `height=auto` pixels; sub-cell widths use a non-pixel fallback.

Reads are serial, including descriptor cleanup: at most four files, 8 MiB per file / 16 MiB total original bytes (not base64 length), four million pixels and 8,192 pixels per side, with a five-second deadline per read. A timeout invalidates the result but does not start another reader while an uninterruptible OS operation is still retiring. Ready/failed results are snapshots, not file watches or repaint retries. Reference removal frees bytes and revokes consent; re-adding an outside reference needs fresh approval. Clearing/submitting the draft, disabling all draft views or image display, or disposing the shared preview service empties the selected list. `draft off` alone leaves access active if rich input still uses it. Changed references invalidate an open confirmation; the loader supplies its AbortSignal to the native dialog so cancellation closes it and restores focus. The shared service also resamples editor text after a dialog before applying its result.

**Keep access policies separate:** automatic transcript thumbnails remain cwd-contained and never inherit draft consent. Browser previews keep their own explicit invocation and per-preview remote-image consent. Draft preview uploads nothing, makes no model calls and adds no prompt or session data; only the local terminal receives rendered output. Pi inserts file paths for clipboard image paste. Seeing a draft image card is therefore **not evidence of actual image bytes attached to a model request**. No attachment transport is implemented here; attachment metadata, uploads and Pi's model-attachment behavior remain unchanged. Draft consent is memory-only; removal uses the same `rich-markdown.json` cleanup as other preferences (including `input`, `assist` and `draft`), with no separate permission file. Terminal logs/caches and already printed scrollback remain outside the plugin's control.

## Full visuals and zoom

`src/visuals.ts` connects complete Mermaid fences and authorized raster snapshots to `VisualCache`. Native Pi applies its smaller grok-mermaid transformer before extension transformers; the guarded Markdown wrapper temporarily masks only Mermaid fence language tags during that display transform and restores them before lexing. The original options object is restored in `finally`; source/messages/tokens and installed Pi files are unchanged. Thinking, explicit Mermaid-off mode and native final/streaming preferences remain respected. Complete diagram failures show a bounded explanation, not a raw-source dump.

`visual-cache.ts` admits serial worker jobs, hashes immutable known request fields, validates returned PNG/camera/thumbnail data, bounds successful and negative caches and rejects stale results after clear/disposal. Evictions receive a negative-cache interval to avoid immediate repaint loops. `getLatest()` admits only one interactive camera request at a time: completion invalidates the view, which requests its latest camera instead of replaying old wheel events. `createVisuals()` owns a transcript `cache` and a separate `draftCache` shared by input and dock. Each explicit viewer owns another independent cache. Queue, worker, result-cache and browser-layout limits apply **per cache**, not once per session or per draft block: eight pending jobs / 24 MiB request storage, 64 completed entries / 16 MiB, 128 negative entries / 30 seconds, and 16 SVG layouts / 4 MB. See the [full resource budgets](../README.md#privacy-and-resource-limits) for request, viewport and transport bounds.

Draft data and placement lifetimes are deliberately different. `src/draft.ts` calls `clearVisuals`, wired to `createVisuals().clearDraftData()`, when the selected draft-image references change, the draft empties/is submitted, all draft views or image display are disabled, or the shared service is disposed. It clears `draftCache`, queued/active requests and derived results, retires its worker/browser and clears both input/dock placements, without clearing transcript or viewer caches. Stale completions cannot restore revoked data. `clearScope()` only clears the selected surface's placements during cursor/layout changes; it does not revoke cached draft data or file consent. Turning off only the dock leaves shared access active for rich input. A viewer retains its invocation-local snapshot independently and disposes its own cache on dismissal.

`visual-browser.mjs` runs the pinned full Mermaid bundle in an already installed Chrome/Chromium through a private DevTools pipe. It uses a fresh private profile, enabled browser sandbox, strict protected Mermaid configuration, request blocking, CSP, disabled downloads and no user callbacks or icon-pack registration. Only the trusted bundle is executable; diagram text is passed as data. Safe ordinary frontmatter remains available. An SVG's declared viewBox, geometry and label bounds are united before fitting; changing the SVG viewBox rerasterizes a bounded camera rather than allocating a giant CSS surface or enlarging a thumbnail. Tests compare fixtures with all 37 registered variants. Supported syntax is still subject to active-resource restrictions and documented limits, not an unbounded promise.

`visual-raster.mjs` revalidates authorized raster data, crops from full-resolution PNG/JPEG/GIF with Resvg, and uses the isolated browser for static WebP. No worker resolves a filename or remote URL. Both paths produce a bounded viewport PNG plus a uniformly downsampled ANSI preview. Zoomed transparent regions are valid camera views, not decoder failures. Initial fit includes the full drawing; zoom intentionally permits a pannable crop.

`visual-painter.ts` owns independent Kitty transports for transcript, input, dock and viewer. It uses normal placeholder cells, not opaque native-image lines, and retains inline placements instead of evicting IDs referenced by cached content. The viewer replaces its one frame and deletes obsolete placements. Other terminal paths use honestly labeled ANSI previews. Input/dock image authorization stays in `DraftImages`; automatic transcript reads stay cwd-contained. An explicit viewer creates fresh exact-file consent for its selected outside image and never broadens automatic access.

`visual-viewer.ts` is a public `ctx.ui.custom()` overlay, not an editor replacement. Its bounded canvas owns zoom/pan and consumes fullscreen wheel/drag events so they cannot scroll the transcript. Keyboard controls remain available in regular mode; Pi exposes no native pinch events. Resize/theme/cache changes invalidate the requested camera, and late frames are checked against its geometry. Shutdown aborts and completes the overlay through `done()` **before** underlying transport teardown; `onHandle` prevents a pre-mount abort from completing an overlay that has not yet been installed.

Browser ownership is published immediately after spawn. The parent independently verifies child PID, parent PID and start time before watchdog signalling, never a process-name-wide kill. Shutdown waits for browser retirement before terminating the worker; missing ownership fails closed and reports an unverified cleanup rather than claiming success. Browser idle cleanup is 30 seconds; launch/render deadlines and all memory/pixel/source budgets appear in the bilingual README. Browser profiles can survive a crash, so visual rendering is not covered by an absolute in-memory-only guarantee.

## Why placeholder pixels travel separately

Pi 1.3.0 treats lines containing a Kitty APC or iTerm2 image sequence as opaque image lines. Inserting a transfer directly inside a paragraph bypasses wrapping and background handling. Instead, the frame contains only U+10EEEE cells with canonical row/column combining marks; foreground RGB and a third mark encode a salted 32-bit image ID. A single virtual placement per image avoids the need for SGR 58: Pi 1.3.0's ANSI tracker would interpret its RGB operands as unrelated styles when wrapping/slicing.

Transfers go through the existing TUI terminal writer, after its current frame. An instance-level observer is reversible and does not replace Pi's rendering loop or globally intercept stdout. It notices clear/alternate-screen sequences and replays retained rasters only after placeholder-bearing output, including cached component output. This covers unchanged-size forced redraws and external-editor/suspend stop/start cycles, not just resize. Graphics transport requires no extra input parser, polling loop, shell utility or system terminal configuration; the separate local typing-assistance wrapper described above is not part of the transport.

IDs are stable across append-only components within the adapter lifetime and salted against other sessions/renderers. Numeric collision probing is bounded; exhaustion fails closed. Only owned IDs are deleted. The transport bounds retained and pending data before scheduling writes. It refuses new placements at capacity instead of evicting images that cached or offscreen rows may still reference. Reset is epoch-safe, including reset/requeue before a pending microtask flush.

Placeholder images must remain whole. Native word wrapping normally moves a complete whitespace-delimited run; a post-layout check detects split runs in glued/overlong words and rerenders that component with inline text. This is conservative, not a new Markdown layout engine. Overlays/viewport clipping may intentionally hide part of a display grid. Selecting placeholders does not recover LaTeX, hence the `source` command.

## Capability and lifecycle choices

- Use Pi's detected protocol and show-images preference. Ghostty/Kitty are the validated placeholder-capability tier; this is not active negotiation or proof of a particular terminal build's behavior.
- No image override can enable Apple Terminal. tmux/screen intentionally use text in this integration, even when a protocol was forced. iTerm2's protocol needs regular Pi mode; Pi disables it in fullscreen.
- `setup` reports reasons, bounded executable/app-bundle detection and steps without writing settings or starting processes. `setup install` is a separate, idle-TUI-only action with an exact-command confirmation. The guided CLI installer shares its Ghostty/Kitty detector and consent/recheck/install/verification logic; it never runs as a package lifecycle hook. Existing compatible terminals suppress optional installation. Only local macOS with existing Homebrew receives a recipe; SSH and other platforms retain manual guidance. See the [installation workflow](../README.md#guided-installation) for stable-copy, dependency, registration, privacy and rollback boundaries.
- `images on|off` is an explicit preference change followed by `ctx.reload()`, synchronizing Pi's own SettingsManager/components rather than merely updating a second settings instance.
- Adapter teardown restores only hooks still owned by this instance, invalidates retained component caches, deletes owned pixels and releases the writer observer. Other prototype-replacing renderers may conflict; do not load competing math extensions together.
- Terminal-managed caches, arbitrary external clears, selection/search foreground overrides and previously printed scrollback are not fully controlled by an extension. `clear-images`, text mode and source-copy are explicit escape hatches. Physical terminal verification remains necessary.

The bilingual README is the user-facing source of truth for all resource budgets, privacy, installation and removal.

## Prior art and licenses

Primary references retained from the existing renderer design:

- [Pi TUI documentation](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/tui.md) and the installed Pi 1.3.0 TUI implementation: component widths, cache invalidation, image-line handling, fullscreen resets, native image reservation rows and ANSI tracking.
- [Kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/#unicode-placeholders): virtual placements, canonical diacritic data, chunking, ID encoding, deletion and the distinction between images and text placeholders. The table in `kitty.ts` is protocol data, not an alternative combining-mark algorithm.
- [@monotykamary/pi-math](https://github.com/monotykamary/pi-math), especially [image layout](https://github.com/monotykamary/pi-math/blob/main/src/image-layout.ts) and [architecture](https://github.com/monotykamary/pi-math/blob/main/docs/ARCHITECTURE.md): MathJax/Resvg, single-row inline placeholders, stable streaming IDs and separate geometry/raster caching. It is MIT licensed, including notices crediting Fadouse. This plugin uses its own bounded worker and host integration, not a runtime dependency on that renderer.
- [pi-formula](https://github.com/yasuhito/pi-formula): closed streaming display formulas, capability probing and safe fallbacks. It retains Unicode **inline** math; it is not evidence of automatic inline images. MIT licensed.
- [Ghostty features](https://ghostty.org/docs/features) and [Unicode graphics implementation](https://github.com/ghostty-org/ghostty/blob/main/src/terminal/kitty/graphics_unicode.zig): actual placeholder support. Terminal versions and multiplexers can still differ.

No external extension is installed or bundled. Preserve source licenses/notices if reusing code in future changes. Dependency licenses are not all MIT: MathJax 3 is Apache-2.0 and Resvg JS is MPL-2.0; their installed package notices remain intact. Rich Markdown's own license remains MIT.

## Verification boundaries

Regression tests use the actual Pi SDK/TUI selected with `PI_SDK_PATH` (exact 1.3.0 or 1.4.0), passing its real version to the guards, with synthetic input and no external completion requests. They check ordered transfer-after-frame output, redraw/resume, inline ANSI attributes, real custom/Translator cards, literal options, closed streaming fences, budgets, timeout retirement, copy-source and settings reload. xterm verifies text cells, not Kitty pixel display. The optional synthetic compositor verifies the actual PNG content and placement geometry, but is **not** a live terminal screenshot.

Draft acceptance additionally requires parser/path/code-literal cases, editor text and custom-editor preservation, default/config/shortcut behavior, bounded dock-tail layout and paused notices, image-setting fallbacks, independent transient graphics placements, clear/submit/off/disposal cleanup and stale-result rejection. Exact-file loader cases must exercise deny/approve, outside symlinks and clipboard-style paths, identity/metadata changes before and during reads, no inherited folder consent, removal/re-add revocation, serial cleanup after deadlines, unsupported-platform/no-I/O rejection and byte/pixel limits.

`test/editor.test.mjs` and `test/editor-assist.test.mjs` cover whole-block projection and source reveal; first/last-position navigation and block-start mouse anchoring; plain-text Unicode coordinates; native paste/undo/submission, history-draft restoration and shortcut precedence; reference-definition context; source/assist/custom-editor/autocomplete fallbacks and fresh uploads after cancellation or same-column cell-geometry changes; later-owner/disposal safety; independent input/dock/transcript placements; ANSI image cells; narrow widths, cursor markers and both TUI modes; list/task/quote continuation; owned-pair edits; math-only completion; literal/command/paste exclusions; and bounded deterministic edit plans. These are coverage descriptions, not a new test-run report. Use synthetic files and isolated host fixtures, never personal drafts, private clipboard contents or paid calls. Final release verification requires measured post-fix checks. Measurements in the bilingual changelog's local-candidate history apply only to those snapshots, not to the final 2026-10-19 release.

**Physical-terminal graphics, IME behavior and Linux execution remain unverified.** Manual validation must still check typing, undo of explicit assistance, autocomplete/history, IME positioning, coexistence with a custom editor, clipboard/path paste, confirmation focus, narrow/short terminals, resizing, graphics/text fallback and both TUI modes. Confirm that completed blocks render inside the field, entry reveals full source rather than pretending to map arbitrary formula glyphs, and Pi submits source with its native paste rules. Also confirm that the optional dock is above the editor, `draft off` leaves rich input active, and neither rendered view changes source or implies model attachments.

The release record must distinguish measured local checks from untested physical Ghostty/Kitty rendering, other OSs and browser appearance. A warm-geometry benchmark is not an end-to-end latency guarantee; streaming responsiveness needs both asynchronous rasterization and bounded main-thread work.
