Skip to content

Commit f92bc73

Browse files
authored
fix(tui): silence trim signals and tool execution times (#52)
Engine housekeeping is not user-actionable and no longer surfaces: agent_signal/trim never reaches the notice strip (other subtypes keep flowing), and the per-step execution time is dropped from the compact tool-step right rail, which now carries only the typed chip (diffstat / test verdict). Durations are still recorded on the step struct. Doc sync: README engine-notices bullet, INTEGRATIONS event table, REDESIGN step-line spec.
1 parent a47b385 commit f92bc73

7 files changed

Lines changed: 43 additions & 20 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -353,10 +353,11 @@ collect an approval for a prompt the engine already abandoned.
353353
- **Fluent by default** — gradient wordmark and hairline, smooth braille
354354
spinner, smart autoscroll that never yanks you while you read history, and a
355355
scroll-position indicator.
356-
- **Engine notices** — skill loads, memory merges, and agent signals appear as
357-
quiet status lines. Nothing lingers: info traces fade after 3s, and
358-
errors, warnings, and disconnect notes autoclose after 10s (connection
359-
state stays visible in the header badge).
356+
- **Engine notices** — skill loads, memory merges, and actionable agent
357+
signals appear as quiet status lines; internal housekeeping (context
358+
trims, tool execution times) stays silent. Nothing lingers: info traces
359+
fade after 3s, and errors, warnings, and disconnect notes autoclose
360+
after 10s (connection state stays visible in the header badge).
360361
- **Attention when backgrounded** — turn completion and pending approvals set
361362
the terminal window title (`✓ done — <model>` / `⚠ approval needed —
362363
<model>`) and ring the bell (`--bel=false` mutes); `--notify` adds OSC 9

‎docs/INTEGRATIONS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,4 @@ These workflows have no server endpoint; bodek cannot offer them:
7676
| `error` | ← | — | error bubble / cancel markers |
7777
| `cancelled` | ← | — | clean cancel close-out |
7878
| `approval_request` / `approval_ack` | ← | — | approval queue |
79-
| `skill_event` / `memory_event` / `agent_signal` | ← | — | transient notes (+ suggestion card) |
79+
| `skill_event` / `memory_event` / `agent_signal` | ← | — | transient notes (+ suggestion card; `agent_signal:trim` stays silent) |

‎docs/REDESIGN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ Typography of the cockpit: values bright, labels muted, glyphs amber. Numbers ne
114114

115115
**Reasoning accordions** adopt the WebUI's proven rule: auto-expand while its turn is live (with auto-follow), auto-collapse when the next turn starts; manually-opened history stays open; resumed transcripts start collapsed. (Today: always-capped excerpt — close, but the live auto-expand is what makes thinking models feel fast.)
116116

117-
**Typed tool renderers.** The step line stays one-line (glyph · name · arg · duration · status). What changes is *inspect depth*: expanding picks a renderer by tool/shape —
117+
**Typed tool renderers.** The step line stays one-line (glyph · name · arg · status chip). What changes is *inspect depth*: expanding picks a renderer by tool/shape —
118118

119119
| Renderer | Trigger | Inspect view |
120120
|----------|---------|--------------|

‎internal/tui/events.go‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,11 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
279279
case "memory_event":
280280
m.addTransientNote("memory · " + strings.TrimSpace(ev.SubType+" "+ev.Target) + eventTail(ev))
281281
case "agent_signal":
282+
if ev.SubType == "trim" {
283+
// Context trimming is engine housekeeping — nothing the user
284+
// can act on, so it never reaches the notice strip.
285+
break
286+
}
282287
m.addTransientNote("signal · " + strings.TrimSpace(ev.SubType+" "+ev.Detail) + eventTail(ev))
283288
case "subagent_log":
284289
line := strings.TrimSpace(ev.SubType + " " + ev.Name)

‎internal/tui/notices_test.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,26 @@ func assertAlertDwell(t *testing.T, m *Model, cmdArmed bool, substr string) {
5858
}
5959
}
6060

61+
// TestTrimSignalSilenced pins the actionable-only contract for agent_signal:
62+
// the "trim" subtype is engine housekeeping (context-window trimming) —
63+
// nothing the user can act on — so it must never surface as a notice.
64+
// Every other subtype keeps flowing into the strip.
65+
func TestTrimSignalSilenced(t *testing.T) {
66+
m := newTestModel()
67+
m.handleEvent(client.Event{Type: "agent_signal", SubType: "trim", Detail: "ctx"})
68+
for _, n := range m.notices {
69+
if strings.Contains(n, "signal · trim") {
70+
t.Fatalf("trim signal surfaced as a notice: %v", m.notices)
71+
}
72+
}
73+
74+
// Silence is per-subtype, not per event class.
75+
m.handleEvent(client.Event{Type: "agent_signal", SubType: "fallback", Detail: "glm-x"})
76+
if note, _ := lastNoteMatching(m, "signal · fallback"); note == "" {
77+
t.Errorf("non-trim agent_signal was silenced too: %v", m.notices)
78+
}
79+
}
80+
6181
// TestNoticesAutoclose is the regression for the never-disappearing
6282
// "error: iteration 22: llm: stream idle…" notice: every addNote path —
6383
// errors with and without an open turn, disconnects — posts into the strip

‎internal/tui/steps_test.go‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ func TestFormatStepDur(t *testing.T) {
281281
}
282282

283283
// TestStepDuration drives a tool call through handleEvent and checks the step
284-
// head: the response time appears once done, and no result excerpt shows.
284+
// head: the duration is recorded internally once done, but the compact head
285+
// never renders it — execution time is internal telemetry, not actionable
286+
// output. No result excerpt shows either.
285287
func TestStepDuration(t *testing.T) {
286288
m := newTestModel()
287289
m.msgs = append(m.msgs, message{role: roleAsst, streaming: true})
@@ -295,14 +297,15 @@ func TestStepDuration(t *testing.T) {
295297
t.Fatalf("tool_result should stamp the step duration: %+v", st)
296298
}
297299

298-
// A done step head shows the duration (fixture-set, for exact rendering).
300+
// A done step head shows no duration even when one was recorded —
301+
// the right rail is reserved for the typed chip (diffstat / verdict).
299302
msg := message{role: roleAsst, steps: []step{
300303
{name: "shell", arg: "go test", done: true, result: "exit status 1", dur: 320 * time.Millisecond},
301304
}}
302305
out, _ := renderStepsForTest(m, msg, 0, 0)
303306
plainOut := plain(out)
304-
if !strings.Contains(plainOut, "320ms") {
305-
t.Errorf("done head missing duration: %q", plainOut)
307+
if strings.Contains(plainOut, "320ms") {
308+
t.Errorf("done head must not render a duration: %q", plainOut)
306309
}
307310
if strings.Contains(plainOut, "→") || strings.Contains(plainOut, "exit status 1") {
308311
t.Errorf("compact head should not show a result excerpt: %q", plainOut)

‎internal/tui/view.go‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -776,19 +776,13 @@ func (m *Model) renderStep(s step, streaming bool, msgIdx, stepIdx, startLine in
776776
left += th.stepArg.Render(" · " + r)
777777
}
778778
}
779-
// Right rail: response time once the call lands, plus the typed chip
780-
// (diffstat / test verdict) — right-aligned so durations read as a
781-
// column down the step list instead of floating mid-line.
779+
// Right rail: the typed chip (diffstat / test verdict), right-aligned.
780+
// Tool execution time is internal telemetry — recorded on the step but
781+
// deliberately never rendered.
782782
right := ""
783-
if s.done && s.dur > 0 {
784-
right = th.stepArg.Render(formatStepDur(s.dur))
785-
}
786783
if s.done {
787784
if chip := stepHeadSuffix(s.name, s.result, th); chip != "" {
788-
if right != "" {
789-
right += th.stepArg.Render(" ")
790-
}
791-
right += chip
785+
right = chip
792786
}
793787
}
794788
// The left side yields to the right rail, then the pair pads to the

0 commit comments

Comments
 (0)