From dd6c23f173b7d39e016607cb828edbd85aa15dd0 Mon Sep 17 00:00:00 2001 From: Rolando Santamaria Maso Date: Mon, 31 Aug 2026 15:25:02 +0200 Subject: [PATCH] fix(tui): silence tool_running agent_signal heartbeats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tool_running signals are engine housekeeping duplicating the in-flight step spinner — extend the trim-silencing contract to cover them. All other agent_signal subtypes still surface as notices. --- internal/tui/events.go | 7 ++++--- internal/tui/notices_test.go | 12 ++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/tui/events.go b/internal/tui/events.go index 4d7150a..c211b11 100644 --- a/internal/tui/events.go +++ b/internal/tui/events.go @@ -279,9 +279,10 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) { case "memory_event": m.addTransientNote("memory · " + strings.TrimSpace(ev.SubType+" "+ev.Target) + eventTail(ev)) case "agent_signal": - if ev.SubType == "trim" { - // Context trimming is engine housekeeping — nothing the user - // can act on, so it never reaches the notice strip. + if ev.SubType == "trim" || ev.SubType == "tool_running" { + // Engine housekeeping: context trimming and tool-running + // heartbeats duplicate what the transcript already shows + // (the in-flight step spinner) — never reach the strip. break } m.addTransientNote("signal · " + strings.TrimSpace(ev.SubType+" "+ev.Detail) + eventTail(ev)) diff --git a/internal/tui/notices_test.go b/internal/tui/notices_test.go index f1eadb1..dbe2d65 100644 --- a/internal/tui/notices_test.go +++ b/internal/tui/notices_test.go @@ -59,8 +59,9 @@ func assertAlertDwell(t *testing.T, m *Model, cmdArmed bool, substr string) { } // TestTrimSignalSilenced pins the actionable-only contract for agent_signal: -// the "trim" subtype is engine housekeeping (context-window trimming) — -// nothing the user can act on — so it must never surface as a notice. +// the "trim" and "tool_running" subtypes are engine housekeeping (context- +// window trimming, tool-execution heartbeats) — nothing the user can act +// on — so they must never surface as notices. // Every other subtype keeps flowing into the strip. func TestTrimSignalSilenced(t *testing.T) { m := newTestModel() @@ -71,6 +72,13 @@ func TestTrimSignalSilenced(t *testing.T) { } } + m.handleEvent(client.Event{Type: "agent_signal", SubType: "tool_running", Detail: "shell"}) + for _, n := range m.notices { + if strings.Contains(n, "signal · tool_running") { + t.Fatalf("tool_running signal surfaced as a notice: %v", m.notices) + } + } + // Silence is per-subtype, not per event class. m.handleEvent(client.Event{Type: "agent_signal", SubType: "fallback", Detail: "glm-x"}) if note, _ := lastNoteMatching(m, "signal · fallback"); note == "" {