Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,10 +353,11 @@ collect an approval for a prompt the engine already abandoned.
- **Fluent by default** — gradient wordmark and hairline, smooth braille
spinner, smart autoscroll that never yanks you while you read history, and a
scroll-position indicator.
- **Engine notices** — skill loads, memory merges, and agent signals appear as
quiet status lines. Nothing lingers: info traces fade after 3s, and
errors, warnings, and disconnect notes autoclose after 10s (connection
state stays visible in the header badge).
- **Engine notices** — skill loads, memory merges, and actionable agent
signals appear as quiet status lines; internal housekeeping (context
trims, tool execution times) stays silent. Nothing lingers: info traces
fade after 3s, and errors, warnings, and disconnect notes autoclose
after 10s (connection state stays visible in the header badge).
- **Attention when backgrounded** — turn completion and pending approvals set
the terminal window title (`✓ done — <model>` / `⚠ approval needed —
<model>`) and ring the bell (`--bel=false` mutes); `--notify` adds OSC 9
Expand Down
2 changes: 1 addition & 1 deletion docs/INTEGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,4 @@ These workflows have no server endpoint; bodek cannot offer them:
| `error` | ← | — | error bubble / cancel markers |
| `cancelled` | ← | — | clean cancel close-out |
| `approval_request` / `approval_ack` | ← | — | approval queue |
| `skill_event` / `memory_event` / `agent_signal` | ← | — | transient notes (+ suggestion card) |
| `skill_event` / `memory_event` / `agent_signal` | ← | — | transient notes (+ suggestion card; `agent_signal:trim` stays silent) |
2 changes: 1 addition & 1 deletion docs/REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Typography of the cockpit: values bright, labels muted, glyphs amber. Numbers ne

**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.)

**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 —
**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 —

| Renderer | Trigger | Inspect view |
|----------|---------|--------------|
Expand Down
5 changes: 5 additions & 0 deletions internal/tui/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ 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.
break
}
m.addTransientNote("signal · " + strings.TrimSpace(ev.SubType+" "+ev.Detail) + eventTail(ev))
case "subagent_log":
line := strings.TrimSpace(ev.SubType + " " + ev.Name)
Expand Down
20 changes: 20 additions & 0 deletions internal/tui/notices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ 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.
// Every other subtype keeps flowing into the strip.
func TestTrimSignalSilenced(t *testing.T) {
m := newTestModel()
m.handleEvent(client.Event{Type: "agent_signal", SubType: "trim", Detail: "ctx"})
for _, n := range m.notices {
if strings.Contains(n, "signal · trim") {
t.Fatalf("trim 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 == "" {
t.Errorf("non-trim agent_signal was silenced too: %v", m.notices)
}
}

// TestNoticesAutoclose is the regression for the never-disappearing
// "error: iteration 22: llm: stream idle…" notice: every addNote path —
// errors with and without an open turn, disconnects — posts into the strip
Expand Down
11 changes: 7 additions & 4 deletions internal/tui/steps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,9 @@ func TestFormatStepDur(t *testing.T) {
}

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

// A done step head shows the duration (fixture-set, for exact rendering).
// A done step head shows no duration even when one was recorded —
// the right rail is reserved for the typed chip (diffstat / verdict).
msg := message{role: roleAsst, steps: []step{
{name: "shell", arg: "go test", done: true, result: "exit status 1", dur: 320 * time.Millisecond},
}}
out, _ := renderStepsForTest(m, msg, 0, 0)
plainOut := plain(out)
if !strings.Contains(plainOut, "320ms") {
t.Errorf("done head missing duration: %q", plainOut)
if strings.Contains(plainOut, "320ms") {
t.Errorf("done head must not render a duration: %q", plainOut)
}
if strings.Contains(plainOut, "→") || strings.Contains(plainOut, "exit status 1") {
t.Errorf("compact head should not show a result excerpt: %q", plainOut)
Expand Down
14 changes: 4 additions & 10 deletions internal/tui/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -776,19 +776,13 @@ func (m *Model) renderStep(s step, streaming bool, msgIdx, stepIdx, startLine in
left += th.stepArg.Render(" · " + r)
}
}
// Right rail: response time once the call lands, plus the typed chip
// (diffstat / test verdict) — right-aligned so durations read as a
// column down the step list instead of floating mid-line.
// Right rail: the typed chip (diffstat / test verdict), right-aligned.
// Tool execution time is internal telemetry — recorded on the step but
// deliberately never rendered.
right := ""
if s.done && s.dur > 0 {
right = th.stepArg.Render(formatStepDur(s.dur))
}
if s.done {
if chip := stepHeadSuffix(s.name, s.result, th); chip != "" {
if right != "" {
right += th.stepArg.Render(" ")
}
right += chip
right = chip
}
}
// The left side yields to the right rail, then the pair pads to the
Expand Down