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
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ feat(tui): compact tool steps with Ctrl+E details toggle

## Workflow rules for agents

- **Never modify the odek project from a bodek session.** bodek is a pure
front-end; if a fix requires changes in odek (protocol, `odek serve`,
engine behaviour), do not edit that repo — instead summarize the required
change and suggest it to the user as a task for an odek session.
- Don't run `git commit`/`git push` unless the user explicitly asks.
- Don't add dependencies without checking `go.mod` first and flagging it
to the user.
Expand Down
10 changes: 10 additions & 0 deletions internal/tui/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
m.winCtxTok = ev.ContextTokens
m.lastLatency = ev.Latency

case "usage":
// Per-iteration window fill from odek serve: keeps the header gauge
// live during multi-turn runs instead of waiting for "done". A zero
// value means the provider reported no usage — keep the last known
// fill rather than zeroing the gauge.
if ev.ContextTokens > 0 {
m.winCtxTok = ev.ContextTokens
}
stream = true

case "error":
if i := m.cur(); i >= 0 && m.msgs[i].content == "" {
m.msgs[i].content = "**Error:** " + ev.Message
Expand Down
35 changes: 35 additions & 0 deletions internal/tui/stats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,41 @@ func TestGaugeFollowsTurnFill(t *testing.T) {
}
}

// TestUsageEventRefreshesGaugeMidTurn is a regression test: odek serve emits
// a per-iteration "usage" event during a run; the header gauge must refresh
// on it instead of staying stale until "done" arrives at the end of the
// whole agent loop.
func TestUsageEventRefreshesGaugeMidTurn(t *testing.T) {
m := newTestModel()
m.model = "big"
m.models = []client.ModelInfo{{ID: "big", MaxContext: 1000}}
m.resolveMaxContext()
m.msgs = append(m.msgs, message{role: roleAsst, streaming: true})
m.curIdx = len(m.msgs) - 1
m.busy = true
m.status = "responding"

m.handleEvent(client.Event{Type: "usage", ContextTokens: 400, OutputTokens: 50})
if m.winCtxTok != 400 {
t.Fatalf("winCtxTok = %d, want 400", m.winCtxTok)
}
if out := plain(m.header()); !strings.Contains(out, "40%") {
t.Errorf("gauge should refresh mid-run (40%%), got:\n%s", out)
}

// A zero-usage event (provider without usage reporting) must not zero a
// previously known fill.
m.handleEvent(client.Event{Type: "usage"})
if m.winCtxTok != 400 {
t.Fatalf("winCtxTok = %d, want 400 (zero usage ignored)", m.winCtxTok)
}

// The mid-run state must not be disturbed: still busy, still responding.
if !m.busy || m.status != "responding" {
t.Errorf("usage event must not end the turn: busy=%v status=%q", m.busy, m.status)
}
}

func TestContextGauge(t *testing.T) {
m := newTestModel()
m.model = "big"
Expand Down