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(`
docs
`, 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 @@ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + ⚠ 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/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:
mecatui · GPT-5
``` -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