From d2342f99cf4922b67c0ca50ed27647416fa5102b Mon Sep 17 00:00:00 2001 From: Harry Bayliss Date: Mon, 25 May 2026 13:06:53 +0100 Subject: [PATCH] Show every agent tab's summary, not just the focused one The tab bar's row-2 summary was painted only for the active tab. Add a per-child summaryTextFor/summaryRawFor helper (active variants now delegate to it), carry each tab's childID on its tabRect, and loop over all visible tabs so each renders its own summary under its column. Layout is unchanged (still 3 rows); narrow tabs clip as before. Resolves the per-tab summary TODO item. --- CHANGELOG.md | 2 ++ TODO.md | 1 - internal/app/app.go | 26 +++++++++++++++--------- internal/app/summarizer_test.go | 35 +++++++++++++++++++++++++++++++++ internal/app/tabbar.go | 12 ++++------- 5 files changed, 58 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 711034c..0dc40a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). to remove a shared project scratchpad. ### Changed +- The tab bar now shows each visible agent tab's own summary instead + of only rendering the focused tab's summary. - Grid-mode `get_process_output` now returns whitespace-normalized text to avoid sending padded terminal rows and repeated blank lines over MCP. diff --git a/TODO.md b/TODO.md index b5dae71..e69de29 100644 --- a/TODO.md +++ b/TODO.md @@ -1 +0,0 @@ -- [ ] The per-tab agent summary text should display below the tab always, not just when the tab is focused. diff --git a/internal/app/app.go b/internal/app/app.go index fcdaf28..a4a9339 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -514,7 +514,14 @@ func (st *uiState) dbgf(format string, args ...any) { } func (st *uiState) activeSummaryText(width int) string { - text := st.activeSummaryRaw() + st.mu.Lock() + active := st.activeAgentID + st.mu.Unlock() + return st.summaryTextFor(active, width) +} + +func (st *uiState) summaryTextFor(childID string, width int) string { + text := st.summaryRawFor(childID) if text == "" || width <= 0 { return "" } @@ -525,7 +532,14 @@ func (st *uiState) activeSummaryText(width int) string { } func (st *uiState) activeSummaryRaw() string { - if st.summaries == nil { + st.mu.Lock() + active := st.activeAgentID + st.mu.Unlock() + return st.summaryRawFor(active) +} + +func (st *uiState) summaryRawFor(childID string) string { + if st.summaries == nil || childID == "" { return "" } st.settingsMu.Lock() @@ -534,13 +548,7 @@ func (st *uiState) activeSummaryRaw() string { if !enabled { return "" } - st.mu.Lock() - active := st.activeAgentID - st.mu.Unlock() - if active == "" { - return "" - } - sum := st.summaries.Summary(active) + sum := st.summaries.Summary(childID) text := strings.TrimSpace(sum.Text) if text == "" { return "" diff --git a/internal/app/summarizer_test.go b/internal/app/summarizer_test.go index a38fc7f..bbaab62 100644 --- a/internal/app/summarizer_test.go +++ b/internal/app/summarizer_test.go @@ -52,6 +52,41 @@ func TestWrapSidebarSummaryKeepsWordBoundaries(t *testing.T) { } } +func TestSummaryTextForSelectsChildAndClips(t *testing.T) { + sess := NewSession(t.TempDir(), "test") + cfg := defaultSettings() + st := &uiState{ + sess: sess, + settings: cfg, + summaries: newSummaryManager(sess, t.TempDir(), preset.Set{}, func() autoSummarySettings { + return cfg.AutoSummary.clone() + }, nil, nil), + } + st.summaries.mu.Lock() + st.summaries.entries["a1"] = &summaryEntry{state: summaryState{Text: " alpha summary "}} + st.summaries.entries["a2"] = &summaryEntry{state: summaryState{Text: "beta summary"}} + st.summaries.entries["empty"] = &summaryEntry{state: summaryState{Text: " "}} + st.summaries.entries["long"] = &summaryEntry{state: summaryState{Text: "abcdefghijklmnopqrstuvwxyz"}} + st.summaries.mu.Unlock() + + if got := st.summaryTextFor("a2", 20); got != "beta summary" { + t.Fatalf("summaryTextFor(a2) = %q, want beta summary", got) + } + if got := st.summaryTextFor("empty", 20); got != "" { + t.Fatalf("summaryTextFor(empty) = %q, want empty", got) + } + if got := st.summaryTextFor("long", 8); got != "abcdefg…" { + t.Fatalf("summaryTextFor(long) = %q, want abcdefg…", got) + } + + st.settingsMu.Lock() + st.settings.AutoSummary.Enabled = false + st.settingsMu.Unlock() + if got := st.summaryTextFor("a1", 20); got != "" { + t.Fatalf("summaryTextFor disabled = %q, want empty", got) + } +} + func TestSummaryManagerArmsOnlyTrackedTopLevelAgents(t *testing.T) { sess := NewSession(t.TempDir(), "test") c := newChildEntry("a1", "agent", KindAgent, []string{"fake"}, nil, "", "", "") diff --git a/internal/app/tabbar.go b/internal/app/tabbar.go index 55341f4..6672187 100644 --- a/internal/app/tabbar.go +++ b/internal/app/tabbar.go @@ -59,6 +59,7 @@ func (st *uiState) drawTabBar() { newHintW := utf8.RuneCountInString(newHint) + 2 // " + new " framing type tabRect struct { + childID string startCol int width int label string @@ -66,8 +67,6 @@ func (st *uiState) drawTabBar() { glyphStyle string active bool } - activeTab := -1 - // Reserve space at the right edge for "+ new". If there are too // many tabs to fit even at minTabWidth, drop tabs from the right // until they do. The current focus stays visible. @@ -139,6 +138,7 @@ func (st *uiState) drawTabBar() { labelW = utf8.RuneCountInString(label) } tabs = append(tabs, tabRect{ + childID: c.ID, startCol: col, width: w, label: label, @@ -146,9 +146,6 @@ func (st *uiState) drawTabBar() { glyphStyle: glyphStyle, active: active, }) - if tabs[len(tabs)-1].active { - activeTab = len(tabs) - 1 - } col += w } } @@ -224,10 +221,9 @@ func (st *uiState) drawTabBar() { hintCol, styleBorder, strings.Repeat("─", newHintW), styleReset) } - if activeTab >= 0 { - tab := tabs[activeTab] + for _, tab := range tabs { summaryWidth := tab.width - 2 - if summary := st.activeSummaryText(summaryWidth); summary != "" { + if summary := st.summaryTextFor(tab.childID, summaryWidth); summary != "" { fmt.Fprintf(&b, "\x1b[2;%dH %s%s%s", tab.startCol, styleDim, summary, styleReset) } }