From 6e59b568c4e7a45c3d1be693ebe360a8a60c71d7 Mon Sep 17 00:00:00 2001 From: Michelangelo Mori <328978+blkt@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:18:47 +0200 Subject: [PATCH 1/2] feat(mecatui): move the session activity line above the input box Render the activity line (ready, thinking, tool progress, approval, connecting) on its own row directly above the input, framed by blank spacers that short terminals drop first. Footer usage and custom footer surfaces are now left-justified across the full row, and the input box shrinks to one text row framed by a padding row above and below. ADR 0376 supersedes ADR 0289, consolidating the status-surface decisions of ADRs 0247 and 0289 with the new placement. Closes #1325 Co-authored-by: Mecatl --- cmd/mecatui/theme/theme.go | 5 + cmd/mecatui/theme/theme_test.go | 2 +- cmd/mecatui/ui/agents_overlay_test.go | 2 +- cmd/mecatui/ui/ask_queue_test.go | 29 ++-- cmd/mecatui/ui/canonical_status_test.go | 4 +- cmd/mecatui/ui/footer_test.go | 42 +++--- cmd/mecatui/ui/input_rail_test.go | 52 ++++--- cmd/mecatui/ui/layout.go | 39 ++--- cmd/mecatui/ui/layout_test.go | 84 ++++++++++- cmd/mecatui/ui/model.go | 4 +- cmd/mecatui/ui/models_test.go | 31 ++-- cmd/mecatui/ui/plan_approval_test.go | 47 +++--- .../ui/prompttextarea/prompttextarea.go | 2 +- .../ui/prompttextarea/prompttextarea_test.go | 13 +- cmd/mecatui/ui/reconnect_live_test.go | 23 +-- cmd/mecatui/ui/selection_test.go | 47 +++--- cmd/mecatui/ui/spinner_gate_test.go | 18 +-- cmd/mecatui/ui/statusline.go | 10 -- cmd/mecatui/ui/statusline_source.go | 8 +- cmd/mecatui/ui/statusline_source_test.go | 5 +- cmd/mecatui/ui/statusline_test.go | 13 +- cmd/mecatui/ui/suspend_quitd_test.go | 9 +- .../ui/testdata/agents_inventory.golden | 4 +- .../agents_inventory_empty_disabled.golden | 6 +- .../agents_inventory_empty_enabled.golden | 4 +- .../testdata/agents_inventory_scroll.golden | 10 +- .../ui/testdata/agents_teams_tab.golden | 6 +- .../testdata/askargs_modal_longshell.golden | 6 +- .../ui/testdata/askargs_view_pretty.golden | 6 +- .../ui/testdata/askargs_view_raw.golden | 6 +- .../ui/testdata/askargs_view_scrolled.golden | 6 +- cmd/mecatui/ui/testdata/effort_picker.golden | 4 +- .../ui/testdata/footer_gateway_notice.golden | 4 +- cmd/mecatui/ui/testdata/help_all_on.golden | 8 +- cmd/mecatui/ui/testdata/help_embedded.golden | 8 +- .../testdata/jev_routing_user_journey.golden | 4 +- cmd/mecatui/ui/testdata/mcp_err_input.golden | 4 +- .../ui/testdata/mcp_err_not_configured.golden | 4 +- cmd/mecatui/ui/testdata/mcp_err_server.golden | 4 +- cmd/mecatui/ui/testdata/mcp_panel.golden | 4 +- .../ui/testdata/mcp_panel_groups.golden | 4 +- .../ui/testdata/mcp_prompt_args.golden | 6 +- cmd/mecatui/ui/testdata/mcp_prompts.golden | 6 +- .../ui/testdata/mcp_resource_preview.golden | 6 +- cmd/mecatui/ui/testdata/mcp_resources.golden | 6 +- cmd/mecatui/ui/testdata/models.golden | 6 +- .../ui/testdata/models_codex_healthy.golden | 4 +- .../testdata/models_codex_unauthorized.golden | 4 +- .../ui/testdata/models_disabled.golden | 6 +- cmd/mecatui/ui/testdata/models_empty.golden | 4 +- .../ui/testdata/models_filtered.golden | 6 +- .../ui/testdata/models_gateway_empty.golden | 4 +- .../ui/testdata/models_global_default.golden | 6 +- .../models_mixed_deployment_empty.golden | 6 +- cmd/mecatui/ui/testdata/models_nomatch.golden | 4 +- .../ui/testdata/models_scrolled.golden | 8 +- .../models_toolhive_unreachable.golden | 6 +- .../ui/testdata/parallel_group_focus.golden | 8 +- .../ui/testdata/parallel_roster.golden | 4 +- .../ui/testdata/paste_placeholder.golden | 4 +- .../testdata/sessions_cleanup_review.golden | 6 +- .../ui/testdata/sessions_picker.golden | 4 +- .../ui/testdata/sessions_picker_empty.golden | 4 +- .../ui/testdata/sessions_picker_error.golden | 4 +- .../testdata/sessions_picker_filtered.golden | 4 +- .../testdata/sessions_picker_nomatch.golden | 4 +- .../testdata/sessions_transcript_error.golden | 5 +- .../sessions_transcript_loaded.golden | 5 +- .../sessions_transcript_loading.golden | 5 +- .../sessions_transcript_rendered.golden | 6 +- cmd/mecatui/ui/testdata/skills.golden | 6 +- .../ui/testdata/skills_filtered.golden | 6 +- cmd/mecatui/ui/testdata/skills_nomatch.golden | 6 +- cmd/mecatui/ui/testdata/skills_scroll.golden | 10 +- cmd/mecatui/ui/testdata/soul.golden | 4 +- .../ui/testdata/soul_empty_disabled.golden | 6 +- .../ui/testdata/soul_empty_enabled.golden | 4 +- cmd/mecatui/ui/testdata/soul_project.golden | 6 +- cmd/mecatui/ui/testdata/steer_card.golden | 6 +- cmd/mecatui/ui/testdata/subagent_focus.golden | 4 +- .../testdata/subagent_focus_background.golden | 6 +- .../ui/testdata/subagent_roster.golden | 6 +- .../subagent_roster_background.golden | 6 +- cmd/mecatui/ui/testdata/team_findings.golden | 4 +- cmd/mecatui/ui/testdata/team_focus.golden | 4 +- .../ui/testdata/team_focus_windowed.golden | 8 +- cmd/mecatui/ui/testdata/team_roster.golden | 6 +- .../testdata/team_roster_midrun_idle.golden | 6 +- .../ui/testdata/team_roster_stopped.golden | 4 +- .../ui/testdata/team_roster_windowed.golden | 8 +- cmd/mecatui/ui/testdata/team_tasks.golden | 4 +- cmd/mecatui/ui/testdata/usermodel.golden | 4 +- .../testdata/usermodel_empty_disabled.golden | 6 +- .../testdata/usermodel_empty_enabled.golden | 4 +- .../ui/testdata/view_aztec_ansi.golden | 6 +- cmd/mecatui/ui/testdata/view_stripped.golden | 4 +- .../ui/testdata/zerostate_all_on.golden | 4 +- .../ui/testdata/zerostate_embedded.golden | 4 +- .../ui/toolcalls_inspector_scenario_test.go | 6 +- cmd/mecatui/ui/view.go | 63 ++++---- cmd/mecatui/ui/welcome_test.go | 4 +- cmd/mecatui/ui/workspace_enrollment.go | 2 +- cmd/mecatui/ui/workspace_enrollment_test.go | 6 +- docs/acceptance/mecatui-status-line.md | 1 + .../0289-hardened-status-command-boundary.md | 2 +- ...catui-status-surfaces-and-activity-line.md | 135 ++++++++++++++++++ docs/adr/README.md | 3 +- docs/tui.md | 10 +- user-docs/mecatui/status-line.md | 16 ++- user-docs/mecatui/using-the-tui.md | 3 + 110 files changed, 708 insertions(+), 437 deletions(-) create mode 100644 docs/adr/0376-mecatui-status-surfaces-and-activity-line.md 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..a9b0c74996 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -36,8 +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 +activity line, prompt, and footer +([ADR 0376](adr/0376-mecatui-status-surfaces-and-activity-line.md)). +`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. @@ -100,8 +102,8 @@ 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 -belongs elsewhere. An error or failed action needs visible feedback, not a +scrollback; use the activity line above the prompt 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..743cff9e04 100644 --- a/user-docs/mecatui/status-line.md +++ b/user-docs/mecatui/status-line.md @@ -18,10 +18,12 @@ 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. +posture/scroll/changed-file indicators, and the activity line above the input +box remain part of the client interface; customization cannot remove them. The +activity line shows whether `mecatui` is ready, thinking, running a tool, +waiting for approval, or connecting. 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 +146,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 +172,8 @@ command that supplies both surfaces:
mecatui · GPT-5
ctx 42%
``` -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 From 837bb347ccbb0336da2eb960055b616b2a2f3c9c Mon Sep 17 00:00:00 2001 From: Michelangelo Mori <328978+blkt@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:44:48 +0200 Subject: [PATCH 2/2] docs(mecatui): trim activity-line doc changes per review Drop the ADR pointer from docs/tui.md and remove the activity-line sentences from the status-line customization guide: the activity line is not a status-line surface. Co-authored-by: Mecatl --- docs/tui.md | 14 ++++++-------- user-docs/mecatui/status-line.md | 8 +++----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/tui.md b/docs/tui.md index a9b0c74996..df0fe82f4a 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -36,12 +36,10 @@ for the operator-facing meaning of safety controls. ## Layout and navigation The frame is a stack of header, conversation, optional transient regions, -activity line, prompt, and footer -([ADR 0376](adr/0376-mecatui-status-surfaces-and-activity-line.md)). -`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 @@ -102,8 +100,8 @@ 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 activity line above the prompt for advisories and activity -whose outcome belongs elsewhere. An error or failed action needs visible feedback, not a +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 743cff9e04..c8039d2ed6 100644 --- a/user-docs/mecatui/status-line.md +++ b/user-docs/mecatui/status-line.md @@ -17,11 +17,9 @@ 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 activity line above the input -box remain part of the client interface; customization cannot remove them. The -activity line shows whether `mecatui` is ready, thinking, running a tool, -waiting for approval, or connecting. In a debug session, the header also keeps a +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.