From e0ec103746c3b5cfe05c6a36817e55b8725154a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:29:09 +0000 Subject: [PATCH 1/3] Initial plan From be564d54b40b6d84f0ede4e4ffe309559488fe7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:46:22 +0000 Subject: [PATCH 2/3] Consolidate formatNumber() implementations into console.FormatNumber() Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/audit.go | 6 +-- pkg/cli/audit_report.go | 4 +- pkg/cli/logs.go | 47 +-------------------- pkg/cli/logs_test.go | 5 ++- pkg/console/render.go | 50 +++++++++++++++-------- pkg/console/render_formatting_test.go | 8 ++-- pkg/console/render_test.go | 59 ++++++++++++++++++++++++++- 7 files changed, 104 insertions(+), 75 deletions(-) diff --git a/pkg/cli/audit.go b/pkg/cli/audit.go index 1c9e30f2ab8..ca6e1b3c8f2 100644 --- a/pkg/cli/audit.go +++ b/pkg/cli/audit.go @@ -466,7 +466,7 @@ func generateAuditReport(processedRun ProcessedRun, metrics LogMetrics) string { // Metrics report.WriteString("## Metrics\n\n") if run.TokenUsage > 0 { - report.WriteString(fmt.Sprintf("- **Token Usage**: %s\n", formatNumber(run.TokenUsage))) + report.WriteString(fmt.Sprintf("- **Token Usage**: %s\n", console.FormatNumber(run.TokenUsage))) } if run.EstimatedCost > 0 { report.WriteString(fmt.Sprintf("- **Estimated Cost**: $%.3f\n", run.EstimatedCost)) @@ -521,11 +521,11 @@ func generateAuditReport(processedRun ProcessedRun, metrics LogMetrics) string { tool := toolStats[name] inputStr := "N/A" if tool.MaxInputSize > 0 { - inputStr = formatNumber(tool.MaxInputSize) + inputStr = console.FormatNumber(tool.MaxInputSize) } outputStr := "N/A" if tool.MaxOutputSize > 0 { - outputStr = formatNumber(tool.MaxOutputSize) + outputStr = console.FormatNumber(tool.MaxOutputSize) } durationStr := "N/A" if tool.MaxDuration > 0 { diff --git a/pkg/cli/audit_report.go b/pkg/cli/audit_report.go index 5eb07057d4b..67cd9e16ef2 100644 --- a/pkg/cli/audit_report.go +++ b/pkg/cli/audit_report.go @@ -492,11 +492,11 @@ func renderToolUsageTable(toolUsage []ToolUsageInfo) { for _, tool := range toolUsage { inputStr := "N/A" if tool.MaxInputSize > 0 { - inputStr = formatNumber(tool.MaxInputSize) + inputStr = console.FormatNumber(tool.MaxInputSize) } outputStr := "N/A" if tool.MaxOutputSize > 0 { - outputStr = formatNumber(tool.MaxOutputSize) + outputStr = console.FormatNumber(tool.MaxOutputSize) } durationStr := "N/A" if tool.MaxDuration != "" { diff --git a/pkg/cli/logs.go b/pkg/cli/logs.go index d5d267bbf7f..6e321266edf 100644 --- a/pkg/cli/logs.go +++ b/pkg/cli/logs.go @@ -1723,7 +1723,7 @@ func displayLogsOverview(processedRuns []ProcessedRun, verbose bool) { // Format tokens tokensStr := "" if run.TokenUsage > 0 { - tokensStr = formatNumber(run.TokenUsage) + tokensStr = console.FormatNumber(run.TokenUsage) totalTokens += run.TokenUsage } @@ -1799,7 +1799,7 @@ func displayLogsOverview(processedRuns []ProcessedRun, verbose bool) { "", "", timeutil.FormatDuration(totalDuration), - formatNumber(totalTokens), + console.FormatNumber(totalTokens), fmt.Sprintf("%.3f", totalCost), fmt.Sprintf("%d", totalTurns), fmt.Sprintf("%d", totalErrors), @@ -1838,49 +1838,6 @@ func ExtractLogMetricsFromRun(processedRun ProcessedRun) workflow.LogMetrics { return metrics } -// formatNumber formats large numbers in a human-readable way (e.g., "1k", "1.2k", "1.12M") -func formatNumber(n int) string { - if n == 0 { - return "0" - } - - f := float64(n) - - if f < 1000 { - return fmt.Sprintf("%d", n) - } else if f < 1000000 { - // Format as thousands (k) - k := f / 1000 - if k >= 100 { - return fmt.Sprintf("%.0fk", k) - } else if k >= 10 { - return fmt.Sprintf("%.1fk", k) - } else { - return fmt.Sprintf("%.2fk", k) - } - } else if f < 1000000000 { - // Format as millions (M) - m := f / 1000000 - if m >= 100 { - return fmt.Sprintf("%.0fM", m) - } else if m >= 10 { - return fmt.Sprintf("%.1fM", m) - } else { - return fmt.Sprintf("%.2fM", m) - } - } else { - // Format as billions (B) - b := f / 1000000000 - if b >= 100 { - return fmt.Sprintf("%.0fB", b) - } else if b >= 10 { - return fmt.Sprintf("%.1fB", b) - } else { - return fmt.Sprintf("%.2fB", b) - } - } -} - // findAgentOutputFile searches for a file named agent_output.json within the logDir tree. // Returns the first path found (depth-first) and a boolean indicating success. func findAgentOutputFile(logDir string) (string, bool) { diff --git a/pkg/cli/logs_test.go b/pkg/cli/logs_test.go index 773481eb5af..d3f3c144748 100644 --- a/pkg/cli/logs_test.go +++ b/pkg/cli/logs_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/workflow" "github.com/githubnext/gh-aw/pkg/workflow/pretty" ) @@ -68,9 +69,9 @@ func TestFormatNumber(t *testing.T) { } for _, test := range tests { - result := formatNumber(test.input) + result := console.FormatNumber(test.input) if result != test.expected { - t.Errorf("formatNumber(%d) = %s, expected %s", test.input, result, test.expected) + t.Errorf("console.FormatNumber(%d) = %s, expected %s", test.input, result, test.expected) } } } diff --git a/pkg/console/render.go b/pkg/console/render.go index 35f5d3bd812..f6c395365e0 100644 --- a/pkg/console/render.go +++ b/pkg/console/render.go @@ -437,22 +437,22 @@ func formatFieldValueWithTag(val reflect.Value, tag consoleTag) string { if val.CanInterface() { switch v := val.Interface().(type) { case int: - return formatNumberForDisplay(v) + return FormatNumber(v) case int64: - return formatNumberForDisplay(int(v)) + return FormatNumber(int(v)) case int32: - return formatNumberForDisplay(int(v)) + return FormatNumber(int(v)) case uint: - return formatNumberForDisplay(int(v)) + return FormatNumber(int(v)) case uint64: - return formatNumberForDisplay(int(v)) + return FormatNumber(int(v)) case uint32: - return formatNumberForDisplay(int(v)) + return FormatNumber(int(v)) } } // Fallback: try to parse from baseValue if it's an integer if val.Kind() >= reflect.Int && val.Kind() <= reflect.Uint64 { - return formatNumberForDisplay(int(val.Int())) + return FormatNumber(int(val.Int())) } case "cost": // Format as currency with $ prefix @@ -539,8 +539,8 @@ func formatFileSize(size int64) string { return fmt.Sprintf("%.1f %s", float64(size)/float64(div), units[exp]) } -// formatNumberForDisplay formats large numbers in a human-readable way (e.g., "1k", "1.2M") -func formatNumberForDisplay(n int) string { +// FormatNumber formats large numbers in a human-readable way (e.g., "1k", "1.2k", "1.12M") +func FormatNumber(n int) string { if n == 0 { return "0" } @@ -551,17 +551,33 @@ func formatNumberForDisplay(n int) string { return fmt.Sprintf("%d", n) } else if f < 1000000 { // Format as thousands (k) - if f < 10000 { - return fmt.Sprintf("%.1fk", f/1000) + k := f / 1000 + if k >= 100 { + return fmt.Sprintf("%.0fk", k) + } else if k >= 10 { + return fmt.Sprintf("%.1fk", k) + } else { + return fmt.Sprintf("%.2fk", k) } - return fmt.Sprintf("%.0fk", f/1000) } else if f < 1000000000 { // Format as millions (M) - if f < 10000000 { - return fmt.Sprintf("%.2fM", f/1000000) + m := f / 1000000 + if m >= 100 { + return fmt.Sprintf("%.0fM", m) + } else if m >= 10 { + return fmt.Sprintf("%.1fM", m) + } else { + return fmt.Sprintf("%.2fM", m) + } + } else { + // Format as billions (B) + b := f / 1000000000 + if b >= 100 { + return fmt.Sprintf("%.0fB", b) + } else if b >= 10 { + return fmt.Sprintf("%.1fB", b) + } else { + return fmt.Sprintf("%.2fB", b) } - return fmt.Sprintf("%.1fM", f/1000000) } - // Format as billions (B) - return fmt.Sprintf("%.2fB", f/1000000000) } diff --git a/pkg/console/render_formatting_test.go b/pkg/console/render_formatting_test.go index d728746b6d3..a3af5a148fe 100644 --- a/pkg/console/render_formatting_test.go +++ b/pkg/console/render_formatting_test.go @@ -170,13 +170,13 @@ func TestFormatFieldValueWithTag_NumberFormat(t *testing.T) { expected string }{ {"int - small", 500, "500"}, - {"int - 1k", 1000, "1.0k"}, - {"int - 1.5k", 1500, "1.5k"}, + {"int - 1k", 1000, "1.00k"}, + {"int - 1.5k", 1500, "1.50k"}, {"int - 1M", 1000000, "1.00M"}, {"int - 5M", 5000000, "5.00M"}, {"int64", int64(250000), "250k"}, - {"int32", int32(1500), "1.5k"}, - {"uint", uint(2000), "2.0k"}, + {"int32", int32(1500), "1.50k"}, + {"uint", uint(2000), "2.00k"}, {"uint64", uint64(3500000), "3.50M"}, {"uint32", uint32(750), "750"}, } diff --git a/pkg/console/render_test.go b/pkg/console/render_test.go index 340a0ac71c2..b593b4cf4ef 100644 --- a/pkg/console/render_test.go +++ b/pkg/console/render_test.go @@ -386,7 +386,62 @@ func TestFormatTag_InTable(t *testing.T) { } // Should format small output size - if !strings.Contains(output, "1.5k") { - t.Errorf("Output should contain formatted number '1.5k', got:\n%s", output) + if !strings.Contains(output, "1.50k") { + t.Errorf("Output should contain formatted number '1.50k', got:\n%s", output) + } +} + +func TestFormatNumber(t *testing.T) { + tests := []struct { + input int + expected string + }{ + // Zero + {0, "0"}, + // Small numbers (under 1000) + {5, "5"}, + {42, "42"}, + {999, "999"}, + // Thousands (1k-999k) + {1000, "1.00k"}, // Under 10k: 2 decimal places + {1200, "1.20k"}, + {1234, "1.23k"}, + {9999, "10.00k"}, // 9999/1000 = 9.999 -> formats as 10.00k + {10000, "10.0k"}, // 10k-99k: 1 decimal place + {12000, "12.0k"}, + {12300, "12.3k"}, + {99999, "100.0k"}, // 99999/1000 = 99.999 -> formats as 100.0k + {100000, "100k"}, // 100k+: no decimal places + {123000, "123k"}, + {999999, "1000k"}, // 999999/1000 = 999.999 -> formats as 1000k + // Millions (1M-999M) + {1000000, "1.00M"}, // Under 10M: 2 decimal places + {1200000, "1.20M"}, + {1234567, "1.23M"}, + {9999999, "10.00M"}, // 9999999/1000000 = 9.999999 -> formats as 10.00M + {10000000, "10.0M"}, // 10M-99M: 1 decimal place + {12000000, "12.0M"}, + {12300000, "12.3M"}, + {99999999, "100.0M"}, // 99999999/1000000 = 99.999999 -> formats as 100.0M + {100000000, "100M"}, // 100M+: no decimal places + {123000000, "123M"}, + {999999999, "1000M"}, // 999999999/1000000 = 999.999999 -> formats as 1000M + // Billions (1B+) + {1000000000, "1.00B"}, // Under 10B: 2 decimal places + {1200000000, "1.20B"}, + {1234567890, "1.23B"}, + {9999999999, "10.00B"}, // 9999999999/1000000000 = 9.999999999 -> formats as 10.00B + {10000000000, "10.0B"}, // 10B-99B: 1 decimal place + {12000000000, "12.0B"}, + {99999999999, "100.0B"}, // 99999999999/1000000000 = 99.999999999 -> formats as 100.0B + {100000000000, "100B"}, // 100B+: no decimal places + {123000000000, "123B"}, + } + + for _, test := range tests { + result := FormatNumber(test.input) + if result != test.expected { + t.Errorf("FormatNumber(%d) = %s, expected %s", test.input, result, test.expected) + } } } From 302718051fbfc3ed24c0a481097889bb59c59284 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 28 Oct 2025 13:53:52 +0000 Subject: [PATCH 3/3] Add changeset for formatNumber() consolidation [skip-ci] --- .changeset/patch-consolidate-format-number.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-consolidate-format-number.md diff --git a/.changeset/patch-consolidate-format-number.md b/.changeset/patch-consolidate-format-number.md new file mode 100644 index 00000000000..1286097fe63 --- /dev/null +++ b/.changeset/patch-consolidate-format-number.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Consolidate duplicate formatNumber() implementations into single shared console.FormatNumber() function