diff --git a/cmd/mecatui/theme/theme.go b/cmd/mecatui/theme/theme.go
index 96d77ba4ca..5ee759a5ef 100644
--- a/cmd/mecatui/theme/theme.go
+++ b/cmd/mecatui/theme/theme.go
@@ -278,6 +278,11 @@ func (t *Theme) compile() {
BorderTop(true).
BorderForeground(col(p.Border)),
+ // Activity line above the input box: muted, same inset as the footer.
+ "activity": lipgloss.NewStyle().
+ Foreground(col(p.TextMuted)).
+ Padding(0, 1),
+
// Conversation viewport surface.
"viewport": lipgloss.NewStyle().
Foreground(col(p.Text)),
diff --git a/cmd/mecatui/theme/theme_test.go b/cmd/mecatui/theme/theme_test.go
index 60385112d7..17fcf89f20 100644
--- a/cmd/mecatui/theme/theme_test.go
+++ b/cmd/mecatui/theme/theme_test.go
@@ -34,7 +34,7 @@ func TestBuiltinsSlotCompleteness(t *testing.T) {
// silently returns an empty style — this guards against that drift.
func TestStylesCompiled(t *testing.T) {
want := []string{
- "header", "footer", "viewport", "userBlock", "userLabel",
+ "header", "footer", "activity", "viewport", "userBlock", "userLabel",
"assistantLabel", "toolCard", "toolName", "toolArgs", "toolOk",
"toolErr", "askCard", "askTitle", "askArgs", "askButton", "askButtonActive",
"spinner", "muted", "warning", "dangerPill", "errorText", "selection",
diff --git a/cmd/mecatui/ui/agents_overlay_test.go b/cmd/mecatui/ui/agents_overlay_test.go
index fcf5721b1a..13c6633292 100644
--- a/cmd/mecatui/ui/agents_overlay_test.go
+++ b/cmd/mecatui/ui/agents_overlay_test.go
@@ -564,7 +564,7 @@ func TestSubagentRosterRowSeparatesTitleAndDetails(t *testing.T) {
func TestSubagentRosterWindowed(t *testing.T) {
const n = 20
m := newMCPModel(t, aztec(), nil)
- m = resize(m, 100, 24)
+ m = resize(m, 100, 25)
msgs := make([]client.SubagentMsg, 0, n)
for i := 0; i < n; i++ {
child := "child-" + string(rune('a'+i))
diff --git a/cmd/mecatui/ui/ask_queue_test.go b/cmd/mecatui/ui/ask_queue_test.go
index d1b88da868..3c19d9345a 100644
--- a/cmd/mecatui/ui/ask_queue_test.go
+++ b/cmd/mecatui/ui/ask_queue_test.go
@@ -51,8 +51,8 @@ func resumeApprovalAskIDs(send *fakeSender) []string {
}
// TestAskEnqueuedWhileModalOpen: a second PermissionAskMsg while a modal is open
-// ENQUEUES behind the visible head (never clobbers it), and both the footer and
-// the modal title advertise the queue with a "(1 of 2)" badge.
+// ENQUEUES behind the visible head (never clobbers it), and both the activity line
+// and the modal title advertise the queue with a "(1 of 2)" badge.
func TestAskEnqueuedWhileModalOpen(t *testing.T) {
m, _ := queuedAskModel(t)
if approvalSurfaceOf(t, m).ask.AskID != askA {
@@ -64,8 +64,11 @@ func TestAskEnqueuedWhileModalOpen(t *testing.T) {
if m.phase != phaseAwaitingApproval {
t.Fatalf("phase = %v, want phaseAwaitingApproval", m.phase)
}
- if footer := stripANSIstr(m.renderFooter()); !strings.Contains(footer, "(1 of 2)") {
- t.Errorf("footer must carry the queue badge, got %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); !strings.Contains(activity, "(1 of 2)") {
+ t.Errorf("activity line must carry the queue badge, got %q", activity)
+ }
+ if footer := stripANSIstr(m.renderFooter()); strings.Contains(footer, "(1 of 2)") {
+ t.Errorf("the queue badge must not be duplicated in the footer, got %q", footer)
}
modal := stripANSIstr(approvalSurfaceOf(t, m).renderPermissionModal(100, 24))
if !strings.Contains(modal, "Permission required (1 of 2)") {
@@ -77,7 +80,7 @@ func TestAskEnqueuedWhileModalOpen(t *testing.T) {
// queued successor sends the approval for the ANSWERED ask, pops the successor
// into the modal, STAYS awaitingApproval, and does NOT re-arm the spinner (the
// spinner is still off-screen under the successor modal — keep in sync with
-// TestSpinnerVisibleMatchesFooterRender).
+// TestSpinnerVisibleMatchesActivityRender).
func TestResolveAskAdvancesQueueNoSpinnerRearm(t *testing.T) {
m, send := queuedAskModel(t)
m, cmd := pressKey(m, tea.KeyPressMsg{Code: 'a', Text: "a"})
@@ -122,8 +125,8 @@ func TestResolveAskFIFOOrderTwoDeep(t *testing.T) {
if approvalSurfaceOf(t, m).ask.AskID != askB {
t.Fatalf("FIFO violated: head = %q, want %q (B before C)", approvalSurfaceOf(t, m).ask.AskID, askB)
}
- if footer := stripANSIstr(m.renderFooter()); !strings.Contains(footer, "(1 of 2)") {
- t.Errorf("with one ask still queued the footer badge must read (1 of 2), got %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); !strings.Contains(activity, "(1 of 2)") {
+ t.Errorf("with one ask still queued the activity badge must read (1 of 2), got %q", activity)
}
// Answer B: C heads, queue now EMPTY → no badge anywhere (never "(1 of 1)").
@@ -134,8 +137,8 @@ func TestResolveAskFIFOOrderTwoDeep(t *testing.T) {
if approvalSurfaceOf(t, m).ask.AskID != askC || len(approvalSurfaceOf(t, m).queue) != 0 {
t.Fatalf("want C visible with an empty queue, got head %q queue %+v", approvalSurfaceOf(t, m).ask.AskID, approvalSurfaceOf(t, m).queue)
}
- if footer := stripANSIstr(m.renderFooter()); strings.Contains(footer, "(1 of") {
- t.Errorf("the badge must vanish at queue-empty, footer = %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); strings.Contains(activity, "(1 of") {
+ t.Errorf("the badge must vanish at queue-empty, activity = %q", activity)
}
modal := stripANSIstr(approvalSurfaceOf(t, m).renderPermissionModal(100, 24))
if strings.Contains(modal, "(1 of") {
@@ -290,8 +293,8 @@ func TestEndRunClearsAskQueue(t *testing.T) {
if m.phase != phaseAwaitingApproval || approvalSurfaceOf(t, m).ask.AskID != askB {
t.Fatalf("a fresh ask after run end must open the modal (not be swallowed by a stale answered-set), got phase=%v ask=%+v", m.phase, approvalSurfaceOf(t, m).ask)
}
- if footer := stripANSIstr(m.renderFooter()); strings.Contains(footer, "(1 of") {
- t.Errorf("a fresh single ask must carry no phantom queue badge, footer = %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); strings.Contains(activity, "(1 of") {
+ t.Errorf("a fresh single ask must carry no phantom queue badge, activity = %q", activity)
}
})
}
@@ -319,8 +322,8 @@ func TestResetSessionDropsAskQueue(t *testing.T) {
func TestAskQueueBadgeWithLongArgsHeadAsk(t *testing.T) {
m, _ := queuedAskModel(t) // A (Shell) visible, B (Write) queued
approvalSurfaceOf(t, m).ask.Args = longShellArgs
- if footer := stripANSIstr(m.renderFooter()); !strings.Contains(footer, "(1 of 2)") {
- t.Errorf("footer must carry the queue badge, got %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); !strings.Contains(activity, "(1 of 2)") {
+ t.Errorf("activity line must carry the queue badge, got %q", activity)
}
modal := stripANSIstr(m.renderBody())
if !strings.Contains(modal, "Permission required (1 of 2)") {
diff --git a/cmd/mecatui/ui/canonical_status_test.go b/cmd/mecatui/ui/canonical_status_test.go
index aaff471425..8b1309272b 100644
--- a/cmd/mecatui/ui/canonical_status_test.go
+++ b/cmd/mecatui/ui/canonical_status_test.go
@@ -65,7 +65,7 @@ func TestCanonicalStatus_Scenario1_EmptySourceDoesNotFallBack(t *testing.T) {
if strings.Contains(header, "session") || strings.Contains(header, "canonical-model") || strings.Contains(header, "oversize") || strings.Contains(header, "·") || strings.Contains(footer, "ctx") || strings.Contains(footer, "oversize") {
t.Fatalf("source fallback: %q / %q", header, footer)
}
- if !strings.Contains(header, "auto") || !strings.Contains(footer, "ready") || !strings.Contains(footer, "help") {
+ if !strings.Contains(header, "auto") || !strings.Contains(stripANSIstr(m.renderActivity()), "ready") || !strings.Contains(footer, "help") {
t.Fatalf("lost mandatory chrome: %q / %q", header, footer)
}
}
@@ -84,7 +84,7 @@ func TestCanonicalStatus_Scenario1_NoSourceMinimalIdentity(t *testing.T) {
} else if !strings.Contains(header, "session "+handle) {
t.Fatalf("known identity: %q", header)
}
- if strings.Contains(header, "canonical-model") || strings.Contains(header, "[31m") || strings.Contains(footer, "ctx") || strings.Contains(footer, "↑") || !strings.Contains(header, "auto") || !strings.Contains(footer, "ready") || !strings.Contains(footer, "help") {
+ if strings.Contains(header, "canonical-model") || strings.Contains(header, "[31m") || strings.Contains(footer, "ctx") || strings.Contains(footer, "↑") || !strings.Contains(header, "auto") || !strings.Contains(stripANSIstr(m.renderActivity()), "ready") || !strings.Contains(footer, "help") {
t.Fatalf("unsafe/legacy identity or lost chrome: %q / %q", header, footer)
}
}
diff --git a/cmd/mecatui/ui/footer_test.go b/cmd/mecatui/ui/footer_test.go
index f5b33d2554..f0db004df0 100644
--- a/cmd/mecatui/ui/footer_test.go
+++ b/cmd/mecatui/ui/footer_test.go
@@ -12,17 +12,17 @@ import (
)
// TestFooterSelectionCount: an idle model with a known multi-line, non-empty
-// selection shows the live "N chars · M lines" count in the footer-left.
+// selection shows the live "N chars · M lines" count in the activity line.
func TestFooterSelectionCount(t *testing.T) {
m, _ := selModel(t)
m.vp.SetContent("hello world\nsecond line\nthird row")
// Select "world\nsecond line\nthird" — line0col6 .. line2col5.
m.sel = selection{active: true, anchorL: 0, anchorC: 6, headL: 2, headC: 5}
m.phase = phaseIdle
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
// "world" (5) + "\n" + "second line" (11) + "\n" + "third" (5) = 23 chars.
if !strings.Contains(got, "23 chars · 3 lines") {
- t.Errorf("footer = %q, want it to contain %q", got, "23 chars · 3 lines")
+ t.Errorf("activity = %q, want it to contain %q", got, "23 chars · 3 lines")
}
}
@@ -34,14 +34,14 @@ func TestFooterSelectionCountSingular(t *testing.T) {
// Select a single character on line0: col0..col1 ("h").
m.sel = selection{active: true, anchorL: 0, anchorC: 0, headL: 0, headC: 1}
m.phase = phaseIdle
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, "1 char · 1 line") {
- t.Errorf("footer = %q, want it to contain %q", got, "1 char · 1 line")
+ t.Errorf("activity = %q, want it to contain %q", got, "1 char · 1 line")
}
}
// TestFooterSelectionCountAfterCopy: after a copy (statusMsg carries "copied …")
-// while the selection is still active, the footer prefixes the count with
+// while the selection is still active, the activity line prefixes the count with
// "copied · " — the selection persists past the copy (Req 7). The selection is
// made via the REAL drag gesture so the identity snapshot matches and the
// copy→refreshView path KEEPS it (a manual SetContent would be overwritten by the
@@ -55,7 +55,7 @@ func TestFooterSelectionCountAfterCopy(t *testing.T) {
if !m.sel.active || m.sel.empty() {
t.Fatal("precondition: an active non-empty selection")
}
- // Compute the count the footer should report from the model's own state.
+ // Compute the count the activity line should report from the model's own state.
chars := len([]rune(selectedText(m.vp.GetContent(), m.sel)))
startL, _, endL, _ := m.sel.normalize()
wantLines := endL - startL + 1
@@ -66,46 +66,46 @@ func TestFooterSelectionCountAfterCopy(t *testing.T) {
if !m.sel.active {
t.Fatal("selection must persist past a copy (Req 7)")
}
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, want) {
- t.Errorf("footer = %q, want it to contain %q", got, want)
+ t.Errorf("activity = %q, want it to contain %q", got, want)
}
}
-// TestFooterNoSelectionShowsStatus: with no active selection the footer shows the
+// TestFooterNoSelectionShowsStatus: with no active selection the activity line shows the
// existing statusMsg, or "ready" when it's empty.
func TestFooterNoSelectionShowsStatus(t *testing.T) {
m, _ := selModel(t)
m.sel = selection{} // inactive
m.phase = phaseIdle
m.statusMsg = ""
- if got := stripANSIstr(m.renderFooter()); !strings.Contains(got, "ready") {
- t.Errorf("footer = %q, want it to contain %q", got, "ready")
+ if got := stripANSIstr(m.renderActivity()); !strings.Contains(got, "ready") {
+ t.Errorf("activity = %q, want it to contain %q", got, "ready")
}
m.statusMsg = "connected"
- if got := stripANSIstr(m.renderFooter()); !strings.Contains(got, "connected") {
- t.Errorf("footer = %q, want it to contain %q", got, "connected")
+ if got := stripANSIstr(m.renderActivity()); !strings.Contains(got, "connected") {
+ t.Errorf("activity = %q, want it to contain %q", got, "connected")
}
}
// TestFooterSelectionCountSuppressedWhileRunning: a running phase owns the
-// footer-left (spinner path), so even with an active selection the count is NOT
+// activity line (spinner path), so even with an active selection the count is NOT
// shown — the count is idle/default-only by construction (Req 5).
func TestFooterSelectionCountSuppressedWhileRunning(t *testing.T) {
m, _ := selModel(t)
m.vp.SetContent("hello world\nsecond line\nthird row")
m.sel = selection{active: true, anchorL: 0, anchorC: 6, headL: 2, headC: 5}
m.phase = phaseRunning
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if strings.Contains(got, "chars · ") {
- t.Errorf("footer while running must NOT show the selection count, got %q", got)
+ t.Errorf("activity while running must NOT show the selection count, got %q", got)
}
}
// TestResultMsgStopReachesFooter is the end-to-end regression guard for the stop
// reason wiring (issue #81 Part 5): a terminal client.ResultMsg{Stop} must drive
-// applyResult → endRun → renderfmt.StopReasonLabel → m.statusMsg, and the rendered footer
-// must show the human label. It covers the explicit-mapped reasons and an unknown
+// applyResult → endRun → renderfmt.StopReasonLabel → m.statusMsg, and the rendered activity
+// line must show the human label. It covers the explicit-mapped reasons and an unknown
// passthrough. structured_output is now explicitly phrased ("stopped · schema
// unmet") so the raw underscore'd token never leaks even though it is a
// subagent-only stop that does not reach the main footer today.
@@ -125,9 +125,9 @@ func TestResultMsgStopReachesFooter(t *testing.T) {
m, _, _ := newTestModel(t, theme.New("aztec", theme.AztecPalette()))
m.phase = phaseRunning
m = applyAll(m, client.ResultMsg{Stop: c.stop})
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, c.want) {
- t.Errorf("ResultMsg{Stop:%q} → footer = %q, want it to contain %q", c.stop, got, c.want)
+ t.Errorf("ResultMsg{Stop:%q} → activity = %q, want it to contain %q", c.stop, got, c.want)
}
}
}
diff --git a/cmd/mecatui/ui/input_rail_test.go b/cmd/mecatui/ui/input_rail_test.go
index 1c55be55ce..8a14d94476 100644
--- a/cmd/mecatui/ui/input_rail_test.go
+++ b/cmd/mecatui/ui/input_rail_test.go
@@ -20,23 +20,34 @@ import (
// match. Defined here because it is the regression guard for renderInputRail.
var plainTail = regexp.MustCompile(`\x1b\[0?m {1,}(?:\x1b\[0?m)*$`)
-// TestInputRailAddsOnlyTopPadRow is the load-bearing layout invariant for the input
+// TestInputRailAddsOnlyPadRows is the load-bearing layout invariant for the input
// mode-rail: the BorderLeft adds ZERO rows, and the ONLY vertical growth is the
-// intentional inputRailPadTop top-padding row, so the rail-wrapped input is exactly
-// bare + inputRailPadTop rows tall. The layout measures region heights via
-// lipgloss.Height, so a height that matches this keeps regionInput/relayout correct; any
-// OTHER drift would silently steal (or add) a viewport row.
-func TestInputRailAddsOnlyTopPadRow(t *testing.T) {
+// intentional top and bottom padding rows, so the rail-wrapped input is exactly
+// bare + inputRailPadTop + inputRailPadBottom rows tall — whether empty or holding
+// multi-row text. The layout measures region heights via lipgloss.Height, so a height
+// that matches this keeps regionInput/relayout correct; any OTHER drift would silently
+// steal (or add) a viewport row.
+func TestInputRailAddsOnlyPadRows(t *testing.T) {
th := theme.New("aztec", theme.AztecPalette())
for _, w := range []int{40, 80, 120} {
- m, _, _ := newTestModel(t, th)
- m = applyAll(m, tea.WindowSizeMsg{Width: w, Height: 30},
- client.SessionReadyMsg{SessionID: "sess-test-0001"})
-
- bare := m.prompt.View()
- railed := m.renderInput()
- if got, want := lipgloss.Height(railed), lipgloss.Height(bare)+inputRailPadTop; got != want {
- t.Errorf("width %d: rail input height = %d rows, want %d (bare %d + top pad %d)", w, got, want, lipgloss.Height(bare), inputRailPadTop)
+ for _, text := range []string{"", strings.Repeat("wrapped text ", 20)} {
+ m, _, _ := newTestModel(t, th)
+ m = applyAll(m, tea.WindowSizeMsg{Width: w, Height: 30},
+ client.SessionReadyMsg{SessionID: "sess-test-0001"})
+ m.prompt.Rewrite(text)
+ m.rend.inputValid = false
+
+ bare := m.prompt.View()
+ railed := m.renderInput()
+ pad := inputRailPadTop + inputRailPadBottom
+ if got, want := lipgloss.Height(railed), lipgloss.Height(bare)+pad; got != want {
+ t.Errorf("width %d, %d text rows: rail input height = %d rows, want %d (bare %d + pad %d)", w, m.prompt.Height(), got, want, lipgloss.Height(bare), pad)
+ }
+ // Typed text never consumes the bottom padding row.
+ rows := strings.Split(stripANSIstr(railed), "\n")
+ if last := strings.TrimLeft(rows[len(rows)-1], "│ "); last != "" {
+ t.Errorf("width %d, %d text rows: bottom pad row holds text %q", w, m.prompt.Height(), last)
+ }
}
}
}
@@ -81,13 +92,14 @@ func TestInputRailFillsUniformly(t *testing.T) {
if emptyW != typedW {
t.Errorf("width %d: empty (%d) and typed (%d) input widths differ — the tint must be uniform regardless of content", w, emptyW, typedW)
}
- // Every textarea row is present and tinted, PLUS the one top-padding row
- // (inputRailPadTop): height == ta.Height() + the pad. No zero-width empty rows.
- if got, want := lipgloss.Height(empty), me.prompt.Height()+inputRailPadTop; got != want {
- t.Errorf("width %d: empty input block height = %d rows, want %d (textarea rows + %d top pad)", w, got, want, inputRailPadTop)
+ // Every textarea row is present and tinted, PLUS the top and bottom padding rows:
+ // height == ta.Height() + the pads. No zero-width empty rows.
+ pad := inputRailPadTop + inputRailPadBottom
+ if got, want := lipgloss.Height(empty), me.prompt.Height()+pad; got != want {
+ t.Errorf("width %d: empty input block height = %d rows, want %d (textarea rows + %d pad)", w, got, want, pad)
}
- if got, want := lipgloss.Height(typed), mt.prompt.Height()+inputRailPadTop; got != want {
- t.Errorf("width %d: typed input block height = %d rows, want %d (textarea rows + %d top pad)", w, got, want, inputRailPadTop)
+ if got, want := lipgloss.Height(typed), mt.prompt.Height()+pad; got != want {
+ t.Errorf("width %d: typed input block height = %d rows, want %d (textarea rows + %d pad)", w, got, want, pad)
}
// EVEN tint: every row's fill must run flush to the right edge — NO trailing
// PLAIN (unstyled) cells. The bug's signature is a bare reset (\x1b[m / \x1b[0m)
diff --git a/cmd/mecatui/ui/layout.go b/cmd/mecatui/ui/layout.go
index 5104426a10..301e824444 100644
--- a/cmd/mecatui/ui/layout.go
+++ b/cmd/mecatui/ui/layout.go
@@ -9,7 +9,7 @@ import (
// layout.go is the SINGLE SOURCE OF TRUTH for the TUI's vertical region stack. The
// frame is a column of stacked regions joined with "\n": a header on top, the
// conversation body, zero or more transient inline regions (slash palette, @-mention
-// menu, queued-follow-ups card), then the input and footer chrome. THREE consumers
+// menu, queued-follow-ups card), then the activity line, input, and footer chrome. THREE consumers
// derive from the SAME model: View() (which renders it), the relayout step in
// update.go (which sizes the viewport so the footer is never pushed off-screen), and
// screenToContent/convTopRow in selection.go (which map a mouse cell to the body).
@@ -44,17 +44,21 @@ const (
regionPalette
regionMention
regionQueue
- // regionInputSpacer is a single blank row directly ABOVE the input box, so the input
- // isn't jammed against the conversation/transient area. It sits in the `below` slice
- // (consumed by relayout's body-height subtraction), so the body shrinks by its one
- // row; convTopRow (which sums only the regions ABOVE the body) is unaffected.
+ // regionInputSpacer is a single blank row on each side of the activity line, so the
+ // line isn't jammed against the conversation/transient area or the input box. The
+ // spacers sit in the `below` slice (consumed by relayout's body-height subtraction);
+ // convTopRow (which sums only the regions ABOVE the body) is unaffected. chrome()
+ // drops both spacers when keeping them would leave no conversation row.
regionInputSpacer
+ // regionActivity is the renderer-owned activity line (ready / thinking / tool
+ // progress / approval / connecting) directly above the input box.
+ regionActivity
regionInput
regionFooter
)
// inputSpacerRow is the content of regionInputSpacer: a single blank row
-// (lipgloss.Height("") == 1) giving the input box one row of top padding.
+// (lipgloss.Height("") == 1).
const inputSpacerRow = ""
// region is one rendered vertical slice of the frame: its role and the exact styled
@@ -93,13 +97,13 @@ func sumHeight(rs []region) int {
// one:
//
// above = [header]
-// below = [palette?, mention?, queue?, input, footer]
+// below = [palette?, mention?, queue?, spacer?, activity, spacer?, input, footer]
//
// The palette/mention/queue regions are appended ONLY when their renderer returns a
-// non-empty string — the same gate View() used — so a no-transient frame produces
-// exactly [header] + body + [input, footer], identical to before. This is the one
-// place the transient conditions live; View(), relayout, and convTopRow all consume
-// the result rather than re-checking them.
+// non-empty string. Both spacers are dropped together when keeping them would leave
+// no conversation row. This is the one place the transient and spacer conditions
+// live; View(), relayout, and convTopRow all consume the result rather than
+// re-checking them.
func (m Model) chrome() (above, below []region) {
above = []region{{role: regionHeader, content: m.renderHeader()}}
@@ -112,11 +116,11 @@ func (m Model) chrome() (above, below []region) {
if s := m.renderSteer(); s != "" {
transients = append(transients, region{role: regionQueue, content: s})
}
- fixed := []region{
- {role: regionInputSpacer, content: inputSpacerRow},
- {role: regionInput, content: m.renderInput()},
- {role: regionFooter, content: m.renderFooter()},
- }
+ activity := region{role: regionActivity, content: m.renderActivity()}
+ input := region{role: regionInput, content: m.renderInput()}
+ footer := region{role: regionFooter, content: m.renderFooter()}
+ spacer := region{role: regionInputSpacer, content: inputSpacerRow}
+ fixed := []region{spacer, activity, spacer, input, footer}
card := m.deps.Theme.Style("askCard")
availableRows := m.height - sumHeight(above) - sumHeight(transients) - sumHeight(fixed) - 1 -
@@ -132,6 +136,9 @@ func (m Model) chrome() (above, below []region) {
below = append(below, region{role: regionMention, content: men})
}
below = append(below, transients...)
+ if m.height-sumHeight(above)-sumHeight(below)-sumHeight(fixed) < 1 {
+ fixed = []region{activity, input, footer}
+ }
below = append(below, fixed...)
return above, below
}
diff --git a/cmd/mecatui/ui/layout_test.go b/cmd/mecatui/ui/layout_test.go
index 63129dcd62..7891bae1c7 100644
--- a/cmd/mecatui/ui/layout_test.go
+++ b/cmd/mecatui/ui/layout_test.go
@@ -2,6 +2,7 @@ package ui
import (
"fmt"
+ "slices"
"strings"
"testing"
@@ -179,9 +180,9 @@ func TestTransientRegionShrinksViewportKeepsFooter(t *testing.T) {
// TestNoTransientBodyHeightMatchesMeasuredChrome is the magic-number-gone guard: in
// the NO-transient case the body height equals m.height minus the MEASURED header +
-// input-top-spacer + input + footer heights (lipgloss.Height of the rendered regions).
-// A reintroduced taH=4/footerH=2 constant — or a header-height assumption, or a forgotten
-// input spacer — would diverge here.
+// spacer + activity + spacer + input + footer heights (lipgloss.Height of the rendered
+// regions). A reintroduced taH=4/footerH=2 constant — or a header-height assumption, or
+// a forgotten spacer or activity row — would diverge here.
func TestNoTransientBodyHeightMatchesMeasuredChrome(t *testing.T) {
m, _ := selModel(t)
// Precondition: no transient is present.
@@ -189,10 +190,77 @@ func TestNoTransientBodyHeightMatchesMeasuredChrome(t *testing.T) {
renderMention(m.deps.Theme, m.mention, m.width) != "" {
t.Fatal("precondition: no transient should be present in selModel")
}
- want := m.height - lipgloss.Height(m.renderHeader()) - lipgloss.Height(inputSpacerRow) -
- lipgloss.Height(m.renderInput()) - lipgloss.Height(m.renderFooter())
+ want := m.height - lipgloss.Height(m.renderHeader()) - 2*lipgloss.Height(inputSpacerRow) -
+ lipgloss.Height(m.renderActivity()) - lipgloss.Height(m.renderInput()) - lipgloss.Height(m.renderFooter())
if got := m.vp.Height(); got != want {
- t.Errorf("no-transient bodyHeight = %d, want %d (height - measured header - input spacer - input - footer)", got, want)
+ t.Errorf("no-transient bodyHeight = %d, want %d (height - measured header - spacers - activity - input - footer)", got, want)
+ }
+}
+
+// TestActivityLineSitsDirectlyAboveInputBetweenSpacers pins the issue #1325 stack: with
+// room to spare, the activity line is framed by one blank spacer row on each side and
+// sits directly above the input; the footer no longer carries the activity text.
+func TestActivityLineSitsDirectlyAboveInputBetweenSpacers(t *testing.T) {
+ m, _ := selModel(t)
+ m.phase = phaseRunning
+ m.activeTool = "Bash"
+ _, below := m.chrome()
+ var roles []regionRole
+ for _, r := range below {
+ roles = append(roles, r.role)
+ }
+ want := []regionRole{regionInputSpacer, regionActivity, regionInputSpacer, regionInput, regionFooter}
+ if !slices.Equal(roles, want) {
+ t.Fatalf("below-body roles = %v, want %v", roles, want)
+ }
+ if got := ansi.Strip(below[1].content); !strings.Contains(got, "Running Bash") {
+ t.Errorf("activity region = %q, want it to carry the running tool", got)
+ }
+ if got := ansi.Strip(m.renderFooter()); strings.Contains(got, "Running Bash") {
+ t.Errorf("footer duplicates the activity text: %q", got)
+ }
+}
+
+// TestShortTerminalDropsActivitySpacersFirst proves both spacers are shed before the
+// conversation loses its last row, keeping the activity line directly above the input.
+func TestShortTerminalDropsActivitySpacersFirst(t *testing.T) {
+ m, _ := selModel(t)
+ m.height = 100
+ _, below := m.chrome()
+ m.height = sumHeight([]region{{content: m.renderHeader()}}) + sumHeight(below) // zero body rows with spacers
+ m.relayout()
+ _, below = m.chrome()
+ var roles []regionRole
+ for _, r := range below {
+ roles = append(roles, r.role)
+ }
+ if want := []regionRole{regionActivity, regionInput, regionFooter}; !slices.Equal(roles, want) {
+ t.Fatalf("short-terminal below-body roles = %v, want %v", roles, want)
+ }
+ if got := m.vp.Height(); got != 2 {
+ t.Errorf("short-terminal body height = %d, want the 2 rows freed by the spacers", got)
+ }
+ if frame := strings.Split(m.View().Content, "\n"); len(frame) > m.height {
+ t.Errorf("frame is %d rows, exceeds terminal height %d", len(frame), m.height)
+ }
+}
+
+// TestActivityLineIsOneBoundedRow keeps an arbitrarily long tool progress string on a
+// single row no wider than the terminal.
+func TestActivityLineIsOneBoundedRow(t *testing.T) {
+ m, _ := selModel(t)
+ m.phase = phaseRunning
+ m.activeTool = "Bash"
+ m.toolProgress = strings.Repeat("very long progress ", 40)
+ got := m.renderActivity()
+ if h := lipgloss.Height(got); h != 1 {
+ t.Fatalf("activity height = %d, want 1: %q", h, got)
+ }
+ if w := lipgloss.Width(got); w > m.width {
+ t.Fatalf("activity width = %d, want <= %d", w, m.width)
+ }
+ if !strings.Contains(ansi.Strip(got), "…") {
+ t.Errorf("truncated activity should end with an ellipsis: %q", ansi.Strip(got))
}
}
@@ -307,7 +375,9 @@ func TestLayoutJoinIdenticalToManualForNoTransient(t *testing.T) {
manual := strings.Join([]string{
m.renderHeader(),
body,
- inputSpacerRow, // one blank row of top padding above the input
+ inputSpacerRow, // blank row above the activity line
+ m.renderActivity(),
+ inputSpacerRow, // blank row between the activity line and the input
m.renderInput(),
m.renderFooter(),
}, "\n")
diff --git a/cmd/mecatui/ui/model.go b/cmd/mecatui/ui/model.go
index 0cbe22706e..ca414dc796 100644
--- a/cmd/mecatui/ui/model.go
+++ b/cmd/mecatui/ui/model.go
@@ -501,8 +501,8 @@ const (
phaseReplay // a stored-session transcript replay is open (read-only; issue #245)
)
-// spinnerVisible reports whether the footer renders the animated spinner in the
-// current phase (renderFooter's phaseRunning/phaseConnecting arms — keep in sync).
+// spinnerVisible reports whether the activity line renders the animated spinner in
+// the current phase (activity's phaseRunning/phaseConnecting arms — keep in sync).
// It gates the spinner.TickMsg handler: a tick in any other phase is dropped,
// which terminates the self-perpetuating tick chain; every transition INTO a
// visible phase must re-arm m.sp.Tick. Session transcript loading is owned and
diff --git a/cmd/mecatui/ui/models_test.go b/cmd/mecatui/ui/models_test.go
index 7ff1a3b731..cb600f0574 100644
--- a/cmd/mecatui/ui/models_test.go
+++ b/cmd/mecatui/ui/models_test.go
@@ -919,7 +919,7 @@ func TestModelsSelectionSanitizesDisplayNameStatus(t *testing.T) {
mm, cmd, _ = m.onOverlayKey(tea.KeyPressMsg{Code: tea.KeyEnter})
m = feedModelSwitchBusiness(t, mm.(Model), cmd)
- for _, rendered := range []string{m.statusMsg, m.renderFooter()} {
+ for _, rendered := range []string{m.statusMsg, m.renderActivity()} {
if strings.Contains(rendered, hostile) {
t.Errorf("hostile OSC sequence leaked into rendered status: %q", rendered)
}
@@ -1885,7 +1885,7 @@ func TestModelsPickerScrolledGolden(t *testing.T) {
m = pressModelsKey(t, m, tea.KeyPressMsg{Code: tea.KeyEnd})
m = goldenStatusFrame(t, m)
got := stripANSI([]byte(m.View().Content))
- if !bytes.Contains(got, []byte("↑ 26 items")) {
+ if !bytes.Contains(got, []byte("↑ 27 items")) {
t.Fatalf("scrolled picker must count complete hidden logical items in its overflow indicator:\n%s", got)
}
compareGolden(t, "models_scrolled.golden", got)
@@ -2154,24 +2154,27 @@ func TestGatewayNoticeClearedOnKeypress(t *testing.T) {
}
}
-// TestGatewayNoticeRenderedAtIdle: the footer renders the notice at idle; at running
-// the running arm owns the footer-left and the notice is NOT shown (even if it were
-// still set, which it is here for the assertion).
+// TestGatewayNoticeRenderedAtIdle: the activity line renders the notice at idle; at
+// running the running arm owns the activity line and the notice is NOT shown (even if
+// it were still set, which it is here for the assertion).
func TestGatewayNoticeRenderedAtIdle(t *testing.T) {
fm := gatewayModels()
m := newModelsModel(t, fm, &fakeStore{}, modelsCaps(), client.ModelSelection{})
mm, _, _ := m.updateModelsMsg(client.ModelsMsg{Models: fm.models, Statuses: fm.statuses})
m = mm.(Model)
m.sel = selection{} // no active selection: notice arm wins over statusMsg/"ready"
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, "toolhive gateway available") {
- t.Errorf("idle footer should render the notice, got:\n%s", got)
+ t.Errorf("idle activity line should render the notice, got:\n%s", got)
}
- // At running the spinner arm owns the footer-left; the notice must NOT appear.
+ if footer := stripANSIstr(m.renderFooter()); strings.Contains(footer, "toolhive gateway available") {
+ t.Errorf("footer must not duplicate the notice, got:\n%s", footer)
+ }
+ // At running the spinner arm owns the activity line; the notice must NOT appear.
m.phase = phaseRunning
- got = stripANSIstr(m.renderFooter())
+ got = stripANSIstr(m.renderActivity())
if strings.Contains(got, "toolhive gateway available") {
- t.Errorf("running footer must NOT render the notice (the running arm owns the slot), got:\n%s", got)
+ t.Errorf("running activity line must NOT render the notice (the running arm owns the slot), got:\n%s", got)
}
}
@@ -2352,17 +2355,17 @@ func TestProvenanceHintSuppressedWhenNoStatus(t *testing.T) {
// --- golden: idle footer gateway notice ---------------------------------------
-// TestFooterGatewayNoticeGolden locks the idle footer-left rendering of the
+// TestFooterGatewayNoticeGolden locks the idle activity-line rendering of the
// once-per-process gateway notice (Proposal 1): an AvailableNotDefault status fires
-// the notice, and at idle (no selection, no statusMsg) the footer-left renders it
-// muted. The snapshot is the stripped full renderFooter at a wide width.
+// the notice, and at idle (no selection, no statusMsg) the activity line renders it
+// muted. The snapshot is the stripped activity line above the full renderFooter.
func TestFooterGatewayNoticeGolden(t *testing.T) {
fm := gatewayModels()
m := newModelsModel(t, fm, &fakeStore{}, modelsCaps(), client.ModelSelection{})
mm, _, _ := m.updateModelsMsg(client.ModelsMsg{Models: fm.models, Statuses: fm.statuses})
m = mm.(Model)
m.sel = selection{} // no selection: the notice arm wins over statusMsg/"ready"
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity() + "\n" + m.renderFooter())
compareGolden(t, "footer_gateway_notice.golden", []byte(got+"\n"))
}
diff --git a/cmd/mecatui/ui/plan_approval_test.go b/cmd/mecatui/ui/plan_approval_test.go
index f15c30375d..9684cf22fc 100644
--- a/cmd/mecatui/ui/plan_approval_test.go
+++ b/cmd/mecatui/ui/plan_approval_test.go
@@ -99,14 +99,17 @@ func TestPlanAskNoAlwaysButtonCopy(t *testing.T) {
}
}
-func TestPlanAskFooterLabel(t *testing.T) {
+func TestPlanAskActivityLabel(t *testing.T) {
m := planAskModel(t, true)
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, "plan review") {
- t.Errorf("plan ask footer must show 'plan review', got %q", got)
+ t.Errorf("plan ask activity line must show 'plan review', got %q", got)
}
if strings.Contains(got, "awaiting approval") {
- t.Errorf("plan ask footer must NOT show 'awaiting approval', got %q", got)
+ t.Errorf("plan ask activity line must NOT show 'awaiting approval', got %q", got)
+ }
+ if footer := stripANSIstr(m.renderFooter()); strings.Contains(footer, "plan review") {
+ t.Errorf("the plan review label must not be duplicated in the footer, got %q", footer)
}
}
@@ -177,10 +180,10 @@ func TestPlanAskDenyThenIterateTerminalReturnsToIdle(t *testing.T) {
if m.phase != phaseIdle {
t.Fatalf("after StopPlanIterate the phase must be phaseIdle (input usable), got %v", m.phase)
}
- // The footer must show the iterate label (muted/transient — awaiting feedback).
- got := stripANSIstr(m.renderFooter())
+ // The activity line must show the iterate label (muted/transient — awaiting feedback).
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, "plan iterate · awaiting your feedback") {
- t.Errorf("footer after StopPlanIterate = %q, want it to contain 'plan iterate · awaiting your feedback'", got)
+ t.Errorf("activity after StopPlanIterate = %q, want it to contain 'plan iterate · awaiting your feedback'", got)
}
// The textarea is focused (input usable): the cursor-blink state is on.
if !m.prompt.Focused() {
@@ -198,13 +201,13 @@ func TestStopPlanApprovedFooterLabel(t *testing.T) {
}
}
-func TestStopPlanApprovedReachesFooter(t *testing.T) {
+func TestStopPlanApprovedReachesActivity(t *testing.T) {
m, _, _ := newTestModel(t, theme.New("aztec", theme.AztecPalette()))
m.phase = phaseRunning
m = applyAll(m, client.ResultMsg{Stop: "plan_approved"})
- got := stripANSIstr(m.renderFooter())
+ got := stripANSIstr(m.renderActivity())
if !strings.Contains(got, "plan approved · executing") {
- t.Errorf("ResultMsg{Stop:plan_approved} → footer = %q, want it to contain 'plan approved · executing'", got)
+ t.Errorf("ResultMsg{Stop:plan_approved} → activity = %q, want it to contain 'plan approved · executing'", got)
}
}
@@ -220,10 +223,10 @@ func TestPlanAskQueueBadge(t *testing.T) {
if !strings.Contains(got, "Plan ready for review (1 of 2)") {
t.Errorf("plan ask with queue must show '(1 of 2)' badge, got %q", got)
}
- // Footer must also carry the badge.
- footer := stripANSIstr(m.renderFooter())
- if !strings.Contains(footer, "plan review (1 of 2)") {
- t.Errorf("plan ask footer with queue must show 'plan review (1 of 2)', got %q", footer)
+ // The activity line must also carry the badge.
+ activity := stripANSIstr(m.renderActivity())
+ if !strings.Contains(activity, "plan review (1 of 2)") {
+ t.Errorf("plan ask activity line with queue must show 'plan review (1 of 2)', got %q", activity)
}
}
@@ -234,14 +237,18 @@ func TestGenericAskFooterIsUnchanged(t *testing.T) {
m.stream = client.NewStream(&fakeRecver{}, &fakeSender{})
m.phase = phaseAwaitingApproval
openApprovalSurface(&m).ask = pendingAsk{AskID: "sess-test-0001:1:c1", Tool: "Shell", Reason: "Shell requires approval"}
- got := stripANSIstr(m.renderFooter())
- if !strings.Contains(got, "awaiting approval") {
- t.Errorf("generic ask footer must show 'awaiting approval', got %q", got)
+ activity := stripANSIstr(m.renderActivity())
+ if !strings.Contains(activity, "awaiting approval") {
+ t.Errorf("generic ask activity line must show 'awaiting approval', got %q", activity)
+ }
+ if strings.Contains(activity, "plan review") {
+ t.Errorf("generic ask activity line must NOT show 'plan review', got %q", activity)
}
- if strings.Contains(got, "plan review") {
- t.Errorf("generic ask footer must NOT show 'plan review', got %q", got)
+ got := stripANSIstr(m.renderFooter())
+ if strings.Contains(got, "awaiting approval") {
+ t.Errorf("the approval label must not be duplicated in the footer, got %q", got)
}
- // The header help line must not leak plan-approval hints.
+ // The footer help line must not leak plan-approval hints.
if strings.Contains(got, "A approve & run") {
t.Errorf("generic ask footer help must not show plan-approval copy: %q", got)
}
diff --git a/cmd/mecatui/ui/prompttextarea/prompttextarea.go b/cmd/mecatui/ui/prompttextarea/prompttextarea.go
index 7efd6dfac5..eaee1ab49d 100644
--- a/cmd/mecatui/ui/prompttextarea/prompttextarea.go
+++ b/cmd/mecatui/ui/prompttextarea/prompttextarea.go
@@ -43,7 +43,7 @@ func New(cfg Config) Editor {
model.ShowLineNumbers = false
model.Placeholder = cfg.Placeholder
model.DynamicHeight = true
- model.MinHeight = 3
+ model.MinHeight = 1
model.MaxHeight = 8
// MaxHeight is also the legacy input limit when MaxContentHeight is unset.
// Keep Bubbles' content limit effectively unbounded while it owns scrolling.
diff --git a/cmd/mecatui/ui/prompttextarea/prompttextarea_test.go b/cmd/mecatui/ui/prompttextarea/prompttextarea_test.go
index 69c79fb914..d83380a649 100644
--- a/cmd/mecatui/ui/prompttextarea/prompttextarea_test.go
+++ b/cmd/mecatui/ui/prompttextarea/prompttextarea_test.go
@@ -65,8 +65,8 @@ func TestKeyUpdatePreservesUpstreamSelectionBehavior(t *testing.T) {
func TestDynamicHeightTracksSoftWrapAndCaps(t *testing.T) {
editor := New(Config{})
editor.SetWidth(10)
- if got := editor.Height(); got != 3 {
- t.Fatalf("empty height = %d, want minimum 3", got)
+ if got := editor.Height(); got != 1 {
+ t.Fatalf("empty height = %d, want minimum 1", got)
}
editor.Rewrite("12345678901234567890123456789012345678901234567890123456789012345678901234567890")
@@ -93,9 +93,14 @@ func TestDynamicHeightTracksSoftWrapAndCaps(t *testing.T) {
t.Fatalf("explicit-newline cursor visual row = %d, want within viewport height %d", editor.Line()+info.RowOffset-editor.ScrollYOffset(), editor.Height())
}
+ editor.Rewrite("123456789012345")
+ if got := editor.Height(); got != 2 {
+ t.Fatalf("two-row soft-wrapped height = %d, want 2", got)
+ }
+
editor.Rewrite("short")
- if got := editor.Height(); got != 3 {
- t.Fatalf("shrunk height = %d, want minimum 3", got)
+ if got := editor.Height(); got != 1 {
+ t.Fatalf("shrunk height = %d, want minimum 1", got)
}
}
diff --git a/cmd/mecatui/ui/reconnect_live_test.go b/cmd/mecatui/ui/reconnect_live_test.go
index 628c423fca..9d3cb91c4d 100644
--- a/cmd/mecatui/ui/reconnect_live_test.go
+++ b/cmd/mecatui/ui/reconnect_live_test.go
@@ -564,9 +564,9 @@ func TestReconnectUI_NoDuplicateConcurrentReconnect(t *testing.T) {
}
}
-// TestReconnectUI_DegradedFooter asserts the footer shows the degraded state
-// while liveReconnecting and clears it after reconnect.
-func TestReconnectUI_DegradedFooter(t *testing.T) {
+// TestReconnectUI_DegradedActivity asserts the activity line shows the degraded
+// state while liveReconnecting, and the footer does not duplicate it.
+func TestReconnectUI_DegradedActivity(t *testing.T) {
defer restoreBackoffClient(t)()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -580,12 +580,15 @@ func TestReconnectUI_DegradedFooter(t *testing.T) {
if !m.liveReconnecting {
t.Fatal("expected liveReconnecting")
}
- footer := stripANSIstr(m.renderFooter())
- if !containsStr(footer, "live feed reconnecting") {
- t.Errorf("footer should show degraded state, got: %q", footer)
+ activity := stripANSIstr(m.renderActivity())
+ if !containsStr(activity, "live feed reconnecting") {
+ t.Errorf("activity line should show degraded state, got: %q", activity)
}
- if !containsStr(footer, "attempt 1") {
- t.Errorf("footer should show attempt 1, got: %q", footer)
+ if !containsStr(activity, "attempt 1") {
+ t.Errorf("activity line should show attempt 1, got: %q", activity)
+ }
+ if footer := stripANSIstr(m.renderFooter()); containsStr(footer, "live feed reconnecting") {
+ t.Errorf("the reconnect cue must not be duplicated in the footer, got: %q", footer)
}
}
@@ -1027,8 +1030,8 @@ func TestADR_0096_BearerLiveReaderRecvAuthRejectedRoutesToConnectRecovery(t *tes
if m.liveCh != nil || m.liveReconCh != nil || m.liveReconnecting || m.liveArmed != "" {
t.Fatalf("auth recovery left live/reconnect state armed: live=%v recon=%v reconnecting=%v armed=%q", m.liveCh, m.liveReconCh, m.liveReconnecting, m.liveArmed)
}
- if footer := stripANSIstr(m.renderFooter()); containsStr(footer, "live feed reconnecting") {
- t.Fatalf("stale reconnect footer remained after auth recovery: %q", footer)
+ if activity := stripANSIstr(m.renderActivity()); containsStr(activity, "live feed reconnecting") {
+ t.Fatalf("stale reconnect activity cue remained after auth recovery: %q", activity)
}
overlay := stripANSIstr(m.View().Content)
for _, guidance := range []string{"Re-login is disabled", "issuer, audience, and CA"} {
diff --git a/cmd/mecatui/ui/selection_test.go b/cmd/mecatui/ui/selection_test.go
index 9c034fdcf5..cc885dc05e 100644
--- a/cmd/mecatui/ui/selection_test.go
+++ b/cmd/mecatui/ui/selection_test.go
@@ -256,11 +256,10 @@ func TestConvTopRowTracksWrappedHeader(t *testing.T) {
// both cases and asserts the wrapped vpH is strictly LESS — proving the measured
// height actually flows into sizing (not a no-op).
func TestOnResizeUsesMeasuredHeaderHeight(t *testing.T) {
- // taH=5 (input region: the 3-row textarea + its rail top-pad row, measured at 4 +
- // historical 1), footerH=2, spacerH=1 (the inter-region spacer above the input). The
- // body is total minus header + spacer + input + footer. (taH grew by 1 vs the
- // pre-top-pad layout — the inputRailPadTop row.)
- const taH, footerH, spacerH, totalH = 5, 2, 1, 30
+ // taH=3 (input region: the 1-row minimum textarea + its rail top and bottom pad rows), footerH=3
+ // (top border + usage row + help row), spacerH=2 (the blank rows around the activity
+ // line), activityH=1. The body is total minus header + spacers + activity + input + footer.
+ const taH, footerH, spacerH, activityH, totalH = 3, 3, 2, 1, 30
// Wrapping case: the renderer-owned debug disclosure at a narrow width.
m, _ := selModel(t)
@@ -271,15 +270,15 @@ func TestOnResizeUsesMeasuredHeaderHeight(t *testing.T) {
if wrappedHeader <= 2 {
t.Fatalf("PRECONDITION: header did not wrap (%d rows); sizing test would be vacuous", wrappedHeader)
}
- if got, want := m.vp.Height(), m.height-taH-footerH-spacerH-wrappedHeader; got != want {
- t.Errorf("wrapped vpH = %d, want %d (height - %d - %d - %d - measured header)", got, want, taH, footerH, spacerH)
+ if got, want := m.vp.Height(), m.height-taH-footerH-spacerH-activityH-wrappedHeader; got != want {
+ t.Errorf("wrapped vpH = %d, want %d (height - %d - %d - %d - %d - measured header)", got, want, taH, footerH, spacerH, activityH)
}
// Non-wrapping steady state: same total height at width 100, header is 2 rows.
m2, _ := selModel(t)
mm2, _ := m2.onResize(tea.WindowSizeMsg{Width: 100, Height: totalH})
m2 = mm2.(Model)
- if got, want := m2.vp.Height(), m2.height-taH-footerH-spacerH-2; got != want {
+ if got, want := m2.vp.Height(), m2.height-taH-footerH-spacerH-activityH-2; got != want {
t.Errorf("steady-state vpH = %d, want %d (header == 2 rows)", got, want)
}
@@ -769,8 +768,19 @@ func TestGestureInDirtyWindowKeepsDisplayedFrame(t *testing.T) {
m.conversationView.mode = followTail
m.refreshView()
+ // Pick a visible text row rather than a fixed offset: which tail lines are on screen
+ // depends on the chrome height.
top := convTopRow(m)
- y := top + 5
+ y := -1
+ for i, line := range strings.Split(stripANSIstr(m.View().Content), "\n")[top : top+m.vp.Height()] {
+ if strings.Contains(line, "question") {
+ y = top + i
+ break
+ }
+ }
+ if y < 0 {
+ t.Fatal("precondition: no user question row visible in the viewport")
+ }
// Anchor + extend a selection (non-empty, so it stays active).
m, _ = pressMouse(m, tea.MouseLeft, 10, y)
@@ -2600,19 +2610,19 @@ func TestDoubleClickIdentitySnapshotSurvivesRefresh(t *testing.T) {
// TestMouseDebugOverlay covers the gated --debug mouse diagnostic: with
// DebugMouse on, a mouse press sets m.mouseDebug to the formatted line (raw coords +
-// content and input mapping) and the footer surfaces it (highest priority — over the phase
-// arms). With DebugMouse off, no press sets it and the footer shows the normal
+// content and input mapping) and the activity line surfaces it (highest priority — over the phase
+// arms). With DebugMouse off, no press sets it and the activity line shows the normal
// status. Default OFF, zero cost when unset.
func TestMouseDebugOverlay(t *testing.T) {
- // Off by default: a press records nothing and the footer shows the normal status.
+ // Off by default: a press records nothing and the activity line shows the normal status.
m, _ := selModel(t)
top := convTopRow(m)
m, _ = pressMouse(m, tea.MouseLeft, 3, top)
if m.mouseDebug != "" {
t.Errorf("DebugMouse off: a press must not set mouseDebug, got %q", m.mouseDebug)
}
- if got := stripANSIstr(m.renderFooter()); strings.Contains(got, "MOUSE raw") {
- t.Errorf("DebugMouse off: footer must not show the mouse diagnostic, got %q", got)
+ if got := stripANSIstr(m.renderActivity()); strings.Contains(got, "MOUSE raw") {
+ t.Errorf("DebugMouse off: activity line must not show the mouse diagnostic, got %q", got)
}
// mouseDebugLine formats the expected shape directly.
@@ -2626,13 +2636,16 @@ func TestMouseDebugOverlay(t *testing.T) {
}
}
- // With DebugMouse on, a press sets m.mouseDebug and the footer surfaces it (over
+ // With DebugMouse on, a press sets m.mouseDebug and the activity line surfaces it (over
// the idle "ready" status).
m2, _ = pressMouse(m2, tea.MouseLeft, 7, convTopRow(m2)+1)
if !strings.Contains(m2.mouseDebug, "MOUSE raw") {
t.Errorf("DebugMouse on: a press should set mouseDebug, got %q", m2.mouseDebug)
}
- if got := stripANSIstr(m2.renderFooter()); !strings.Contains(got, "MOUSE raw") {
- t.Errorf("DebugMouse on: footer should surface the mouse diagnostic, got %q", got)
+ if got := stripANSIstr(m2.renderActivity()); !strings.Contains(got, "MOUSE raw") {
+ t.Errorf("DebugMouse on: activity line should surface the mouse diagnostic, got %q", got)
+ }
+ if got := stripANSIstr(m2.renderFooter()); strings.Contains(got, "MOUSE raw") {
+ t.Errorf("DebugMouse on: footer must not duplicate the mouse diagnostic, got %q", got)
}
}
diff --git a/cmd/mecatui/ui/spinner_gate_test.go b/cmd/mecatui/ui/spinner_gate_test.go
index dfe5fa0896..8975b4e22c 100644
--- a/cmd/mecatui/ui/spinner_gate_test.go
+++ b/cmd/mecatui/ui/spinner_gate_test.go
@@ -309,13 +309,13 @@ func TestSpinnerRearmedOnEveryVisibleTransition(t *testing.T) {
})
}
-// TestSpinnerVisibleMatchesFooterRender sweeps every phase and ties
-// spinnerVisible() to renderFooter's ACTUAL render set, so the gate predicate and
-// the footer's spinner arms cannot drift silently: a phase whose footer renders
-// the spinner glyph must be spinner-visible (or its animation would be gated off
-// while on screen), and a phase whose footer doesn't must not be (or a dead chain
-// would keep ticking off screen).
-func TestSpinnerVisibleMatchesFooterRender(t *testing.T) {
+// TestSpinnerVisibleMatchesActivityRender sweeps every phase and ties
+// spinnerVisible() to renderActivity's ACTUAL render set, so the gate predicate and
+// the activity line's spinner arms cannot drift silently: a phase whose activity
+// line renders the spinner glyph must be spinner-visible (or its animation would be
+// gated off while on screen), and a phase whose activity line doesn't must not be
+// (or a dead chain would keep ticking off screen).
+func TestSpinnerVisibleMatchesActivityRender(t *testing.T) {
for _, tc := range []struct {
name string
ph phase
@@ -329,9 +329,9 @@ func TestSpinnerVisibleMatchesFooterRender(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
m, _ := newQueueModel(t)
m.phase = tc.ph
- rendered := strings.Contains(m.renderFooter(), m.sp.View())
+ rendered := strings.Contains(m.renderActivity(), m.sp.View())
if rendered != m.spinnerVisible() {
- t.Errorf("phase %s: footer renders the spinner = %v but spinnerVisible() = %v — keep spinnerVisible in sync with renderFooter's spinner arms",
+ t.Errorf("phase %s: activity line renders the spinner = %v but spinnerVisible() = %v — keep spinnerVisible in sync with renderActivity's spinner arms",
tc.name, rendered, m.spinnerVisible())
}
})
diff --git a/cmd/mecatui/ui/statusline.go b/cmd/mecatui/ui/statusline.go
index 63f47855fe..64bf8f885a 100644
--- a/cmd/mecatui/ui/statusline.go
+++ b/cmd/mecatui/ui/statusline.go
@@ -22,16 +22,6 @@ func statusSurfaceFits(surface customization.Surface, width int) bool {
return lipgloss.Width(statusSpansText(surface.Spans)) <= width
}
-func renderStatusSurface(th theme.Theme, surface customization.Surface, width int, rightAlign bool) string {
- line := renderStatusSpans(th, surface.Spans)
- if !rightAlign {
- return line
- }
- if gap := width - lipgloss.Width(line); gap > 0 {
- return strings.Repeat(" ", gap) + line
- }
- return line
-}
func renderStatusSpans(th theme.Theme, spans []customization.Span) string {
var b strings.Builder
for _, span := range spans {
diff --git a/cmd/mecatui/ui/statusline_source.go b/cmd/mecatui/ui/statusline_source.go
index 1ac15b1f2f..f6bdad3c9e 100644
--- a/cmd/mecatui/ui/statusline_source.go
+++ b/cmd/mecatui/ui/statusline_source.go
@@ -4,7 +4,6 @@ import (
"time"
tea "charm.land/bubbletea/v2"
- "charm.land/lipgloss/v2"
"github.com/stacklok/mecatl/cmd/mecatui/client"
customization "github.com/stacklok/mecatl/cmd/mecatui/customization"
@@ -135,18 +134,17 @@ type statusLineGeometry struct {
}
// statusLineGeometry is the single source of status-surface lane reservations.
-// It uses renderer-owned header safety/navigation and footer activity lanes, but
-// does not render or submit anything.
+// It reserves the renderer-owned header safety/navigation lanes and the footer
+// row's padding, but does not render or submit anything.
func (m Model) statusLineGeometry() statusLineGeometry {
badge, badgeWidth := m.postureBadgeRender()
tail := m.scrollIndicator()
if tail == "" {
tail = m.changedFilesIndicator()
}
- left := m.footerActivity()
return statusLineGeometry{
headerAvailable: m.statusHeaderAvailable(badge, badgeWidth, tail),
- footerAvailable: max(0, m.widthOr()-2-lipgloss.Width(left)-footerGapPad),
+ footerAvailable: max(0, m.widthOr()-m.deps.Theme.Style("footer").GetHorizontalFrameSize()),
}
}
diff --git a/cmd/mecatui/ui/statusline_source_test.go b/cmd/mecatui/ui/statusline_source_test.go
index a35be70b1e..8c4b9dabaf 100644
--- a/cmd/mecatui/ui/statusline_source_test.go
+++ b/cmd/mecatui/ui/statusline_source_test.go
@@ -10,7 +10,6 @@ import (
"time"
tea "charm.land/bubbletea/v2"
- "charm.land/lipgloss/v2"
"github.com/stacklok/mecatl/cmd/mecatui/client"
customization "github.com/stacklok/mecatl/cmd/mecatui/customization"
@@ -213,8 +212,8 @@ func TestStatusCustomization_Scenario2_ReservedLanesAndResponsiveSelection(t *te
if input.Terminal.HeaderAvailCols != geometry.headerAvailable || input.Terminal.FooterAvailCols != geometry.footerAvailable {
t.Fatalf("reserved-lane widths = %#v, want %#v", input.Terminal, geometry)
}
- if want := m.widthOr() - 2 - lipgloss.Width(m.footerActivity()) - footerGapPad; input.Terminal.FooterAvailCols != want {
- t.Fatalf("footer available columns = %d, want %d after footer padding and activity reservation", input.Terminal.FooterAvailCols, want)
+ if want := m.widthOr() - m.deps.Theme.Style("footer").GetHorizontalFrameSize(); input.Terminal.FooterAvailCols != want {
+ t.Fatalf("footer available columns = %d, want %d after footer padding only (activity renders above the input)", input.Terminal.FooterAvailCols, want)
}
updated, _ = m.Update(client.ToolCallMsg{Name: "Read"})
diff --git a/cmd/mecatui/ui/statusline_test.go b/cmd/mecatui/ui/statusline_test.go
index d45fc2d7e3..d66816f9c1 100644
--- a/cmd/mecatui/ui/statusline_test.go
+++ b/cmd/mecatui/ui/statusline_test.go
@@ -36,11 +36,14 @@ func TestStatusLine_Scenario2_DefaultTemplatesPreserveChrome(t *testing.T) {
}
}
footer := stripANSIstr(m.renderFooter())
- for _, want := range []string{"ready", "ctx", "4K/20K", "cache", "0%"} {
+ for _, want := range []string{"ctx", "4K/20K", "cache", "0%"} {
if !strings.Contains(footer, want) {
t.Fatalf("footer %q is missing shipped chrome %q", footer, want)
}
}
+ if activity := stripANSIstr(m.renderActivity()); !strings.Contains(activity, "ready") || strings.Contains(footer, "ready") {
+ t.Fatalf("activity %q must carry \"ready\" and footer %q must not duplicate it", activity, footer)
+ }
}
func TestStatusLine_StaleGeneratedSurfaceNeverWraps(t *testing.T) {
m, _, _ := newTestModel(t, theme.New("aztec", theme.AztecPalette()))
@@ -159,7 +162,7 @@ func TestStatusHyperlinks_Scenario1_ValidLinkEmitsPairedOSC8(t *testing.T) {
customization.Render(``, nil).Header,
customization.Render(``, nil).Footer,
} {
- got := renderStatusSurface(th, surface, 20, false)
+ got := renderStatusSpans(th, surface.Spans)
if openAt, textAt, closeAt := strings.Index(got, open), strings.Index(got, "docs"), strings.LastIndex(got, hyperlinkClose); openAt < 0 || textAt < openAt || closeAt < textAt {
t.Fatalf("link output %q does not contain paired OSC 8 hyperlink", got)
}
@@ -206,8 +209,8 @@ func TestStatusHyperlinks_Scenario1_HyperlinksPreserveStatusLayout(t *testing.T)
if !statusSurfaceFits(surface, 8) || statusSurfaceFits(surface, 7) {
t.Fatalf("visible width changed: %q", statusSpansText(surface.Spans))
}
- got := renderStatusSurface(th, surface, 12, true)
- if gotWidth := lipgloss.Width(got); gotWidth != 12 {
- t.Fatalf("rendered width = %d, want 12: %q", gotWidth, got)
+ got := renderStatusSpans(th, surface.Spans)
+ if gotWidth := lipgloss.Width(got); gotWidth != 8 {
+ t.Fatalf("rendered width = %d, want 8: %q", gotWidth, got)
}
}
diff --git a/cmd/mecatui/ui/suspend_quitd_test.go b/cmd/mecatui/ui/suspend_quitd_test.go
index bc046ba1d7..5c49c9db67 100644
--- a/cmd/mecatui/ui/suspend_quitd_test.go
+++ b/cmd/mecatui/ui/suspend_quitd_test.go
@@ -1,6 +1,7 @@
package ui
import (
+ "strings"
"testing"
tea "charm.land/bubbletea/v2"
@@ -41,8 +42,8 @@ func TestQuitDFirstOnEmptyArms(t *testing.T) {
if m.statusMsg != quitDHintFor(defaultKeys().QuitD) {
t.Errorf("status should carry the quitD hint, got %q", m.statusMsg)
}
- if got := stripANSIstr(m.renderFooter()); got == "" {
- t.Error("footer should render with the armed hint present")
+ if got := stripANSIstr(m.renderActivity()); !strings.Contains(got, quitDHintFor(defaultKeys().QuitD)) {
+ t.Errorf("activity line should render the armed hint, got %q", got)
}
}
@@ -111,8 +112,8 @@ func TestQuitDIndependentOfQuitC(t *testing.T) {
func TestQuitDHintNamesLiveChord(t *testing.T) {
m := quitModel(t)
m, _ = pressKey(m, ctrlD())
- if got := stripANSIstr(m.renderFooter()); got == "" {
- t.Error("footer should render")
+ if got := stripANSIstr(m.renderActivity()); !strings.Contains(got, "press ctrl+d again to quit") {
+ t.Errorf("activity line should render the live quitD hint, got %q", got)
}
if m.statusMsg != "press ctrl+d again to quit" {
t.Errorf("default hint = %q, want 'press ctrl+d again to quit'", m.statusMsg)
diff --git a/cmd/mecatui/ui/testdata/agents_inventory.golden b/cmd/mecatui/ui/testdata/agents_inventory.golden
index 565a3ce05e..fe7b9e87a4 100644
--- a/cmd/mecatui/ui/testdata/agents_inventory.golden
+++ b/cmd/mecatui/ui/testdata/agents_inventory.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/agents_inventory_empty_disabled.golden b/cmd/mecatui/ui/testdata/agents_inventory_empty_disabled.golden
index d201f0d36d..24fb45911e 100644
--- a/cmd/mecatui/ui/testdata/agents_inventory_empty_disabled.golden
+++ b/cmd/mecatui/ui/testdata/agents_inventory_empty_disabled.golden
@@ -4,7 +4,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Agent definitions ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/agents_inventory_empty_enabled.golden b/cmd/mecatui/ui/testdata/agents_inventory_empty_enabled.golden
index 46cec4749e..699f687a58 100644
--- a/cmd/mecatui/ui/testdata/agents_inventory_empty_enabled.golden
+++ b/cmd/mecatui/ui/testdata/agents_inventory_empty_enabled.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/agents_inventory_scroll.golden b/cmd/mecatui/ui/testdata/agents_inventory_scroll.golden
index ceebd00946..1772786eb0 100644
--- a/cmd/mecatui/ui/testdata/agents_inventory_scroll.golden
+++ b/cmd/mecatui/ui/testdata/agents_inventory_scroll.golden
@@ -4,6 +4,7 @@
┃ ┃
┃ Agent definitions ┃
┃ ┃
+┃ agent-10 ┃
┃ agent-11 ┃
┃ agent-12 ┃
┃ agent-13 ┃
@@ -13,18 +14,17 @@
┃ agent-17 ┃
┃ agent-18 ┃
┃ agent-19 ┃
-┃ agent-20 ┃
-┃ agent-21 ┃
-┃ lines 12–22 of 30 ┃
+┃ lines 11–20 of 30 ┃
┃ ┃
┃ agent definitions route Subagent delegations · pgup/pgdn scroll · esc close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/agents_teams_tab.golden b/cmd/mecatui/ui/testdata/agents_teams_tab.golden
index bfd9a25d50..39d8f9addf 100644
--- a/cmd/mecatui/ui/testdata/agents_teams_tab.golden
+++ b/cmd/mecatui/ui/testdata/agents_teams_tab.golden
@@ -14,17 +14,17 @@
┃ ↑1.2K ↓80 ┃
┃ ◆ ✎ builder ┃
┃ Edit… · implementer ┃
- ┃ ↑3.4K ↓220 ┃
┃ ┃
┃ ↑/↓ select · enter focus · x cancel · t tasks · f findings · tab switch · esc close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⛭ subagents 1◐ 0✓ ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⛭ subagents 1◐ 0✓ ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/askargs_modal_longshell.golden b/cmd/mecatui/ui/testdata/askargs_modal_longshell.golden
index 2e4176e956..27791f039a 100644
--- a/cmd/mecatui/ui/testdata/askargs_modal_longshell.golden
+++ b/cmd/mecatui/ui/testdata/askargs_modal_longshell.golden
@@ -1,7 +1,6 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Permission required ┃
@@ -21,10 +20,11 @@
+ ⚠ awaiting approval
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⚠ awaiting approval ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/askargs_view_pretty.golden b/cmd/mecatui/ui/testdata/askargs_view_pretty.golden
index 1b6f53a14c..6c35fcdd65 100644
--- a/cmd/mecatui/ui/testdata/askargs_view_pretty.golden
+++ b/cmd/mecatui/ui/testdata/askargs_view_pretty.golden
@@ -13,17 +13,17 @@ Shell requires approval
-
╭───────────╮ ╭────────────╮ ╭──────────╮
│ [A]llow │ │ Al[w]ays │ │ [D]eny │
╰───────────╯ ╰────────────╯ ╰──────────╯
al[w]ays allows this exact command for the rest of this session
scroll: ↑/↓ · pgup/pgdn · home/end · mouse wheel · r raw|pretty · esc back
+ ⚠ awaiting approval
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⚠ awaiting approval ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/askargs_view_raw.golden b/cmd/mecatui/ui/testdata/askargs_view_raw.golden
index 9376347940..22f06f0e51 100644
--- a/cmd/mecatui/ui/testdata/askargs_view_raw.golden
+++ b/cmd/mecatui/ui/testdata/askargs_view_raw.golden
@@ -13,17 +13,17 @@ Shell requires approval
-
╭───────────╮ ╭────────────╮ ╭──────────╮
│ [A]llow │ │ Al[w]ays │ │ [D]eny │
╰───────────╯ ╰────────────╯ ╰──────────╯
al[w]ays allows this exact command for the rest of this session
scroll: ↑/↓ · pgup/pgdn · home/end · mouse wheel · r raw|pretty · esc back
+ ⚠ awaiting approval
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⚠ awaiting approval ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/askargs_view_scrolled.golden b/cmd/mecatui/ui/testdata/askargs_view_scrolled.golden
index 2553364780..6335505975 100644
--- a/cmd/mecatui/ui/testdata/askargs_view_scrolled.golden
+++ b/cmd/mecatui/ui/testdata/askargs_view_scrolled.golden
@@ -13,17 +13,17 @@ echo step-xxxxj-yy
echo step-xxxxk-yyy
echo step-xxxxl-yyyy
echo step-xxxxm-yyyyy
-echo step-xxxxn-yyyyyy
╭───────────╮ ╭────────────╮ ╭──────────╮
│ [A]llow │ │ Al[w]ays │ │ [D]eny │
╰───────────╯ ╰────────────╯ ╰──────────╯
al[w]ays allows this exact command for the rest of this session
scroll: ↑/↓ · pgup/pgdn · home/end · mouse wheel · r raw|pretty · esc back
+ ⚠ awaiting approval
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⚠ awaiting approval ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/effort_picker.golden b/cmd/mecatui/ui/testdata/effort_picker.golden
index 850ec8d795..ec670c4e7d 100644
--- a/cmd/mecatui/ui/testdata/effort_picker.golden
+++ b/cmd/mecatui/ui/testdata/effort_picker.golden
@@ -20,11 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+u clear · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/footer_gateway_notice.golden b/cmd/mecatui/ui/testdata/footer_gateway_notice.golden
index 9e51e2f1be..d1aded8515 100644
--- a/cmd/mecatui/ui/testdata/footer_gateway_notice.golden
+++ b/cmd/mecatui/ui/testdata/footer_gateway_notice.golden
@@ -1,4 +1,4 @@
+ toolhive gateway available (5 models, no API key needed) — /models to use it, or --default-provid…
────────────────────────────────────────────────────────────────────────────────────────────────────
- toolhive gateway available (5 models, no API key needed) — /models to use it, or --default-
- provider toolhive
+
? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+u clear · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/help_all_on.golden b/cmd/mecatui/ui/testdata/help_all_on.golden
index aed887c921..5b9def7cdd 100644
--- a/cmd/mecatui/ui/testdata/help_all_on.golden
+++ b/cmd/mecatui/ui/testdata/help_all_on.golden
@@ -15,16 +15,16 @@
┃ esc, release, esc clear the current idle draft within 500ms; the first press makes no visible change ┃
┃ also clears attachments, large pasted text, and pending media; requires a terminal with enhanced key-event support ┃
┃ fixed shortcut; repeats do not count, and other views handle esc first ┃
-┃ use the remappable Clear prompt action (ctrl+u) on any terminal ┃
-┃ lines 1–14 of 69 · esc or ? close · ↑/↓ scroll · pgup/pgdn page · home/end jump ┃
+┃ lines 1–13 of 69 · esc or ? close · ↑/↓ scroll · pgup/pgdn page · home/end jump ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/help_embedded.golden b/cmd/mecatui/ui/testdata/help_embedded.golden
index e62dfccdf9..86bbf005df 100644
--- a/cmd/mecatui/ui/testdata/help_embedded.golden
+++ b/cmd/mecatui/ui/testdata/help_embedded.golden
@@ -15,16 +15,16 @@
┃ esc, release, esc clear the current idle draft within 500ms; the first press makes no visible change ┃
┃ also clears attachments, large pasted text, and pending media; requires a terminal with enhanced key-event support ┃
┃ fixed shortcut; repeats do not count, and other views handle esc first ┃
-┃ use the remappable Clear prompt action (ctrl+u) on any terminal ┃
-┃ lines 1–14 of 68 · esc or ? close · ↑/↓ scroll · pgup/pgdn page · home/end jump ┃
+┃ lines 1–13 of 68 · esc or ? close · ↑/↓ scroll · pgup/pgdn page · home/end jump ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/jev_routing_user_journey.golden b/cmd/mecatui/ui/testdata/jev_routing_user_journey.golden
index 6e4dbfcd22..391a0e0d6e 100644
--- a/cmd/mecatui/ui/testdata/jev_routing_user_journey.golden
+++ b/cmd/mecatui/ui/testdata/jev_routing_user_journey.golden
@@ -70,11 +70,11 @@
+ team done · 1 round
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
- team done · 1 round ⑂ parallel 0◐ 1✓ ⛭ subagents 0◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⑂ parallel 0◐ 1✓ ⛭ subagents 0◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_err_input.golden b/cmd/mecatui/ui/testdata/mcp_err_input.golden
index 1be63fcaf1..aab0bb0967 100644
--- a/cmd/mecatui/ui/testdata/mcp_err_input.golden
+++ b/cmd/mecatui/ui/testdata/mcp_err_input.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_err_not_configured.golden b/cmd/mecatui/ui/testdata/mcp_err_not_configured.golden
index 0b4dbbcfe7..7e78f7a27c 100644
--- a/cmd/mecatui/ui/testdata/mcp_err_not_configured.golden
+++ b/cmd/mecatui/ui/testdata/mcp_err_not_configured.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_err_server.golden b/cmd/mecatui/ui/testdata/mcp_err_server.golden
index a834e6d838..b22b7a3835 100644
--- a/cmd/mecatui/ui/testdata/mcp_err_server.golden
+++ b/cmd/mecatui/ui/testdata/mcp_err_server.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_panel.golden b/cmd/mecatui/ui/testdata/mcp_panel.golden
index 2c9050e1a5..e00bd47268 100644
--- a/cmd/mecatui/ui/testdata/mcp_panel.golden
+++ b/cmd/mecatui/ui/testdata/mcp_panel.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_panel_groups.golden b/cmd/mecatui/ui/testdata/mcp_panel_groups.golden
index 0a26681290..ad287888e3 100644
--- a/cmd/mecatui/ui/testdata/mcp_panel_groups.golden
+++ b/cmd/mecatui/ui/testdata/mcp_panel_groups.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_prompt_args.golden b/cmd/mecatui/ui/testdata/mcp_prompt_args.golden
index beff24ad56..04210185f2 100644
--- a/cmd/mecatui/ui/testdata/mcp_prompt_args.golden
+++ b/cmd/mecatui/ui/testdata/mcp_prompt_args.golden
@@ -4,7 +4,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ arguments for code-review ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_prompts.golden b/cmd/mecatui/ui/testdata/mcp_prompts.golden
index f8dbac22aa..7e725cb4fe 100644
--- a/cmd/mecatui/ui/testdata/mcp_prompts.golden
+++ b/cmd/mecatui/ui/testdata/mcp_prompts.golden
@@ -3,7 +3,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ MCP prompts ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_resource_preview.golden b/cmd/mecatui/ui/testdata/mcp_resource_preview.golden
index d07311a314..d10f1d192d 100644
--- a/cmd/mecatui/ui/testdata/mcp_resource_preview.golden
+++ b/cmd/mecatui/ui/testdata/mcp_resource_preview.golden
@@ -4,7 +4,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ resource preview ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/mcp_resources.golden b/cmd/mecatui/ui/testdata/mcp_resources.golden
index dd72d33e1b..1fb4a64e9c 100644
--- a/cmd/mecatui/ui/testdata/mcp_resources.golden
+++ b/cmd/mecatui/ui/testdata/mcp_resources.golden
@@ -3,7 +3,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ MCP resources ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models.golden b/cmd/mecatui/ui/testdata/models.golden
index d6c7dfffed..d63648ff91 100644
--- a/cmd/mecatui/ui/testdata/models.golden
+++ b/cmd/mecatui/ui/testdata/models.golden
@@ -1,6 +1,5 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Models (4) ┃
@@ -21,10 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_codex_healthy.golden b/cmd/mecatui/ui/testdata/models_codex_healthy.golden
index 05f2c4b4db..705808dc61 100644
--- a/cmd/mecatui/ui/testdata/models_codex_healthy.golden
+++ b/cmd/mecatui/ui/testdata/models_codex_healthy.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_codex_unauthorized.golden b/cmd/mecatui/ui/testdata/models_codex_unauthorized.golden
index 7bf38d1fd9..7663d0a27e 100644
--- a/cmd/mecatui/ui/testdata/models_codex_unauthorized.golden
+++ b/cmd/mecatui/ui/testdata/models_codex_unauthorized.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_disabled.golden b/cmd/mecatui/ui/testdata/models_disabled.golden
index 5d1283ef71..97b58d3ced 100644
--- a/cmd/mecatui/ui/testdata/models_disabled.golden
+++ b/cmd/mecatui/ui/testdata/models_disabled.golden
@@ -1,7 +1,6 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Models ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_empty.golden b/cmd/mecatui/ui/testdata/models_empty.golden
index 1f57a6dbae..bb58d2a125 100644
--- a/cmd/mecatui/ui/testdata/models_empty.golden
+++ b/cmd/mecatui/ui/testdata/models_empty.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_filtered.golden b/cmd/mecatui/ui/testdata/models_filtered.golden
index 4cb66636c6..119c428d3c 100644
--- a/cmd/mecatui/ui/testdata/models_filtered.golden
+++ b/cmd/mecatui/ui/testdata/models_filtered.golden
@@ -1,7 +1,6 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Models (2) ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_gateway_empty.golden b/cmd/mecatui/ui/testdata/models_gateway_empty.golden
index cb31529146..802f873c00 100644
--- a/cmd/mecatui/ui/testdata/models_gateway_empty.golden
+++ b/cmd/mecatui/ui/testdata/models_gateway_empty.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_global_default.golden b/cmd/mecatui/ui/testdata/models_global_default.golden
index e0708ceb29..457d19ec8e 100644
--- a/cmd/mecatui/ui/testdata/models_global_default.golden
+++ b/cmd/mecatui/ui/testdata/models_global_default.golden
@@ -1,6 +1,5 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Models (4) ┃
@@ -21,10 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_mixed_deployment_empty.golden b/cmd/mecatui/ui/testdata/models_mixed_deployment_empty.golden
index edf12bc176..2ca4a9707f 100644
--- a/cmd/mecatui/ui/testdata/models_mixed_deployment_empty.golden
+++ b/cmd/mecatui/ui/testdata/models_mixed_deployment_empty.golden
@@ -11,7 +11,6 @@
┃ ▶● openai · GPT-5 image input reason 200K ┃
┃ openai · GPT-5 mini reason 128K ┃
┃ openai · text-embed ┃
-┃ openrouter · Claude image input reason 1M ┃
┃ ┃
┃ toolhive: credential lists no models — ask your platform admin or re-run `thv llm setup` ┃
┃ ┃
@@ -21,10 +20,11 @@
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_nomatch.golden b/cmd/mecatui/ui/testdata/models_nomatch.golden
index e137849405..6dfe4f1cba 100644
--- a/cmd/mecatui/ui/testdata/models_nomatch.golden
+++ b/cmd/mecatui/ui/testdata/models_nomatch.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_scrolled.golden b/cmd/mecatui/ui/testdata/models_scrolled.golden
index 8840262a63..dcc49548cb 100644
--- a/cmd/mecatui/ui/testdata/models_scrolled.golden
+++ b/cmd/mecatui/ui/testdata/models_scrolled.golden
@@ -8,8 +8,7 @@
┃ ┃
┃ Switching models is expensive as it clears caches. ┃
┃ ┃
-┃ ↑ 26 items ┃
-┃ prov · model-026 ┃
+┃ ↑ 27 items ┃
┃ prov · model-027 ┃
┃ prov · model-028 ┃
┃ ▶ prov · wrapped model label wrapped model label wrapped model label wrapped model label wra ┃
@@ -21,10 +20,11 @@
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/models_toolhive_unreachable.golden b/cmd/mecatui/ui/testdata/models_toolhive_unreachable.golden
index 3684151a08..7ca1837b5a 100644
--- a/cmd/mecatui/ui/testdata/models_toolhive_unreachable.golden
+++ b/cmd/mecatui/ui/testdata/models_toolhive_unreachable.golden
@@ -11,7 +11,6 @@
┃ ▶● openai · GPT-5 image input reason 200K ┃
┃ openai · GPT-5 mini reason 128K ┃
┃ openai · text-embed ┃
-┃ openrouter · Claude image input reason 1M ┃
┃ ┃
┃ toolhive: gateway not reachable — start it with `thv llm proxy start` ┃
┃ ┃
@@ -21,10 +20,11 @@
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/parallel_group_focus.golden b/cmd/mecatui/ui/testdata/parallel_group_focus.golden
index 20c0a584a9..d82b58f0dd 100644
--- a/cmd/mecatui/ui/testdata/parallel_group_focus.golden
+++ b/cmd/mecatui/ui/testdata/parallel_group_focus.golden
@@ -13,18 +13,18 @@
┃ ▶ ✓ branch-1 · refactor with a map ┃
┃ done · 900ms · 2 tools · ↑12K ↓3K ┃
┃ │ ✓ Edit ┃
- ┃ ★ ✓ branch-2 · refactor with a slice ┃
- ┃ done · 900ms · 3 tools · ↑15K ↓4.2K ┃
+ ┃ · +2 branches below ┃
┃ ┃
┃ esc back · ↑/↓ select · pgup/pgdn page · home/end first/last ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⑂ parallel 0◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⑂ parallel 0◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/parallel_roster.golden b/cmd/mecatui/ui/testdata/parallel_roster.golden
index dbf1ed2ed1..1bf99b39d0 100644
--- a/cmd/mecatui/ui/testdata/parallel_roster.golden
+++ b/cmd/mecatui/ui/testdata/parallel_roster.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⑂ parallel 0◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⑂ parallel 0◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/paste_placeholder.golden b/cmd/mecatui/ui/testdata/paste_placeholder.golden
index 09b83cfe3e..405c146622 100644
--- a/cmd/mecatui/ui/testdata/paste_placeholder.golden
+++ b/cmd/mecatui/ui/testdata/paste_placeholder.golden
@@ -70,11 +70,11 @@
+ connected
│
│ [Pasted text #1]
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+u clear · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_cleanup_review.golden b/cmd/mecatui/ui/testdata/sessions_cleanup_review.golden
index e149ad7e37..8d52d525ee 100644
--- a/cmd/mecatui/ui/testdata/sessions_cleanup_review.golden
+++ b/cmd/mecatui/ui/testdata/sessions_cleanup_review.golden
@@ -25,8 +25,7 @@ enter: delete these sessions esc: back
-
-
+ ⣾ connecting…
│
│ Ask mecatl to do something…
@@ -35,6 +34,7 @@ enter: delete these sessions esc: back
│
│
│
+│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⣾ connecting… ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_picker.golden b/cmd/mecatui/ui/testdata/sessions_picker.golden
index e9ae1da335..c61ea89360 100644
--- a/cmd/mecatui/ui/testdata/sessions_picker.golden
+++ b/cmd/mecatui/ui/testdata/sessions_picker.golden
@@ -30,11 +30,11 @@ enter: continue y: copy ID v: view f: fork r: rename d: delete tab: switch
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_picker_empty.golden b/cmd/mecatui/ui/testdata/sessions_picker_empty.golden
index 27d9fa5d74..e4816a7215 100644
--- a/cmd/mecatui/ui/testdata/sessions_picker_empty.golden
+++ b/cmd/mecatui/ui/testdata/sessions_picker_empty.golden
@@ -30,11 +30,11 @@ tab: switch esc: close
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_picker_error.golden b/cmd/mecatui/ui/testdata/sessions_picker_error.golden
index f89110d252..f152ec3e68 100644
--- a/cmd/mecatui/ui/testdata/sessions_picker_error.golden
+++ b/cmd/mecatui/ui/testdata/sessions_picker_error.golden
@@ -30,11 +30,11 @@ r: retry esc: close
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_picker_filtered.golden b/cmd/mecatui/ui/testdata/sessions_picker_filtered.golden
index e9ae1da335..c61ea89360 100644
--- a/cmd/mecatui/ui/testdata/sessions_picker_filtered.golden
+++ b/cmd/mecatui/ui/testdata/sessions_picker_filtered.golden
@@ -30,11 +30,11 @@ enter: continue y: copy ID v: view f: fork r: rename d: delete tab: switch
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_picker_nomatch.golden b/cmd/mecatui/ui/testdata/sessions_picker_nomatch.golden
index b63f807f09..783f102e59 100644
--- a/cmd/mecatui/ui/testdata/sessions_picker_nomatch.golden
+++ b/cmd/mecatui/ui/testdata/sessions_picker_nomatch.golden
@@ -30,11 +30,11 @@ tab: switch esc: close
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_transcript_error.golden b/cmd/mecatui/ui/testdata/sessions_transcript_error.golden
index f4afed86f0..ecc1660d90 100644
--- a/cmd/mecatui/ui/testdata/sessions_transcript_error.golden
+++ b/cmd/mecatui/ui/testdata/sessions_transcript_error.golden
@@ -5,10 +5,11 @@ conversation unavailable
This conversation could not be loaded. You cannot continue this session.
r: Retry esc: Back
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_transcript_loaded.golden b/cmd/mecatui/ui/testdata/sessions_transcript_loaded.golden
index f5fdaa67c4..90c1214982 100644
--- a/cmd/mecatui/ui/testdata/sessions_transcript_loaded.golden
+++ b/cmd/mecatui/ui/testdata/sessions_transcript_loaded.golden
@@ -3,10 +3,11 @@
Inspecting session · read-only
(empty conversation)esc: Back
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_transcript_loading.golden b/cmd/mecatui/ui/testdata/sessions_transcript_loading.golden
index 08c508a1d6..8887a8bd43 100644
--- a/cmd/mecatui/ui/testdata/sessions_transcript_loading.golden
+++ b/cmd/mecatui/ui/testdata/sessions_transcript_loading.golden
@@ -5,10 +5,11 @@ loading conversation
Loading Nightly checks…
esc: Back
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/sessions_transcript_rendered.golden b/cmd/mecatui/ui/testdata/sessions_transcript_rendered.golden
index 64a1841acf..b01f3b1959 100644
--- a/cmd/mecatui/ui/testdata/sessions_transcript_rendered.golden
+++ b/cmd/mecatui/ui/testdata/sessions_transcript_rendered.golden
@@ -27,15 +27,15 @@ Inspecting Nightly checks · read-only
-
esc: Back
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/skills.golden b/cmd/mecatui/ui/testdata/skills.golden
index ef93617132..329d5d13a3 100644
--- a/cmd/mecatui/ui/testdata/skills.golden
+++ b/cmd/mecatui/ui/testdata/skills.golden
@@ -15,16 +15,16 @@
┃ ┃
┃ ┃
┃ ┃
-┃ ┃
┃ the agent uses skills when relevant · tab changes region · pgup/pgdn scroll · esc clear filter ┃
┃ / close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/skills_filtered.golden b/cmd/mecatui/ui/testdata/skills_filtered.golden
index ea129b39e9..70b28657c4 100644
--- a/cmd/mecatui/ui/testdata/skills_filtered.golden
+++ b/cmd/mecatui/ui/testdata/skills_filtered.golden
@@ -15,16 +15,16 @@
┃ ┃
┃ ┃
┃ ┃
-┃ ┃
┃ the agent uses skills when relevant · tab changes region · pgup/pgdn scroll · esc clear filter ┃
┃ / close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/skills_nomatch.golden b/cmd/mecatui/ui/testdata/skills_nomatch.golden
index 59c5c4c38a..e737bafab1 100644
--- a/cmd/mecatui/ui/testdata/skills_nomatch.golden
+++ b/cmd/mecatui/ui/testdata/skills_nomatch.golden
@@ -15,16 +15,16 @@
┃ ┃
┃ ┃
┃ ┃
-┃ ┃
┃ the agent uses skills when relevant · tab changes region · pgup/pgdn scroll · esc clear filter ┃
┃ / close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/skills_scroll.golden b/cmd/mecatui/ui/testdata/skills_scroll.golden
index fbffd949b6..f002e9a3db 100644
--- a/cmd/mecatui/ui/testdata/skills_scroll.golden
+++ b/cmd/mecatui/ui/testdata/skills_scroll.golden
@@ -4,6 +4,7 @@
┃ ┃
┃ Skills inventory ┃
┃ > filter skills… ┃
+┃ skill-10 ┃
┃ skill-11 ┃
┃ skill-12 ┃
┃ skill-13 ┃
@@ -13,18 +14,17 @@
┃ skill-17 ┃
┃ skill-18 ┃
┃ skill-19 ┃
-┃ skill-20 ┃
-┃ skill-21 ┃
-┃ lines 12–22 of 30 ┃
+┃ lines 11–20 of 30 ┃
┃ the agent uses skills when relevant · tab changes region · pgup/pgdn scroll · esc clear filter ┃
┃ / close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/soul.golden b/cmd/mecatui/ui/testdata/soul.golden
index d72e527151..39b92b463f 100644
--- a/cmd/mecatui/ui/testdata/soul.golden
+++ b/cmd/mecatui/ui/testdata/soul.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/soul_empty_disabled.golden b/cmd/mecatui/ui/testdata/soul_empty_disabled.golden
index a006a14931..cbaebde173 100644
--- a/cmd/mecatui/ui/testdata/soul_empty_disabled.golden
+++ b/cmd/mecatui/ui/testdata/soul_empty_disabled.golden
@@ -4,7 +4,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Soul (persona) ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/soul_empty_enabled.golden b/cmd/mecatui/ui/testdata/soul_empty_enabled.golden
index c74b9858cf..197c8c8a61 100644
--- a/cmd/mecatui/ui/testdata/soul_empty_enabled.golden
+++ b/cmd/mecatui/ui/testdata/soul_empty_enabled.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/soul_project.golden b/cmd/mecatui/ui/testdata/soul_project.golden
index 45240161f1..a765da5929 100644
--- a/cmd/mecatui/ui/testdata/soul_project.golden
+++ b/cmd/mecatui/ui/testdata/soul_project.golden
@@ -3,7 +3,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Soul (persona) ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/steer_card.golden b/cmd/mecatui/ui/testdata/steer_card.golden
index 238b20c722..53f4f98d9a 100644
--- a/cmd/mecatui/ui/testdata/steer_card.golden
+++ b/cmd/mecatui/ui/testdata/steer_card.golden
@@ -13,7 +13,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ ⏳ steer queued · ↑ edit · esc cancel ┃
@@ -21,10 +20,11 @@
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ ⣾ thinking…
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⣾ thinking… ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
enter queue · ctrl+u clear · esc cancel · ? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/subagent_focus.golden b/cmd/mecatui/ui/testdata/subagent_focus.golden
index 61678299c5..298aeedce6 100644
--- a/cmd/mecatui/ui/testdata/subagent_focus.golden
+++ b/cmd/mecatui/ui/testdata/subagent_focus.golden
@@ -20,11 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⛭ subagents 3◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⛭ subagents 3◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/subagent_focus_background.golden b/cmd/mecatui/ui/testdata/subagent_focus_background.golden
index 740f96dbb1..753da31e9f 100644
--- a/cmd/mecatui/ui/testdata/subagent_focus_background.golden
+++ b/cmd/mecatui/ui/testdata/subagent_focus_background.golden
@@ -1,7 +1,6 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ ▸ Subagents Parallel Teams ┃
@@ -21,10 +20,11 @@
+ background subagent #r-g7b7 done — result ready for the agent
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- background subagent #r-g7b7 done — result ready for the agent ⛭ 4◐ 3✓ ctx ▒▒▒▒░░░░ 50%
+ ⛭ subagents 4◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/subagent_roster.golden b/cmd/mecatui/ui/testdata/subagent_roster.golden
index 51193b5d05..a00e8fc445 100644
--- a/cmd/mecatui/ui/testdata/subagent_roster.golden
+++ b/cmd/mecatui/ui/testdata/subagent_roster.golden
@@ -14,17 +14,17 @@
┃ Shell… · 11 tools · ↑0 ↓0 ┃
┃ ✓ map test coverage #r-d0c4 ┃
┃ done · 9 tools · ↑15K ↓4K ┃
- ┃ ✗ check error handling #r-e9b5 ┃
┃ ┃
┃ ↑/↓ select · pgup/pgdn · home/end · enter focus · x cancel · tab switch · esc close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⛭ subagents 3◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⛭ subagents 3◐ 2✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/subagent_roster_background.golden b/cmd/mecatui/ui/testdata/subagent_roster_background.golden
index bbf533258a..95fe43535e 100644
--- a/cmd/mecatui/ui/testdata/subagent_roster_background.golden
+++ b/cmd/mecatui/ui/testdata/subagent_roster_background.golden
@@ -13,7 +13,6 @@
┃ ◐ find dead code #r-b2e2 ┃
┃ Read… · 4 tools · ↑0 ↓0 ┃
┃ ◐ trace config loading #r-c1d3 ┃
- ┃ Shell… · 11 tools · ↑0 ↓0 ┃
┃ · +3 rows below ┃
┃ ┃
┃ ↑/↓ select · pgup/pgdn · home/end · enter focus · x cancel · tab switch · esc close ┃
@@ -21,10 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ background subagent #r-g7b7 done — result ready for the agent
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- background subagent #r-g7b7 done — result ready for the agent ⛭ 4◐ 3✓ ctx ▒▒▒▒░░░░ 50%
+ ⛭ subagents 4◐ 3✓ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_findings.golden b/cmd/mecatui/ui/testdata/team_findings.golden
index 1212c75e4c..c2111897d2 100644
--- a/cmd/mecatui/ui/testdata/team_findings.golden
+++ b/cmd/mecatui/ui/testdata/team_findings.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 2/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 2/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_focus.golden b/cmd/mecatui/ui/testdata/team_focus.golden
index 26e1f45499..8971f23d34 100644
--- a/cmd/mecatui/ui/testdata/team_focus.golden
+++ b/cmd/mecatui/ui/testdata/team_focus.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_focus_windowed.golden b/cmd/mecatui/ui/testdata/team_focus_windowed.golden
index ee3212d6e0..d6d1df8397 100644
--- a/cmd/mecatui/ui/testdata/team_focus_windowed.golden
+++ b/cmd/mecatui/ui/testdata/team_focus_windowed.golden
@@ -14,17 +14,17 @@
┃ ✓ Read — 103 lines ┃
┃ ✓ Read — 104 lines ┃
┃ ✓ Read — 105 lines ┃
- ┃ ✓ Read — 106 lines ┃
┃ ┃
- ┃ lines 1–7 of 12 · esc back · ↑/↓ scroll · pgup/pgdn · home/end ┃
+ ┃ lines 1–6 of 12 · esc back · ↑/↓ scroll · pgup/pgdn · home/end ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 1/1 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 1/1 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_roster.golden b/cmd/mecatui/ui/testdata/team_roster.golden
index 2f2962e1bd..331c7e9128 100644
--- a/cmd/mecatui/ui/testdata/team_roster.golden
+++ b/cmd/mecatui/ui/testdata/team_roster.golden
@@ -14,17 +14,17 @@
┃ ↑1.2K ↓80 ┃
┃ ◆ ✎ builder ┃
┃ Edit… · implementer ┃
- ┃ ↑3.4K ↓220 ┃
┃ ┃
┃ ↑/↓ select · enter focus · x cancel · t tasks · f findings · tab switch · esc close ┃
┃ ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 4/4 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_roster_midrun_idle.golden b/cmd/mecatui/ui/testdata/team_roster_midrun_idle.golden
index 1d7f9b112c..adef0ff882 100644
--- a/cmd/mecatui/ui/testdata/team_roster_midrun_idle.golden
+++ b/cmd/mecatui/ui/testdata/team_roster_midrun_idle.golden
@@ -1,7 +1,6 @@
mecatui · Golden session · fixture/fixture-model/default · mode default
────────────────────────────────────────────────────────────────────────────────────────────────────
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Subagents Parallel ▸ Teams ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 1/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 1/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_roster_stopped.golden b/cmd/mecatui/ui/testdata/team_roster_stopped.golden
index c2c2383300..6c2287c3f9 100644
--- a/cmd/mecatui/ui/testdata/team_roster_stopped.golden
+++ b/cmd/mecatui/ui/testdata/team_roster_stopped.golden
@@ -20,11 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_roster_windowed.golden b/cmd/mecatui/ui/testdata/team_roster_windowed.golden
index bbde12b4b0..680893b1ea 100644
--- a/cmd/mecatui/ui/testdata/team_roster_windowed.golden
+++ b/cmd/mecatui/ui/testdata/team_roster_windowed.golden
@@ -6,8 +6,7 @@
┃ ┃
┃ agents · 20 members ┃
┃ ┃
- ┃ · +6 rows above ┃
- ┃ ↑0 ↓0 ┃
+ ┃ · +7 rows above ┃
┃ ◆ · member-g ┃
┃ working… · worker ┃
┃ ↑0 ↓0 ┃
@@ -21,10 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 20/20 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 20/20 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/team_tasks.golden b/cmd/mecatui/ui/testdata/team_tasks.golden
index b9a700343b..70141a7dd6 100644
--- a/cmd/mecatui/ui/testdata/team_tasks.golden
+++ b/cmd/mecatui/ui/testdata/team_tasks.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ⟳ team- · 2/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ⟳ team- · 2/2 working ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/usermodel.golden b/cmd/mecatui/ui/testdata/usermodel.golden
index 4d58190515..a6bd58e044 100644
--- a/cmd/mecatui/ui/testdata/usermodel.golden
+++ b/cmd/mecatui/ui/testdata/usermodel.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/usermodel_empty_disabled.golden b/cmd/mecatui/ui/testdata/usermodel_empty_disabled.golden
index c54b3ac5ec..9723add9bc 100644
--- a/cmd/mecatui/ui/testdata/usermodel_empty_disabled.golden
+++ b/cmd/mecatui/ui/testdata/usermodel_empty_disabled.golden
@@ -5,7 +5,6 @@
-
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ┃
┃ Saved memory ┃
@@ -21,10 +20,11 @@
+ connected
+
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/usermodel_empty_enabled.golden b/cmd/mecatui/ui/testdata/usermodel_empty_enabled.golden
index b502011882..0e810bd9c5 100644
--- a/cmd/mecatui/ui/testdata/usermodel_empty_enabled.golden
+++ b/cmd/mecatui/ui/testdata/usermodel_empty_enabled.golden
@@ -20,11 +20,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/view_aztec_ansi.golden b/cmd/mecatui/ui/testdata/view_aztec_ansi.golden
index 9714422ac3..81f0fff85f 100644
--- a/cmd/mecatui/ui/testdata/view_aztec_ansi.golden
+++ b/cmd/mecatui/ui/testdata/view_aztec_ansi.golden
@@ -20,11 +20,11 @@
[38;2;233;185;73m┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛[m
+ [38;2;138;153;143m[1;38;2;233;185;73m⚠ awaiting approval[m[m
[38;2;233;185;73m│[m[48;2;21;32;28m [m
[38;2;233;185;73m│[m[48;2;21;32;28m [m[48;2;21;32;28m[48;2;21;32;28m[48;2;21;32;28m[38;2;231;226;211;48;2;21;32;28m[37;48;2;21;32;28m[m[m[38;2;231;226;211;48;2;21;32;28m[7;38;2;233;185;73mA[m[m[38;2;231;226;211;48;2;21;32;28m[38;2;233;185;73;48;2;21;32;28msk mecatl to do something… (enter to send · shift+enter for newline · ? for help)[m[m[38;2;231;226;211;48;2;21;32;28m[48;2;21;32;28m [m[m[48;2;21;32;28m [m
-[38;2;233;185;73m│[m[48;2;21;32;28m [m[48;2;21;32;28m[48;2;21;32;28m[48;2;21;32;28m[38;2;233;185;73;48;2;21;32;28m[37;48;2;21;32;28m[m[m[30;48;2;21;32;28m[48;2;21;32;28m [m[m[48;2;21;32;28m [m
-[38;2;233;185;73m│[m[48;2;21;32;28m [m[48;2;21;32;28m[48;2;21;32;28m[48;2;21;32;28m[38;2;233;185;73;48;2;21;32;28m[37;48;2;21;32;28m[m[m[30;48;2;21;32;28m[48;2;21;32;28m [m[m[48;2;21;32;28m [m
+[38;2;233;185;73m│[m[48;2;21;32;28m [m
[38;2;46;64;57m────────────────────────────────────────────────────────────────────────────────────────────────────[m
- [38;2;138;153;143m[1;38;2;233;185;73m⚠ awaiting approval[m [38;2;76;195;138mctx ▒▒▒▒░░░░ 50%[m[38;2;231;226;211m · 2K/4K · ↑0 ↓0 cache 0%[m[m
+ [38;2;138;153;143m[38;2;76;195;138mctx ▒▒▒▒░░░░ 50%[m[38;2;231;226;211m · 2K/4K · ↑0 ↓0 cache 0%[m[m
[3;38;2;138;153;143m? help · / commands · ctrl+c quit[m
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/view_stripped.golden b/cmd/mecatui/ui/testdata/view_stripped.golden
index 9c6d53a6ed..2805d96b1a 100644
--- a/cmd/mecatui/ui/testdata/view_stripped.golden
+++ b/cmd/mecatui/ui/testdata/view_stripped.golden
@@ -20,11 +20,11 @@
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+ ⚠ awaiting approval
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- ⚠ awaiting approval ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/zerostate_all_on.golden b/cmd/mecatui/ui/testdata/zerostate_all_on.golden
index bad94f596c..4576c2ba91 100644
--- a/cmd/mecatui/ui/testdata/zerostate_all_on.golden
+++ b/cmd/mecatui/ui/testdata/zerostate_all_on.golden
@@ -70,11 +70,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+u clear · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/testdata/zerostate_embedded.golden b/cmd/mecatui/ui/testdata/zerostate_embedded.golden
index bad94f596c..4576c2ba91 100644
--- a/cmd/mecatui/ui/testdata/zerostate_embedded.golden
+++ b/cmd/mecatui/ui/testdata/zerostate_embedded.golden
@@ -70,11 +70,11 @@
+ connected
│
│ Ask mecatl to do something… (enter to send · shift+enter for newline · ? for help)
│
-│
────────────────────────────────────────────────────────────────────────────────────────────────────
- connected ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
+ ctx ▒▒▒▒░░░░ 50% · 2K/4K · ↑0 ↓0 cache 0%
? help · / commands · ctrl+g select all · ctrl+y copy · ctrl+u clear · ctrl+c quit
\ No newline at end of file
diff --git a/cmd/mecatui/ui/toolcalls_inspector_scenario_test.go b/cmd/mecatui/ui/toolcalls_inspector_scenario_test.go
index 6f52e83b75..971c66e281 100644
--- a/cmd/mecatui/ui/toolcalls_inspector_scenario_test.go
+++ b/cmd/mecatui/ui/toolcalls_inspector_scenario_test.go
@@ -998,9 +998,9 @@ func newToolcallsInspectorModel(t *testing.T) Model {
t.Helper()
m := newTestModelFromDeps(Deps{Theme: testTheme(), Ctx: t.Context()})
m.phase = phaseIdle
- m.width, m.height = 100, 30
- m.relayout()
- return m
+ // A real resize sizes the prompt too, so the input box keeps its production height.
+ resized, _ := m.onResize(tea.WindowSizeMsg{Width: 100, Height: 30})
+ return resized.(Model)
}
func openToolcallsForTest(t *testing.T, m Model) Model {
diff --git a/cmd/mecatui/ui/view.go b/cmd/mecatui/ui/view.go
index 2b0d247456..1abd5cd78e 100644
--- a/cmd/mecatui/ui/view.go
+++ b/cmd/mecatui/ui/view.go
@@ -147,7 +147,7 @@ func (m Model) renderHeader() string {
badge, badgeW := m.postureBadgeRender()
available := m.statusHeaderAvailable(badge, badgeW, tail)
if surface := m.generatedStatusLine.Header; m.deps.StatusSource != nil && surface.Present && statusSurfaceFits(surface, available) && statusSpansText(surface.Spans) != "" {
- generated := renderStatusSurface(m.deps.Theme, surface, available, false)
+ generated := renderStatusSpans(m.deps.Theme, surface.Spans)
if line != "" {
line += " · "
}
@@ -331,8 +331,7 @@ func (m Model) changedFilesIndicator() string {
}
// headerGapPad is the minimum blank gap kept between the header identity segment
-// and the right-aligned changed-files indicator so they never touch. Mirrors the
-// footer's footerGapPad but is owned by the header path (naming honesty).
+// and the right-aligned changed-files indicator so they never touch.
const headerGapPad = 2
// statusHeaderAvailable is the custom-header budget after renderer-owned system
@@ -391,9 +390,17 @@ func (m Model) fitHeader(line, badge string, badgeW int, tail string, width int)
return line + strings.Repeat(" ", gap) + styled
}
-// footerActivity renders only the renderer-owned activity lane. Status sources
-// receive its reserved width but cannot replace this chrome.
-func (m Model) footerActivity() string {
+// renderActivity renders the renderer-owned activity line above the input box as
+// exactly one row. Status sources cannot replace this chrome.
+func (m Model) renderActivity() string {
+ width := m.widthOr()
+ style := m.deps.Theme.Style("activity")
+ line := ansi.Truncate(m.activity(), max(0, width-style.GetHorizontalFrameSize()), "…")
+ return style.Width(width).Render(line)
+}
+
+// activity is the unstyled-width content of the activity line for the current phase.
+func (m Model) activity() string {
approval := approvalFooterProjection{}
if m.phase == phaseAwaitingApproval {
approval = approvalFooterProjectionFor(approvalSurfaceFor(&m))
@@ -425,7 +432,7 @@ func (m Model) footerActivity() string {
left = m.sp.View() + " " + m.statusMsg
}
default:
- left = m.idleFooterLeft()
+ left = m.idleActivity()
}
if (m.deps.Debug || m.deps.DebugMouse) && m.mouseDebug != "" {
left = m.deps.Theme.Style("muted").Render(m.mouseDebug)
@@ -433,13 +440,13 @@ func (m Model) footerActivity() string {
return left
}
-// renderFooter is the status bar: spinner + active tool + status + usage.
+// renderFooter is the status bar: left-justified usage (or the custom footer
+// surface) above the key-help line. Activity renders above the input instead.
func (m Model) renderFooter() string {
approval := approvalFooterProjection{}
if m.phase == phaseAwaitingApproval {
approval = approvalFooterProjectionFor(approvalSurfaceFor(&m))
}
- left := m.footerActivity()
// The full decompressed chord list now lives in the "?" help overlay, so the
// footer leads with its two entry points and carries only the most useful prompt
@@ -474,30 +481,30 @@ func (m Model) renderFooter() string {
}
}
// While the double-quit guard is armed, prepend a loud "again to quit" cue to
- // the help line. The footer is the one chrome line present in every phase (the
- // left status differs by phase), so it is the robust place for the hint.
+ // the help line. The help line is present in every phase, so it is the robust
+ // place for the hint.
if m.quitArmed {
help = m.deps.Theme.Style("ctxWarn").Render(hk.quit+" again to quit") + " · " + help
}
width := m.widthOr()
- line := left
+ line := ""
available := m.statusLineGeometry().footerAvailable
if surface := m.generatedStatusLine.Footer; m.deps.StatusSource != nil && surface.Present && statusSurfaceFits(surface, available) && statusSpansText(surface.Spans) != "" {
- line += strings.Repeat(" ", footerGapPad) + renderStatusSurface(m.deps.Theme, surface, available, true)
+ line = renderStatusSpans(m.deps.Theme, surface.Spans)
}
footer := m.deps.Theme.Style("footer").Width(width).Render(line)
return footer + "\n" + m.deps.Theme.Style("muted").Render(help)
}
-// idleFooterLeft renders the footer-left for the idle/default phase, extracted
-// from renderFooter to keep that dispatcher under the cyclomatic-complexity
+// idleActivity renders the activity line for the idle/default phase, extracted
+// from activity to keep that dispatcher under the cyclomatic-complexity
// bound. Precedence: the live-feed reconnecting cue (issue #387) → the
// selection count → the gateway notice → the bare statusMsg / "ready". The
// selection count appears ONLY in this phase (the running/approval/connecting
-// arms own the footer-left there), so it is never shown mid-run by construction
+// arms own the activity line there), so it is never shown mid-run by construction
// (Req 5); the reconnecting cue and gateway notice likewise only surface here.
-func (m Model) idleFooterLeft() string {
+func (m Model) idleActivity() string {
switch {
case m.liveReconnecting:
// The live session event feed dropped and the client is reconnecting with
@@ -545,10 +552,6 @@ func (m Model) selectionStatus() string {
return muted.Render(count)
}
-// footerGapPad is the minimum blank gap kept between the left status and the
-// right-aligned usage segment so they never touch.
-const footerGapPad = 2
-
// queuePreviewLimit is the number of staged follow-ups previewed in the queue
// card; the rest are summarised as a "+K more" line so a deep queue stays compact.
const queuePreviewLimit = 3
@@ -741,7 +744,7 @@ func inputRailStyle(th theme.Theme, mode string) lipgloss.Style {
BorderLeft(true).
BorderForeground(modeAccentStyle(th, mode).GetForeground()).
Background(th.Color("bgPanel")).
- Padding(inputRailPadTop, inputRailPadX, 0, inputRailPadX)
+ Padding(inputRailPadTop, inputRailPadX, inputRailPadBottom, inputRailPadX)
}
// inputRailPadX is the horizontal padding inside the input panel (each side). With the
@@ -751,13 +754,15 @@ func inputRailStyle(th theme.Theme, mode string) lipgloss.Style {
// so this value flows through automatically.
const inputRailPadX = 1
-// inputRailPadTop is the TOP inner padding of the input panel: one tinted blank row
-// above the input content so the placeholder/typed text isn't pressed against the top
-// border. It INTENTIONALLY makes the input region one row taller — the layout measures
-// region heights via lipgloss.Height, so the body shrinks by it automatically (the
-// input height-invariance test expects exactly this +1). lipgloss renders the pad row
-// with the style's Background, so it is bgPanel-tinted full-width like the content rows.
-const inputRailPadTop = 1
+// inputRailPadTop and inputRailPadBottom frame the input content with one tinted
+// blank row above and below, so a one-line prompt isn't pressed against its
+// neighbours. The layout measures region heights via lipgloss.Height, so the body
+// shrinks by them automatically. lipgloss renders the pad rows with the style's
+// Background, so they are bgPanel-tinted full-width like the content rows.
+const (
+ inputRailPadTop = 1
+ inputRailPadBottom = 1
+)
// renderInputRail wraps the textarea view in the mode-coloured rail AND fills the faint
// panel tint UNIFORMLY across the whole input block — full terminal width and every
diff --git a/cmd/mecatui/ui/welcome_test.go b/cmd/mecatui/ui/welcome_test.go
index d541ae7b9c..4febb63c3a 100644
--- a/cmd/mecatui/ui/welcome_test.go
+++ b/cmd/mecatui/ui/welcome_test.go
@@ -249,11 +249,11 @@ func TestKittyTransmitLifecycle(t *testing.T) {
// (c) a tier-CROSSING resize (still mascot-bearing, smaller tier) re-fires and
// updates the tier. A height that drops to a smaller-but-nonzero tier.
- mm, cmd = m.Update(tea.WindowSizeMsg{Width: 100, Height: 40})
+ mm, cmd = m.Update(tea.WindowSizeMsg{Width: 100, Height: 41})
m = mm.(Model)
crossCols, _ := welcome.Tier(m.width, m.vp.Height())
if crossCols == 0 {
- t.Fatalf("test precondition: 100x40 should still fit a (smaller) mascot (vp=%d)", m.vp.Height())
+ t.Fatalf("test precondition: 100x41 should still fit a (smaller) mascot (vp=%d)", m.vp.Height())
}
if m.kittyTier != crossCols {
t.Errorf("tier-crossing resize: kittyTier = %d, want %d", m.kittyTier, crossCols)
diff --git a/cmd/mecatui/ui/workspace_enrollment.go b/cmd/mecatui/ui/workspace_enrollment.go
index e22bd4f1f0..96aa2c47c7 100644
--- a/cmd/mecatui/ui/workspace_enrollment.go
+++ b/cmd/mecatui/ui/workspace_enrollment.go
@@ -190,7 +190,7 @@ func (m Model) runToolsConnect() (tea.Model, tea.Cmd) {
}
m.enrollment.busy = true
m.enrollment.err = ""
- // workspaceEnrollmentNotice, not statusMsg: idleFooterLeft gives the notice
+ // workspaceEnrollmentNotice, not statusMsg: idleActivity gives the notice
// priority over statusMsg (issue: a /tools-connect outcome written to
// statusMsg renders invisibly whenever the ambient "not connected" notice
// is also set — which it always is, right up until this call clears it).
diff --git a/cmd/mecatui/ui/workspace_enrollment_test.go b/cmd/mecatui/ui/workspace_enrollment_test.go
index 49ff903fe5..f2c5951632 100644
--- a/cmd/mecatui/ui/workspace_enrollment_test.go
+++ b/cmd/mecatui/ui/workspace_enrollment_test.go
@@ -28,7 +28,7 @@ func TestWorkspaceEnrollmentIsNonBlocking(t *testing.T) {
if !strings.Contains(m.workspaceEnrollmentNotice, "/tools-connect") {
t.Fatalf("workspaceEnrollmentNotice = %q, want /tools-connect", m.workspaceEnrollmentNotice)
}
- if got := stripANSIstr(m.idleFooterLeft()); !strings.Contains(got, "/tools-connect") {
+ if got := stripANSIstr(m.idleActivity()); !strings.Contains(got, "/tools-connect") {
t.Fatalf("footer-left = %q, want enrollment notice", got)
}
@@ -46,10 +46,10 @@ func TestWorkspaceEnrollmentIsNonBlocking(t *testing.T) {
if control.connectCalls != 1 || m.enrollment.ID != "bundle-1" {
t.Fatalf("connect calls/state = %d/%+v", control.connectCalls, m.enrollment)
}
- if got := stripANSIstr(m.idleFooterLeft()); !strings.Contains(got, "notified when connected") {
+ if got := stripANSIstr(m.idleActivity()); !strings.Contains(got, "notified when connected") {
t.Fatalf("footer-left after a pending connect = %q, want automatic completion notice", got)
}
- if got := stripANSIstr(m.idleFooterLeft()); strings.Contains(got, "https://") || strings.Contains(got, "token-canary") {
+ if got := stripANSIstr(m.idleActivity()); strings.Contains(got, "https://") || strings.Contains(got, "token-canary") {
t.Fatalf("footer rendered private presentation data: %s", got)
}
if got := fmt.Sprintf("%+v", m.enrollment); strings.Contains(got, "https://") || strings.Contains(got, "token-canary") {
diff --git a/docs/acceptance/mecatui-status-line.md b/docs/acceptance/mecatui-status-line.md
index 3605744e3d..6cddbb1099 100644
--- a/docs/acceptance/mecatui-status-line.md
+++ b/docs/acceptance/mecatui-status-line.md
@@ -3,6 +3,7 @@
**Phase:** capability — local TUI presentation extension
**Status:** in-progress, 2026-08-27. Updated after the template, shared-markup, and mecatui-settings decisions.
**ADR:** [ADR-0289](../adr/0289-hardened-status-command-boundary.md) — hardened command output and explicit environment extension; it supersedes [ADR-0247](../adr/0247-mecatui-status-line.md).
+**Superseded placement:** [ADR-0376](../adr/0376-mecatui-status-surfaces-and-activity-line.md) supersedes ADR-0289 and moves the activity lane described below out of the footer to its own line above the input box; custom footers no longer share their row with it.
**Accumulator branch:** `acc/mecatui-status-line` (off `main`).
The smallest useful capability makes both the current mecatui header and status/usage row shipped default templates. Operators may replace either surface through the user-global mecatui settings document with responsive templates or one local command. Both paths consume the same status input and produce the same safe, theme-aware `StatusML` document. The keyboard-help row remains mecatui chrome, and the renderer preserves mandatory header safety/navigation indicators outside template control.
diff --git a/docs/adr/0289-hardened-status-command-boundary.md b/docs/adr/0289-hardened-status-command-boundary.md
index db162247ac..c6fd96c745 100644
--- a/docs/adr/0289-hardened-status-command-boundary.md
+++ b/docs/adr/0289-hardened-status-command-boundary.md
@@ -4,7 +4,7 @@
- Date: 2026-09-01
- Scope: `cmd/mecatui` local status-command boundary
- Supersedes: ADR 0247
-- Superseded by: none
+- Superseded by: ADR 0376
## Context
diff --git a/docs/adr/0376-mecatui-status-surfaces-and-activity-line.md b/docs/adr/0376-mecatui-status-surfaces-and-activity-line.md
new file mode 100644
index 0000000000..a5eb4d2e7a
--- /dev/null
+++ b/docs/adr/0376-mecatui-status-surfaces-and-activity-line.md
@@ -0,0 +1,135 @@
+# ADR 0376 — mecatui status surfaces and activity line
+
+- Status: Accepted
+- Date: 2026-10-01
+- Scope: `cmd/mecatui` local status-line generation, status-command boundary, and the
+ placement of status chrome in the frame
+- Supersedes: ADR 0289 (and, through it, ADR 0247)
+- Superseded by: none
+
+## Context
+
+ADR 0247 established user-configurable header and footer status surfaces for
+mecatui. ADR 0289 superseded it to harden the status-command boundary, but restated
+only the command environment and output trimming. The rest of 0247's decision,
+including where renderer-owned chrome sits, had no current record.
+
+0247 placed the session activity signal ("thinking", running tools, approval state) in
+the footer, below the input box. Operators finishing a prompt look above the input
+for signs of progress, so the most important "is anything happening?" signal was easy
+to miss ([issue #1325](https://github.com/stacklok/mecatl/issues/1325)). 0247 also
+fixed alignment details of individual surfaces. Those details are presentation choices
+that do not need an architecture record.
+
+This ADR consolidates the decisions from 0247 and 0289 that remain in force and
+records the new placement of status chrome.
+
+## Decision
+
+### Configuration and sources
+
+Place `status_customization:` only in the user-global
+`$XDG_CONFIG_HOME/mecatui/settings.yaml`, beside key bindings. It selects exactly one
+source: responsive Go-template surfaces or a direct local executable at an absolute
+`executable` path with literal `args`. Shell/source and command-string forms are not
+configuration. The configuration may supply optional header and footer surfaces;
+absent surfaces retain shipped defaults.
+
+Compose one UI-agnostic `Source` outside `ui`. The UI submits the latest canonical raw
+`Input` whenever display facts or the per-surface available widths change. The source
+owns template-versus-command selection, template evaluation, command
+debounce/cancellation/timeout/process cleanup, interval-driven clock refresh,
+stale-completion suppression, and per-surface default/last-good degradation.
+
+```go
+type Source interface {
+ Submit(Input)
+ Changed() <-chan struct{}
+ Latest() Result
+ Close(context.Context) error
+}
+```
+
+`Changed` is a capacity-one wake-up edge, never a result queue. The source stores its
+latest result behind a mutex, allocates fresh bounded span slices on publish, and
+`Latest` deep-copies them before return. Private generations prevent stale
+command/template completions from publishing; no request token is exposed in input or
+result. One Bubble Tea adapter command waits on `Changed`, snapshots `Latest`, posts a
+message, and re-arms after each nonterminal notification. The listener captures the
+TUI root context; source shutdown cancels owned work, joins it, and closes `Changed` so
+the listener stops cleanly.
+
+### Input and output boundary
+
+Both source modes consume the same raw `Input`. Commands receive its JSON serialization
+on stdin and never receive template interpolation. Templates receive a private
+automatically StatusML-escaped projection; a direct executable must escape any dynamic
+values that it interpolates into StatusML. `Input` contains only display-safe facts:
+credential-free server target/connection mode, session/model facts, named usage/cache
+atoms, context facts, main/delegation activity, session-workspace provenance, terminal
+dimensions, per-surface available width, and clock. It excludes prompts,
+transcript/tool content, credentials, authentication metadata, diagnostics, raw command
+output, and the private local launch-workspace fallback.
+
+Keep the status-command environment fixed to its safe baseline. Permit only additional
+names listed in the user-global `status_customization.command.passthrough_env`
+allowlist. Each name must match `[A-Za-z_][A-Za-z0-9_]*`; unset names are omitted, with
+no fallback from `os.Environ`. Deduplicate names, and reject requested baseline or
+source-owned names such as terminal `COLUMNS` and `LINES` during settings validation.
+
+Immediately before StatusML parsing, trim only leading and trailing ASCII space, tab,
+LF, CR, vertical tab, and form feed. Preserve all interior bytes; malformed StatusML
+retains the safe fallback.
+
+`Result` contains independently optional, width-selected header and footer semantic
+spans. It contains no ANSI or OSC bytes. StatusML has optional `header` and `footer`
+surfaces containing styled text/link nodes directly, with semantic style tokens and a
+`link` node with separate display text and a bounded quoted `href`. Only `http` and
+`https` links are accepted. The UI applies the active theme, renders validated links,
+and performs final clipping and alignment.
+
+### Placement of status chrome
+
+Renderer-owned chrome cannot be replaced or removed by customization:
+
+- The header row carries the header surface beside the header safety and navigation
+ indicators.
+- The activity line sits directly above the input box, below the conversation and any
+ inline menus or queued follow-ups. It carries the session activity: idle and
+ notice text, model thinking, running tools, approval state, and connection state.
+- The footer sits below the input box. It carries the footer surface, followed by the
+ keyboard help.
+
+Status sources receive the width available to their surface after renderer-owned
+chrome on the same row is reserved. The activity line no longer shares a row with the
+footer surface.
+
+## Consequences
+
+Activity feedback sits where operators look while they wait, and custom footers gain
+the width the activity text used to reserve. The frame spends more rows on chrome
+below the conversation, so very short terminals trade padding around the activity
+line before conversation rows.
+
+Templates customize status without a process; local commands can run trusted
+user-global integrations from a local session workspace when known, otherwise the local
+launch workspace. A remote workspace never becomes a local command CWD. Commands
+inherit only the explicit minimal environment plus operator-listed names and have
+bounded output and lifetime. Operators must not list credentials in
+`passthrough_env`.
+
+The UI remains free of settings parsing, timers, subprocesses, and command policy while
+receiving autonomous latest-wins updates. The source is an outlives-a-call resource
+requiring lifecycle inventory, shutdown/leak, stale-result, and coalescing tests.
+Generated semantic spans preserve theme control and terminal safety, at the cost of
+maintaining a small StatusML parser.
+
+## See also
+
+- [ADR 0247 — mecatui generated status lines](./0247-mecatui-status-line.md)
+- [ADR 0289 — Hardened status-command output and environment extension](./0289-hardened-status-command-boundary.md)
+- [mecatui status-line acceptance plan](../acceptance/mecatui-status-line.md)
+- [mecatui guide](../tui.md)
+- The public `user-docs/mecatui/status-line.md` status-line customization guide
+- [ADR 0027 — cloud-native lifecycle](./0027-cloud-native.md)
+- [ADR 0002 — documentation lifecycle](./0002-documentation-lifecycle.md)
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 66887dde0a..fbb89c8cc2 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -298,7 +298,8 @@ Documentation/citation conventions are in [`docs/design/README.md`](../design/RE
- [0088 — Explicit daemon.yaml (listener topology config)](./0088-daemon-config-file.md)
- [0222 — mecatui: ctrl+t routes by ask type; full-screen ask-args view](./0222-mecatui-ask-args-view.md)
- [0247 — mecatui generated status lines](./0247-mecatui-status-line.md) *(superseded by 0289)*
-- [0289 — Hardened status-command output and environment extension](./0289-hardened-status-command-boundary.md)
+- [0289 — Hardened status-command output and environment extension](./0289-hardened-status-command-boundary.md) *(superseded by 0376)*
+- [0376 — mecatui status surfaces and activity line](./0376-mecatui-status-surfaces-and-activity-line.md)
- [0344 — Mecatui-owned terminal titles](./0344-mecatui-terminal-title-controller.md) *(proposed)*
- [0280 — Automatic light theme selection in mecatui](./0280-mecatui-light-theme-autodetect.md)
- [0291 — Server-owned session placement](./0291-server-owned-session-placement.md)
diff --git a/docs/tui.md b/docs/tui.md
index 0975589b16..df0fe82f4a 100644
--- a/docs/tui.md
+++ b/docs/tui.md
@@ -36,10 +36,10 @@ for the operator-facing meaning of safety controls.
## Layout and navigation
The frame is a stack of header, conversation, optional transient regions,
-prompt, and footer. `cmd/mecatui/ui/layout.go` derives that stack for rendering,
-viewport sizing, and mouse hit testing. Add regions there so a transient shrinks
-the conversation instead of displacing the footer. Measure the rendered frame
-rather than maintaining a second set of row offsets.
+activity line, prompt, and footer. `cmd/mecatui/ui/layout.go` derives that stack
+for rendering, viewport sizing, and mouse hit testing. Add regions there so a
+transient shrinks the conversation instead of displacing the footer. Measure the
+rendered frame rather than maintaining a second set of row offsets.
Overlays that take over the conversation region own their interaction state and
size themselves from the geometry offered on each render. The parent owns their
@@ -100,7 +100,7 @@ high-risk posture badge uses a fixed, contrast-checked danger style rather than
a theme-controlled color.
Choose the lifetime of a notice deliberately. Put durable conversation facts in
-scrollback; use the transient footer for advisories and activity whose outcome
+scrollback; use the activity line for advisories and activity whose outcome
belongs elsewhere. An error or failed action needs visible feedback, not a
silent reset or an optimistic success claim. Do not show raw transport or
credential material in a notice.
diff --git a/user-docs/mecatui/status-line.md b/user-docs/mecatui/status-line.md
index c0b909710b..c8039d2ed6 100644
--- a/user-docs/mecatui/status-line.md
+++ b/user-docs/mecatui/status-line.md
@@ -17,11 +17,11 @@ sessions cannot change it.
With no `status_customization:` entry, `mecatui` uses its shipped responsive
templates. The header includes the active session title at every width and the
-remote target in its full variant. Keyboard help, the header
-posture/scroll/changed-file indicators, and the footer activity lane remain part
-of the client interface; customization cannot remove them. In a debug session,
-the header also keeps a `⚠ DEBUG target` cue ahead of generated content and a
-privacy disclosure below it, even if a custom header is empty.
+remote target in its full variant. Keyboard help and the header
+posture/scroll/changed-file indicators remain part of the client interface;
+customization cannot remove them. In a debug session, the header also keeps a
+`⚠ DEBUG target` cue ahead of generated content and a privacy disclosure below
+it, even if a custom header is empty.
## Choose a source
@@ -144,7 +144,7 @@ refreshes it.
|`Workspace.Name`|string|Provider-supplied workspace display metadata. It is not a directory basename or a usable path.|
|`Workspace.Path`|string|Exact local root returned by the privileged local-context RPC. It is available to templates through their escaped projection and to a configured direct local status command. It is empty for remote, untrusted, no-FS, unavailable, and otherwise ineligible sessions.|
|`Terminal.Rows`, `Terminal.Cols`|integers|Measured terminal dimensions.|
-|`Terminal.HeaderAvailCols`, `Terminal.FooterAvailCols`|integers|Columns remaining after the client reserves mandatory header and footer lanes.|
+|`Terminal.HeaderAvailCols`, `Terminal.FooterAvailCols`|integers|Columns available to each surface. The header value excludes the mandatory header indicators; the footer value is the full footer row less its padding.|
|`MainAgent.State`|string|`connecting`, `idle`, `thinking`, `running_tool`, `awaiting_approval`, `completed`, `failed`, or `cancelled`.|
|`MainAgent.Activity`|string|Bounded display activity label.|
|`MainAgent.Approval`|string|`none` or `awaiting`.|
@@ -170,8 +170,8 @@ command that supplies both surfaces:
```
-Each surface contains its styled text and link nodes directly. Headers are
-left-aligned by the renderer; footers are right-aligned. Text may be wrapped in
+Each surface contains its styled text and link nodes directly. The renderer
+left-aligns both headers and footers. Text may be wrapped in
these semantic tokens:
`text`, `muted`, `primary`, `secondary`, `accent`, `success`, `warning`,
diff --git a/user-docs/mecatui/using-the-tui.md b/user-docs/mecatui/using-the-tui.md
index d7c4f3fa3e..927a1ae0a6 100644
--- a/user-docs/mecatui/using-the-tui.md
+++ b/user-docs/mecatui/using-the-tui.md
@@ -56,6 +56,9 @@ home directory, `@~/…` also remains prose.
## Keep working while a run is active
+The line directly above the prompt shows what the agent is doing: thinking,
+running a tool, waiting for approval, or ready for input.
+
Type your next instruction while the agent is running, then press `enter`.
`mecatui` steers the run at the next safe turn boundary when the server supports
steering. Otherwise, it queues the instruction as a follow-up. Images and other