Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
966f34b
feat(tui): open the .ctml migration feature branch
Snider Jul 22, 2026
f7b8e31
feat(tui): render the Settings form through settings.ctml
Snider Jul 22, 2026
f43a03f
merge: settings form on .ctml — slice 2 (lane/ctml-settings)
Snider Jul 22, 2026
a886ea2
feat(tui): render the Models picker and Tools tab through .ctml
Snider Jul 22, 2026
5112236
merge: picker + tools on .ctml — slice 3 (lane/ctml-widgets)
Snider Jul 22, 2026
17f5b33
feat(tui): overlay layer renders through ctml layout documents
Snider Jul 22, 2026
6ba215a
merge: overlays on .ctml — slice 4 (lane/ctml-overlays)
Snider Jul 22, 2026
8b03cce
build(cli): go-html v0.12.0 — dt wrap, scalar bindings, interpolation…
Snider Jul 22, 2026
d040246
refactor(tui): adopt go-html v0.12.0 surface; tables slice is stores,…
Snider Jul 22, 2026
f863f4b
merge: slice 5 — the 'table screens' are database tables; v0.12.0 acc…
Snider Jul 22, 2026
92c4c84
feat(tui): wire the Settings form in as an F2 overlay
Snider Jul 22, 2026
c97be57
merge: Settings wired — F2 overlay, store-backed (lane/settings-wire)
Snider Jul 22, 2026
4c40cf3
build(cli): go-html v0.13.0 — row-scoped attribute binds, FitSlots, g…
Snider Jul 22, 2026
7ac49ce
feat(tui): Data panel and inspector render through ctml
Snider Jul 22, 2026
7ea35a0
refactor(tui): retire the sequence split and the tab-strip box bridge
Snider Jul 22, 2026
4644887
merge: Data panel + inspector on ctml; sequence split and tab-strip b…
Snider Jul 22, 2026
c271828
feat(tui): app shell header/footer bands render through ctml (slice 7)
Snider Jul 22, 2026
3757e8f
merge: app shell H/F bands on ctml (lane/ctml-shell)
Snider Jul 22, 2026
1f684ac
build(cli): go-html v0.13.1 — measured FitSlots chrome, §15.5 inner-w…
Snider Jul 22, 2026
a9c07a7
feat(tui): migrate the command palette to .ctml (slice 8)
Snider Jul 22, 2026
73d8c93
merge: command palette on ctml (lane/ctml-palette)
Snider Jul 22, 2026
8c76486
build(cli): go-html v0.14.0 — TermTheme.Content lever, row-scoped ver…
Snider Jul 22, 2026
fdf3c9b
feat(tui): shell region and chat transcript join the declarative surface
Snider Jul 22, 2026
3ce6ba6
merge: shell region + declarative transcript on ctml (lane/ctml-region)
Snider Jul 22, 2026
4e18eaa
build(cli): go-html v0.15.0 — AsideWidth request, GutterRule, vertica…
Snider Jul 22, 2026
59a3487
feat(tui): restore the 32-col inspector, the │ rule, and the HC pane-…
Snider Jul 22, 2026
60f97bf
merge: v0.15.0 restores — 32-col inspector, │ rule, HC pane pair (lan…
Snider Jul 22, 2026
b67f287
merge: sync dev — CI matrix, artifact grid, PR gate (post #13/#14)
Snider Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
charm.land/log/v2 v2.0.0 // indirect
charm.land/wish/v2 v2.0.0 // indirect
dappco.re/go/cgo v0.11.2 // indirect
dappco.re/go/html v0.11.0 // direct: cli/tui renders .ctml (tabs.go)
dappco.re/go/html v0.15.0 // direct: cli/tui renders .ctml (tabs.go)
dappco.re/go/i18n v0.12.1 // indirect
dappco.re/go/log v0.13.1 // indirect
dappco.re/go/ratelimit v0.12.1 // indirect
Expand Down
10 changes: 10 additions & 0 deletions cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ dappco.re/go/container v0.11.0 h1:XbZS3VdMoxHIn1EQTnU44kJSB/H95rVrZ6ceujAUPSE=
dappco.re/go/container v0.11.0/go.mod h1:DTA3lcbMG3bfve8hlDls1UNxRgGAZELCGuuM5XKSpgg=
dappco.re/go/html v0.11.0 h1:Qau6MinQATThPmMamnzyaSV06mr10QzDdAcvkIqwlBk=
dappco.re/go/html v0.11.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/html v0.12.0 h1:/5hDG/AcOrFagkGk/7BeRnIichJ6+2qqRAbEdBgYrfE=
dappco.re/go/html v0.12.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/html v0.13.0 h1:EgMdKzZ2mX++2tlNSVk2VrexKM74SmwAAtlwSuG0cSA=
dappco.re/go/html v0.13.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/html v0.13.1 h1:ILgEjxaiof8eOqp44791Gtij+t5PfvT6u7E2K0Wbw2s=
dappco.re/go/html v0.13.1/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/html v0.14.0 h1:vUDiFe4b5IXF2xFCFPSPwwlXwWW5Cf+OhDQq0gnt/iY=
dappco.re/go/html v0.14.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/html v0.15.0 h1:XMFA7fKY07F3gskoF/9+NYo1WlLaNmoGEFmOofWvnRw=
dappco.re/go/html v0.15.0/go.mod h1:vhB4+z5cz6WYRqG7c/sDAOxm7Esc8GHEVnOpoN48+r4=
dappco.re/go/i18n v0.12.1 h1:Pm1DF9I0O8bckTr815OrnR+G+7EvbQhP2Vk4ItC0mB4=
dappco.re/go/i18n v0.12.1/go.mod h1:AYx1QkPfW+qvzLCfJnuOlc4J+grwlgFzoHnoke4TAYg=
dappco.re/go/inference v0.14.0 h1:3Lddj4W8T7/Mg8iTmvS+hCUkUqYYhRPYGe6i/ILF1+o=
Expand Down
329 changes: 317 additions & 12 deletions cli/tui/README.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions cli/tui/agentanswer.ctml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- SPDX-Licence-Identifier: EUPL-1.2 -->
<!--
agentanswer.ctml : the answer-agent-question overlay — one textarea
feeding a native run's pending question.

Idiom: <layout variant="HF"> via ctml.ParseLayout — the live textarea
emits pre-styled ANSI, which cannot ride a .ctml document, so the
layout renders the header and footer bands and the host composes the
textarea between them (renderOverlayFrame splits at the H box height).

Host seams (agentoverlay.go):
- Sequence question : zero-or-one row {text} — the question line
(a lone dynamic value rides a one-row sequence).
- Classes overlay-keys : mapped onto the existing uiStyles palette
by overlayFrameTheme.
-->
<layout variant="HF">
<h><h2>Answer agent question</h2><each items="question" as="row"><p>{{row.text}}</p></each></h>
<f><p class="overlay-keys">enter submits · esc cancels</p></f>
</layout>
172 changes: 156 additions & 16 deletions cli/tui/agentoverlay.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
package tui

import (
_ "embed"

Check warning on line 6 in cli/tui/agentoverlay.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add a comment explaining why this blank import is needed.

See more on https://sonarcloud.io/project/issues?id=dAppCore_go-inference&issues=AZ-Ji06TSmOdQpsKhN3z&open=AZ-Ji06TSmOdQpsKhN3z&pullRequest=12

"github.com/charmbracelet/bubbles/textarea"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"

core "dappco.re/go"
"dappco.re/go/html/ctml"
)

// workEditor keeps Work creation and editing local to the TUI. It deliberately
Expand Down Expand Up @@ -97,6 +100,35 @@
return core.Trim(editor.title.Value()), core.Trim(editor.task.Value()), core.Trim(editor.repository.Value())
}

// workEditorCTML is the Work editor overlay's markup — see workeditor.ctml
// for the seams it exposes (the create/edit title split, the conditional
// validation line, class tokens).
//
//go:embed workeditor.ctml
var workEditorCTML []byte

// workEditorBindings selects the create-vs-edit title — both texts are
// static markup, so the mode is carried by which zero-or-one-row sequence
// holds the row, the selection-as-sequence-split idiom — and binds the
// save-validation error (zero-or-one row; an empty sequence renders
// nothing).
func workEditorBindings(editor *workEditor) ctml.Bindings {
sequences := map[string][]map[string]any{
"createTitle": {},
"editTitle": {},
"validation": {},
}
if editor != nil && editor.editingID != "" {
sequences["editTitle"] = append(sequences["editTitle"], map[string]any{})
} else {
sequences["createTitle"] = append(sequences["createTitle"], map[string]any{})
}
if editor != nil && editor.validation != "" {
sequences["validation"] = append(sequences["validation"], map[string]any{"text": editor.validation})
}
return ctml.Bindings{Sequences: sequences}
}

func (editor *workEditor) View(width, height int, styles uiStyles) string {
if editor == nil {
return ""
Expand All @@ -105,11 +137,8 @@
editor.title.Width = fieldWidth
editor.repository.Width = fieldWidth
editor.task.SetWidth(fieldWidth)
title := "Create Work"
if editor.editingID != "" {
title = "Edit Work"
}
return fitPane(core.Join("\n", title, "", "Work title", editor.title.View(), "", "Full task", editor.task.View(), "", "Repository", editor.repository.View(), "", "tab changes field · ctrl+s saves · esc cancels", editor.validation), width, height, styles.panel)
head, foot := renderOverlayFrame(workEditorCTML, width, styles, workEditorBindings(editor))
return fitPane(core.Join("\n", head, editor.title.View(), "", "Full task", editor.task.View(), "", "Repository", editor.repository.View(), foot), width, height, styles.panel)
}

type launchReviewOverlay struct {
Expand Down Expand Up @@ -160,12 +189,31 @@
return core.Trim(overlay.input.Value())
}

// agentAnswerCTML is the answer overlay's markup — see agentanswer.ctml
// for the seams it exposes (the question row, class tokens).
//
//go:embed agentanswer.ctml
var agentAnswerCTML []byte

// agentAnswerBindings binds the pending question — a lone dynamic value
// riding a one-row sequence.
func agentAnswerBindings(overlay *agentAnswerOverlay) ctml.Bindings {
question := ""
if overlay != nil {
question = overlay.question
}
return ctml.Bindings{Sequences: map[string][]map[string]any{
"question": {{"text": question}},
}}
}

func (overlay *agentAnswerOverlay) View(width, height int, styles uiStyles) string {
if overlay == nil {
return ""
}
overlay.input.SetWidth(max(12, width-6))
return fitPane(core.Join("\n", "Answer agent question", "", overlay.question, "", overlay.input.View(), "", "enter submits · esc cancels"), width, height, styles.panel)
head, foot := renderOverlayFrame(agentAnswerCTML, width, styles, agentAnswerBindings(overlay))
return fitPane(core.Join("\n", head, "", overlay.input.View(), foot), width, height, styles.panel)
}

type changeAcceptanceOverlay struct {
Expand Down Expand Up @@ -208,20 +256,50 @@
return true
}

// changeReviewCTML is the change-acceptance overlay's markup — see
// changereview.ctml for the seams it exposes (the title row, the
// conditional warning, the three-way prompt split, class tokens).
//
//go:embed changereview.ctml
var changeReviewCTML []byte

// changeAcceptanceBindings binds the review title and warning, and selects
// the gate prompt: all three prompt texts are static markup, so the gate
// stage is carried by which zero-or-one-row sequence holds the row, the
// selection-as-sequence-split idiom.
func changeAcceptanceBindings(overlay *changeAcceptanceOverlay) ctml.Bindings {
sequences := map[string][]map[string]any{
"review": {},
"warn": {},
"promptContinue": {},
"promptAcknowledge": {},
"promptApply": {},
}
if overlay != nil {
sequences["review"] = append(sequences["review"], map[string]any{"title": overlay.review.Title})
if overlay.review.Warning != "" {
sequences["warn"] = append(sequences["warn"], map[string]any{"text": overlay.review.Warning})
}
switch {
case overlay.final:
sequences["promptApply"] = append(sequences["promptApply"], map[string]any{})
case overlay.review.NeedsAcknowledgement && !overlay.acknowledged:
sequences["promptAcknowledge"] = append(sequences["promptAcknowledge"], map[string]any{})
default:
sequences["promptContinue"] = append(sequences["promptContinue"], map[string]any{})
}
}
return ctml.Bindings{Sequences: sequences}
}

func (overlay *changeAcceptanceOverlay) View(width, height int, styles uiStyles) string {
if overlay == nil {
return ""
}
prompt := "enter continues · esc cancels"
if overlay.review.NeedsAcknowledgement && !overlay.acknowledged {
prompt = "a acknowledges no validation · enter continues · esc cancels"
}
if overlay.final {
prompt = "enter applies this exact reviewed receipt · esc cancels"
}
overlay.viewport.Width, overlay.viewport.Height = max(1, width-4), max(1, height-6)
overlay.viewport.SetContent(overlay.review.Body)
return fitPane(core.Join("\n", overlay.review.Title, overlay.review.Warning, "", overlay.viewport.View(), "", prompt), width, height, styles.panel)
head, foot := renderOverlayFrame(changeReviewCTML, width, styles, changeAcceptanceBindings(overlay))
return fitPane(core.Join("\n", head, overlay.viewport.View(), foot), width, height, styles.panel)
}

func newLaunchReviewOverlay(review agentReview, provider, model string) *launchReviewOverlay {
Expand Down Expand Up @@ -311,6 +389,67 @@
return core.Trim(overlay.providerInput.Value()), core.Trim(overlay.modelInput.Value())
}

// launchReviewCTML and agentSelectCTML are the launch-review overlay's two
// markup shapes — launchreview.ctml (read-only receipt review, full HCF)
// and agentselect.ctml (editable provider/model selection, HF around the
// inputs); see each file for the seams it exposes.
//
//go:embed launchreview.ctml
var launchReviewCTML []byte

//go:embed agentselect.ctml
var agentSelectCTML []byte

// reviewBodyRows splits a multi-line receipt body into one row per line —
// a bound value cannot carry a line break through an inline run, so each
// row closes with <br> in the markup; empty lines ride as empty rows.
func reviewBodyRows(body string) []map[string]any {
rows := []map[string]any{}
for _, line := range core.Split(body, "\n") {
rows = append(rows, map[string]any{"line": line})
}
return rows
}

// launchReviewBindings binds the read-only shape: the title with the
// defaulted provider/model pair (one row), the conditional warning, and
// the receipt body lines.
func launchReviewBindings(overlay *launchReviewOverlay, provider, model string) ctml.Bindings {
sequences := map[string][]map[string]any{
"review": {},
"warn": {},
"body": {},
}
if overlay != nil {
sequences["review"] = append(sequences["review"], map[string]any{
"title": overlay.review.Title, "provider": provider, "model": model,
})
if overlay.review.Warning != "" {
sequences["warn"] = append(sequences["warn"], map[string]any{"text": overlay.review.Warning})
}
sequences["body"] = reviewBodyRows(overlay.review.Body)
}
return ctml.Bindings{Sequences: sequences}
}

// agentSelectionBindings binds the editable shape: the title (one row),
// the receipt body lines, and the conditional warning.
func agentSelectionBindings(overlay *launchReviewOverlay) ctml.Bindings {
sequences := map[string][]map[string]any{
"review": {},
"body": {},
"warn": {},
}
if overlay != nil {
sequences["review"] = append(sequences["review"], map[string]any{"title": overlay.review.Title})
sequences["body"] = reviewBodyRows(overlay.review.Body)
if overlay.review.Warning != "" {
sequences["warn"] = append(sequences["warn"], map[string]any{"text": overlay.review.Warning})
}
}
return ctml.Bindings{Sequences: sequences}
}

func (overlay *launchReviewOverlay) View(width, height int, styles uiStyles) string {
if overlay == nil {
return ""
Expand All @@ -323,12 +462,13 @@
model = "default model"
}
if !overlay.editable {
return fitPane(core.Join("\n", overlay.review.Title, "", "Provider: "+provider, "Model: "+model, "", overlay.review.Warning, "", overlay.review.Body, "", "enter confirms · esc cancels"), width, height, styles.panel)
return fitPane(renderOverlayLayout(launchReviewCTML, width, styles, launchReviewBindings(overlay, provider, model)), width, height, styles.panel)
}
fieldWidth := max(12, width-6)
overlay.providerInput.Width = fieldWidth
overlay.modelInput.Width = fieldWidth
return fitPane(core.Join("\n", overlay.review.Title, "", "Provider", overlay.providerInput.View(), "Model", overlay.modelInput.View(), "", overlay.review.Body, "", overlay.review.Warning, "", "tab selects provider/model · enter confirms · esc cancels"), width, height, styles.panel)
head, foot := renderOverlayFrame(agentSelectCTML, width, styles, agentSelectionBindings(overlay))
return fitPane(core.Join("\n", head, overlay.providerInput.View(), "Model", overlay.modelInput.View(), foot), width, height, styles.panel)
}

type agentActionMsg struct {
Expand Down
Loading
Loading