detached e139922

fix(status): refresh context usage for cached turns

commit
e139922ef95f621a59a3de7e6efc9bac86ed1268
author
brice · · 2026-08-07 09:25 UTC
committer
brice · · 2026-08-07 09:30 UTC
parent
49bea57
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +60 -4
M packages/agent/modes/bot/runner.go +2 -2
@@ -161,8 +161,8 @@ func (r *Runner) runTurn(ctx context.Context, t queuedTurn) {
161161 replyBuilder.WriteString(e.Delta)
162162 case core.EvUsage:
163163 r.mu.Lock()
164- if e.Usage.InputTokens > 0 {
165- r.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens
164+ if contextUsed := e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens; contextUsed > 0 {
165+ r.lastCtxInput = contextUsed
166166 }
167167 r.mu.Unlock()
168168 case core.EvAssistantMessage:
@@ -161,8 +161,8 @@ func (r *Runner) runTurn(ctx context.Context, t queuedTurn) {
161 replyBuilder.WriteString(e.Delta)161 replyBuilder.WriteString(e.Delta)
162 case core.EvUsage:162 case core.EvUsage:
163 r.mu.Lock()163 r.mu.Lock()
164- if e.Usage.InputTokens > 0 {164+ if contextUsed := e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens; contextUsed > 0 {
165- r.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens165+ r.lastCtxInput = contextUsed
166 }166 }
167 r.mu.Unlock()167 r.mu.Unlock()
168 case core.EvAssistantMessage:168 case core.EvAssistantMessage:
M packages/agent/modes/bot/runner_test.go +29 -0
@@ -103,3 +103,32 @@ func TestHandleMessageClaimsDrainSlotBeforeSpawningDrainer(t *testing.T) {
103103
104104 close(client.release)
105105 }
106+
107+type cacheOnlyUsageClient struct{}
108+
109+func (cacheOnlyUsageClient) Name() string { return "cache-only-usage" }
110+
111+func (cacheOnlyUsageClient) Stream(context.Context, provider.Request) (<-chan provider.Event, error) {
112+ events := make(chan provider.Event, 2)
113+ events <- provider.EventUsage{Usage: provider.Usage{
114+ CacheReadTokens: 400,
115+ CacheWriteTokens: 50,
116+ }}
117+ events <- provider.EventDone{Stop: provider.StopEnd}
118+ close(events)
119+ return events, nil
120+}
121+
122+func TestRunTurnUpdatesContextUsageFromCacheOnlyTurn(t *testing.T) {
123+ runner := NewRunner(testAdapter{}, core.NewAgent(cacheOnlyUsageClient{}, "test-model", "", nil), Config{})
124+ runner.lastCtxInput = 99
125+
126+ runner.runTurn(context.Background(), queuedTurn{channelID: "channel", prompt: "prompt"})
127+
128+ runner.mu.Lock()
129+ got := runner.lastCtxInput
130+ runner.mu.Unlock()
131+ if got != 450 {
132+ t.Fatalf("context usage = %d, want 450", got)
133+ }
134+}
@@ -103,3 +103,32 @@ func TestHandleMessageClaimsDrainSlotBeforeSpawningDrainer(t *testing.T) {
103 103
104 close(client.release)104 close(client.release)
105 }105 }
106+
107+type cacheOnlyUsageClient struct{}
108+
109+func (cacheOnlyUsageClient) Name() string { return "cache-only-usage" }
110+
111+func (cacheOnlyUsageClient) Stream(context.Context, provider.Request) (<-chan provider.Event, error) {
112+ events := make(chan provider.Event, 2)
113+ events <- provider.EventUsage{Usage: provider.Usage{
114+ CacheReadTokens: 400,
115+ CacheWriteTokens: 50,
116+ }}
117+ events <- provider.EventDone{Stop: provider.StopEnd}
118+ close(events)
119+ return events, nil
120+}
121+
122+func TestRunTurnUpdatesContextUsageFromCacheOnlyTurn(t *testing.T) {
123+ runner := NewRunner(testAdapter{}, core.NewAgent(cacheOnlyUsageClient{}, "test-model", "", nil), Config{})
124+ runner.lastCtxInput = 99
125+
126+ runner.runTurn(context.Background(), queuedTurn{channelID: "channel", prompt: "prompt"})
127+
128+ runner.mu.Lock()
129+ got := runner.lastCtxInput
130+ runner.mu.Unlock()
131+ if got != 450 {
132+ t.Fatalf("context usage = %d, want 450", got)
133+ }
134+}
M packages/agent/modes/interactive.go +2 -2
@@ -8109,8 +8109,8 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
81098109 }
81108110 case core.EvUsage:
81118111 i.cumUsage = e.Cumulative
8112- if e.Usage.InputTokens > 0 {
8113- i.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens
8112+ if contextUsed := e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens; contextUsed > 0 {
8113+ i.lastCtxInput = contextUsed
81148114 }
81158115 case core.EvTurnEnd:
81168116 if e.Stop == provider.StopAborted {
@@ -8109,8 +8109,8 @@ func (i *Interactive) handleEvent(ev core.AgentEvent) {
8109 }8109 }
8110 case core.EvUsage:8110 case core.EvUsage:
8111 i.cumUsage = e.Cumulative8111 i.cumUsage = e.Cumulative
8112- if e.Usage.InputTokens > 0 {8112+ if contextUsed := e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens; contextUsed > 0 {
8113- i.lastCtxInput = e.Usage.InputTokens + e.Usage.CacheReadTokens + e.Usage.CacheWriteTokens8113+ i.lastCtxInput = contextUsed
8114 }8114 }
8115 case core.EvTurnEnd:8115 case core.EvTurnEnd:
8116 if e.Stop == provider.StopAborted {8116 if e.Stop == provider.StopAborted {
A packages/agent/modes/interactive_usage_test.go +27 -0
@@ -0,0 +1,27 @@
1+package modes
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/zut/packages/core"
7+ "github.com/bnema/zut/packages/provider"
8+)
9+
10+func TestHandleEventUpdatesContextUsageFromCacheOnlyTurn(t *testing.T) {
11+ interactive := NewInteractive(InteractiveConfig{})
12+ interactive.lastCtxInput = 99
13+
14+ interactive.handleEvent(core.EvUsage{Usage: provider.Usage{
15+ CacheReadTokens: 400,
16+ CacheWriteTokens: 50,
17+ }})
18+
19+ if got := interactive.lastCtxInput; got != 450 {
20+ t.Fatalf("context usage = %d, want 450", got)
21+ }
22+
23+ interactive.handleEvent(core.EvUsage{})
24+ if got := interactive.lastCtxInput; got != 450 {
25+ t.Fatalf("empty usage cleared context usage to %d, want 450", got)
26+ }
27+}
@@ -0,0 +1,27 @@
1+package modes
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/zut/packages/core"
7+ "github.com/bnema/zut/packages/provider"
8+)
9+
10+func TestHandleEventUpdatesContextUsageFromCacheOnlyTurn(t *testing.T) {
11+ interactive := NewInteractive(InteractiveConfig{})
12+ interactive.lastCtxInput = 99
13+
14+ interactive.handleEvent(core.EvUsage{Usage: provider.Usage{
15+ CacheReadTokens: 400,
16+ CacheWriteTokens: 50,
17+ }})
18+
19+ if got := interactive.lastCtxInput; got != 450 {
20+ t.Fatalf("context usage = %d, want 450", got)
21+ }
22+
23+ interactive.handleEvent(core.EvUsage{})
24+ if got := interactive.lastCtxInput; got != 450 {
25+ t.Fatalf("empty usage cleared context usage to %d, want 450", got)
26+ }
27+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close