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)