From 2e859adef6acda5d34a76fc31e7a2788fd4f64f3 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Mon, 7 Sep 2026 14:35:11 +0530 Subject: [PATCH] feat: include turns and model in --print usage footer --print's usage footer showed tokens and elapsed but omitted turn count and the model, unlike exec's footer which surfaces both. Thread the turn counter (incremented per usage event) and the resolved model into printTextUsageFooter so one-shot text runs report which model ran and how many turns, matching exec and giving scripts/users the same useful context. Model is omitted when empty. Footer still writes to stderr and honors --quiet. --- cmd/chat_print.go | 12 +++++++++--- cmd/chat_print_usage_test.go | 30 ++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cmd/chat_print.go b/cmd/chat_print.go index 35aab773..916d3aff 100644 --- a/cmd/chat_print.go +++ b/cmd/chat_print.go @@ -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 { @@ -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) @@ -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": @@ -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": @@ -186,7 +188,7 @@ 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 } @@ -194,7 +196,11 @@ func printTextUsageFooter(usage *engine.StreamUsage, started time.Time) { 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)) } diff --git a/cmd/chat_print_usage_test.go b/cmd/chat_print_usage_test.go index da19d3ed..dca4f0be 100644 --- a/cmd/chat_print_usage_test.go +++ b/cmd/chat_print_usage_test.go @@ -33,12 +33,12 @@ 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) @@ -46,6 +46,12 @@ func TestPrintTextUsageFooter(t *testing.T) { 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) } @@ -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) @@ -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) @@ -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)