Skip to content
Merged
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
49 changes: 38 additions & 11 deletions codestrain_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -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()


Expand Down Expand Up @@ -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 "<synthetic>" 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 != "<synthetic>")
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()
Expand Down Expand Up @@ -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()
Expand Down
Loading