Skip to content

Commit 056e7ad

Browse files
authored
Merge pull request #3714 from docker/feat/tui-agent-usage
feat(tui): show usage details per agent
2 parents f2f750b + 8a4f28e commit 056e7ad

27 files changed

Lines changed: 1978 additions & 214 deletions

pkg/tui/components/sidebar/agent_click_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func TestSidebar_AgentClickZones_EveryRenderedLineMapped(t *testing.T) {
8080
}
8181
m.width = 40
8282
m.height = 50
83+
m.SetAgentInfoMode(AgentInfoDetailed)
8384

8485
_ = sb.View()
8586

@@ -92,10 +93,10 @@ func TestSidebar_AgentClickZones_EveryRenderedLineMapped(t *testing.T) {
9293
}
9394
assert.Positive(t, counts["agent1"], "agent1 should own rendered lines")
9495
assert.Positive(t, counts["agent2"], "agent2 should own rendered lines")
95-
// agent2 is a non-current roster agent: its entry spans two lines (name+badge
96-
// then the indented model), and BOTH must map to it so a click on either
97-
// switches to the agent.
98-
assert.Equal(t, 2, counts["agent2"], "a roster agent owns both of its two entry lines")
96+
// agent2 is a non-current roster agent: its mini-card spans the name line,
97+
// the model line and two metric lines at this width, and ALL of them must
98+
// map to it so a click on any card line switches to the agent.
99+
assert.Equal(t, 4, counts["agent2"], "a roster agent owns every line of its card")
99100

100101
// The number of click zones equals the number of owned (non-blank) lines:
101102
// every owned line is clickable.

pkg/tui/components/sidebar/agent_context_test.go

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ func recordAgentUsage(m *model, sessionID, agentName string, contextLen, context
2525
})
2626
}
2727

28-
// TestAgentRosterShowsPerAgentContextPercent verifies line 2 of each roster
29-
// entry carries the agent's own context percentage, right-aligned, once that
30-
// agent has emitted usage — and stays bare for agents that have not run.
28+
// TestAgentRosterShowsPerAgentContextPercent verifies each card's metric line
29+
// carries the agent's own labeled context percentage once that agent has
30+
// emitted usage — and an explicit "—" for agents that have not run.
3131
func TestAgentRosterShowsPerAgentContextPercent(t *testing.T) {
3232
t.Parallel()
3333

@@ -40,17 +40,17 @@ func TestAgentRosterShowsPerAgentContextPercent(t *testing.T) {
4040
recordAgentUsage(m, "session-root", "root", 30_000, 100_000)
4141
recordAgentUsage(m, "session-child", "developer", 42_000, 100_000)
4242

43+
assert.Contains(t, agentMetrics(m, "root"), "Context 30%",
44+
"root's card shows its own labeled context percent")
4345
_, rootLine2 := agentLines(m, "root")
44-
assert.True(t, strings.HasSuffix(strings.TrimRight(rootLine2, " "), "30%"),
45-
"root's line 2 ends with its context percent, got %q", rootLine2)
4646
assert.Contains(t, rootLine2, "anthropic/opus", "model text stays on line 2")
4747

48-
_, devLine2 := agentLines(m, "developer")
49-
assert.True(t, strings.HasSuffix(strings.TrimRight(devLine2, " "), "42%"),
50-
"developer's line 2 ends with its context percent, got %q", devLine2)
48+
assert.Contains(t, agentMetrics(m, "developer"), "Context 42%",
49+
"developer's card shows its own labeled context percent")
5150

52-
_, idleLine2 := agentLines(m, "idle")
53-
assert.NotContains(t, idleLine2, "%", "agents that never ran show no percent")
51+
idleMetrics := agentMetrics(m, "idle")
52+
assert.Contains(t, idleMetrics, "Context —", "agents that never ran show an explicit —")
53+
assert.NotContains(t, idleMetrics, "%", "agents that never ran show no percent")
5454
}
5555

5656
// TestAgentContextPercent_LatestSnapshotWins verifies a fresh delegation to the
@@ -71,7 +71,7 @@ func TestAgentContextPercent_LatestSnapshotWins(t *testing.T) {
7171

7272
// TestAgentContextPercent_UnknownLimit verifies no percent is shown when the
7373
// context limit is unknown (e.g. harness-backed agents) or nothing was
74-
// recorded for the agent.
74+
// recorded for the agent — the card shows an explicit "—" instead.
7575
func TestAgentContextPercent_UnknownLimit(t *testing.T) {
7676
t.Parallel()
7777

@@ -84,8 +84,9 @@ func TestAgentContextPercent_UnknownLimit(t *testing.T) {
8484
recordAgentUsage(m, "session-1", "root", 30_000, 0)
8585
assert.Empty(t, m.agentContextPercent("root"), "no percent without a context limit")
8686

87-
_, line2 := agentLines(m, "root")
88-
assert.NotContains(t, line2, "%")
87+
metrics := agentMetrics(m, "root")
88+
assert.Contains(t, metrics, "Context —", "unknown context reads —, not a blank")
89+
assert.NotContains(t, metrics, "%")
8990
}
9091

9192
// TestAgentContextPercent_BackgroundAgentUsage verifies a usage event from a
@@ -106,29 +107,33 @@ func TestAgentContextPercent_BackgroundAgentUsage(t *testing.T) {
106107
recordAgentUsage(m, "session-root", "root", 20_000, 100_000)
107108
recordAgentUsage(m, "bg-session", "worker", 55_000, 100_000)
108109

109-
_, workerLine2 := agentLines(m, "worker")
110-
assert.True(t, strings.HasSuffix(strings.TrimRight(workerLine2, " "), "55%"),
111-
"background worker's line 2 ends with its context percent, got %q", workerLine2)
110+
assert.Contains(t, agentMetrics(m, "worker"), "Context 55%",
111+
"background worker's card shows its own context percent")
112112

113113
assert.Equal(t, "20%", m.contextPercent(),
114114
"the main context gauge keeps tracking the active session, not the background one")
115115
}
116116

117-
// TestAgentRosterLine2ReservesRoomForPercent verifies the model text yields
118-
// space to the percent instead of colliding with it at narrow widths.
119-
func TestAgentRosterLine2ReservesRoomForPercent(t *testing.T) {
117+
// TestAgentCardMetricLinesFitNarrowWidth verifies the labeled metrics wrap
118+
// into lines that never exceed the content width at the minimum sidebar
119+
// width, instead of colliding with or truncating one another.
120+
func TestAgentCardMetricLinesFitNarrowWidth(t *testing.T) {
120121
t.Parallel()
121122

122-
m := newAgentPanelSidebar(t, 28,
123-
runtime.AgentDetails{Name: "root", Provider: "anthropic", Model: "claude-sonnet-4-6"},
123+
m := newAgentPanelSidebar(t, MinWidth,
124+
runtime.AgentDetails{Name: "root", Provider: "anthropic", Model: "claude-sonnet-4-6", Thinking: "high"},
124125
)
125126
recordAgentUsage(m, "session-1", "root", 100_000, 100_000)
126127

127-
_, line2 := agentLines(m, "root")
128-
require.NotEmpty(t, line2)
129-
trimmed := strings.TrimRight(line2, " ")
130-
assert.True(t, strings.HasSuffix(trimmed, " 100%"),
131-
"percent stays separated from the truncated model, got %q", line2)
132-
assert.Contains(t, line2, "…", "overflowing model is still left-truncated")
133-
assert.Contains(t, line2, "-4-6", "informative model tail survives")
128+
contentWidth := m.contentWidth(false)
129+
card := agentCard(m, "root")
130+
require.NotEmpty(t, card)
131+
for _, line := range card {
132+
assert.LessOrEqualf(t, len([]rune(line)), contentWidth,
133+
"card line must fit the content width: %q", line)
134+
}
135+
136+
metrics := strings.Join(card[2:], "\n")
137+
assert.Contains(t, metrics, gaugePattern(4), "the full six-cell gauge survives the minimum width")
138+
assert.Contains(t, metrics, "Ctx 100%", "the compact context label keeps its percent")
134139
}

0 commit comments

Comments
 (0)