Conversation
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>
…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
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
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (66)
📝 WalkthroughWalkthroughThis pull request separates CPU, Metal, and release workflows, adds cross-platform CI, introduces ChangesRelease and updater
TUI rendering
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 🔧 Checkov (3.3.8).github/workflows/build.ymlTraceback (most recent call last): .github/workflows/ci.ymlTraceback (most recent call last): Taskfile.ymlTraceback (most recent call last):
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. Comment |
|



Summary by CodeRabbit
New Features
lem updateto check and apply stable, beta, alpha, or development updates.Improvements
Documentation
Bug Fixes