htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/.htmlgraph/agents.json +72 -0
- htmlgraph/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/__init__.py +51 -1
- htmlgraph/__init__.pyi +123 -0
- htmlgraph/agent_detection.py +26 -10
- htmlgraph/agent_registry.py +2 -1
- htmlgraph/analytics/__init__.py +8 -1
- htmlgraph/analytics/cli.py +86 -20
- htmlgraph/analytics/cost_analyzer.py +391 -0
- htmlgraph/analytics/cost_monitor.py +664 -0
- htmlgraph/analytics/cost_reporter.py +675 -0
- htmlgraph/analytics/cross_session.py +617 -0
- htmlgraph/analytics/dependency.py +10 -6
- htmlgraph/analytics/pattern_learning.py +771 -0
- htmlgraph/analytics/session_graph.py +707 -0
- htmlgraph/analytics/strategic/__init__.py +80 -0
- htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
- htmlgraph/analytics/strategic/pattern_detector.py +876 -0
- htmlgraph/analytics/strategic/preference_manager.py +709 -0
- htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
- htmlgraph/analytics/work_type.py +67 -27
- htmlgraph/analytics_index.py +53 -20
- htmlgraph/api/__init__.py +3 -0
- htmlgraph/api/cost_alerts_websocket.py +416 -0
- htmlgraph/api/main.py +2498 -0
- htmlgraph/api/static/htmx.min.js +1 -0
- htmlgraph/api/static/style-redesign.css +1344 -0
- htmlgraph/api/static/style.css +1079 -0
- htmlgraph/api/templates/dashboard-redesign.html +1366 -0
- htmlgraph/api/templates/dashboard.html +794 -0
- htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
- htmlgraph/api/templates/partials/activity-feed.html +1100 -0
- htmlgraph/api/templates/partials/agents-redesign.html +317 -0
- htmlgraph/api/templates/partials/agents.html +317 -0
- htmlgraph/api/templates/partials/event-traces.html +373 -0
- htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
- htmlgraph/api/templates/partials/features.html +578 -0
- htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
- htmlgraph/api/templates/partials/metrics.html +346 -0
- htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
- htmlgraph/api/templates/partials/orchestration.html +198 -0
- htmlgraph/api/templates/partials/spawners.html +375 -0
- htmlgraph/api/templates/partials/work-items.html +613 -0
- htmlgraph/api/websocket.py +538 -0
- htmlgraph/archive/__init__.py +24 -0
- htmlgraph/archive/bloom.py +234 -0
- htmlgraph/archive/fts.py +297 -0
- htmlgraph/archive/manager.py +583 -0
- htmlgraph/archive/search.py +244 -0
- htmlgraph/atomic_ops.py +560 -0
- htmlgraph/attribute_index.py +2 -1
- htmlgraph/bounded_paths.py +539 -0
- htmlgraph/builders/base.py +57 -2
- htmlgraph/builders/bug.py +19 -3
- htmlgraph/builders/chore.py +19 -3
- htmlgraph/builders/epic.py +19 -3
- htmlgraph/builders/feature.py +27 -3
- htmlgraph/builders/insight.py +2 -1
- htmlgraph/builders/metric.py +2 -1
- htmlgraph/builders/pattern.py +2 -1
- htmlgraph/builders/phase.py +19 -3
- htmlgraph/builders/spike.py +29 -3
- htmlgraph/builders/track.py +42 -1
- htmlgraph/cigs/__init__.py +81 -0
- htmlgraph/cigs/autonomy.py +385 -0
- htmlgraph/cigs/cost.py +475 -0
- htmlgraph/cigs/messages_basic.py +472 -0
- htmlgraph/cigs/messaging.py +365 -0
- htmlgraph/cigs/models.py +771 -0
- htmlgraph/cigs/pattern_storage.py +427 -0
- htmlgraph/cigs/patterns.py +503 -0
- htmlgraph/cigs/posttool_analyzer.py +234 -0
- htmlgraph/cigs/reporter.py +818 -0
- htmlgraph/cigs/tracker.py +317 -0
- htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/cli/.htmlgraph/agents.json +72 -0
- htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
- htmlgraph/cli/__init__.py +42 -0
- htmlgraph/cli/__main__.py +6 -0
- htmlgraph/cli/analytics.py +1424 -0
- htmlgraph/cli/base.py +685 -0
- htmlgraph/cli/constants.py +206 -0
- htmlgraph/cli/core.py +954 -0
- htmlgraph/cli/main.py +147 -0
- htmlgraph/cli/models.py +475 -0
- htmlgraph/cli/templates/__init__.py +1 -0
- htmlgraph/cli/templates/cost_dashboard.py +399 -0
- htmlgraph/cli/work/__init__.py +239 -0
- htmlgraph/cli/work/browse.py +115 -0
- htmlgraph/cli/work/features.py +568 -0
- htmlgraph/cli/work/orchestration.py +676 -0
- htmlgraph/cli/work/report.py +728 -0
- htmlgraph/cli/work/sessions.py +466 -0
- htmlgraph/cli/work/snapshot.py +559 -0
- htmlgraph/cli/work/tracks.py +486 -0
- htmlgraph/cli_commands/__init__.py +1 -0
- htmlgraph/cli_commands/feature.py +195 -0
- htmlgraph/cli_framework.py +115 -0
- htmlgraph/collections/__init__.py +2 -0
- htmlgraph/collections/base.py +197 -14
- htmlgraph/collections/bug.py +2 -1
- htmlgraph/collections/chore.py +2 -1
- htmlgraph/collections/epic.py +2 -1
- htmlgraph/collections/feature.py +2 -1
- htmlgraph/collections/insight.py +2 -1
- htmlgraph/collections/metric.py +2 -1
- htmlgraph/collections/pattern.py +2 -1
- htmlgraph/collections/phase.py +2 -1
- htmlgraph/collections/session.py +194 -0
- htmlgraph/collections/spike.py +13 -2
- htmlgraph/collections/task_delegation.py +241 -0
- htmlgraph/collections/todo.py +14 -1
- htmlgraph/collections/traces.py +487 -0
- htmlgraph/config/cost_models.json +56 -0
- htmlgraph/config.py +190 -0
- htmlgraph/context_analytics.py +2 -1
- htmlgraph/converter.py +116 -7
- htmlgraph/cost_analysis/__init__.py +5 -0
- htmlgraph/cost_analysis/analyzer.py +438 -0
- htmlgraph/dashboard.html +2246 -248
- htmlgraph/dashboard.html.backup +6592 -0
- htmlgraph/dashboard.html.bak +7181 -0
- htmlgraph/dashboard.html.bak2 +7231 -0
- htmlgraph/dashboard.html.bak3 +7232 -0
- htmlgraph/db/__init__.py +38 -0
- htmlgraph/db/queries.py +790 -0
- htmlgraph/db/schema.py +1788 -0
- htmlgraph/decorators.py +317 -0
- htmlgraph/dependency_models.py +2 -1
- htmlgraph/deploy.py +26 -27
- htmlgraph/docs/API_REFERENCE.md +841 -0
- htmlgraph/docs/HTTP_API.md +750 -0
- htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
- htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
- htmlgraph/docs/README.md +532 -0
- htmlgraph/docs/__init__.py +77 -0
- htmlgraph/docs/docs_version.py +55 -0
- htmlgraph/docs/metadata.py +93 -0
- htmlgraph/docs/migrations.py +232 -0
- htmlgraph/docs/template_engine.py +143 -0
- htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
- htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
- htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
- htmlgraph/docs/templates/base_agents.md.j2 +78 -0
- htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
- htmlgraph/docs/version_check.py +163 -0
- htmlgraph/edge_index.py +2 -1
- htmlgraph/error_handler.py +544 -0
- htmlgraph/event_log.py +86 -37
- htmlgraph/event_migration.py +2 -1
- htmlgraph/file_watcher.py +12 -8
- htmlgraph/find_api.py +2 -1
- htmlgraph/git_events.py +67 -9
- htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
- htmlgraph/hooks/.htmlgraph/agents.json +72 -0
- htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
- htmlgraph/hooks/__init__.py +8 -0
- htmlgraph/hooks/bootstrap.py +169 -0
- htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
- htmlgraph/hooks/concurrent_sessions.py +208 -0
- htmlgraph/hooks/context.py +350 -0
- htmlgraph/hooks/drift_handler.py +525 -0
- htmlgraph/hooks/event_tracker.py +790 -99
- htmlgraph/hooks/git_commands.py +175 -0
- htmlgraph/hooks/installer.py +5 -1
- htmlgraph/hooks/orchestrator.py +327 -76
- htmlgraph/hooks/orchestrator_reflector.py +31 -4
- htmlgraph/hooks/post_tool_use_failure.py +32 -7
- htmlgraph/hooks/post_tool_use_handler.py +257 -0
- htmlgraph/hooks/posttooluse.py +92 -19
- htmlgraph/hooks/pretooluse.py +527 -7
- htmlgraph/hooks/prompt_analyzer.py +637 -0
- htmlgraph/hooks/session_handler.py +668 -0
- htmlgraph/hooks/session_summary.py +395 -0
- htmlgraph/hooks/state_manager.py +504 -0
- htmlgraph/hooks/subagent_detection.py +202 -0
- htmlgraph/hooks/subagent_stop.py +369 -0
- htmlgraph/hooks/task_enforcer.py +99 -4
- htmlgraph/hooks/validator.py +212 -91
- htmlgraph/ids.py +2 -1
- htmlgraph/learning.py +125 -100
- htmlgraph/mcp_server.py +2 -1
- htmlgraph/models.py +217 -18
- htmlgraph/operations/README.md +62 -0
- htmlgraph/operations/__init__.py +79 -0
- htmlgraph/operations/analytics.py +339 -0
- htmlgraph/operations/bootstrap.py +289 -0
- htmlgraph/operations/events.py +244 -0
- htmlgraph/operations/fastapi_server.py +231 -0
- htmlgraph/operations/hooks.py +350 -0
- htmlgraph/operations/initialization.py +597 -0
- htmlgraph/operations/initialization.py.backup +228 -0
- htmlgraph/operations/server.py +303 -0
- htmlgraph/orchestration/__init__.py +58 -0
- htmlgraph/orchestration/claude_launcher.py +179 -0
- htmlgraph/orchestration/command_builder.py +72 -0
- htmlgraph/orchestration/headless_spawner.py +281 -0
- htmlgraph/orchestration/live_events.py +377 -0
- htmlgraph/orchestration/model_selection.py +327 -0
- htmlgraph/orchestration/plugin_manager.py +140 -0
- htmlgraph/orchestration/prompts.py +137 -0
- htmlgraph/orchestration/spawner_event_tracker.py +383 -0
- htmlgraph/orchestration/spawners/__init__.py +16 -0
- htmlgraph/orchestration/spawners/base.py +194 -0
- htmlgraph/orchestration/spawners/claude.py +173 -0
- htmlgraph/orchestration/spawners/codex.py +435 -0
- htmlgraph/orchestration/spawners/copilot.py +294 -0
- htmlgraph/orchestration/spawners/gemini.py +471 -0
- htmlgraph/orchestration/subprocess_runner.py +36 -0
- htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
- htmlgraph/orchestration.md +563 -0
- htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
- htmlgraph/orchestrator.py +2 -1
- htmlgraph/orchestrator_config.py +357 -0
- htmlgraph/orchestrator_mode.py +115 -4
- htmlgraph/parallel.py +2 -1
- htmlgraph/parser.py +86 -6
- htmlgraph/path_query.py +608 -0
- htmlgraph/pattern_matcher.py +636 -0
- htmlgraph/pydantic_models.py +476 -0
- htmlgraph/quality_gates.py +350 -0
- htmlgraph/query_builder.py +2 -1
- htmlgraph/query_composer.py +509 -0
- htmlgraph/reflection.py +443 -0
- htmlgraph/refs.py +344 -0
- htmlgraph/repo_hash.py +512 -0
- htmlgraph/repositories/__init__.py +292 -0
- htmlgraph/repositories/analytics_repository.py +455 -0
- htmlgraph/repositories/analytics_repository_standard.py +628 -0
- htmlgraph/repositories/feature_repository.py +581 -0
- htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
- htmlgraph/repositories/feature_repository_memory.py +607 -0
- htmlgraph/repositories/feature_repository_sqlite.py +858 -0
- htmlgraph/repositories/filter_service.py +620 -0
- htmlgraph/repositories/filter_service_standard.py +445 -0
- htmlgraph/repositories/shared_cache.py +621 -0
- htmlgraph/repositories/shared_cache_memory.py +395 -0
- htmlgraph/repositories/track_repository.py +552 -0
- htmlgraph/repositories/track_repository_htmlfile.py +619 -0
- htmlgraph/repositories/track_repository_memory.py +508 -0
- htmlgraph/repositories/track_repository_sqlite.py +711 -0
- htmlgraph/sdk/__init__.py +398 -0
- htmlgraph/sdk/__init__.pyi +14 -0
- htmlgraph/sdk/analytics/__init__.py +19 -0
- htmlgraph/sdk/analytics/engine.py +155 -0
- htmlgraph/sdk/analytics/helpers.py +178 -0
- htmlgraph/sdk/analytics/registry.py +109 -0
- htmlgraph/sdk/base.py +484 -0
- htmlgraph/sdk/constants.py +216 -0
- htmlgraph/sdk/core.pyi +308 -0
- htmlgraph/sdk/discovery.py +120 -0
- htmlgraph/sdk/help/__init__.py +12 -0
- htmlgraph/sdk/help/mixin.py +699 -0
- htmlgraph/sdk/mixins/__init__.py +15 -0
- htmlgraph/sdk/mixins/attribution.py +113 -0
- htmlgraph/sdk/mixins/mixin.py +410 -0
- htmlgraph/sdk/operations/__init__.py +12 -0
- htmlgraph/sdk/operations/mixin.py +427 -0
- htmlgraph/sdk/orchestration/__init__.py +17 -0
- htmlgraph/sdk/orchestration/coordinator.py +203 -0
- htmlgraph/sdk/orchestration/spawner.py +204 -0
- htmlgraph/sdk/planning/__init__.py +19 -0
- htmlgraph/sdk/planning/bottlenecks.py +93 -0
- htmlgraph/sdk/planning/mixin.py +211 -0
- htmlgraph/sdk/planning/parallel.py +186 -0
- htmlgraph/sdk/planning/queue.py +210 -0
- htmlgraph/sdk/planning/recommendations.py +87 -0
- htmlgraph/sdk/planning/smart_planning.py +319 -0
- htmlgraph/sdk/session/__init__.py +19 -0
- htmlgraph/sdk/session/continuity.py +57 -0
- htmlgraph/sdk/session/handoff.py +110 -0
- htmlgraph/sdk/session/info.py +309 -0
- htmlgraph/sdk/session/manager.py +103 -0
- htmlgraph/sdk/strategic/__init__.py +26 -0
- htmlgraph/sdk/strategic/mixin.py +563 -0
- htmlgraph/server.py +295 -107
- htmlgraph/session_hooks.py +300 -0
- htmlgraph/session_manager.py +285 -3
- htmlgraph/session_registry.py +587 -0
- htmlgraph/session_state.py +436 -0
- htmlgraph/session_warning.py +2 -1
- htmlgraph/sessions/__init__.py +23 -0
- htmlgraph/sessions/handoff.py +756 -0
- htmlgraph/system_prompts.py +450 -0
- htmlgraph/templates/orchestration-view.html +350 -0
- htmlgraph/track_builder.py +33 -1
- htmlgraph/track_manager.py +38 -0
- htmlgraph/transcript.py +18 -5
- htmlgraph/validation.py +115 -0
- htmlgraph/watch.py +2 -1
- htmlgraph/work_type_utils.py +2 -1
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
- htmlgraph-0.27.5.dist-info/RECORD +337 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
- htmlgraph/cli.py +0 -4839
- htmlgraph/sdk.py +0 -2359
- htmlgraph-0.20.1.dist-info/RECORD +0 -118
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
- {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
- {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""HtmlGraph CLI - Session report commands.
|
|
4
|
+
|
|
5
|
+
Commands for generating "What Did Claude Do?" reports:
|
|
6
|
+
- report: Show chronological timeline of tool calls in a session
|
|
7
|
+
|
|
8
|
+
THE killer feature that differentiates HtmlGraph - complete observability
|
|
9
|
+
of AI agent activities with cost attribution and tool usage analysis.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import sqlite3
|
|
15
|
+
from datetime import datetime, timedelta
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
from htmlgraph.cli.base import BaseCommand, CommandError, CommandResult
|
|
22
|
+
from htmlgraph.cli.constants import DEFAULT_GRAPH_DIR
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from argparse import _SubParsersAction
|
|
26
|
+
|
|
27
|
+
console = Console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def register_report_commands(subparsers: _SubParsersAction) -> None:
|
|
31
|
+
"""Register report commands."""
|
|
32
|
+
report_parser = subparsers.add_parser(
|
|
33
|
+
"report", help="Generate 'What Did Claude Do?' session report"
|
|
34
|
+
)
|
|
35
|
+
report_parser.add_argument(
|
|
36
|
+
"--session",
|
|
37
|
+
help="Session ID (or 'latest', 'today')",
|
|
38
|
+
default="latest",
|
|
39
|
+
)
|
|
40
|
+
report_parser.add_argument(
|
|
41
|
+
"--report-format",
|
|
42
|
+
choices=["terminal", "html", "markdown"],
|
|
43
|
+
default="terminal",
|
|
44
|
+
help="Report output format (terminal=rich formatting, html=self-contained HTML, markdown=markdown file)",
|
|
45
|
+
)
|
|
46
|
+
report_parser.add_argument(
|
|
47
|
+
"--detail",
|
|
48
|
+
choices=["basic", "full"],
|
|
49
|
+
default="basic",
|
|
50
|
+
help="Detail level (basic=summary, full=inputs/outputs)",
|
|
51
|
+
)
|
|
52
|
+
report_parser.add_argument(
|
|
53
|
+
"--output", "-o", help="Output file path (for html/markdown formats)"
|
|
54
|
+
)
|
|
55
|
+
report_parser.add_argument(
|
|
56
|
+
"--graph-dir", "-g", default=DEFAULT_GRAPH_DIR, help="Graph directory"
|
|
57
|
+
)
|
|
58
|
+
report_parser.set_defaults(func=SessionReportCommand.from_args)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SessionReportCommand(BaseCommand):
|
|
62
|
+
"""Generate 'What Did Claude Do?' session report."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
session: str,
|
|
68
|
+
report_format: str,
|
|
69
|
+
detail: str,
|
|
70
|
+
output: str | None,
|
|
71
|
+
) -> None:
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.session = session
|
|
74
|
+
self.report_format = report_format
|
|
75
|
+
self.detail = detail
|
|
76
|
+
self.output = output
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_args(cls, args: argparse.Namespace) -> SessionReportCommand:
|
|
80
|
+
return cls(
|
|
81
|
+
session=getattr(args, "session", "latest"),
|
|
82
|
+
report_format=getattr(args, "report_format", "terminal"),
|
|
83
|
+
detail=getattr(args, "detail", "basic"),
|
|
84
|
+
output=getattr(args, "output", None),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def execute(self) -> CommandResult:
|
|
88
|
+
"""Generate and display session report."""
|
|
89
|
+
if not self.graph_dir:
|
|
90
|
+
raise CommandError("Graph directory not specified")
|
|
91
|
+
|
|
92
|
+
graph_dir = Path(self.graph_dir)
|
|
93
|
+
db_path = graph_dir / "htmlgraph.db"
|
|
94
|
+
|
|
95
|
+
if not db_path.exists():
|
|
96
|
+
console.print(
|
|
97
|
+
f"[yellow]No database found at {db_path}[/yellow]\n"
|
|
98
|
+
"Run some work to generate reports!"
|
|
99
|
+
)
|
|
100
|
+
raise CommandError("No database found", exit_code=1)
|
|
101
|
+
|
|
102
|
+
# Resolve session ID
|
|
103
|
+
conn = sqlite3.connect(str(db_path))
|
|
104
|
+
conn.row_factory = sqlite3.Row
|
|
105
|
+
try:
|
|
106
|
+
session_id = self._resolve_session_id(conn, self.session)
|
|
107
|
+
if not session_id:
|
|
108
|
+
console.print(f"[red]Session not found: {self.session}[/red]")
|
|
109
|
+
raise CommandError("Session not found", exit_code=1)
|
|
110
|
+
|
|
111
|
+
# Get session data
|
|
112
|
+
session_data = self._get_session_data(conn, session_id)
|
|
113
|
+
events = self._get_session_events(conn, session_id)
|
|
114
|
+
|
|
115
|
+
if not events:
|
|
116
|
+
console.print(
|
|
117
|
+
f"[yellow]No events found for session {session_id}[/yellow]"
|
|
118
|
+
)
|
|
119
|
+
# Still return success, just no events to report
|
|
120
|
+
return CommandResult(text="")
|
|
121
|
+
|
|
122
|
+
# Generate report in requested format
|
|
123
|
+
if self.report_format == "terminal":
|
|
124
|
+
self._render_terminal_report(session_data, events)
|
|
125
|
+
elif self.report_format == "html":
|
|
126
|
+
self._render_html_report(session_data, events)
|
|
127
|
+
elif self.report_format == "markdown":
|
|
128
|
+
self._render_markdown_report(session_data, events)
|
|
129
|
+
|
|
130
|
+
# Return empty result to prevent default formatter output
|
|
131
|
+
# (report commands handle their own output)
|
|
132
|
+
return CommandResult(text="")
|
|
133
|
+
|
|
134
|
+
finally:
|
|
135
|
+
conn.close()
|
|
136
|
+
|
|
137
|
+
def _resolve_session_id(self, conn: sqlite3.Connection, session: str) -> str | None:
|
|
138
|
+
"""Resolve session identifier to actual session_id."""
|
|
139
|
+
cursor = conn.cursor()
|
|
140
|
+
|
|
141
|
+
if session == "latest":
|
|
142
|
+
# Get most recent session
|
|
143
|
+
cursor.execute(
|
|
144
|
+
"""
|
|
145
|
+
SELECT session_id FROM sessions
|
|
146
|
+
ORDER BY created_at DESC
|
|
147
|
+
LIMIT 1
|
|
148
|
+
"""
|
|
149
|
+
)
|
|
150
|
+
row = cursor.fetchone()
|
|
151
|
+
return row[0] if row else None
|
|
152
|
+
|
|
153
|
+
elif session == "today":
|
|
154
|
+
# Get all sessions from today and combine them
|
|
155
|
+
# For now, just return the most recent today
|
|
156
|
+
today_start = (
|
|
157
|
+
datetime.now()
|
|
158
|
+
.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
159
|
+
.isoformat()
|
|
160
|
+
)
|
|
161
|
+
cursor.execute(
|
|
162
|
+
"""
|
|
163
|
+
SELECT session_id FROM sessions
|
|
164
|
+
WHERE created_at >= ?
|
|
165
|
+
ORDER BY created_at DESC
|
|
166
|
+
LIMIT 1
|
|
167
|
+
""",
|
|
168
|
+
(today_start,),
|
|
169
|
+
)
|
|
170
|
+
row = cursor.fetchone()
|
|
171
|
+
return row[0] if row else None
|
|
172
|
+
|
|
173
|
+
else:
|
|
174
|
+
# Assume it's a session ID (or partial match)
|
|
175
|
+
cursor.execute(
|
|
176
|
+
"""
|
|
177
|
+
SELECT session_id FROM sessions
|
|
178
|
+
WHERE session_id LIKE ?
|
|
179
|
+
LIMIT 1
|
|
180
|
+
""",
|
|
181
|
+
(f"%{session}%",),
|
|
182
|
+
)
|
|
183
|
+
row = cursor.fetchone()
|
|
184
|
+
return row[0] if row else None
|
|
185
|
+
|
|
186
|
+
def _get_session_data(
|
|
187
|
+
self, conn: sqlite3.Connection, session_id: str
|
|
188
|
+
) -> dict[str, Any]:
|
|
189
|
+
"""Get session metadata."""
|
|
190
|
+
cursor = conn.cursor()
|
|
191
|
+
cursor.execute(
|
|
192
|
+
"""
|
|
193
|
+
SELECT
|
|
194
|
+
session_id,
|
|
195
|
+
agent_assigned,
|
|
196
|
+
created_at,
|
|
197
|
+
completed_at,
|
|
198
|
+
total_events,
|
|
199
|
+
total_tokens_used,
|
|
200
|
+
status
|
|
201
|
+
FROM sessions
|
|
202
|
+
WHERE session_id = ?
|
|
203
|
+
""",
|
|
204
|
+
(session_id,),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
row = cursor.fetchone()
|
|
208
|
+
if not row:
|
|
209
|
+
return {}
|
|
210
|
+
|
|
211
|
+
data = dict(row)
|
|
212
|
+
|
|
213
|
+
# Calculate duration (handle both timezone-aware and naive datetimes)
|
|
214
|
+
if data.get("created_at") and data.get("completed_at"):
|
|
215
|
+
start = datetime.fromisoformat(data["created_at"])
|
|
216
|
+
end = datetime.fromisoformat(data["completed_at"])
|
|
217
|
+
# Remove timezone info if present to avoid comparison issues
|
|
218
|
+
if start.tzinfo is not None:
|
|
219
|
+
start = start.replace(tzinfo=None)
|
|
220
|
+
if end.tzinfo is not None:
|
|
221
|
+
end = end.replace(tzinfo=None)
|
|
222
|
+
duration = end - start
|
|
223
|
+
elif data.get("created_at"):
|
|
224
|
+
start = datetime.fromisoformat(data["created_at"])
|
|
225
|
+
# Remove timezone info if present
|
|
226
|
+
if start.tzinfo is not None:
|
|
227
|
+
start = start.replace(tzinfo=None)
|
|
228
|
+
duration = datetime.now() - start
|
|
229
|
+
else:
|
|
230
|
+
duration = timedelta(0)
|
|
231
|
+
|
|
232
|
+
data["duration"] = duration
|
|
233
|
+
return data
|
|
234
|
+
|
|
235
|
+
def _get_session_events(
|
|
236
|
+
self, conn: sqlite3.Connection, session_id: str
|
|
237
|
+
) -> list[dict[str, Any]]:
|
|
238
|
+
"""Get all tool_call events for a session in chronological order."""
|
|
239
|
+
cursor = conn.cursor()
|
|
240
|
+
cursor.execute(
|
|
241
|
+
"""
|
|
242
|
+
SELECT
|
|
243
|
+
event_id,
|
|
244
|
+
tool_name,
|
|
245
|
+
timestamp,
|
|
246
|
+
input_summary,
|
|
247
|
+
output_summary,
|
|
248
|
+
status,
|
|
249
|
+
parent_event_id,
|
|
250
|
+
cost_tokens,
|
|
251
|
+
execution_duration_seconds,
|
|
252
|
+
subagent_type
|
|
253
|
+
FROM agent_events
|
|
254
|
+
WHERE session_id = ? AND event_type = 'tool_call'
|
|
255
|
+
ORDER BY timestamp ASC
|
|
256
|
+
""",
|
|
257
|
+
(session_id,),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
events = []
|
|
261
|
+
for row in cursor.fetchall():
|
|
262
|
+
event = dict(row)
|
|
263
|
+
# Parse timestamp
|
|
264
|
+
if event.get("timestamp"):
|
|
265
|
+
if isinstance(event["timestamp"], str):
|
|
266
|
+
event["timestamp"] = datetime.fromisoformat(event["timestamp"])
|
|
267
|
+
events.append(event)
|
|
268
|
+
|
|
269
|
+
return events
|
|
270
|
+
|
|
271
|
+
def _render_terminal_report(
|
|
272
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
273
|
+
) -> None:
|
|
274
|
+
"""Render report to terminal using Rich."""
|
|
275
|
+
# Header
|
|
276
|
+
session_id = session_data.get("session_id", "unknown")
|
|
277
|
+
agent = session_data.get("agent_assigned", "unknown")
|
|
278
|
+
duration = session_data.get("duration", timedelta(0))
|
|
279
|
+
total_tokens = session_data.get("total_tokens_used", 0)
|
|
280
|
+
|
|
281
|
+
# Calculate estimated cost (rough approximation)
|
|
282
|
+
# Average: $4.50 per 1M tokens
|
|
283
|
+
est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
|
|
284
|
+
|
|
285
|
+
# Format duration
|
|
286
|
+
duration_mins = int(duration.total_seconds() / 60)
|
|
287
|
+
duration_str = f"{duration_mins} minutes" if duration_mins > 0 else "< 1 minute"
|
|
288
|
+
|
|
289
|
+
console.print(f"\n[bold cyan]Session Report: {session_id[:16]}...[/bold cyan]")
|
|
290
|
+
console.print(
|
|
291
|
+
f"[dim]Agent: {agent} | Duration: {duration_str} | "
|
|
292
|
+
f"Tokens: {total_tokens:,} | Est. Cost: ${est_cost:.2f}[/dim]\n"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Timeline
|
|
296
|
+
console.print("[bold]TIMELINE:[/bold]")
|
|
297
|
+
console.print("─" * 80)
|
|
298
|
+
|
|
299
|
+
prev_timestamp = None
|
|
300
|
+
for i, event in enumerate(events, 1):
|
|
301
|
+
timestamp = event.get("timestamp")
|
|
302
|
+
tool_name = event.get("tool_name", "unknown")
|
|
303
|
+
status = event.get("status", "")
|
|
304
|
+
subagent = event.get("subagent_type")
|
|
305
|
+
|
|
306
|
+
# Format timestamp
|
|
307
|
+
time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
|
|
308
|
+
|
|
309
|
+
# Format status indicator
|
|
310
|
+
if status == "completed" or not status:
|
|
311
|
+
status_icon = "✓"
|
|
312
|
+
status_color = "green"
|
|
313
|
+
elif status == "failed":
|
|
314
|
+
status_icon = "✗"
|
|
315
|
+
status_color = "red"
|
|
316
|
+
else:
|
|
317
|
+
status_icon = "○"
|
|
318
|
+
status_color = "yellow"
|
|
319
|
+
|
|
320
|
+
# Calculate time since last event (thinking time)
|
|
321
|
+
think_time = ""
|
|
322
|
+
if prev_timestamp and timestamp:
|
|
323
|
+
delta = (timestamp - prev_timestamp).total_seconds()
|
|
324
|
+
if delta > 5: # Only show if > 5 seconds
|
|
325
|
+
think_time = f" [dim](+{int(delta)}s)[/dim]"
|
|
326
|
+
|
|
327
|
+
prev_timestamp = timestamp
|
|
328
|
+
|
|
329
|
+
# Format tool name with subagent context
|
|
330
|
+
tool_display = tool_name
|
|
331
|
+
if subagent:
|
|
332
|
+
tool_display = f"{tool_name} [dim]({subagent})[/dim]"
|
|
333
|
+
|
|
334
|
+
# Get input/output summaries (sanitized)
|
|
335
|
+
input_summary = self._sanitize_summary(event.get("input_summary", ""))
|
|
336
|
+
output_summary = self._sanitize_summary(event.get("output_summary", ""))
|
|
337
|
+
|
|
338
|
+
# Basic display
|
|
339
|
+
console.print(
|
|
340
|
+
f"{time_str} [{status_color}]{status_icon}[/{status_color}] "
|
|
341
|
+
f"[cyan]{tool_display}[/cyan]{think_time}"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Full detail mode
|
|
345
|
+
if self.detail == "full":
|
|
346
|
+
if input_summary:
|
|
347
|
+
console.print(f" [dim]→ {input_summary[:80]}[/dim]")
|
|
348
|
+
if output_summary:
|
|
349
|
+
console.print(f" [dim]← {output_summary[:80]}[/dim]")
|
|
350
|
+
|
|
351
|
+
console.print("─" * 80)
|
|
352
|
+
|
|
353
|
+
# Summary statistics
|
|
354
|
+
self._render_summary_stats(session_data, events)
|
|
355
|
+
|
|
356
|
+
def _render_summary_stats(
|
|
357
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Render summary statistics."""
|
|
360
|
+
console.print("\n[bold]SUMMARY:[/bold]")
|
|
361
|
+
|
|
362
|
+
# Tool usage counts
|
|
363
|
+
tool_counts: dict[str, int] = {}
|
|
364
|
+
total_cost = 0
|
|
365
|
+
for event in events:
|
|
366
|
+
tool = event.get("tool_name", "unknown")
|
|
367
|
+
tool_counts[tool] = tool_counts.get(tool, 0) + 1
|
|
368
|
+
total_cost += event.get("cost_tokens", 0)
|
|
369
|
+
|
|
370
|
+
# Sort by count
|
|
371
|
+
sorted_tools = sorted(tool_counts.items(), key=lambda x: x[1], reverse=True)
|
|
372
|
+
|
|
373
|
+
console.print(
|
|
374
|
+
f"Tools used: {', '.join(f'{tool}({count})' for tool, count in sorted_tools[:5])}"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Files touched (extract from input summaries)
|
|
378
|
+
files_touched = self._extract_files_from_events(events)
|
|
379
|
+
if files_touched:
|
|
380
|
+
console.print(f"Files touched: {', '.join(list(files_touched)[:5])}")
|
|
381
|
+
if len(files_touched) > 5:
|
|
382
|
+
console.print(f" [dim](+{len(files_touched) - 5} more)[/dim]")
|
|
383
|
+
|
|
384
|
+
# Tests run (look for Bash events with pytest/test)
|
|
385
|
+
test_events = [
|
|
386
|
+
e
|
|
387
|
+
for e in events
|
|
388
|
+
if e.get("tool_name") == "Bash"
|
|
389
|
+
and e.get("input_summary")
|
|
390
|
+
and ("pytest" in e["input_summary"] or "test" in e["input_summary"])
|
|
391
|
+
]
|
|
392
|
+
if test_events:
|
|
393
|
+
console.print(f"Tests run: {len(test_events)}")
|
|
394
|
+
|
|
395
|
+
# Cost breakdown
|
|
396
|
+
if total_cost > 0:
|
|
397
|
+
console.print(f"Total cost: {total_cost:,} tokens")
|
|
398
|
+
|
|
399
|
+
console.print()
|
|
400
|
+
|
|
401
|
+
def _extract_files_from_events(self, events: list[dict[str, Any]]) -> set[str]:
|
|
402
|
+
"""Extract file paths from event summaries."""
|
|
403
|
+
files = set()
|
|
404
|
+
for event in events:
|
|
405
|
+
tool = event.get("tool_name", "")
|
|
406
|
+
input_summary = event.get("input_summary", "")
|
|
407
|
+
|
|
408
|
+
# Read/Write/Edit events typically have file paths
|
|
409
|
+
if tool in ["Read", "Write", "Edit"] and input_summary:
|
|
410
|
+
# Extract file path (simple heuristic)
|
|
411
|
+
parts = input_summary.split()
|
|
412
|
+
for part in parts:
|
|
413
|
+
if "/" in part and len(part) > 3:
|
|
414
|
+
# Extract filename only
|
|
415
|
+
files.add(part.split("/")[-1])
|
|
416
|
+
if len(files) >= 10: # Limit collection
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
return files
|
|
420
|
+
|
|
421
|
+
def _sanitize_summary(self, summary: str | None) -> str:
|
|
422
|
+
"""Sanitize summary to remove secrets."""
|
|
423
|
+
if not summary:
|
|
424
|
+
return ""
|
|
425
|
+
|
|
426
|
+
# Simple sanitization: remove potential secrets
|
|
427
|
+
# (passwords, tokens, keys)
|
|
428
|
+
sensitive_patterns = [
|
|
429
|
+
"password",
|
|
430
|
+
"token",
|
|
431
|
+
"secret",
|
|
432
|
+
"key",
|
|
433
|
+
"api_key",
|
|
434
|
+
"auth",
|
|
435
|
+
]
|
|
436
|
+
|
|
437
|
+
for pattern in sensitive_patterns:
|
|
438
|
+
if pattern.lower() in summary.lower():
|
|
439
|
+
return "[REDACTED - contains sensitive data]"
|
|
440
|
+
|
|
441
|
+
return summary
|
|
442
|
+
|
|
443
|
+
def _render_html_report(
|
|
444
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
445
|
+
) -> None:
|
|
446
|
+
"""Render report to HTML file."""
|
|
447
|
+
html_content = self._generate_html(session_data, events)
|
|
448
|
+
|
|
449
|
+
# Determine output path
|
|
450
|
+
if self.output:
|
|
451
|
+
output_path = Path(self.output)
|
|
452
|
+
else:
|
|
453
|
+
output_path = Path(self.graph_dir or ".") / "session-report.html"
|
|
454
|
+
|
|
455
|
+
output_path.write_text(html_content)
|
|
456
|
+
console.print(f"[green]✓ HTML report saved to: {output_path}[/green]")
|
|
457
|
+
|
|
458
|
+
def _generate_html(
|
|
459
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
460
|
+
) -> str:
|
|
461
|
+
"""Generate self-contained HTML report."""
|
|
462
|
+
session_id = session_data.get("session_id", "unknown")
|
|
463
|
+
agent = session_data.get("agent_assigned", "unknown")
|
|
464
|
+
duration = session_data.get("duration", timedelta(0))
|
|
465
|
+
total_tokens = session_data.get("total_tokens_used", 0)
|
|
466
|
+
est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
|
|
467
|
+
|
|
468
|
+
duration_mins = int(duration.total_seconds() / 60)
|
|
469
|
+
|
|
470
|
+
# Build timeline HTML
|
|
471
|
+
timeline_html = ""
|
|
472
|
+
prev_timestamp = None
|
|
473
|
+
|
|
474
|
+
for event in events:
|
|
475
|
+
timestamp = event.get("timestamp")
|
|
476
|
+
tool_name = event.get("tool_name", "unknown")
|
|
477
|
+
status = event.get("status", "")
|
|
478
|
+
input_summary = self._sanitize_summary(event.get("input_summary", ""))
|
|
479
|
+
output_summary = self._sanitize_summary(event.get("output_summary", ""))
|
|
480
|
+
|
|
481
|
+
time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
|
|
482
|
+
|
|
483
|
+
# Status indicator
|
|
484
|
+
if status == "completed" or not status:
|
|
485
|
+
status_class = "success"
|
|
486
|
+
status_icon = "✓"
|
|
487
|
+
elif status == "failed":
|
|
488
|
+
status_class = "error"
|
|
489
|
+
status_icon = "✗"
|
|
490
|
+
else:
|
|
491
|
+
status_class = "pending"
|
|
492
|
+
status_icon = "○"
|
|
493
|
+
|
|
494
|
+
# Calculate thinking time
|
|
495
|
+
think_time = ""
|
|
496
|
+
if prev_timestamp and timestamp:
|
|
497
|
+
delta = (timestamp - prev_timestamp).total_seconds()
|
|
498
|
+
if delta > 5:
|
|
499
|
+
think_time = f'<span class="think-time">(+{int(delta)}s)</span>'
|
|
500
|
+
|
|
501
|
+
prev_timestamp = timestamp
|
|
502
|
+
|
|
503
|
+
# Build event row
|
|
504
|
+
timeline_html += f"""
|
|
505
|
+
<div class="event">
|
|
506
|
+
<span class="time">{time_str}</span>
|
|
507
|
+
<span class="status {status_class}">{status_icon}</span>
|
|
508
|
+
<span class="tool">{tool_name}</span>
|
|
509
|
+
{think_time}
|
|
510
|
+
"""
|
|
511
|
+
|
|
512
|
+
if self.detail == "full":
|
|
513
|
+
if input_summary:
|
|
514
|
+
timeline_html += (
|
|
515
|
+
f'<div class="detail input">→ {input_summary[:100]}</div>'
|
|
516
|
+
)
|
|
517
|
+
if output_summary:
|
|
518
|
+
timeline_html += (
|
|
519
|
+
f'<div class="detail output">← {output_summary[:100]}</div>'
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
timeline_html += "</div>\n"
|
|
523
|
+
|
|
524
|
+
# Complete HTML document
|
|
525
|
+
html = f"""<!DOCTYPE html>
|
|
526
|
+
<html>
|
|
527
|
+
<head>
|
|
528
|
+
<meta charset="UTF-8">
|
|
529
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
530
|
+
<title>Session Report: {session_id[:16]}</title>
|
|
531
|
+
<style>
|
|
532
|
+
body {{
|
|
533
|
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
534
|
+
max-width: 1200px;
|
|
535
|
+
margin: 0 auto;
|
|
536
|
+
padding: 20px;
|
|
537
|
+
background: #f5f5f5;
|
|
538
|
+
}}
|
|
539
|
+
.header {{
|
|
540
|
+
background: white;
|
|
541
|
+
padding: 20px;
|
|
542
|
+
border-radius: 8px;
|
|
543
|
+
margin-bottom: 20px;
|
|
544
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
545
|
+
}}
|
|
546
|
+
.header h1 {{
|
|
547
|
+
margin: 0 0 10px 0;
|
|
548
|
+
color: #333;
|
|
549
|
+
}}
|
|
550
|
+
.header .meta {{
|
|
551
|
+
color: #666;
|
|
552
|
+
font-size: 14px;
|
|
553
|
+
}}
|
|
554
|
+
.timeline {{
|
|
555
|
+
background: white;
|
|
556
|
+
padding: 20px;
|
|
557
|
+
border-radius: 8px;
|
|
558
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
559
|
+
}}
|
|
560
|
+
.event {{
|
|
561
|
+
padding: 10px;
|
|
562
|
+
border-bottom: 1px solid #eee;
|
|
563
|
+
font-family: 'Courier New', monospace;
|
|
564
|
+
font-size: 14px;
|
|
565
|
+
}}
|
|
566
|
+
.event:last-child {{
|
|
567
|
+
border-bottom: none;
|
|
568
|
+
}}
|
|
569
|
+
.time {{
|
|
570
|
+
color: #666;
|
|
571
|
+
margin-right: 10px;
|
|
572
|
+
}}
|
|
573
|
+
.status {{
|
|
574
|
+
margin-right: 10px;
|
|
575
|
+
font-weight: bold;
|
|
576
|
+
}}
|
|
577
|
+
.status.success {{
|
|
578
|
+
color: #28a745;
|
|
579
|
+
}}
|
|
580
|
+
.status.error {{
|
|
581
|
+
color: #dc3545;
|
|
582
|
+
}}
|
|
583
|
+
.status.pending {{
|
|
584
|
+
color: #ffc107;
|
|
585
|
+
}}
|
|
586
|
+
.tool {{
|
|
587
|
+
color: #007bff;
|
|
588
|
+
font-weight: bold;
|
|
589
|
+
}}
|
|
590
|
+
.think-time {{
|
|
591
|
+
color: #999;
|
|
592
|
+
margin-left: 10px;
|
|
593
|
+
}}
|
|
594
|
+
.detail {{
|
|
595
|
+
margin-left: 120px;
|
|
596
|
+
color: #666;
|
|
597
|
+
font-size: 12px;
|
|
598
|
+
margin-top: 5px;
|
|
599
|
+
}}
|
|
600
|
+
.detail.input {{
|
|
601
|
+
color: #666;
|
|
602
|
+
}}
|
|
603
|
+
.detail.output {{
|
|
604
|
+
color: #28a745;
|
|
605
|
+
}}
|
|
606
|
+
.summary {{
|
|
607
|
+
background: white;
|
|
608
|
+
padding: 20px;
|
|
609
|
+
border-radius: 8px;
|
|
610
|
+
margin-top: 20px;
|
|
611
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
612
|
+
}}
|
|
613
|
+
.summary h2 {{
|
|
614
|
+
margin-top: 0;
|
|
615
|
+
}}
|
|
616
|
+
</style>
|
|
617
|
+
</head>
|
|
618
|
+
<body>
|
|
619
|
+
<div class="header">
|
|
620
|
+
<h1>Session Report: {session_id[:16]}...</h1>
|
|
621
|
+
<div class="meta">
|
|
622
|
+
Agent: {agent} | Duration: {duration_mins} minutes |
|
|
623
|
+
Tokens: {total_tokens:,} | Est. Cost: ${est_cost:.2f}
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
|
|
627
|
+
<div class="timeline">
|
|
628
|
+
<h2>Timeline</h2>
|
|
629
|
+
{timeline_html}
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<div class="summary">
|
|
633
|
+
<h2>Summary</h2>
|
|
634
|
+
<p>Total events: {len(events)}</p>
|
|
635
|
+
<p>Generated by HtmlGraph - "HTML is All You Need"</p>
|
|
636
|
+
</div>
|
|
637
|
+
</body>
|
|
638
|
+
</html>"""
|
|
639
|
+
|
|
640
|
+
return html
|
|
641
|
+
|
|
642
|
+
def _render_markdown_report(
|
|
643
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
644
|
+
) -> None:
|
|
645
|
+
"""Render report to Markdown file."""
|
|
646
|
+
markdown_content = self._generate_markdown(session_data, events)
|
|
647
|
+
|
|
648
|
+
# Determine output path
|
|
649
|
+
if self.output:
|
|
650
|
+
output_path = Path(self.output)
|
|
651
|
+
else:
|
|
652
|
+
output_path = Path(self.graph_dir or ".") / "session-report.md"
|
|
653
|
+
|
|
654
|
+
output_path.write_text(markdown_content)
|
|
655
|
+
console.print(f"[green]✓ Markdown report saved to: {output_path}[/green]")
|
|
656
|
+
|
|
657
|
+
def _generate_markdown(
|
|
658
|
+
self, session_data: dict[str, Any], events: list[dict[str, Any]]
|
|
659
|
+
) -> str:
|
|
660
|
+
"""Generate Markdown report."""
|
|
661
|
+
session_id = session_data.get("session_id", "unknown")
|
|
662
|
+
agent = session_data.get("agent_assigned", "unknown")
|
|
663
|
+
duration = session_data.get("duration", timedelta(0))
|
|
664
|
+
total_tokens = session_data.get("total_tokens_used", 0)
|
|
665
|
+
est_cost = (total_tokens / 1_000_000) * 4.5 if total_tokens else 0
|
|
666
|
+
|
|
667
|
+
duration_mins = int(duration.total_seconds() / 60)
|
|
668
|
+
|
|
669
|
+
# Build timeline markdown
|
|
670
|
+
timeline_md = ""
|
|
671
|
+
prev_timestamp = None
|
|
672
|
+
|
|
673
|
+
for event in events:
|
|
674
|
+
timestamp = event.get("timestamp")
|
|
675
|
+
tool_name = event.get("tool_name", "unknown")
|
|
676
|
+
status = event.get("status", "")
|
|
677
|
+
input_summary = self._sanitize_summary(event.get("input_summary", ""))
|
|
678
|
+
|
|
679
|
+
time_str = timestamp.strftime("%H:%M:%S") if timestamp else "??:??:??"
|
|
680
|
+
|
|
681
|
+
# Status indicator
|
|
682
|
+
if status == "completed" or not status:
|
|
683
|
+
status_icon = "✓"
|
|
684
|
+
elif status == "failed":
|
|
685
|
+
status_icon = "✗"
|
|
686
|
+
else:
|
|
687
|
+
status_icon = "○"
|
|
688
|
+
|
|
689
|
+
# Calculate thinking time
|
|
690
|
+
think_time = ""
|
|
691
|
+
if prev_timestamp and timestamp:
|
|
692
|
+
delta = (timestamp - prev_timestamp).total_seconds()
|
|
693
|
+
if delta > 5:
|
|
694
|
+
think_time = f" *(+{int(delta)}s)*"
|
|
695
|
+
|
|
696
|
+
prev_timestamp = timestamp
|
|
697
|
+
|
|
698
|
+
timeline_md += f"**{time_str}** {status_icon} `{tool_name}`{think_time}\n"
|
|
699
|
+
|
|
700
|
+
if self.detail == "full" and input_summary:
|
|
701
|
+
timeline_md += f" → {input_summary[:100]}\n"
|
|
702
|
+
|
|
703
|
+
timeline_md += "\n"
|
|
704
|
+
|
|
705
|
+
# Complete markdown document
|
|
706
|
+
markdown = f"""# Session Report: {session_id}
|
|
707
|
+
|
|
708
|
+
**Agent:** {agent}
|
|
709
|
+
**Duration:** {duration_mins} minutes
|
|
710
|
+
**Tokens:** {total_tokens:,}
|
|
711
|
+
**Estimated Cost:** ${est_cost:.2f}
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Timeline
|
|
716
|
+
|
|
717
|
+
{timeline_md}
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Summary
|
|
722
|
+
|
|
723
|
+
Total events: {len(events)}
|
|
724
|
+
|
|
725
|
+
*Generated by HtmlGraph - "HTML is All You Need"*
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
return markdown
|