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
12 changes: 9 additions & 3 deletions cmd/chat_print.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func runPrint(text string) error {
var printed strings.Builder
var countdownShown bool
var lastUsage *engine.StreamUsage
turns := 0
started := time.Now()
for ev := range ch {
switch ev.Type {
Expand Down Expand Up @@ -122,6 +123,7 @@ func runPrint(text string) error {
case "usage":
if ev.Usage != nil {
lastUsage = ev.Usage
turns++
}
if outputFormat == "stream-json" && ev.Usage != nil {
writePrintUsageEvent(sessionID, ev.Usage)
Expand All @@ -135,7 +137,7 @@ func runPrint(text string) error {
switch outputFormat {
case "text":
printTextResponse(printed.String())
printTextUsageFooter(lastUsage, started)
printTextUsageFooter(lastUsage, started, turns, effectiveModel)
case "json":
writePrintResult(printed.String(), sessionID, sess, false, nil)
case "stream-json":
Expand All @@ -150,7 +152,7 @@ func runPrint(text string) error {
switch outputFormat {
case "text":
printTextResponse(printed.String())
printTextUsageFooter(lastUsage, started)
printTextUsageFooter(lastUsage, started, turns, effectiveModel)
case "json":
writePrintResult(printed.String(), sessionID, sess, false, nil)
case "stream-json":
Expand Down Expand Up @@ -186,15 +188,19 @@ func writePrintUsageEvent(sessionID string, usage *engine.StreamUsage) {
// printTextUsageFooter renders a muted token/elapsed summary to stderr after a
// one-shot text-mode run. It writes to stderr so stdout stays pure for scripts,
// and is skipped entirely when no usage event was received or --quiet is set.
func printTextUsageFooter(usage *engine.StreamUsage, started time.Time) {
func printTextUsageFooter(usage *engine.StreamUsage, started time.Time, turns int, model string) {
if usage == nil || IsQuiet() {
return
}
parts := []string{fmt.Sprintf("%d in · %d out", usage.PromptTokens, usage.CompletionTokens)}
if usage.CacheReadTokens > 0 || usage.CacheWriteTokens > 0 {
parts = append(parts, fmt.Sprintf("cache %d read · %d write", usage.CacheReadTokens, usage.CacheWriteTokens))
}
parts = append(parts, fmt.Sprintf("%d turn(s)", turns))
parts = append(parts, time.Since(started).Round(time.Second).String())
if model != "" {
parts = append(parts, model)
}
_, _ = fmt.Fprintf(os.Stderr, "%s\n", auditTint("tokens: "+strings.Join(parts, " · "), textMuted))
}

Expand Down
30 changes: 24 additions & 6 deletions cmd/chat_print_usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,25 @@ func captureStderr(t *testing.T, fn func()) string {
func TestPrintTextUsageFooter(t *testing.T) {
started := time.Now().Add(-2 * time.Second)

t.Run("renders token and elapsed summary", func(t *testing.T) {
t.Run("renders token, turns, model and elapsed summary", func(t *testing.T) {
got := captureStderr(t, func() {
printTextUsageFooter(&engine.StreamUsage{
PromptTokens: 100,
CompletionTokens: 50,
}, started)
}, started, 3, "graycode-pro")
})
if !strings.Contains(got, "100 in · 50 out") {
t.Errorf("footer missing token counts: %q", got)
}
if !strings.Contains(got, "tokens:") {
t.Errorf("footer missing prefix: %q", got)
}
if !strings.Contains(got, "3 turn(s)") {
t.Errorf("footer missing turn count: %q", got)
}
if !strings.Contains(got, "graycode-pro") {
t.Errorf("footer missing model: %q", got)
}
if !strings.Contains(got, "2s") {
t.Errorf("footer missing elapsed: %q", got)
}
Expand All @@ -58,7 +64,7 @@ func TestPrintTextUsageFooter(t *testing.T) {
CompletionTokens: 5,
CacheReadTokens: 90,
CacheWriteTokens: 7,
}, started)
}, started, 1, "")
})
if !strings.Contains(got, "cache 90 read · 7 write") {
t.Errorf("footer missing cache summary: %q", got)
Expand All @@ -70,16 +76,28 @@ func TestPrintTextUsageFooter(t *testing.T) {
printTextUsageFooter(&engine.StreamUsage{
PromptTokens: 10,
CompletionTokens: 5,
}, started)
}, started, 1, "")
})
if strings.Contains(got, "cache") {
t.Errorf("footer should omit zero cache: %q", got)
}
})

t.Run("omits model when empty", func(t *testing.T) {
got := captureStderr(t, func() {
printTextUsageFooter(&engine.StreamUsage{
PromptTokens: 10,
CompletionTokens: 5,
}, started, 1, "")
})
if strings.Contains(got, "graycode-pro") {
t.Errorf("footer should omit empty model: %q", got)
}
})

t.Run("skips output when usage is nil", func(t *testing.T) {
got := captureStderr(t, func() {
printTextUsageFooter(nil, started)
printTextUsageFooter(nil, started, 1, "m")
})
if got != "" {
t.Errorf("expected no output for nil usage, got: %q", got)
Expand All @@ -94,7 +112,7 @@ func TestPrintTextUsageFooter(t *testing.T) {
printTextUsageFooter(&engine.StreamUsage{
PromptTokens: 100,
CompletionTokens: 50,
}, started)
}, started, 1, "m")
})
if got != "" {
t.Errorf("expected no output under --quiet, got: %q", got)
Expand Down
Loading