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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
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>
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>
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>
…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>
|



The lem workspace TUI's rendering moves from hand-composed lipgloss strings to
.ctmlmarkup rendered by go-html's terminal compositor (dappco.re/go/html v0.11.0) — screens become declarative markup a reader can see whole; behaviour, state, and keyboard handling stay untouched. One slice per commit-set, old render path deleted in the same commit as its replacement, tui test suite green at every step.Landed pre-flip (already in dev/main): slice 1 — tab strip on
.ctmlwith per-tab mouse switching through the box map +teabox(first mouse support in the TUI), palette mapped ontoTermThemeclasses.This PR collects, in order:
slice 2 — settings form (row-binding idiom lands; scope finding: preferences.go is the preference STORE, no render path existed; the settings screen is currently unwired — pre-existing, flagged)
slice 3 — leaf widgets: picker + tools (markdown.go = the Glamour render cache, nothing to convert; and
<raw>cannot carry ANSI — transcript chrome composes around Glamour, noted for slice 6)slice 4 — overlays: 8 converted on two layout idioms (full HCF; HF band frames split at the H slot's recorded box height, Bubbles widgets between bands); agentcap.go = the capability model, not a view
slice 5 — scope truth: records/migrations/datasetmigrations are DB tables (ORM schemas + DuckDB migration runners), zero rendering; the lane applied the go-html v0.12.0 acceptance across slices 1-4 instead (17 interpolation contortions removed, tools scalar on Values, renders byte-identical)
slice 6 — datapanel + inspector on ctml (first Verbatim use: Glamour ANSI bodies byte-exact); export.go = the session exporter writing Markdown/JSON files, no render path (fifth store-shaped strike); retirement pass: before/active/after sequence splits die repo-wide for v0.13.0 row-scoped class binds, tabs rebuilt on FitSlots native slot boxes, the tab-strip box bridge deleted
slice 7 — app shell H/F bands on shell.ctml (verbatim header/footer; app.go untouched — View already delegated); no Responsive (all three breakpoints share the HF shape, chooseLayout owns the axis); the REGION stays host-composed pending go-html round 4 (middle-band slots hardcode chrome that word-wraps pre-fitted verbatim ANSI — probed; and verbatim resolves parse-time, blocking a declarative transcript); pin bumped v0.13.1
slice 8 — command palette on palette.ctml (SubcommandList adjudicated not-fit: it walks a core.Core Command registry, unrelated to the palette catalogue — a registry bridge is not a rendering slice); old View had zero test coverage, Good/Bad/Ugly added (419); pin bumped v0.14.0
slice 7b — region on shellwide.ctml (HCRF) / shellregion.ctml (HCF) with zero-chrome themes; transcript = each + row-scoped verbatim (Builder loop deleted); two doctrine-tied deltas (wide inspector 28 not 32 — fixed unexported termAsideWidth, pinned; separator rule glyph → blank gutter — no theme hook) with round-5 frictions filed; Overlay+inspector stays host-composed (no HLCRF mid-page pair); 420 tests
settings wired — F2 opens the Settings overlay (F1-help precedent, restores the original tab-era first-class path); edits the same a.cfg + preferenceStore the inspector uses
Parked by design: the shared
ui/dual-target (HTML+console) idea — a working TUI first; renderer portability stays an inherited property, not a structure.Gate note: the CI pr gate covers lint + host tests; the
metal_runtimesuite runs only on the Mac and is verified locally before merge (the declared flip-time constraint).🤖 Generated with Claude Code
Co-Authored-By: Virgil virgil@lethean.io