diff --git a/codestrain_cli.py b/codestrain_cli.py index 1d49121..d255e02 100755 --- a/codestrain_cli.py +++ b/codestrain_cli.py @@ -17,6 +17,9 @@ import sys from pathlib import Path +# Keep in sync with pyproject.toml [project].version at every release. +__version__ = "0.1.8" + # ── ANSI Colors ────────────────────────────────────────────────────────────── @@ -590,7 +593,7 @@ def print_header_adaptive(mode="auto"): # else: skip logo entirely on cramped terminals if mode != "none": print() - print(c(Colors.DIM, " Your AI coding recovery score.")) + print(c(Colors.DIM, f" Your AI coding recovery score. v{__version__}")) print() @@ -686,10 +689,15 @@ def print_session_summary(stats_list, label=""): print(f" Tokens: {c(Colors.CYAN, format_tokens(total_input))} in / {c(Colors.CYAN, format_tokens(total_output))} out") print(f" Cost: {c(Colors.AMBER, format_cost(total_cost))}") - if all_models: - models_str = ", ".join(sorted(all_models)[:3]) - if len(all_models) > 3: - models_str += f" +{len(all_models) - 3} more" + # Hide "" from the displayed list: it's not a real model, just + # Claude Code's marker for locally-fabricated events (cached API errors, + # interrupted turns, no-response slash commands). Token/turn counts still + # include them — only the user-facing Models row is filtered. + display_models = sorted(m for m in all_models if m != "") + if display_models: + models_str = ", ".join(display_models[:3]) + if len(display_models) > 3: + models_str += f" +{len(display_models) - 3} more" print(f" Models: {c(Colors.DIM, models_str)}") print() @@ -728,20 +736,39 @@ def print_project_breakdown(project_stats, anonymize=False): reverse=True, ) + # Pre-compute each row's plain (uncoloured) text so we can size columns + # off the visible width. Padding inside an f-string field on already- + # coloured strings doesn't work: ANSI escapes count toward the width + # spec and silently eat the padding, which leaves the breakdown jagged. + rows = [] for i, (project, stats_list) in enumerate(sorted_projects, start=1): total_duration = sum(s["duration_seconds"] for s in stats_list) total_cost = sum(s["total_cost"] for s in stats_list) total_turns = sum(s["turn_count"] for s in stats_list) if anonymize: - project_display = f"project-{i}" + name = f"project-{i}" else: - project_display = project[:30] + "..." if len(project) > 30 else project + name = project[:30] + "..." if len(project) > 30 else project + + rows.append(( + name, + format_duration(total_duration), + str(total_turns), + format_cost(total_cost), + )) + + name_w = max(len(r[0]) for r in rows) + dur_w = max(len(r[1]) for r in rows) + turns_w = max(len(r[2]) for r in rows) + cost_w = max(len(r[3]) for r in rows) + + for name, dur, turns, cost in rows: print( - f" {c(Colors.WHITE, project_display):<36}" - f"{bold(format_duration(total_duration)):>10} " - f"{c(Colors.CYAN, str(total_turns)):>6} turns " - f"{c(Colors.AMBER, format_cost(total_cost)):>8}" + f" {c(Colors.WHITE, name.ljust(name_w))} " + f"{bold(dur.rjust(dur_w))} " + f"{c(Colors.CYAN, turns.rjust(turns_w))} turns " + f"{c(Colors.AMBER, cost.rjust(cost_w))}" ) print()