diff --git a/src/copilot_usage/models.py b/src/copilot_usage/models.py index 2e14ee8..a8d84ed 100644 --- a/src/copilot_usage/models.py +++ b/src/copilot_usage/models.py @@ -124,7 +124,7 @@ class AssistantMessageData(BaseModel): reasoningText: str | None = None reasoningOpaque: str | None = None toolRequests: list[dict[str, object]] = Field( - default_factory=lambda: list[dict[str, object]]() + default_factory=list[dict[str, object]] ) diff --git a/src/copilot_usage/report.py b/src/copilot_usage/report.py index 0d55787..2ca8c64 100644 --- a/src/copilot_usage/report.py +++ b/src/copilot_usage/report.py @@ -111,6 +111,18 @@ def _estimated_output_tokens(session: SessionSummary) -> int: return sum(m.usage.outputTokens for m in session.model_metrics.values()) +def _format_session_running_time(session: SessionSummary) -> str: + """Return a human-readable running time for *session*. + + Returns ``"—"`` when the session has no ``start_time``. Otherwise + delegates to :func:`_format_elapsed_since`, preferring + ``last_resume_time`` over ``start_time``. + """ + if not session.start_time: + return "—" + return _format_elapsed_since(session.last_resume_time or session.start_time) + + def render_live_sessions(sessions: list[SessionSummary]) -> None: """Render overview of active sessions only. @@ -144,11 +156,7 @@ def render_live_sessions(sessions: list[SessionSummary]) -> None: short_id = s.session_id[:8] if s.session_id else "—" name = s.name or "—" model = s.model or "—" - running = ( - _format_elapsed_since(s.last_resume_time or s.start_time) - if s.start_time - else "—" - ) + running = _format_session_running_time(s) messages = str(s.user_messages) tokens = f"{_estimated_output_tokens(s):,}" cwd = s.cwd or "—" @@ -580,10 +588,9 @@ def _render_totals(console: Console, sessions: list[SessionSummary]) -> None: total_duration = sum(s.total_api_duration_ms for s in sessions) total_sessions = len(sessions) - total_output = 0 - for s in sessions: - for mm in s.model_metrics.values(): - total_output += mm.usage.outputTokens + total_output = sum( + mm.usage.outputTokens for s in sessions for mm in s.model_metrics.values() + ) pr_label = "premium request" if total_premium == 1 else "premium requests" session_label = "session" if total_sessions == 1 else "sessions" @@ -788,11 +795,7 @@ def _render_active_section( for s in active: name = s.name or s.session_id[:12] model = s.model or "—" - running = ( - _format_elapsed_since(s.last_resume_time or s.start_time) - if s.start_time - else "—" - ) + running = _format_session_running_time(s) table.add_row( name, @@ -847,9 +850,9 @@ def render_cost_view( for premium and the active model calls / output tokens. """ console = target_console or Console() - sessions = _filter_sessions(sessions, since, until) + filtered = _filter_sessions(sessions, since, until) - if not sessions: + if not filtered: console.print("[yellow]No sessions found.[/yellow]") return @@ -866,7 +869,7 @@ def render_cost_view( grand_model_calls = 0 grand_output = 0 - for s in sessions: + for s in filtered: name = s.name or s.session_id[:12] model_calls_display = str(s.model_calls) diff --git a/tests/copilot_usage/test_report.py b/tests/copilot_usage/test_report.py index 7bfa26e..447932d 100644 --- a/tests/copilot_usage/test_report.py +++ b/tests/copilot_usage/test_report.py @@ -27,6 +27,7 @@ _filter_sessions, _format_detail_duration, _format_relative_time, + _format_session_running_time, format_duration, format_tokens, render_cost_view, @@ -1699,3 +1700,32 @@ def test_one_session_one_premium_request(self) -> None: # "session" appears without trailing 's' stripped = output.replace("sessions", "") assert "session" in stripped + + +# --------------------------------------------------------------------------- +# Issue #66 — _format_session_running_time helper +# --------------------------------------------------------------------------- + + +class TestFormatSessionRunningTime: + """Tests for the extracted _format_session_running_time helper.""" + + def test_returns_dash_when_no_start_time(self) -> None: + session = SessionSummary(session_id="no-start") + assert _format_session_running_time(session) == "—" + + def test_uses_start_time_when_no_resume(self) -> None: + start = datetime.now(tz=UTC) - timedelta(minutes=5, seconds=30) + session = SessionSummary(session_id="s1", start_time=start) + result = _format_session_running_time(session) + assert "5m" in result + + def test_uses_last_resume_time_over_start_time(self) -> None: + start = datetime.now(tz=UTC) - timedelta(hours=2) + resume = datetime.now(tz=UTC) - timedelta(minutes=3) + session = SessionSummary( + session_id="s2", start_time=start, last_resume_time=resume + ) + result = _format_session_running_time(session) + assert "3m" in result + assert "2h" not in result