From b7ad152a5f7df20d8c2ccc69f059482064d66b7a Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 4 Oct 2026 17:03:11 -0700 Subject: [PATCH 1/4] mecatui: show humanized main and aux usage on exit The embedded exit summary now prints a humanized Tokens (main) line and, when nonzero, a Tokens (aux) line summing every non-main usage bucket. Co-authored-by: Mecatl --- cmd/mecatui/client/session.go | 20 +++++++++++++++++ cmd/mecatui/client/session_test.go | 9 ++++++-- cmd/mecatui/exit_handoff.go | 25 ++++++++++++++++------ cmd/mecatui/exit_handoff_scenarios_test.go | 9 +++++--- cmd/mecatui/ui/statusline_source.go | 4 ++++ user-docs/mecatui/sessions.md | 16 +++++++++++--- 6 files changed, 68 insertions(+), 15 deletions(-) diff --git a/cmd/mecatui/client/session.go b/cmd/mecatui/client/session.go index 230d3a0378..278b2c29c7 100644 --- a/cmd/mecatui/client/session.go +++ b/cmd/mecatui/client/session.go @@ -25,6 +25,9 @@ type SessionSnapshot struct { // Usage is the canonical cumulative main-session ledger. It is distinct from // ContextOccupancy, which is only the latest context-meter display state. Usage Usage + // AuxiliaryUsage is the sum of every non-main canonical usage bucket (title + // generation, compaction, reflection, routing, reviewers, guardrails, judges). + AuxiliaryUsage Usage // ContextOccupancy is nil when a legacy or pre-turn snapshot has no known // context-meter numerator. ContextOccupancy *ContextOccupancy @@ -54,6 +57,22 @@ func contextOccupancyFrom(occupancy *mecatlv1.ContextOccupancy) *ContextOccupanc return &ContextOccupancy{InputTokens: occupancy.GetInputTokens(), Estimated: occupancy.GetEstimated()} } +func auxiliaryUsageFrom(buckets map[string]*mecatlv1.TokenUsage) Usage { + var total Usage + for kind, bucket := range buckets { + if kind == "main" { + continue + } + u := usageFrom(bucket.GetTotal()) + total.InputTokens += u.InputTokens + total.OutputTokens += u.OutputTokens + total.CacheReadTokens += u.CacheReadTokens + total.CacheWriteTokens += u.CacheWriteTokens + total.ReasoningTokens += u.ReasoningTokens + } + return total +} + func snapshotFrom(s *mecatlv1.Session) SessionSnapshot { return snapshotFromWithGlobalCapabilities(s, Capabilities{}) } @@ -70,6 +89,7 @@ func snapshotFromWithGlobalCapabilities(s *mecatlv1.Session, global Capabilities CreatedAt: s.GetCreatedAtUnix(), ResolvedModel: resolvedModelFrom(s.GetResolvedModel()), Usage: usageFrom(s.GetTokenUsage()["main"].GetTotal()), + AuxiliaryUsage: auxiliaryUsageFrom(s.GetTokenUsage()), ContextOccupancy: contextOccupancyFrom(s.GetLatestContextOccupancy()), Title: titleFromProto(s), TitleProvenance: titleProvenanceFromProto(s), diff --git a/cmd/mecatui/client/session_test.go b/cmd/mecatui/client/session_test.go index e9ce1c1967..f51d5c6fe2 100644 --- a/cmd/mecatui/client/session_test.go +++ b/cmd/mecatui/client/session_test.go @@ -157,18 +157,23 @@ func TestSnapshotFromReadsTitle(t *testing.T) { func TestSnapshotFromProjectsMainUsageAndOptionalContextOccupancy(t *testing.T) { snap := snapshotFrom(&mecatlv1.Session{ TokenUsage: map[string]*mecatlv1.TokenUsage{ - "main": {Total: &mecatlv1.Usage{InputTokens: 120_000, OutputTokens: 4_000, CacheReadTokens: 90_000}}, + "main": {Total: &mecatlv1.Usage{InputTokens: 120_000, OutputTokens: 4_000, CacheReadTokens: 90_000}}, + "session_title": {Total: &mecatlv1.Usage{InputTokens: 300, OutputTokens: 10}}, + "guardrail": {Total: &mecatlv1.Usage{InputTokens: 700, OutputTokens: 5, CacheReadTokens: 200}}, }, LatestContextOccupancy: &mecatlv1.ContextOccupancy{InputTokens: 40_000, Estimated: true}, }) if snap.Usage != (Usage{InputTokens: 120_000, OutputTokens: 4_000, CacheReadTokens: 90_000}) { t.Fatalf("main usage = %+v", snap.Usage) } + if snap.AuxiliaryUsage != (Usage{InputTokens: 1_000, OutputTokens: 15, CacheReadTokens: 200}) { + t.Fatalf("auxiliary usage = %+v", snap.AuxiliaryUsage) + } if snap.ContextOccupancy == nil || *snap.ContextOccupancy != (ContextOccupancy{InputTokens: 40_000, Estimated: true}) { t.Fatalf("context occupancy = %+v", snap.ContextOccupancy) } legacy := snapshotFrom(&mecatlv1.Session{TokenUsage: map[string]*mecatlv1.TokenUsage{"main": {Total: &mecatlv1.Usage{InputTokens: 120_000}}}}) - if legacy.ContextOccupancy != nil || legacy.Usage.InputTokens != 120_000 { + if legacy.ContextOccupancy != nil || legacy.Usage.InputTokens != 120_000 || legacy.AuxiliaryUsage != (Usage{}) { t.Fatalf("legacy snapshot = %+v", legacy) } } diff --git a/cmd/mecatui/exit_handoff.go b/cmd/mecatui/exit_handoff.go index b126516f0f..43c5fade2b 100644 --- a/cmd/mecatui/exit_handoff.go +++ b/cmd/mecatui/exit_handoff.go @@ -14,6 +14,7 @@ import ( "github.com/stacklok/mecatl/cmd/mecatui/client" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" + "github.com/stacklok/mecatl/cmd/mecatui/ui" ) const finalSessionHandoffPrefix = "mecatui: final-session-id=" @@ -48,14 +49,11 @@ func finishFinalSessionHandoff(w io.Writer, final tea.Model, runErr error, inter if title := strings.TrimSpace(terminaltext.SanitizeSingleLine(snapshot.Title)); title != "" { _, _ = fmt.Fprintf(&human, "Session: %s\n", title) } - _, _ = fmt.Fprintf(&human, "Model calls: %d\nTokens (main): %d input, %d output", snapshot.Turns, snapshot.Usage.InputTokens, snapshot.Usage.OutputTokens) - if snapshot.Usage.CacheReadTokens != 0 { - _, _ = fmt.Fprintf(&human, ", %d cache read", snapshot.Usage.CacheReadTokens) + _, _ = fmt.Fprintf(&human, "Model calls: %d\n", snapshot.Turns) + writeHandoffTokens(&human, "main", snapshot.Usage) + if snapshot.AuxiliaryUsage != (client.Usage{}) { + writeHandoffTokens(&human, "aux", snapshot.AuxiliaryUsage) } - if snapshot.Usage.CacheWriteTokens != 0 { - _, _ = fmt.Fprintf(&human, ", %d cache write", snapshot.Usage.CacheWriteTokens) - } - human.WriteByte('\n') } if safeHandoffID(id) { _, _ = fmt.Fprintf(&human, "Resume: mecatui --resume '%s'\n", strings.ReplaceAll(id, "'", "'\"'\"'")) @@ -64,6 +62,19 @@ func finishFinalSessionHandoff(w io.Writer, final tea.Model, runErr error, inter _, _ = io.WriteString(w, human.String()) } +// writeHandoffTokens writes one humanized usage line; cache counts are labelled +// components of input, never summed with it. +func writeHandoffTokens(b *strings.Builder, label string, u client.Usage) { + _, _ = fmt.Fprintf(b, "Tokens (%s): %s input, %s output", label, ui.HumanizeTokens(u.InputTokens), ui.HumanizeTokens(u.OutputTokens)) + if u.CacheReadTokens != 0 { + _, _ = fmt.Fprintf(b, ", %s cache read", ui.HumanizeTokens(u.CacheReadTokens)) + } + if u.CacheWriteTokens != 0 { + _, _ = fmt.Fprintf(b, ", %s cache write", ui.HumanizeTokens(u.CacheWriteTokens)) + } + b.WriteByte('\n') +} + func safeHandoffID(id string) bool { for _, r := range id { if !unicode.IsPrint(r) || unicode.Is(unicode.Cf, r) { diff --git a/cmd/mecatui/exit_handoff_scenarios_test.go b/cmd/mecatui/exit_handoff_scenarios_test.go index 57facf3100..4b3e1ac2aa 100644 --- a/cmd/mecatui/exit_handoff_scenarios_test.go +++ b/cmd/mecatui/exit_handoff_scenarios_test.go @@ -92,10 +92,13 @@ func TestMecatuiExitHandoff_Scenario1_AuthoritativeSummary(t *testing.T) { if id != "final" { t.Fatalf("requested session %q, want final", id) } - return client.SessionSnapshot{Title: "Server display title", State: "completed", Turns: 7, Usage: client.Usage{InputTokens: 42, OutputTokens: 13, CacheReadTokens: 9, CacheWriteTokens: 3}, ContextOccupancy: &client.ContextOccupancy{InputTokens: 999}}, nil + return client.SessionSnapshot{Title: "Server display title", State: "completed", Turns: 7, + Usage: client.Usage{InputTokens: 1_234_567, OutputTokens: 13_400, CacheReadTokens: 9, CacheWriteTokens: 3}, + AuxiliaryUsage: client.Usage{InputTokens: 2_500, OutputTokens: 40, CacheReadTokens: 1_000}, + ContextOccupancy: &client.ContextOccupancy{InputTokens: 999}}, nil }) got := handoffOutput(t, "final", true, get, nil, false) - for _, line := range []string{"Session: Server display title\n", "Model calls: 7\n", "Tokens (main): 42 input, 13 output, 9 cache read, 3 cache write\n"} { + for _, line := range []string{"Session: Server display title\n", "Model calls: 7\n", "Tokens (main): 1.2M input, 13.4K output, 9 cache read, 3 cache write\nTokens (aux): 2.5K input, 40 output, 1K cache read\n"} { if !strings.Contains(got, line) { t.Fatalf("missing %q: %q", line, got) } @@ -106,7 +109,7 @@ func TestMecatuiExitHandoff_Scenario1_AuthoritativeSummary(t *testing.T) { got = handoffOutput(t, "final", true, handoffSnapshotGetter(func(context.Context, string) (client.SessionSnapshot, error) { return client.SessionSnapshot{State: "completed", Turns: 0}, nil }), nil, false) - if strings.Contains(got, "Session:") || strings.Contains(got, "cache read") || !strings.Contains(got, "Model calls: 0\n") { + if strings.Contains(got, "Session:") || strings.Contains(got, "cache read") || strings.Contains(got, "Tokens (aux)") || !strings.Contains(got, "Model calls: 0\n") { t.Fatalf("empty title/zero cache = %q", got) } } diff --git a/cmd/mecatui/ui/statusline_source.go b/cmd/mecatui/ui/statusline_source.go index 25718ba417..033c1ef0a3 100644 --- a/cmd/mecatui/ui/statusline_source.go +++ b/cmd/mecatui/ui/statusline_source.go @@ -58,6 +58,10 @@ func (m Model) statusLineWaitCmd() tea.Cmd { } } +// HumanizeTokens exposes the shared token formatter for mecatui's post-teardown +// exit summary. +func HumanizeTokens(n int64) string { return renderfmt.HumanizeTokens(n) } + func usageAtom(raw int64) customization.UsageAtom { return customization.UsageAtom{Raw: raw, Human: renderfmt.HumanizeTokens(raw)} } diff --git a/user-docs/mecatui/sessions.md b/user-docs/mecatui/sessions.md index d5c554af64..24e423382b 100644 --- a/user-docs/mecatui/sessions.md +++ b/user-docs/mecatui/sessions.md @@ -89,9 +89,19 @@ A connected session prints the ID record; to resume it, use When the session snapshot is available, **Model calls** counts model calls begun in the chat, and **Tokens (main)** shows lifetime input and output tokens for -that chat. Nonzero cache-read and cache-write counts appear separately, not -added to input or output. Title-generation tokens and the latest context-meter -reading are not included. If the snapshot is unavailable, the exact embedded +the chat's agent runs. **Tokens (aux)** appears when auxiliary model work, such +as title generation, compaction, routing, reviewers, or guardrails, used tokens +in the chat. Counts are abbreviated, for example `29.7K` or `1.2M`. Nonzero +cache-read and cache-write counts appear separately, not added to input or +output. The latest context-meter reading is not included: + +```text +Model calls: 12 +Tokens (main): 29.7K input, 1.8K output, 6.4K cache read +Tokens (aux): 2.1K input, 85 output +``` + +If the snapshot is unavailable, the exact embedded resume command and ID record still appear without a summary. An ID that cannot be safely displayed as a single terminal line retains its JSON ID record but has no copyable command. From 7fe7f570d3a68dc6eb6385a38c8b5a14e55d6cf9 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 4 Oct 2026 17:09:55 -0700 Subject: [PATCH 2/4] mecatui: align exit summary fields Print the session ID and every summary field under aligned labels, with the resume-latest alternative indented beneath the exact resume command. Co-authored-by: Mecatl --- cmd/mecatui/exit_handoff.go | 37 ++++++++++------ cmd/mecatui/exit_handoff_composition_test.go | 6 +-- cmd/mecatui/exit_handoff_scenarios_test.go | 27 +++++++----- user-docs/mecatui/sessions.md | 45 +++++++++++--------- 4 files changed, 68 insertions(+), 47 deletions(-) diff --git a/cmd/mecatui/exit_handoff.go b/cmd/mecatui/exit_handoff.go index 43c5fade2b..b65a119692 100644 --- a/cmd/mecatui/exit_handoff.go +++ b/cmd/mecatui/exit_handoff.go @@ -44,35 +44,46 @@ func finishFinalSessionHandoff(w io.Writer, final tea.Model, runErr error, inter return } id := reporter.ActiveSessionID() + safeID := safeHandoffID(id) var human strings.Builder + if safeID { + writeHandoffField(&human, "Session ID:", id) + } if available { if title := strings.TrimSpace(terminaltext.SanitizeSingleLine(snapshot.Title)); title != "" { - _, _ = fmt.Fprintf(&human, "Session: %s\n", title) + writeHandoffField(&human, "Title:", title) } - _, _ = fmt.Fprintf(&human, "Model calls: %d\n", snapshot.Turns) - writeHandoffTokens(&human, "main", snapshot.Usage) + writeHandoffField(&human, "Model calls:", fmt.Sprint(snapshot.Turns)) + writeHandoffField(&human, "Tokens (main):", handoffTokens(snapshot.Usage)) if snapshot.AuxiliaryUsage != (client.Usage{}) { - writeHandoffTokens(&human, "aux", snapshot.AuxiliaryUsage) + writeHandoffField(&human, "Tokens (aux):", handoffTokens(snapshot.AuxiliaryUsage)) } } - if safeHandoffID(id) { - _, _ = fmt.Fprintf(&human, "Resume: mecatui --resume '%s'\n", strings.ReplaceAll(id, "'", "'\"'\"'")) - human.WriteString("Or: mecatui --resume-latest (may select a different chat)\n") + if safeID { + writeHandoffField(&human, "Resume:", "mecatui --resume '"+strings.ReplaceAll(id, "'", "'\"'\"'")+"'") + writeHandoffField(&human, "", "mecatui --resume-latest (may select a different chat)") } _, _ = io.WriteString(w, human.String()) } -// writeHandoffTokens writes one humanized usage line; cache counts are labelled +// handoffLabelWidth aligns values after the widest label, "Tokens (main): ". +const handoffLabelWidth = len("Tokens (main): ") + +func writeHandoffField(b *strings.Builder, label, value string) { + _, _ = fmt.Fprintf(b, "%-*s%s\n", handoffLabelWidth, label, value) +} + +// handoffTokens humanizes one usage value; cache counts are labelled // components of input, never summed with it. -func writeHandoffTokens(b *strings.Builder, label string, u client.Usage) { - _, _ = fmt.Fprintf(b, "Tokens (%s): %s input, %s output", label, ui.HumanizeTokens(u.InputTokens), ui.HumanizeTokens(u.OutputTokens)) +func handoffTokens(u client.Usage) string { + s := ui.HumanizeTokens(u.InputTokens) + " input, " + ui.HumanizeTokens(u.OutputTokens) + " output" if u.CacheReadTokens != 0 { - _, _ = fmt.Fprintf(b, ", %s cache read", ui.HumanizeTokens(u.CacheReadTokens)) + s += ", " + ui.HumanizeTokens(u.CacheReadTokens) + " cache read" } if u.CacheWriteTokens != 0 { - _, _ = fmt.Fprintf(b, ", %s cache write", ui.HumanizeTokens(u.CacheWriteTokens)) + s += ", " + ui.HumanizeTokens(u.CacheWriteTokens) + " cache write" } - b.WriteByte('\n') + return s } func safeHandoffID(id string) bool { diff --git a/cmd/mecatui/exit_handoff_composition_test.go b/cmd/mecatui/exit_handoff_composition_test.go index 542b177d56..a91168e4e9 100644 --- a/cmd/mecatui/exit_handoff_composition_test.go +++ b/cmd/mecatui/exit_handoff_composition_test.go @@ -219,14 +219,14 @@ func checkEmbeddedCompositionChild(t *testing.T, scenario string) { if scenario != "missing" && id != finalID { t.Fatalf("handoff ID = %q, want seeded final ID %q", id, finalID) } - if !strings.Contains(got[line:], "Resume: mecatui --resume '"+id+"'\n") || !strings.Contains(got[line:], "Or: mecatui --resume-latest (may select a different chat)\n") { + if !strings.Contains(got[line:], "Resume: mecatui --resume '"+id+"'\n") || !strings.Contains(got[line:], " mecatui --resume-latest (may select a different chat)\n") { t.Fatalf("final ID command missing: %q", got[line:]) } if scenario == "missing" { - if id != "missing-final" || strings.Contains(got[line:], "Session:") || strings.Contains(got[line:], "Model calls:") || strings.Contains(got[line:], "Tokens (main):") { + if id != "missing-final" || strings.Contains(got[line:], "Title:") || strings.Contains(got[line:], "Model calls:") || strings.Contains(got[line:], "Tokens (main):") { t.Fatalf("failed lookup retained stale summary: %q", got[line:]) } - } else if !strings.Contains(got[line:], "Session: final title\nModel calls: 1\nTokens (main): 0 input, 0 output\n") || strings.Contains(got[line:], "first title") { + } else if !strings.Contains(got[line:], "Title: final title\nModel calls: 1\nTokens (main): 0 input, 0 output\n") || strings.Contains(got[line:], "first title") { t.Fatalf("wrong final session snapshot: %q", got[line:]) } } diff --git a/cmd/mecatui/exit_handoff_scenarios_test.go b/cmd/mecatui/exit_handoff_scenarios_test.go index 4b3e1ac2aa..f8db2f452f 100644 --- a/cmd/mecatui/exit_handoff_scenarios_test.go +++ b/cmd/mecatui/exit_handoff_scenarios_test.go @@ -82,7 +82,7 @@ func TestMecatuiExitHandoff_Scenario1_EmbeddedResumeAfterTeardown(t *testing.T) if err := json.Unmarshal([]byte(strings.TrimPrefix(lines[0], finalSessionHandoffPrefix)), &decoded); err != nil || decoded != "final chat" { t.Fatalf("ID record = %q, decoded=%q, err=%v", lines[0], decoded, err) } - if !strings.Contains(got, "Resume: mecatui --resume 'final chat'\n") || !strings.Contains(got, "Or: mecatui --resume-latest (may select a different chat)\n") { + if !strings.Contains(got, "Resume: mecatui --resume 'final chat'\n") || !strings.Contains(got, " mecatui --resume-latest (may select a different chat)\n") { t.Fatalf("missing exact/qualified continuation: %q", got) } } @@ -98,10 +98,15 @@ func TestMecatuiExitHandoff_Scenario1_AuthoritativeSummary(t *testing.T) { ContextOccupancy: &client.ContextOccupancy{InputTokens: 999}}, nil }) got := handoffOutput(t, "final", true, get, nil, false) - for _, line := range []string{"Session: Server display title\n", "Model calls: 7\n", "Tokens (main): 1.2M input, 13.4K output, 9 cache read, 3 cache write\nTokens (aux): 2.5K input, 40 output, 1K cache read\n"} { - if !strings.Contains(got, line) { - t.Fatalf("missing %q: %q", line, got) - } + want := "Session ID: final\n" + + "Title: Server display title\n" + + "Model calls: 7\n" + + "Tokens (main): 1.2M input, 13.4K output, 9 cache read, 3 cache write\n" + + "Tokens (aux): 2.5K input, 40 output, 1K cache read\n" + + "Resume: mecatui --resume 'final'\n" + + " mecatui --resume-latest (may select a different chat)\n" + if !strings.HasSuffix(got, "\n"+want) { + t.Fatalf("aligned summary = %q, want suffix %q", got, want) } if strings.Contains(got, "999") { t.Fatalf("context meter leaked into lifetime totals: %q", got) @@ -109,7 +114,7 @@ func TestMecatuiExitHandoff_Scenario1_AuthoritativeSummary(t *testing.T) { got = handoffOutput(t, "final", true, handoffSnapshotGetter(func(context.Context, string) (client.SessionSnapshot, error) { return client.SessionSnapshot{State: "completed", Turns: 0}, nil }), nil, false) - if strings.Contains(got, "Session:") || strings.Contains(got, "cache read") || strings.Contains(got, "Tokens (aux)") || !strings.Contains(got, "Model calls: 0\n") { + if strings.Contains(got, "Title:") || strings.Contains(got, "cache read") || strings.Contains(got, "Tokens (aux)") || !strings.Contains(got, "Model calls: 0\n") { t.Fatalf("empty title/zero cache = %q", got) } } @@ -119,8 +124,8 @@ func TestMecatuiExitHandoff_Scenario1_SafePresentation(t *testing.T) { got := handoffOutput(t, id, true, nil, nil, false) line := "" for _, l := range strings.Split(got, "\n") { - if strings.HasPrefix(l, "Resume: ") { - line = strings.TrimPrefix(l, "Resume: ") + if strings.HasPrefix(l, "Resume:") { + line = strings.TrimSpace(strings.TrimPrefix(l, "Resume:")) } } if line == "" { @@ -134,14 +139,14 @@ func TestMecatuiExitHandoff_Scenario1_SafePresentation(t *testing.T) { for _, unsafeID := range []string{"two\nlines", "escape\x1b[31m", "bidi\u202eright"} { got := handoffOutput(t, unsafeID, true, nil, nil, false) quoted, _ := json.Marshal(unsafeID) - if !strings.Contains(got, finalSessionHandoffPrefix+string(quoted)+"\n") || strings.Contains(got, "Resume:") || strings.Contains(got, "Or:") { + if !strings.Contains(got, finalSessionHandoffPrefix+string(quoted)+"\n") || strings.Contains(got, "Resume:") || strings.Contains(got, "--resume-latest") || strings.Contains(got, "Session ID:") { t.Fatalf("unsafe ID %q: %q", unsafeID, got) } } got = handoffOutput(t, "final", true, handoffSnapshotGetter(func(context.Context, string) (client.SessionSnapshot, error) { return client.SessionSnapshot{State: "completed", Title: "ok\nFAKE LINE\x1b[31m\u202eevil", Turns: 1}, nil }), nil, false) - if strings.Contains(got, "\x1b") || strings.Contains(got, "\u202e") || strings.Contains(got, "\nFAKE LINE") || strings.Count(got, "Session:") != 1 || !strings.Contains(got, "Session: okFAKE LINE[31mevil\n") { + if strings.Contains(got, "\x1b") || strings.Contains(got, "\u202e") || strings.Contains(got, "\nFAKE LINE") || strings.Count(got, "Title:") != 1 || !strings.Contains(got, "Title: okFAKE LINE[31mevil\n") { t.Fatalf("untrusted title controls/lines: %q", got) } } @@ -166,7 +171,7 @@ func TestMecatuiExitHandoff_Scenario2_SnapshotUnavailable(t *testing.T) { if time.Since(start) > 2*time.Second { t.Fatalf("snapshot blocked cleanup beyond deadline: %s", time.Since(start)) } - if !strings.Contains(got, finalSessionHandoffPrefix+`"final"`+"\n") || !strings.Contains(got, "Resume: mecatui --resume 'final'\n") || strings.Contains(got, "Session:") || strings.Contains(got, "Model calls:") || strings.Contains(got, "Tokens (main):") { + if !strings.Contains(got, finalSessionHandoffPrefix+`"final"`+"\n") || !strings.Contains(got, "Resume: mecatui --resume 'final'\n") || strings.Contains(got, "Title:") || strings.Contains(got, "Model calls:") || strings.Contains(got, "Tokens (main):") { t.Fatalf("failed snapshot must retain only safe guidance: %q", got) } }) diff --git a/user-docs/mecatui/sessions.md b/user-docs/mecatui/sessions.md index 24e423382b..35f09729c3 100644 --- a/user-docs/mecatui/sessions.md +++ b/user-docs/mecatui/sessions.md @@ -80,31 +80,36 @@ mecatui: final-session-id="01JOPAQUESESSIONID" A blank line separates the exit record from startup notices. `mecatui` does not print the embedded server's private socket address. -After an embedded session ends normally, the lines that follow show a -shell-quoted `mecatui --resume ''` command for that exact final chat. -Use it to return to the same chat. The separately labelled `--resume-latest` -alternative looks for the newest eligible chat and can select a different one. -A connected session prints the ID record; to resume it, use -`mecatui connect
--resume ` with the original server address. - -When the session snapshot is available, **Model calls** counts model calls begun -in the chat, and **Tokens (main)** shows lifetime input and output tokens for -the chat's agent runs. **Tokens (aux)** appears when auxiliary model work, such -as title generation, compaction, routing, reviewers, or guardrails, used tokens -in the chat. Counts are abbreviated, for example `29.7K` or `1.2M`. Nonzero -cache-read and cache-write counts appear separately, not added to input or -output. The latest context-meter reading is not included: +After an embedded session ends normally, an aligned summary follows the record: ```text -Model calls: 12 +Session ID: 01JOPAQUESESSIONID +Title: Fix the flaky CI job +Model calls: 12 Tokens (main): 29.7K input, 1.8K output, 6.4K cache read -Tokens (aux): 2.1K input, 85 output +Tokens (aux): 2.1K input, 85 output +Resume: mecatui --resume '01JOPAQUESESSIONID' + mecatui --resume-latest (may select a different chat) ``` -If the snapshot is unavailable, the exact embedded -resume command and ID record still appear without a summary. An ID that cannot -be safely displayed as a single terminal line retains its JSON ID record but -has no copyable command. +The shell-quoted `mecatui --resume` command returns to that exact final chat. +The `--resume-latest` alternative below it looks for the newest eligible chat +and can select a different one. A connected session prints only the ID record; +to resume it, use `mecatui connect
--resume ` with the +original server address. + +**Model calls** counts model calls begun in the chat, and **Tokens (main)** +shows lifetime input and output tokens for the chat's agent runs. **Tokens +(aux)** appears when auxiliary model work, such as title generation, +compaction, routing, reviewers, or guardrails, used tokens in the chat. Counts +are abbreviated, for example `29.7K` or `1.2M`. Nonzero cache-read and +cache-write counts appear separately, not added to input or output. The latest +context-meter reading is not included. + +If the session snapshot is unavailable, the title, model-call, and token lines +are omitted; the ID record, session ID, and resume commands still appear. An ID +that cannot be safely displayed as a single terminal line retains its JSON ID +record but has no session ID line or copyable command. ## Inspect the active session during a run From 7536c8e377f8796652886e034951f67770f30f6d Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 4 Oct 2026 17:17:36 -0700 Subject: [PATCH 3/4] mecatui: replace embedded final-session-id record with Session ID line Embedded exits with a displayable ID now print only the aligned summary. Connected mode and unsafe IDs keep the JSON-quoted final-session-id record. Co-authored-by: Mecatl --- cmd/mecatui/exit_handoff.go | 20 ++++++------ cmd/mecatui/exit_handoff_composition_test.go | 13 ++++---- cmd/mecatui/exit_handoff_scenarios_test.go | 15 +++------ user-docs/mecatui/sessions.md | 33 ++++++++++---------- 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/cmd/mecatui/exit_handoff.go b/cmd/mecatui/exit_handoff.go index b65a119692..5f85b6388a 100644 --- a/cmd/mecatui/exit_handoff.go +++ b/cmd/mecatui/exit_handoff.go @@ -40,15 +40,19 @@ func finishFinalSessionHandoff(w io.Writer, final tea.Model, runErr error, inter cancel() } cleanup() - if !shouldWriteFinalSessionHandoff(final, runErr, interrupted) || !writeFinalSessionHandoff(w, final) || !embedded || !ok { + if !shouldWriteFinalSessionHandoff(final, runErr, interrupted) || !ok { return } id := reporter.ActiveSessionID() - safeID := safeHandoffID(id) - var human strings.Builder - if safeID { - writeHandoffField(&human, "Session ID:", id) + // Connected mode, and an ID unsafe to print as one terminal line, keep the + // JSON-quoted record; an embedded safe ID gets the aligned human summary instead. + if !embedded || id == "" || !utf8.ValidString(id) || !safeHandoffID(id) { + writeFinalSessionHandoff(w, final) + return } + var human strings.Builder + human.WriteByte('\n') + writeHandoffField(&human, "Session ID:", id) if available { if title := strings.TrimSpace(terminaltext.SanitizeSingleLine(snapshot.Title)); title != "" { writeHandoffField(&human, "Title:", title) @@ -59,10 +63,8 @@ func finishFinalSessionHandoff(w io.Writer, final tea.Model, runErr error, inter writeHandoffField(&human, "Tokens (aux):", handoffTokens(snapshot.AuxiliaryUsage)) } } - if safeID { - writeHandoffField(&human, "Resume:", "mecatui --resume '"+strings.ReplaceAll(id, "'", "'\"'\"'")+"'") - writeHandoffField(&human, "", "mecatui --resume-latest (may select a different chat)") - } + writeHandoffField(&human, "Resume:", "mecatui --resume '"+strings.ReplaceAll(id, "'", "'\"'\"'")+"'") + writeHandoffField(&human, "", "mecatui --resume-latest (may select a different chat)") _, _ = io.WriteString(w, human.String()) } diff --git a/cmd/mecatui/exit_handoff_composition_test.go b/cmd/mecatui/exit_handoff_composition_test.go index a91168e4e9..7945af8d3a 100644 --- a/cmd/mecatui/exit_handoff_composition_test.go +++ b/cmd/mecatui/exit_handoff_composition_test.go @@ -198,14 +198,15 @@ func checkEmbeddedCompositionChild(t *testing.T, scenario string) { t.Fatalf("stdout changed by program: %q", stdout.String()) } got := stderr.String() - line := strings.Index(got, finalSessionHandoffPrefix) - if line < 1 || got[line-1] != '\n' || strings.Contains(got, "hosting an embedded mecated at") || strings.Count(got, finalSessionHandoffPrefix) != 1 || strings.LastIndex(got[:line], "\x1b[?1049l") < 0 || !strings.Contains(got[line:], "handoff-child-cleaned\n") { + const idLabel = "Session ID: " + line := strings.Index(got, "\n"+idLabel) + if line < 0 || strings.Contains(got, "hosting an embedded mecated at") || strings.Contains(got, finalSessionHandoffPrefix) || strings.Count(got, idLabel) != 1 || strings.LastIndex(got[:line], "\x1b[?1049l") < 0 || !strings.Contains(got[line:], "handoff-child-cleaned\n") { t.Fatalf("handoff must follow teardown with a separating line and no startup socket address: %q", got) } - fields := strings.SplitN(got[line:], "\n", 2) - var id string - if err := json.Unmarshal([]byte(strings.TrimPrefix(fields[0], finalSessionHandoffPrefix)), &id); err != nil || id == "" { - t.Fatalf("invalid ID: %q: %v", fields[0], err) + line++ + id := strings.SplitN(strings.TrimPrefix(got[line:], idLabel), "\n", 2)[0] + if id == "" { + t.Fatalf("missing session ID: %q", got[line:]) } const finalIDPrefix = "handoff-child-final-id=" finalLine := strings.Index(got, finalIDPrefix) diff --git a/cmd/mecatui/exit_handoff_scenarios_test.go b/cmd/mecatui/exit_handoff_scenarios_test.go index f8db2f452f..7b03c620f3 100644 --- a/cmd/mecatui/exit_handoff_scenarios_test.go +++ b/cmd/mecatui/exit_handoff_scenarios_test.go @@ -73,14 +73,9 @@ func TestMecatuiExitHandoff_Scenario1_EmbeddedResumeAfterTeardown(t *testing.T) got := stderr.String() teardown := strings.Index(got, "\x1b[?1049l") cleanup := strings.Index(got, "cleanup-complete\n") - line := strings.Index(got, finalSessionHandoffPrefix) - if teardown < 0 || cleanup <= teardown || line <= cleanup || strings.Count(got, finalSessionHandoffPrefix) != 1 { - t.Fatalf("teardown/cleanup/ID record order = %q", got) - } - lines := strings.Split(got[line:], "\n") - var decoded string - if err := json.Unmarshal([]byte(strings.TrimPrefix(lines[0], finalSessionHandoffPrefix)), &decoded); err != nil || decoded != "final chat" { - t.Fatalf("ID record = %q, decoded=%q, err=%v", lines[0], decoded, err) + line := strings.Index(got, "\nSession ID: final chat\n") + if teardown < 0 || cleanup <= teardown || line <= cleanup || strings.Contains(got, finalSessionHandoffPrefix) { + t.Fatalf("teardown/cleanup/session ID order = %q", got) } if !strings.Contains(got, "Resume: mecatui --resume 'final chat'\n") || !strings.Contains(got, " mecatui --resume-latest (may select a different chat)\n") { t.Fatalf("missing exact/qualified continuation: %q", got) @@ -105,7 +100,7 @@ func TestMecatuiExitHandoff_Scenario1_AuthoritativeSummary(t *testing.T) { "Tokens (aux): 2.5K input, 40 output, 1K cache read\n" + "Resume: mecatui --resume 'final'\n" + " mecatui --resume-latest (may select a different chat)\n" - if !strings.HasSuffix(got, "\n"+want) { + if got != "\n"+want { t.Fatalf("aligned summary = %q, want suffix %q", got, want) } if strings.Contains(got, "999") { @@ -171,7 +166,7 @@ func TestMecatuiExitHandoff_Scenario2_SnapshotUnavailable(t *testing.T) { if time.Since(start) > 2*time.Second { t.Fatalf("snapshot blocked cleanup beyond deadline: %s", time.Since(start)) } - if !strings.Contains(got, finalSessionHandoffPrefix+`"final"`+"\n") || !strings.Contains(got, "Resume: mecatui --resume 'final'\n") || strings.Contains(got, "Title:") || strings.Contains(got, "Model calls:") || strings.Contains(got, "Tokens (main):") { + if got != "\nSession ID: final\nResume: mecatui --resume 'final'\n mecatui --resume-latest (may select a different chat)\n" || strings.Contains(got, "Title:") || strings.Contains(got, "Model calls:") || strings.Contains(got, "Tokens (main):") { t.Fatalf("failed snapshot must retain only safe guidance: %q", got) } }) diff --git a/user-docs/mecatui/sessions.md b/user-docs/mecatui/sessions.md index 35f09729c3..cc8ba9492b 100644 --- a/user-docs/mecatui/sessions.md +++ b/user-docs/mecatui/sessions.md @@ -70,17 +70,9 @@ use another supported client or control path. Repeating the unchanged command will not add watch support. Plan approvals and guardrail reviews use their existing dedicated flows and cannot be recovered this way. -To get the active session ID, run `/session` and press `c` to copy it. On a -normal exit, `mecatui` writes a JSON-quoted ID record to standard error: - -```text -mecatui: final-session-id="01JOPAQUESESSIONID" -``` - -A blank line separates the exit record from startup notices. `mecatui` does not -print the embedded server's private socket address. - -After an embedded session ends normally, an aligned summary follows the record: +To get the active session ID, run `/session` and press `c` to copy it. When an +embedded session ends normally, `mecatui` writes an aligned summary to standard +error: ```text Session ID: 01JOPAQUESESSIONID @@ -94,8 +86,17 @@ Resume: mecatui --resume '01JOPAQUESESSIONID' The shell-quoted `mecatui --resume` command returns to that exact final chat. The `--resume-latest` alternative below it looks for the newest eligible chat -and can select a different one. A connected session prints only the ID record; -to resume it, use `mecatui connect
--resume ` with the +and can select a different one. A blank line separates the summary from startup +notices, and `mecatui` does not print the embedded server's private socket +address. + +A connected session instead prints a JSON-quoted ID record: + +```text +mecatui: final-session-id="01JOPAQUESESSIONID" +``` + +To resume it, use `mecatui connect
--resume ` with the original server address. **Model calls** counts model calls begun in the chat, and **Tokens (main)** @@ -107,9 +108,9 @@ cache-write counts appear separately, not added to input or output. The latest context-meter reading is not included. If the session snapshot is unavailable, the title, model-call, and token lines -are omitted; the ID record, session ID, and resume commands still appear. An ID -that cannot be safely displayed as a single terminal line retains its JSON ID -record but has no session ID line or copyable command. +are omitted; the session ID and resume commands still appear. An embedded ID +that cannot be safely displayed as a single terminal line gets the JSON-quoted +ID record instead of the summary. ## Inspect the active session during a run From 316463916d774c9acdb9ecc293566bd78ba3a15a Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Sun, 4 Oct 2026 19:04:31 -0700 Subject: [PATCH 4/4] mecatui: hoist renderfmt so the exit summary can use it directly Move ui/internal/renderfmt to cmd/mecatui/internal/renderfmt so package main can import the shared token humanizer, and drop the ui wrapper. Co-authored-by: Mecatl --- cmd/mecatui/exit_handoff.go | 8 ++++---- cmd/mecatui/{ui => }/internal/renderfmt/renderfmt.go | 0 cmd/mecatui/{ui => }/internal/renderfmt/renderfmt_test.go | 2 +- cmd/mecatui/ui/agents_overlay.go | 2 +- cmd/mecatui/ui/models_surface.go | 2 +- cmd/mecatui/ui/noprogress_test.go | 2 +- cmd/mecatui/ui/plan_approval_test.go | 2 +- cmd/mecatui/ui/render.go | 2 +- cmd/mecatui/ui/sessions_surface.go | 2 +- cmd/mecatui/ui/statusline_source.go | 6 +----- cmd/mecatui/ui/team.go | 2 +- cmd/mecatui/ui/tool_block.go | 2 +- cmd/mecatui/ui/update.go | 2 +- cmd/mecatui/ui/view.go | 2 +- 14 files changed, 16 insertions(+), 20 deletions(-) rename cmd/mecatui/{ui => }/internal/renderfmt/renderfmt.go (100%) rename cmd/mecatui/{ui => }/internal/renderfmt/renderfmt_test.go (99%) diff --git a/cmd/mecatui/exit_handoff.go b/cmd/mecatui/exit_handoff.go index 5f85b6388a..22e1151cee 100644 --- a/cmd/mecatui/exit_handoff.go +++ b/cmd/mecatui/exit_handoff.go @@ -13,8 +13,8 @@ import ( tea "charm.land/bubbletea/v2" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" - "github.com/stacklok/mecatl/cmd/mecatui/ui" ) const finalSessionHandoffPrefix = "mecatui: final-session-id=" @@ -78,12 +78,12 @@ func writeHandoffField(b *strings.Builder, label, value string) { // handoffTokens humanizes one usage value; cache counts are labelled // components of input, never summed with it. func handoffTokens(u client.Usage) string { - s := ui.HumanizeTokens(u.InputTokens) + " input, " + ui.HumanizeTokens(u.OutputTokens) + " output" + s := renderfmt.HumanizeTokens(u.InputTokens) + " input, " + renderfmt.HumanizeTokens(u.OutputTokens) + " output" if u.CacheReadTokens != 0 { - s += ", " + ui.HumanizeTokens(u.CacheReadTokens) + " cache read" + s += ", " + renderfmt.HumanizeTokens(u.CacheReadTokens) + " cache read" } if u.CacheWriteTokens != 0 { - s += ", " + ui.HumanizeTokens(u.CacheWriteTokens) + " cache write" + s += ", " + renderfmt.HumanizeTokens(u.CacheWriteTokens) + " cache write" } return s } diff --git a/cmd/mecatui/ui/internal/renderfmt/renderfmt.go b/cmd/mecatui/internal/renderfmt/renderfmt.go similarity index 100% rename from cmd/mecatui/ui/internal/renderfmt/renderfmt.go rename to cmd/mecatui/internal/renderfmt/renderfmt.go diff --git a/cmd/mecatui/ui/internal/renderfmt/renderfmt_test.go b/cmd/mecatui/internal/renderfmt/renderfmt_test.go similarity index 99% rename from cmd/mecatui/ui/internal/renderfmt/renderfmt_test.go rename to cmd/mecatui/internal/renderfmt/renderfmt_test.go index 3ce01fa149..978d31883e 100644 --- a/cmd/mecatui/ui/internal/renderfmt/renderfmt_test.go +++ b/cmd/mecatui/internal/renderfmt/renderfmt_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/stacklok/mecatl/cmd/mecatui/client" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" ) func TestHumanizeTokens(t *testing.T) { diff --git a/cmd/mecatui/ui/agents_overlay.go b/cmd/mecatui/ui/agents_overlay.go index 049483c0ce..e11b98335a 100644 --- a/cmd/mecatui/ui/agents_overlay.go +++ b/cmd/mecatui/ui/agents_overlay.go @@ -12,10 +12,10 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/bounded" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/scrollback" ) diff --git a/cmd/mecatui/ui/models_surface.go b/cmd/mecatui/ui/models_surface.go index 5c4da771e1..c22a4e814c 100644 --- a/cmd/mecatui/ui/models_surface.go +++ b/cmd/mecatui/ui/models_surface.go @@ -14,9 +14,9 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/bounded" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" ) // modelsView is the active /models overlay. diff --git a/cmd/mecatui/ui/noprogress_test.go b/cmd/mecatui/ui/noprogress_test.go index b088e653a4..4bd8a16ac4 100644 --- a/cmd/mecatui/ui/noprogress_test.go +++ b/cmd/mecatui/ui/noprogress_test.go @@ -5,8 +5,8 @@ import ( "testing" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/theme" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/scrollback" ) diff --git a/cmd/mecatui/ui/plan_approval_test.go b/cmd/mecatui/ui/plan_approval_test.go index 8b2a0e19ec..f15c30375d 100644 --- a/cmd/mecatui/ui/plan_approval_test.go +++ b/cmd/mecatui/ui/plan_approval_test.go @@ -10,8 +10,8 @@ import ( tea "charm.land/bubbletea/v2" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/theme" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" mecatlv1 "github.com/stacklok/mecatl/contracts/gen/go/mecatl/v1" ) diff --git a/cmd/mecatui/ui/render.go b/cmd/mecatui/ui/render.go index ebfc1d9f3c..bd6f07046b 100644 --- a/cmd/mecatui/ui/render.go +++ b/cmd/mecatui/ui/render.go @@ -16,9 +16,9 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/scrollback" ) diff --git a/cmd/mecatui/ui/sessions_surface.go b/cmd/mecatui/ui/sessions_surface.go index 7efaacf685..71adc83dd6 100644 --- a/cmd/mecatui/ui/sessions_surface.go +++ b/cmd/mecatui/ui/sessions_surface.go @@ -16,10 +16,10 @@ import ( tea "charm.land/bubbletea/v2" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/bounded" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" ) type sessionsTab int diff --git a/cmd/mecatui/ui/statusline_source.go b/cmd/mecatui/ui/statusline_source.go index 033c1ef0a3..1ac15b1f2f 100644 --- a/cmd/mecatui/ui/statusline_source.go +++ b/cmd/mecatui/ui/statusline_source.go @@ -8,7 +8,7 @@ import ( "github.com/stacklok/mecatl/cmd/mecatui/client" customization "github.com/stacklok/mecatl/cmd/mecatui/customization" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" ) type statusLineChangedMsg struct { @@ -58,10 +58,6 @@ func (m Model) statusLineWaitCmd() tea.Cmd { } } -// HumanizeTokens exposes the shared token formatter for mecatui's post-teardown -// exit summary. -func HumanizeTokens(n int64) string { return renderfmt.HumanizeTokens(n) } - func usageAtom(raw int64) customization.UsageAtom { return customization.UsageAtom{Raw: raw, Human: renderfmt.HumanizeTokens(raw)} } diff --git a/cmd/mecatui/ui/team.go b/cmd/mecatui/ui/team.go index e3092c2349..35d69a2a8d 100644 --- a/cmd/mecatui/ui/team.go +++ b/cmd/mecatui/ui/team.go @@ -7,10 +7,10 @@ import ( "charm.land/bubbles/v2/key" tea "charm.land/bubbletea/v2" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/bounded" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" ) // teamView is the active agent-team overlay (none = closed). The overlay is a diff --git a/cmd/mecatui/ui/tool_block.go b/cmd/mecatui/ui/tool_block.go index b25a6d16c8..6eadce3e4d 100644 --- a/cmd/mecatui/ui/tool_block.go +++ b/cmd/mecatui/ui/tool_block.go @@ -5,9 +5,9 @@ import ( "strings" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/blocks" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/scrollback" ) diff --git a/cmd/mecatui/ui/update.go b/cmd/mecatui/ui/update.go index 3c2caa0598..2f43259ae6 100644 --- a/cmd/mecatui/ui/update.go +++ b/cmd/mecatui/ui/update.go @@ -15,10 +15,10 @@ import ( "github.com/stacklok/mecatl/cmd/mecatui/client" customization "github.com/stacklok/mecatl/cmd/mecatui/customization" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/bounded" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/ui/welcome" ) diff --git a/cmd/mecatui/ui/view.go b/cmd/mecatui/ui/view.go index a587a028fd..2b0d247456 100644 --- a/cmd/mecatui/ui/view.go +++ b/cmd/mecatui/ui/view.go @@ -9,9 +9,9 @@ import ( "github.com/charmbracelet/x/ansi" "github.com/stacklok/mecatl/cmd/mecatui/client" + "github.com/stacklok/mecatl/cmd/mecatui/internal/renderfmt" "github.com/stacklok/mecatl/cmd/mecatui/internal/terminaltext" "github.com/stacklok/mecatl/cmd/mecatui/theme" - "github.com/stacklok/mecatl/cmd/mecatui/ui/internal/renderfmt" ) // centerCard frames body in the askCard style and centers it over the