Skip to content

Latest commit

 

History

History
166 lines (117 loc) · 6.34 KB

File metadata and controls

166 lines (117 loc) · 6.34 KB

Rendering boundary

Status

Accepted direction for the extension runtime.

This document records the lesson from the first transcript-rendering migration attempt: Lua is useful as an optional control plane for librecode, but the Go runtime must remain the default UI implementation and fast rendering backend.

Related docs:

Decision

librecode will keep Go as the product/runtime/rendering kernel and Lua as an optional extension layer.

Lua extensions may decide, when explicitly enabled:

  • which windows exist
  • which buffers are shown
  • which window owns rendering
  • what blocks, spans, or UI elements should be drawn
  • how keymaps, modes, statuslines, layouts, and assistant policy behave

Go should provide:

  • terminal I/O and screen flushing
  • clipping, measuring, wrapping, and style application
  • efficient draw batching
  • viewport/virtual-list helpers
  • cached rendering for large histories
  • core invariants and bounded work in hot paths

The goal is not to make Lua manually reimplement every pixel/string rule from the existing Go renderer. The goal is to let optional Lua extensions compose custom behavior while Go keeps the stock UI sharp and fast.

Why this boundary exists

Experiments showed that small UI overlays are feasible, but making Lua own default UI surfaces can degrade polish and performance. The transcript migration did not meet the quality bar because transcript rendering is a hot and complex UI surface. It includes:

  • wrapping and measuring text
  • spacing and grouping message blocks
  • thinking/tool styling
  • streaming block ordering
  • scrollback and viewport slicing
  • color and border consistency
  • cached rendering for large histories
  • performance during high-frequency model/thinking deltas

The existing Go path already handles many of those concerns. Moving that logic wholesale into Lua before the runtime exposes better generic rendering primitives causes visual regressions and render-loop slowdowns.

Current rule

Do not move default renderers to Lua just because a window can be extension-owned.

An optional Lua renderer is acceptable only when either:

  1. the Lua implementation can match the Go renderer's visual and interaction behavior with existing primitives, or
  2. missing generic primitives are added to the Go kernel first.

Until then, keep the mature Go renderer and expose bounded, generic data to Lua.

Primitive-first migration

When a Lua renderer cannot match the stock renderer, do not add product-specific host APIs like transcript.render() or thinking.draw().

Instead, add generic primitives that are useful outside chat:

  • ui.measure(text[, opts])
  • ui.wrap(text, width[, opts])
  • ui.truncate(text, width[, opts])
  • ui.draw_lines(window, row, col, lines[, style])
  • ui.draw_spans(window, row, col, spans)
  • ui.draw_box(window, opts)
  • ui.clear_region(window, row, col, height, width)
  • ui.clip(window, fn) or explicit clipped draw operations
  • named highlight groups and theme token resolution
  • namespace-scoped highlights/extmarks/virtual text
  • window viewport/scroll helpers
  • virtual-list helpers for large block lists
  • batched draw operations

These primitives keep Go responsible for the parts that require performance and terminal correctness while still letting Lua own product decisions.

Render ownership levels

A window can be in one of three practical states:

  1. Stock-rendered — Go renders the default UI for that window.
  2. Extension overlay — Go renders the stock UI, and Lua draws additional UI on top.
  3. Extension-owned — Lua marks renderer = "extension"; Go skips stock rendering for that window and only applies extension draw operations.

Extension-owned rendering should be used carefully for hot/complex windows until the primitive set is strong enough.

Transcript policy

Transcript rendering stays Go-owned for now.

Lua may inspect bounded transcript data through generic buffer blocks and metadata, but the stock transcript renderer remains in Go until render parity is achievable.

Specifically:

  • keep transcript snapshots bounded by block count and text length
  • do not rebuild full transcript text on every render or streaming delta
  • do not add transcript-specific host write/render APIs
  • add generic buffer/UI/viewport primitives instead
  • maintain a render parity checklist before retrying a Lua transcript renderer

Render parity checklist

Before enabling an optional Lua replacement for a complex stock renderer, verify at least:

  • visual spacing matches the Go renderer
  • borders and text colors do not bleed
  • wide/UTF-8 characters measure correctly
  • wrapping matches terminal width
  • thinking text remains dim/italic where intended
  • tool blocks keep their current styling and expansion behavior
  • streaming thinking/tool/answer blocks remain chronological
  • scrolling remains smooth with long sessions
  • render work is bounded by visible rows, not full history size
  • high-frequency model/thinking deltas do not trigger unbounded Lua work
  • tests cover small, large, streaming, and scrolled transcripts

Near-term direction

The next rendering work should continue strengthening generic primitives rather than migrate transcript wholesale.

Implemented first-pass Go-backed primitives:

  • ui.measure
  • ui.truncate
  • ui.pad_right
  • ui.wrap
  • ui.draw_lines
  • ui.draw_spans
  • ui.draw_box
  • ui.clear_region
  • ui.viewport
  • ui.virtual_list
  • ui.draw_batch
  • ui.theme_tokens

Recommended remaining order:

  1. Add namespace-scoped highlights/extmarks/virtual text.
  2. Add richer window scroll state helpers.
  3. Add renderer registration helpers for reusable renderer functions.
  4. Only then retry Lua-owned transcript rendering behind a feature flag or experimental extension.

What still belongs in Lua now

Lua is still the right place for optional customization:

  • keymaps and commands
  • hooks and prompt/context tweaks
  • simple panels and overlays
  • custom windows and opt-in render experiments
  • assistant policy hooks as primitives mature
  • project-specific helper modules over buf, win, layout, ui, and event

The boundary is not "Go UI versus Lua UI". The boundary is:

Go owns the default product and hot rendering; Lua is an optional customization layer.