Skip to content

Dev#17

Merged
Snider merged 41 commits into
mainfrom
dev
Jul 23, 2026
Merged

Dev#17
Snider merged 41 commits into
mainfrom
dev

Conversation

@Snider

@Snider Snider commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added lem update to check and apply stable, beta, alpha, or development updates.
    • Added a Settings overlay, opened with F2, with editable generation options and save controls.
    • Improved command, model, data, tools, inspector, transcript, and review overlays.
  • Improvements

    • Refreshed terminal layouts, tab navigation, mouse hit-testing, styling, and Markdown rendering.
    • Added broader cross-platform testing and release builds.
  • Documentation

    • Added release-artifact guidance and expanded TUI usage documentation.
  • Bug Fixes

    • Improved handling of narrow layouts, missing releases, update errors, and platform-specific test environments.

Snider and others added 30 commits July 22, 2026 11:26
The lem TUI rendering moves from hand-composed lipgloss to .ctml markup
rendered by go-html's terminal compositor (dappco.re/go/html v0.11.0),
slice by slice, behaviour unchanged. Slice 1 (tab strip + teabox mouse)
landed on dev pre-flip; slices 2-8 land here, one PR per the post-v1
PR-gated workflow.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 2 of the .ctml migration: the Settings screen's hand-composed
lipgloss view is replaced by embedded settings.ctml parsed with
ctml.Parse and rendered by go-html's terminal renderer. One <dl> per
knob row gives the form shape natively (dt value line, dd hint, blank
separator); rows flow through the rowsBefore/rowsActive/rowsAfter
sequence split around the cursor, copying the tab strip's idiom for
static class attributes; settingsFormTheme maps the class tokens onto
the existing uiStyles palette. Selection gutters become marker glyphs
(active >, idle o) because the parser drops whitespace-only runs, so a
two-space gutter cannot be expressed in markup. State, cursor movement,
and adjust logic are untouched; the old lipgloss composition is deleted
in the same change.

preferences.go turns out to be the preference STORE (config
load/set/commit) with no rendering of its own — the preference-editing
UI is the inspector — so there is no preferences render path to
migrate; the README's new 'Form screens' note records that and the
row-binding idiom slices 3-6 copy.

Gates (cli/): go vet clean, go build clean,
go test -count=1 ./tui/ 377 passed.

Co-Authored-By: Virgil <virgil@lethean.io>
The form-row idiom lands: one dl per knob, selection carried by the
before/active/after sequence split, palette via theme classes. Scope
finding recorded: preferences.go is the preference STORE (no render path
existed to migrate); the settings screen itself is currently unwired
(zero production callers, pre-existing). 377 tui tests green. Three new
go-html friction items filed in the design doc for upstream.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 3 of the .ctml migration — the leaf widgets.

picker.go + picker.ctml: the Models panel renders from markup while ALL
state stays in the Bubbles list.Model (items, cursor, fuzzy filter,
pagination through a.picker.Update — behaviour untouched). Bindings
derive from the model's own state: the current page of visible items
split rowsBefore/rowsActive/rowsAfter around the cursor, plus
zero-or-one-row sequences for the typed filter line, the "No items."
empty state, and the "page x/y" position. Rows are the settings <dl>
shape (marker + name, indented type/path hint) at the same three-cell
density the default delegate paginates by, with host-side ansi.Truncate
because <dt> lines never wrap. The old lipgloss path — the delegate
render behind a.picker.View() and the Styles.Title paint — is deleted;
newPicker() drops its styles parameter.

tools.go + tools.ctml: the Tools tab view converts with two new idioms:
adjacent single-line rows ride ONE <p> with a <br> closing each <each>
row (separate blocks would gain blank separators), and the conditional
"recent calls" section is an <each> over a zero-or-one-row sequence.
The name-description gutter travels in the bound value because
whitespace-only source runs drop. toolState and its keyboard seams
(setEnabled/toggle via the inspector) are untouched; the old
core.Builder composition is deleted.

markdown.go turns out to have no ctml-shaped rendering to convert: it
is the Glamour render CACHE (per-width renderers, hashed turn results,
stream refresh plumbing) — a store in the preferences.go sense, not a
view. Its pre-styled ANSI output cannot ride a .ctml document at all —
ANSI escapes are invalid XML characters, <raw> content is static
(bindings stay literal inside it), and termRawContent StripTags-es any
content carrying '<' — so the future transcript slice must compose
Glamour output around ctml chrome. Left untouched; README records the
finding.

Gates (cli/): go vet clean, go build clean,
go test -count=1 ./tui/ 383 passed (377 at base + 6 new).

Co-Authored-By: Virgil <virgil@lethean.io>
Leaf widgets land with two new idioms (single-line rows via <p>+<br> per
each-row; conditional sections as zero-or-one-row sequences) and the
picker's state-stays-in-Bubbles shape (list.Model keeps cursor/filter/
pagination; bindings derive the page). markdown.go honestly not converted
— it is the Glamour render cache, nothing ctml-shaped. 383 tui tests
green. Key upstream finding: <raw> cannot carry pre-styled ANSI at all —
the transcript slice must compose Glamour output around ctml chrome, or
go-html needs a true verbatim block node.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 4 of the .ctml migration: the nine overlays in dataoverlay.go and
agentoverlay.go swap their hand-composed Lip Gloss strings for
<layout>/HLCRF markup, exercising ctml.ParseLayout. Two idioms, chosen
per overlay and noted in each .ctml header:

- All-text overlays (databulk, launchreview read-only) render as full
  <layout variant="HCF"> in one RenderTerm call.
- Widget-carrying overlays (datanote, datafilter, dataeditor,
  workeditor, agentanswer, changereview, agentselect) render as
  <layout variant="HF">: renderOverlayFrame splits the render at the H
  slot's recorded box height — the renderer's own receipt — and the
  host composes the live Bubbles widgets (whose ANSI cannot ride a
  .ctml document) between the bands. Captions trapped between two
  widgets stay host-side; a .ctml document renders contiguously.

State, keyboard, and open/close logic untouched; alternate texts
(armed prompts, create/edit titles, the acknowledge/apply gate) ride
zero-or-one-row sequence splits; receipt bodies bind one row per line
with <br>, blank lines surviving as empty rows. overlayFrameTheme maps
the overlay classes onto the existing uiStyles palette and owns the
footer band's blank spacing row.

agentcap.go is the agent capability model, not a view — nothing to
migrate; recorded in the README with the overlay idiom.

Gates: cli go vet clean, go build clean, go test ./tui/ 413 passed
(383 at base + 30 new Good/Bad/Ugly render + frame tests).

Co-Authored-By: Virgil <virgil@lethean.io>
Eight rendering overlays land on two layout idioms: full HCF for all-text
overlays, HF band frames for widget-carrying ones — the frame renders via
Layout.RenderTermBoxes and splits at the H slot's recorded box height (the
renderer's own receipt), with Bubbles widgets composed between the bands.
agentcap.go honestly untouched: it is the capability model, not a view.
413 tui tests green (+30).

Co-Authored-By: Virgil <virgil@lethean.io>
…, Verbatim

The four friction fixes from the migration land upstream: dt wraps to the
render width; Bindings.Values gives scalar bindings (path misses render
empty, row scope still wins); mixed text+bind runs interpolate; and the
Verbatim node passes pre-styled ANSI through the term renderer byte-exact
via the verbatim element — the transcript unlock for slice 6.

Co-Authored-By: Virgil <virgil@lethean.io>
… not views

Slice 5 of the .ctml migration inspected its named files first and found
all three are tables in the database sense only — records.go (ORM record
schemas), migrations.go (lem.duckdb migration runner),
datasetmigrations.go (datasets.duckdb migration runner). None composes a
screen, so there is no rendering to migrate; README's new "Tables and
stores" note records that plus the decision for a future genuinely
columnar screen (RenderTerm's real <table> path via lipgloss/table vs
the <dl> idiom for label+detail rows).

The slice's live acceptance of the v0.12.0 surface lands on the screens
slices 1-4 converted:

- Mixed text+bind interpolation retires the separate-runs contortion at
  all 17 sites — the unclassed inner <span> isolating a bind from
  adjacent literal text drops from tabs, settings, picker, databulk and
  launchreview ("o <span>{{row.name}}</span>" becomes "o {{row.name}}").
- Bindings.Values retires the one-row-sequence carrier for the tools
  enabled-state line ({{state}} at document scope). The nil-guarded
  one-row sequences (databulk bulk, launchreview review) stay: a Values
  miss renders empty while surrounding literal text remains, so they are
  load-bearing conditionals, not carriers.
- The dt-never-wraps truncation rationale in picker/README is replaced
  with the real one: the page math budgets three cells per row, so a
  wrapped dt would overflow the delegate-sized page.

Render output is unchanged: all 413 tui tests green, no test dropped.

Co-Authored-By: Virgil <virgil@lethean.io>
…eptance applied across slices 1-4 (lane/ctml-tables)

records/migrations/datasetmigrations are ORM schemas and DuckDB migration
runners — zero rendering to migrate (the store-shaped pattern's fourth
strike). The lane instead landed the v0.12.0 acceptance across the
existing markup: 17 interpolation-contortion sites removed, the tools
state line on Bindings.Values, dt-wrap rationale corrected — renders
byte-identical under the pinned tests. Boundary banked: nil-guarded
zero-or-one-row sequences stay the conditional idiom (a Values miss
leaves surrounding literals standing); Values serves always-present
scalars only. 413 tui tests green.

Co-Authored-By: Virgil <virgil@lethean.io>
The Settings form (settings.go + settings.ctml) rendered a working knob
list — cursor, adjust/move, renderSettings — but had zero production
callers: git history shows Settings began as a top-level TAB
(f74b414), was folded into the contextual inspector when the tabs were
reworked, and the standalone form was migrated to .ctml (f7b8e31) as
the row-binding idiom demo yet never re-wired to a screen.

Make it reachable as a modal in the slice-4 overlay layer, opened by
F2. Entry point rationale: F1 (help) is the established precedent — a
modal screen opened by a function key and advertised in the footer — so
F2 (settings) sits beside it, is reliable across terminals (unlike
ctrl+punctuation), and restores a first-class path to the generation
knobs the original tab gave.

Data path is honest — no second source of truth. The overlay edits
a.cfg, the exact settings value the inspector already edits and that
contextLen()/maxTokens()/thinking() read at load and generation time,
through the form's own move/adjust. Ctrl+S commits the three
generation.* knobs (context length, max tokens, thinking) to the SAME
preferenceStore the inspector's Save writes; Esc closes without
reverting the live edit, exactly as closing the inspector does. All
three knobs map to real preference keys, so there are no dead toggles.

F2 is a first-class keyMap binding (footer + F1 full help + README
Keys table). The .ctml footer now advertises the save/close gestures.

Deliberately NOT wired: mouse row-click. Overlays are not mouse-routed
(onMouse defers to route() whenever an overlay is active) and form rows
record no per-row boxes (an <each> row cannot vary its id) — wiring a
click would need new box-derivation and overlay mouse routing, which
the brief scopes out ("do not build new derivation machinery").

Gates (cli/): go vet clean, go build clean,
go test -count=1 ./tui/ 416 passed (413 base + 3). New tests:
TestSettingsOverlay_Good (F2 opens it, renders in the overlay layer, a
knob adjust round-trips through the real store on ctrl+s, esc closes
cleanly) and _Bad (a commit with no store fails loudly and keeps the
overlay open); keymap gains the F2 row.

Co-Authored-By: Virgil <virgil@lethean.io>
The orphan resolves: F2 opens Settings in the slice-4 overlay layer (the
F1-help precedent; git history shows Settings began as a top-level tab
before folding into the inspector). Edits the same a.cfg the inspector
edits; Ctrl+S commits the three generation.* knobs through the SAME
preferenceStore path — no second source of truth, no dead toggles. 416
tui tests green (+3). Mouse row-click deliberately out: overlays are not
mouse-routed and each-rows record no boxes — no derivation machinery.

Co-Authored-By: Virgil <virgil@lethean.io>
…utter/indent doctrine

Round-2 frictions land upstream: {{path}} interpolation in ANY attribute
resolved per each-row at construction (rows carry real classes AND record
distinct boxes — the three-sequence split is obsolete); opt-in
TermOptions.FitSlots packs layout slots to content width (the tab strip's
true fix); whitespace gutters closed as marker-glyph doctrine; the
C-region indent documented as band alignment.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 6 part A: the two remaining working panels leave hand-composed Lip
Gloss. export.go was inspected and does not render — it is the session
exporter (Markdown/JSON files through coreio), the fifth store-shaped
strike — so it is untouched.

- datalist.ctml: the Data list as an HF band frame — the overlays' chrome+
  widget idiom applied to a primary panel, the live Bubbles filter input
  composed between the bands. Rows are ONE sequence with row-scoped class
  binds (class="{{row.state}}", go-html v0.13.0) instead of the
  before/active/after split; the cursor marker is bound ("› "/"○ ") because
  a row can open the F band, whose leading edge trims a bare gutter; the
  slug truncates to the row budget (band inner width minus the fixed
  segments) so rows stay one line each.
- datadetail.ctml: the detail pane as a plain document. First real use of
  <verbatim value="key"/>: the Glamour-rendered CONTENT and SCORES bodies
  pass through byte-for-byte; both values are always supplied (empty when
  nothing is selected) because a verbatim value must exist at parse time,
  and each carries one trailing newline so the blank before the next
  heading travels in the caller-owned bytes. Review/lineage lines ride
  {s,t,v} triple-span rows; metadata fields, the welfare flag, and the
  no-selection hint are zero-or-more-row sequences.
- inspector.ctml: all four panel-shaped bodies in one document, exactly one
  gated in per render by its zero-or-one-row body sequence. Sections use
  the <p>-per-section + <br>-row idiom so the pane's vertical rhythm is
  unchanged inside its height budget; knob rows are a single sequence with
  row-scoped classes; work/runtime/knowledge/feature-group lines flatten to
  {class, text} rows, each line choosing its own palette class — the group
  headings, feature rows, and inter-group blank separators ride one
  sequence.
- overlayframe.go: renderBandFrame/renderBandLayout are the theme-agnostic
  cores of the HF/HCF seams; renderOverlayFrame/renderOverlayLayout wrap
  them with the overlay theme, and the Data list passes its own band theme
  (plain bands, no footer padding) so the bands join back contiguously.

Render deltas, all block-native geometry: CONTENT/SCORES headings sit as
their own <p> above a <verbatim>, gaining the renderer's paragraph blank
beneath them; long unstyled runs (the key-hint footer, capability reasons)
now wrap at the pane width where fitPane previously clipped them; the
Data list's idle rows carry the ○ marker in place of the two-space gutter
(a gutter cannot open a flushed paragraph); the empty state's leading
blank line is trimmed at the band junction. State and keyboard handling
are untouched; all 416 tui tests pass unchanged.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 6 part B: the landed .ctml markup moves onto go-html v0.13.0's
row-scoped attribute binds, and the first slice's ANSI-segment derivation
dies.

- tabs: the strip is now a <layout variant="LC"> rendered with
  TermOptions.FitSlots — brand and tabs as content-sized slots packing
  edge-to-edge on one row, recording native slot boxes ("L", "C") that
  tile the strip. mergePanelTabBoxes and panelTabBlockID are deleted;
  panelBarHit resolves the C slot through teabox and walks panelBarCells —
  the ONE cell source both the bindings and the hit test read — so the
  render and the resolution cannot disagree, and inter-tab gaps stay
  non-hits. panelBarTheme keeps the fit geometry's fixed 4-column L-slot
  chrome one row tall (space-glyph left/right border), verified
  column-exact against the live render; recorded boxes are clamped to the
  fitted width, matching fitLine's truncation. Tabs ride one sequence
  with class="{{tab.class}}"; the tabsBefore/Active/After split is gone.
- settings + picker: rowsBefore/Active/After collapse to ONE sequence per
  screen — selection styling rides class="{{row.state}}" with the marker
  glyph bound per row — and rows gain real box ids
  (id="knob-{{row.name}}", id="model-{{row.id}}" keyed by the model path
  discovery de-duplicates on): inert under RenderTerm, recorded under a
  box-recording render, author-owned uniqueness.
- The zero-or-one-row splits in the overlays are NOT the retired idiom
  and stay: they select between alternate static texts, which are markup
  copy (i18n keys), not a per-row style variation.
- README: the .ctml section now teaches the single-sequence + row-scoped
  bind idioms, the FitSlots strip, the panel band frames, the <verbatim>
  contract, and adds export.go to the store-shaped roster.

Test updates, each because the derived per-tab boxes no longer exist:
TestRenderPanelBarBoxes_Good/_Ugly now assert the native slot boxes and
per-tab hit spans (panelTabSpan mirrors the production cell walk);
TestPanelBarHit_* pass the active/kind cell inputs; app_test's tabClick
derives its click point from the tabs slot + cell walk. Strip render
delta, slot chrome only: one leading border column and a wider
brand-to-first-tab gap. All 416 tui tests pass.

Co-Authored-By: Virgil <virgil@lethean.io>
…ridge retired (lane/ctml-panels)

Slice 6 of the .ctml TUI migration. datapanel/inspector render through
datalist.ctml, datadetail.ctml, inspector.ctml — first Verbatim use
(Glamour ANSI bodies byte-exact). Repo-wide retirement: before/active/after
sequence splits die in favour of v0.13.0 row-scoped class binds;
tabs rebuilt on FitSlots native slot boxes, mergePanelTabBoxes deleted.
export.go adjudicated store-shaped (fifth strike) — untouched.

Gates: go vet clean, go build clean, go test ./tui/ 416 passed.

Co-Authored-By: Virgil <virgil@lethean.io>
renderFrame's header (tab strip + session strip) and footer (status +
key hints) now ride a `<layout variant="HF">` (shell.ctml) via
renderBandFrame — the same HF band-split idiom overlays and the Data
list's live filter input already use — instead of a hand-built
JoinVertical. Both bands arrive as pre-rendered, pre-fitted verbatim
values, byte-identical to the prior composition (confirmed by the full
existing ./tui suite, unchanged: 416 passed, 3 pre-existing
environment-gated skips, 0 failed).

The region (main panel + wide/toggled inspector) and the chat
transcript stay hand-composed: go-html's Content slot (and Sidebar/
Aside) hardcode width offsets with no theme override, so a full-width
verbatim placed there is corrupted (word-wrapped mid-line); a
per-turn transcript verbatim is separately blocked because <verbatim>
resolves its value once at parse time, not per <each> row. Both
findings, and the resulting scope, are documented in shell.ctml and
README.md's new "The app shell" section.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 7. shell.ctml (HF layout, verbatim header/footer bands) rendered
through renderBandFrame in layout.go; app.go untouched (View already
delegated). Region stays host-composed: go-html's middle-band slots
(C/L/R) hardcode chrome offsets that word-wrap pre-fitted verbatim ANSI
onto spurious rows — probed, filed as friction round 4 with the
parse-time-verbatim limit that blocks a declarative transcript.
No Responsive wrapper: all three breakpoints share the HF shape.

Gates: vet+build clean, 416 tui tests, zero edits, byte-identical.

Co-Authored-By: Virgil <virgil@lethean.io>
…idth contract, §15.4 gutter asymmetry

Co-Authored-By: Virgil <virgil@lethean.io>
…ifact grid

The GitLab side is green; the failure emails were GitHub Actions. Two verified
signatures, each fixed with the documented built-in (config is code — no hand-rolling):

1. "Restore cache failed: Dependencies file is not found ... pattern: go.sum"
   The module root is go/ (and cli/), not the repo root. Added setup-go's
   cache-dependency-path input to every failing setup-go (ci.yml -> go/go.sum;
   build.yml -> go/go.sum + cli/go.sum). deps.yml left alone (it was passing).

2. `task: Command "sysctl -n hw.ncpu" failed` (ubuntu exit 1 / windows 127)
   go-task evaluates global sh-vars eagerly, so the NCPU var must succeed on every
   platform. Hardened the fallback chain with a literal floor:
   getconf ... || nproc ... || sysctl -n hw.ncpu 2>/dev/null || echo 4
   Mac path unchanged (getconf _NPROCESSORS_ONLN already returns the core count = 32).

build.yml rewritten to an HONEST matrix + the Driver/Native release grid:
- Probe finding: lem links DuckDB via cgo; duckdb-go-bindings is //go:build cgo, so
  CGO_ENABLED=0 fails for every target. There is NO pure-Go lem and NO free
  cross-compile — each cell is a native cgo build. duckdb supports darwin/{amd64,arm64},
  linux/{amd64,arm64}, windows/amd64 (NOT windows/arm64).
- GitHub now builds only the cpu cells it can build natively (macos aarch64 + x86_64,
  linux x86_64 + aarch64, windows x86_64); the dishonest hosted-metal cell is removed.
- Metal rides a self-hosted macOS 26 lane, dormant behind `if vars.ENABLE_MACOS_METAL`
  so it never queues or fails on a push; interim = built manually on the maintainer's Mac.
  Hosted runners are < macOS 26 and cannot compile the Metal 4 metallibs; a plain build
  would ship a non-runnable, metallib-less binary.
- Each cell is packaged twice from one build: Native {os}-{arch}-lem-{backend}-{ver}.zip
  (binary `lem`) and Driver {os}-{arch}-lem-driver-{backend}-{ver}.zip (binary
  `lem-{backend}`); rolling dev prerelease kept, version aligned to dev-<short-sha>.
- Dropped GOWORK=off from the binary build: cli pins go/inference v0.14.0 which lags the
  local tree (missing the dataset pkg), so the workspace must wire cli -> local go/.

docs/release-artifacts.md settles the grid, the two packagings, per-cell CI ownership,
the metal trade-off, the cgo finding, and the not-yet-wired follow-ups (tagged GitHub
release; GitLab grid-named zips — left for on-box validation).

.gitlab-ci.yml untouched (green; gates own WHEN). Makefile untouched. cli/tui untouched.

Co-Authored-By: Virgil <virgil@lethean.io>
Converts commandPalette.View from raw list.Model.View() to
palette.ctml: an HF band (renderBandFrame) with a fixed "Commands"
header, row-scoped class binds for selection (picker.ctml's <dl>
idiom), and the live Bubbles FilterInput composed between the bands
while filtering — datalist.ctml's own borrowing of the overlay
widget-carrying idiom, lent back to an actual overlay.

go-html's ctml.SubcommandList (docs/ctml.md S:16) was checked and does
not fit: it walks a *core.Core CommandAction registry and renders a
bare <ul> of I18nKey() labels, with no selection state, filtering, or
availability/reason — none of which workspaceCommand has any
relationship to core.Command. The palette instead composes the
existing generic row-list idiom.

Dead list.Model.Styles.Title assignments (newCommandPalette,
rebuildTheme) are removed now that ctml owns the palette's paint via
commandPaletteTheme, matching picker.go's precedent of never setting
that field once a screen renders through go-html.

Adds TestRenderCommandPalette_Good/_Bad/_Ugly (419 passed, was 416) —
the palette's rendered output had no prior test coverage at all.
README.md gains a matching ".ctml" subsection.
Slice 8. palette.ctml (HF bands, dl rows with row-scoped state class,
zero-or-one empty/page sequences, host-composed live FilterInput
between bands); Bubbles list rendering deleted. SubcommandList
adjudicated not-fit: it walks a core.Core Command registry — the
palette's workspaceCommand catalogue has no relationship to it; a
registry bridge is not a rendering slice. Old View had zero test
coverage; Good/Bad/Ugly added.

Gates: vet+build clean, 419 tui tests (floor 416, +3).

Co-Authored-By: Virgil <virgil@lethean.io>
…batim

Co-Authored-By: Virgil <virgil@lethean.io>
Four failures, three platform-blind and one real fixture bug — all
reproduced and re-verified on the linux box (full portable suite
exit=0 there; agent/ai + driver 335 green on darwin):

- engine/metal gemm_steel_test.go had no build constraint — on linux
  the package collapsed to that one file and failed to build. Tagged
  darwin && arm64 like every sibling; linux now excludes the package
  cleanly.
- agent/ai TestReadEvents compared time.Time with != after a disk
  round-trip — passes only when the local zone makes time.Parse hand
  back time.Local (BST Macs), fails on UTC runners. Now .Equal.
- engine/hip: two contract tests carried pre-truth-pass expectations
  (standalone-native qwen3_6_moe, fitting 16GB plan). The truth-pass
  source (ArchitectureProfileNotes post-#50) is explicit: no native
  path replaced the retired composed detour. Realigned to
  metadata_only / Fits=false with the architecture gap reported
  honestly; the memory-plan maths assertions stay.
- engine/driver: fakeDriverScript ran a bare 'sleep' under the
  harness's deliberately EMPTIED PATH — exit 127 on spawn, every test
  driver an instant corpse, the crash-restart test racing its own
  restart storm (darwin's poll won the race; linux's never did).
  /bin/sleep by absolute path — the restart loop is now genuinely
  exercised on both platforms.

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 7b, unblocked by go-html v0.14.0's two levers: a themeable Content
slot (TermTheme.Content, matching the pre-existing themeable Aside) and
row-scoped verbatim (<verbatim value="{{row.field}}"/> inside <each>).

Region (layout.go, renderFrame): shellwide.ctml (<layout variant="HCRF">)
binds the main panel to C and the inspector to R for the Wide layout kind,
one RenderTerm call (renderBandLayout) replacing lipgloss.JoinHorizontal
plus a manual "|" separator column. shellregion.ctml (<layout
variant="HCF">) does the same for every single-pane shape (Narrow either
way; Overlay with the inspector closed), replacing a bare fitPane call.
shell.ctml narrows to the ONE shape that still cannot join them: Overlay
with the inspector open, stacking two independently-sized panes vertically
with a rule between -- HLCRF's five-letter vocabulary has no pair for that
(H/F are whole-page bands, not a reusable mid-page pair), so
renderInspectorStack keeps the pre-.ctml host composition for that one case
only. This is the friction this slice reports rather than works around.

Width delta: go-html's non-FitSlots middle band gives R a FIXED 28-column
budget whenever present (termAsideWidth, unexported, ctml.md S:15.1) -- no
TermOptions/Layout lever requests a different width, confirmed by reading
term_layout.go and a throwaway probe. The Wide inspector is now 28 columns,
not 32; the main panel gains the 4 columns back. regionAsideWidth mirrors
the fixed budget as a documented local constant (go-html exports no
accessor, S:15.5); TestRegionAsideWidth_MatchesGoHTML pins it against a
live RenderTermBoxes call so upstream drift fails loudly rather than
silently reflowing the frame. The old visible "|" rule is also gone: a
zero-chrome C does not collapse the C/R junction, go-html always inserts
its own blank single-space gutter before R, and there is no theme lever to
paint a glyph into it.

Transcript (app.go, transcript.ctml): the hand-grown core.Builder
accumulation loop becomes a zero-or-one-row "notice" sequence plus one row
per turn, each row's verbatim "body" the turn's complete pre-formatted
output (renderTranscriptTurn). What did NOT convert: a turn's STRUCTURE
(inline "you "+text for user, label+result for tool, optional thinking
line + label + Glamour/streaming body + tool calls + spinner for
assistant) stays host-decided, because <switch>/<if>/<unless> resolve
against Context.Data once per render (ctml.md S:7) -- the same context for
every row an <each> produces, not a per-row scope. A row can vary its
VALUES, never its STRUCTURE; this is the documented closed-vocabulary
boundary (S:1.1), not a workaround.

Mouse hit-testing needed no re-plumbing: no region content resolves a
mouse coordinate against its own composition today, so there was no "old
composition maths" to move onto boxes.

Test floor: 419 -> 420 (TestRegionAsideWidth_MatchesGoHTML added; no
existing test dropped or weakened -- TestWorkspaceFrame_Good/_Bad/_Ugly
and every Transcript* test pass unchanged).

Co-Authored-By: Virgil <virgil@lethean.io>
Slice 7b on go-html v0.14.0's two levers. Region renders through
shellwide.ctml (HCRF, wide) / shellregion.ctml (HCF, single-pane) with
zero-chrome themes; the chat transcript is an each + row-scoped
verbatim (transcript.ctml) — the Builder accumulation loop deleted,
per-turn role branching factored host-side unchanged. Overlay-with-
inspector-open stays host-composed (HLCRF has no mid-page pair —
friction filed). Two doctrine-tied deltas: wide inspector 28 not 32
(fixed unexported termAsideWidth, pinned by
TestRegionAsideWidth_MatchesGoHTML), separator rule glyph → go-html's
own blank gutter (no theme hook). Round 5 items filed for both.

Gates: vet+build clean, 420 tui tests (floor 419, +1).

Co-Authored-By: Virgil <virgil@lethean.io>
…or pin

macos/x86_64: macos-13 runner label retired — queued forever, no hosted
Intel Mac remains. windows/x86_64: lem is not Windows-portable
(syscall.Kill + raw-fd reads outside constraints — three live compile
errors); the cell returns when a portability campaign lands, not
before. sdk: @openapitools/openapi-generator-cli npm wrapper is 2.x
(2.40.0) — 7.22.0 conflated it with the Java generator version it
manages; ETARGET on every run.

Live receipts: CI (portable suite) PASSED on this branch; cpu cells
macos/aarch64 + linux/x86_64 + linux/aarch64 built green; metal gate
skipped dormant as designed.

Co-Authored-By: Virgil <virgil@lethean.io>
ci: stop GitHub Actions failure spam + Driver/Native artifact grid
The sdk job's lem-sdks artifact (alive again since the 2.40.0 pin fix)
matched the lem-*/ packaging glob and its directory listing was treated
as a binary name. cpu/metal globs only, with an unmatched-glob guard
for the dormant metal lane.

Co-Authored-By: Virgil <virgil@lethean.io>
CI becomes the PR go/no-go: the model-free suite (engine/metal skips
without a metallib, hip contract tests host-side) runs on linux+macos
as REQUIRED lanes and windows as EXPERIMENTAL (continue-on-error) until
the cgo-on windows graph is proven — its logs name the real offenders
either way. Each lane: go/ module suite (GOWORK=off, the published dep
surface), cli tui suite (workspace), and the examples module compiled +
vetted (the acceptance surface — every public feature ships with its
example, so compiling all of them catches API breakage without a model).

Build gains tags: v* — a version tag publishes the REAL release, guarded
by a merge-base ancestry check (tag not on main = loud red, no release);
dev pushes keep the rolling dev prerelease. Binary main.version and the
zip names carry the tag on tag builds.

Co-Authored-By: Virgil <virgil@lethean.io>
Snider and others added 11 commits July 22, 2026 16:36
…l pane pairs

Co-Authored-By: Virgil <virgil@lethean.io>
…pair idiom (slice 7c)

Three shipped-vs-historic deltas from slice 7, all unblocked by go-html
v0.15.0:

- The Wide inspector requests its historic 32 columns again via
  TermOptions.AsideWidth (renderWideLayout, layout.go) instead of go-html's
  unrequested 28-column default; wideInspectorWidth restores the pre-.ctml
  constant name and value, and the main panel gets its 4 columns back.
- shellWideTheme sets TermTheme.GutterRule = "│" (Rule repointed at
  styles.separator's own colour) so the C/R junction gutter go-html always
  reserves paints the historic visible rule again instead of a blank gap.
- renderInspectorStack (the Overlay-with-inspector-open region: 80-119
  cols) no longer hand-stacks lipgloss.JoinVertical plus a manually
  core.Repeat("─", ...) rule line. docs/ctml.md S:15.7 documents a mid-page
  vertical pane pair as an ordinary "HC" layout — H the upper pane with its
  own bottom border as the divider, C the lower pane — since only the
  middle band's L/C/R packing is width-gated; H/middle/F stack vertically
  at any width. shellinspectorpair.ctml + inspectorPairTheme render that
  pair through its own RenderTerm call.

That pair render stays a SEPARATE call from shell.ctml's H/F band rather
than nesting fully into one page-wide call, and this is the one friction
beyond S:15.7's worked example: TermTheme is one flat struct threaded
through every nested Layout in a single render (term_layout.go's
termRenderer carries exactly one theme field), so a unified call would
force the page header's H (needing zero chrome, pre-fitted byte-exact like
every sibling shell) and the pair's own H (needing its natural bordered
chrome, since that border IS the divider) to share one Header style. A
throwaway probe during this slice confirmed it: zeroing Header for the
page header silently zeroed the pair's divider too. shellinspectorpair.ctml
keeps its own theme instead, and shell.ctml's H/F split stays exactly as
slice 7 left it — host-joined by renderFrame, not part of the shipped
diff's probe.

TestRegionAsideWidth_MatchesGoHTML is reframed as
TestWideInspectorWidth_MatchesRequest: since AsideWidth is a request, not
an upstream constant to mirror, the test now renders the real
shellwide.ctml path with AsideWidth set and reads the honoured width back
from the live box map (S:15.5's "box map is the render-time source of
truth"), rather than asserting a hardcoded upstream default.

No other render deltas: go vet, go build, and go test -count=1 ./tui/ all
pass at the 420-test floor with zero test count change (one test renamed
and reframed in place, none added or removed).

Co-Authored-By: Virgil <virgil@lethean.io>
…e/ctml-restore)

Slice 7c. AsideWidth request restores the wide inspector to 32
(pin test reframed to read the honoured request from the box map);
GutterRule │ in the historic styles.separator colour;
renderInspectorStack rides shellinspectorpair.ctml (HC — H's bottom
border IS the divider), the JoinVertical hand-stack deleted. One
friction proven two ways and reported: nested layouts share ONE
renderer theme (no per-instance override) — the pair renders
standalone with its own theme; filed for a future round if a real
screen needs a unified call.

Gates: vet+build clean, 420 tui tests, floor exact.

Co-Authored-By: Virgil <virgil@lethean.io>
ci: three-OS PR test gate + tag-on-main versioned releases
Co-Authored-By: Virgil <virgil@lethean.io>
feat(tui): lem TUI on .ctml — compositor-rendered screens
Adds `lem update` (+ --check, --channel override), using dappco.re/go/update
v0.13.0's primitives. The default channel is derived from the running
binary's own stamped version (deriveUpdateChannel): a v1.2.3 build follows
stable, v1.2.3-beta.1 follows beta, v1.2.3-alpha.2 follows alpha, and a
dev-<sha12> build follows the rolling dev prerelease — release channels as
a side effect of the version string, not a separately tracked setting.

Asset self-selection (selectReleaseAsset/updateAssetPrefix) replaces
go-update's GetDownloadURL: this repo's asset names spell os/arch as
macos/x86_64/aarch64, which never match runtime.GOOS/GOARCH substrings
(darwin never appears in a macos-* name), so lem maps the dimensions itself
and exact-prefix-matches against release.Assets. Zero or multiple matches is
a loud typed error naming what was looked for. Native vs Driver role comes
from the invoked binary's own name — the only signal that exists, since
build.yml compiles one binary per cell and copies it into both zips after
the fact, so no ldflags var could distinguish them.

The zip problem: every release asset is a zip holding one binary (never a
raw binary stream), so go-update's own DoUpdate — which feeds an HTTP
response body straight into selfupdate.Apply — cannot be used directly.
applyReleaseAsset downloads and unzips here, then hands the extracted binary
bytes to go-update's new DoUpdateFromReader, reusing its apply/rollback
machinery instead of re-implementing it.

The dev channel is fetched by its exact tag (GetReleaseByTag) rather than
through channel classification, and compared by plain string inequality
against the asset's own embedded version — never a semver.Compare on a
rolling tag. The semver channels go through CheckForNewerVersion, unpacked
via the new VersionCheckResult interface so this reuses go-update's own
comparison instead of re-implementing it.

CI: build.yml now stamps -X main.backend=<cpu|metal> alongside main.version
for the cpu and metal cells (asset-selection needs it), and a tag containing
'-' (v1.2.3-beta.1) now publishes prerelease: true instead of every v* tag
always publishing false, so go-update's channel filter sees prerelease tags
correctly.

Co-Authored-By: Virgil <virgil@lethean.io>
feat(cli): lem update — self-update from GitHub releases with version-derived channels
v0.16.0 ships dappco.re/go/html/tui — the terminal-UI runtime that wraps
charmbracelet — so cli/tui can drop its direct charm imports next.

Co-Authored-By: Virgil <virgil@lethean.io>
19 cli/tui files swapped onto go-html's tui runtime — widget path-swaps
(bubbletea/bubbles) plus style/markdown semantic renames (lipgloss→style,
glamour→markdown, x/ansi folded into style). Behaviour-preserving:
go test ./tui/... green. No direct charmbracelet imports remain in
cli/tui non-test files.

Co-Authored-By: Virgil <virgil@lethean.io>
cli/tui charm-free behind go-html/tui v0.16.0
@Snider
Snider merged commit b488697 into main Jul 23, 2026
10 of 12 checks passed
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e09b525-5383-4665-81b7-3a4d3c29dfe5

📥 Commits

Reviewing files that changed from the base of the PR and between 0036245 and b99b329.

⛔ Files ignored due to path filters (1)
  • cli/go.sum is excluded by !**/*.sum
📒 Files selected for processing (66)
  • .github/workflows/build.yml
  • .github/workflows/ci.yml
  • Taskfile.yml
  • cli/go.mod
  • cli/main.go
  • cli/main_test.go
  • cli/tui/README.md
  • cli/tui/agentanswer.ctml
  • cli/tui/agentoverlay.go
  • cli/tui/agentoverlay_test.go
  • cli/tui/agentselect.ctml
  • cli/tui/app.go
  • cli/tui/app_test.go
  • cli/tui/changereview.ctml
  • cli/tui/databulk.ctml
  • cli/tui/datadetail.ctml
  • cli/tui/dataeditor.ctml
  • cli/tui/datafilter.ctml
  • cli/tui/datalist.ctml
  • cli/tui/datanote.ctml
  • cli/tui/dataoverlay.go
  • cli/tui/dataoverlay_test.go
  • cli/tui/datapanel.go
  • cli/tui/inspector.ctml
  • cli/tui/inspector.go
  • cli/tui/keymap.go
  • cli/tui/keymap_test.go
  • cli/tui/launchreview.ctml
  • cli/tui/layout.go
  • cli/tui/layout_test.go
  • cli/tui/markdown.go
  • cli/tui/overlayframe.go
  • cli/tui/overlayframe_test.go
  • cli/tui/palette.ctml
  • cli/tui/palette.go
  • cli/tui/palette_test.go
  • cli/tui/picker.ctml
  • cli/tui/picker.go
  • cli/tui/picker_test.go
  • cli/tui/service.go
  • cli/tui/settings.ctml
  • cli/tui/settings.go
  • cli/tui/settings_test.go
  • cli/tui/shell.ctml
  • cli/tui/shellinspectorpair.ctml
  • cli/tui/shellregion.ctml
  • cli/tui/shellwide.ctml
  • cli/tui/stream.go
  • cli/tui/style.go
  • cli/tui/tabs.ctml
  • cli/tui/tabs.go
  • cli/tui/tabs_test.go
  • cli/tui/tools.ctml
  • cli/tui/tools.go
  • cli/tui/tools_test.go
  • cli/tui/transcript.ctml
  • cli/tui/tui.go
  • cli/tui/work.go
  • cli/tui/workeditor.ctml
  • cli/update.go
  • cli/update_test.go
  • docs/release-artifacts.md
  • go/agent/ai/ai_test.go
  • go/engine/driver/fixtures_test.go
  • go/engine/hip/native_contract_test.go
  • go/engine/metal/gemm_steel_test.go

📝 Walkthrough

Walkthrough

This pull request separates CPU, Metal, and release workflows, adds cross-platform CI, introduces lem update, and migrates TUI rendering, overlays, panels, layouts, settings, tabs, and pickers to CTML and go-html components with expanded tests and documentation.

Changes

Release and updater

Layer / File(s) Summary
Cross-platform build and release pipeline
.github/workflows/*, Taskfile.yml, docs/release-artifacts.md
Builds CPU and optional Metal artifacts, packages native and driver archives, expands CI coverage, and documents the release grid.
Self-update command
cli/main.go, cli/update.go, cli/update_test.go, cli/go.mod
Adds lem update, channel detection, release asset selection, zip extraction, update application, and command-level tests.

TUI rendering

Layer / File(s) Summary
CTML rendering foundation and shell layout
cli/tui/layout.go, cli/tui/overlayframe.go, cli/tui/style.go, cli/tui/app.go, cli/tui/*.ctml
Migrates shared terminal styling, shell layouts, overlays, markdown, and transcript rendering to go-html and CTML.
Data, inspector, tools, and agent overlays
cli/tui/data*, cli/tui/agent*, cli/tui/inspector*, cli/tui/tools*
Adds template-driven bindings and rendering for panels, inspectors, tools, transcript content, and agent workflows.
Pickers, palette, tabs, and settings
cli/tui/picker*, cli/tui/palette*, cli/tui/tabs*, cli/tui/settings*, cli/tui/keymap*
Updates model and command lists, tab hit testing, Settings persistence and controls, and keyboard help integration.
Supporting contract updates
go/agent/ai/ai_test.go, go/engine/driver/fixtures_test.go, go/engine/hip/native_contract_test.go, go/engine/metal/gemm_steel_test.go
Adjusts platform-specific and hermetic test expectations.
CTML and interaction documentation
cli/tui/README.md
Documents CTML bindings, layout seams, selection styling, mouse boxes, and Settings controls.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions

🔧 Checkov (3.3.8)
.github/workflows/build.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

.github/workflows/ci.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

Taskfile.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

  • 1 others

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Snider
Snider deleted the dev branch July 23, 2026 09:10
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant