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.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,1366 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HtmlGraph Dashboard - Agent Activity & Orchestration</title>
7
+ <script src="/static/htmx.min.js"></script>
8
+ <link rel="stylesheet" href="/static/style-redesign.css">
9
+ </head>
10
+ <body>
11
+ <div class="dashboard-container">
12
+ <!-- HEADER -->
13
+ <header class="dashboard-header">
14
+ <div class="header-content">
15
+ <div class="logo">
16
+ <span class="logo-icon">▲</span>
17
+ <span>HtmlGraph</span>
18
+ </div>
19
+ <div class="header-stats">
20
+ <div class="stat-badge">
21
+ <span class="stat-label">Events</span>
22
+ <span class="stat-value" id="event-count">0</span>
23
+ </div>
24
+ <div class="stat-badge">
25
+ <span class="stat-label">Agents</span>
26
+ <span class="stat-value" id="agent-count">0</span>
27
+ </div>
28
+ <div class="stat-badge">
29
+ <span class="stat-label">Sessions</span>
30
+ <span class="stat-value" id="session-count">0</span>
31
+ </div>
32
+ <div class="ws-indicator">
33
+ <div class="ws-dot connected" id="ws-indicator"></div>
34
+ <span id="ws-status">Connected</span>
35
+ </div>
36
+ </div>
37
+ </div>
38
+ </header>
39
+
40
+ <!-- NAVIGATION TABS -->
41
+ <nav class="tabs-navigation">
42
+ <button class="tab-button active" data-tab="activity"
43
+ hx-get="/views/activity-feed"
44
+ hx-target="#content-area"
45
+ hx-trigger="click">
46
+ <span class="tab-icon">▤</span>
47
+ ACTIVITY
48
+ </button>
49
+ <button class="tab-button" data-tab="orchestration"
50
+ hx-get="/views/orchestration"
51
+ hx-target="#content-area"
52
+ hx-trigger="click">
53
+ <span class="tab-icon">◊</span>
54
+ ORCHESTRATION
55
+ </button>
56
+ <button class="tab-button" data-tab="work-items"
57
+ hx-get="/views/work-items"
58
+ hx-target="#content-area"
59
+ hx-trigger="click">
60
+ <span class="tab-icon">█</span>
61
+ WORK ITEMS
62
+ </button>
63
+ <button class="tab-button" data-tab="agents"
64
+ hx-get="/views/agents"
65
+ hx-target="#content-area"
66
+ hx-trigger="click">
67
+ <span class="tab-icon">◆</span>
68
+ AGENTS
69
+ </button>
70
+ <button class="tab-button" data-tab="metrics"
71
+ hx-get="/views/metrics"
72
+ hx-target="#content-area"
73
+ hx-trigger="click">
74
+ <span class="tab-icon">▼</span>
75
+ METRICS
76
+ </button>
77
+ </nav>
78
+
79
+ <!-- CONTENT AREA -->
80
+ <main class="content-area" id="content-area">
81
+ <div class="loading-indicator">
82
+ <div class="spinner"></div>
83
+ <p>Loading dashboard...</p>
84
+ </div>
85
+ </main>
86
+ </div>
87
+
88
+ <!-- SCRIPTS -->
89
+ <script>
90
+ let eventCount = 0;
91
+ let agentSet = new Set();
92
+ let sessionCount = 0;
93
+ let processedEventIds = new Set();
94
+ let wsConnected = false;
95
+
96
+ // Load initial stats from server
97
+ async function loadInitialStats() {
98
+ try {
99
+ const response = await fetch('/api/initial-stats');
100
+ const data = await response.json();
101
+
102
+ eventCount = data.total_events || 0;
103
+ sessionCount = data.total_sessions || 0;
104
+
105
+ if (data.agents) {
106
+ data.agents.forEach(agent => agentSet.add(agent));
107
+ }
108
+
109
+ document.getElementById('event-count').textContent = eventCount;
110
+ document.getElementById('agent-count').textContent = agentSet.size;
111
+ document.getElementById('session-count').textContent = sessionCount;
112
+
113
+ console.log('Initial stats loaded:', data);
114
+ } catch (error) {
115
+ console.error('Failed to load initial stats:', error);
116
+ }
117
+ }
118
+
119
+ // Initialize dashboard on load
120
+ document.addEventListener('DOMContentLoaded', function() {
121
+ loadInitialStats();
122
+ htmx.ajax('GET', '/views/activity-feed', {target: '#content-area'});
123
+ connectWebSocket();
124
+ });
125
+
126
+ // Tab switching
127
+ document.querySelectorAll('.tab-button').forEach(button => {
128
+ button.addEventListener('click', function() {
129
+ document.querySelectorAll('.tab-button').forEach(b => {
130
+ b.classList.remove('active');
131
+ });
132
+ this.classList.add('active');
133
+ });
134
+ });
135
+
136
+ // WebSocket Connection
137
+ function connectWebSocket() {
138
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
139
+ const ws = new WebSocket(wsProtocol + '//' + window.location.host + '/ws/events');
140
+
141
+ ws.onopen = function(event) {
142
+ console.log('WebSocket connected');
143
+ wsConnected = true;
144
+ updateWSStatus(true);
145
+ };
146
+
147
+ ws.onmessage = function(event) {
148
+ try {
149
+ const data = JSON.parse(event.data);
150
+
151
+ if (data.type === 'event') {
152
+ if (processedEventIds.has(data.event_id)) {
153
+ return;
154
+ }
155
+ processedEventIds.add(data.event_id);
156
+
157
+ eventCount++;
158
+ if (data.agent_id) {
159
+ agentSet.add(data.agent_id);
160
+ }
161
+
162
+ document.getElementById('event-count').textContent = eventCount;
163
+ document.getElementById('agent-count').textContent = agentSet.size;
164
+
165
+ const badge = document.getElementById('event-count').parentElement;
166
+ badge.classList.add('pulse');
167
+ setTimeout(() => badge.classList.remove('pulse'), 500);
168
+
169
+ insertNewEventIntoActivityFeed(data);
170
+ }
171
+ } catch (e) {
172
+ console.error('WebSocket message error:', e);
173
+ }
174
+ };
175
+
176
+ ws.onerror = function(event) {
177
+ console.error('WebSocket error:', event);
178
+ updateWSStatus(false);
179
+ };
180
+
181
+ ws.onclose = function(event) {
182
+ console.log('WebSocket disconnected, reconnecting in 3s...');
183
+ updateWSStatus(false);
184
+ setTimeout(connectWebSocket, 3000);
185
+ };
186
+ }
187
+
188
+ function updateWSStatus(isConnected) {
189
+ wsConnected = isConnected;
190
+ const indicator = document.getElementById('ws-indicator');
191
+ const status = document.getElementById('ws-status');
192
+ if (indicator) {
193
+ indicator.classList.toggle('connected', isConnected);
194
+ indicator.classList.toggle('disconnected', !isConnected);
195
+ }
196
+ if (status) {
197
+ status.textContent = isConnected ? 'Connected' : 'Disconnected';
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Insert new event into the grouped conversation turn activity feed
203
+ * Handles both UserQuery events (new turns) and child events (tool calls, etc.)
204
+ */
205
+ function insertNewEventIntoActivityFeed(eventData) {
206
+ const conversationFeed = document.querySelector('.conversation-feed');
207
+ if (!conversationFeed) return;
208
+
209
+ // Check for empty state and remove it
210
+ const emptyState = conversationFeed.querySelector('.empty-state');
211
+ if (emptyState) {
212
+ emptyState.remove();
213
+ }
214
+
215
+ // Get or create the turns list container
216
+ let turnsList = conversationFeed.querySelector('.conversation-turns-list');
217
+ if (!turnsList) {
218
+ turnsList = document.createElement('div');
219
+ turnsList.className = 'conversation-turns-list';
220
+ conversationFeed.appendChild(turnsList);
221
+ }
222
+
223
+ // Check for duplicates
224
+ if (document.querySelector(`[data-event-id="${eventData.event_id}"]`)) {
225
+ console.log('Event already exists:', eventData.event_id);
226
+ return;
227
+ }
228
+
229
+ // Handle UserQuery events - create new conversation turn
230
+ if (eventData.tool_name === 'UserQuery') {
231
+ insertNewConversationTurn(eventData, turnsList);
232
+ return;
233
+ }
234
+
235
+ // Handle child events (tool calls, etc.)
236
+ if (eventData.parent_event_id) {
237
+ insertChildEvent(eventData);
238
+ return;
239
+ }
240
+
241
+ console.warn('Event with no parent_event_id and not UserQuery:', eventData);
242
+ }
243
+
244
+ /**
245
+ * Create and insert a new conversation turn for a UserQuery event
246
+ */
247
+ function insertNewConversationTurn(userQueryEvent, turnsList) {
248
+ const turnId = userQueryEvent.event_id;
249
+ const prompt = userQueryEvent.input_summary || userQueryEvent.summary || '';
250
+ const timestamp = formatTimestamp(userQueryEvent.timestamp);
251
+
252
+ // Determine if this turn has spawner delegation
253
+ const hasSpawner = userQueryEvent.context && userQueryEvent.context.spawner_type ? 'spawner' : 'direct';
254
+ const agentId = userQueryEvent.agent_id || 'unknown';
255
+
256
+ const turnHtml = `
257
+ <div class="conversation-turn"
258
+ data-turn-id="${turnId}"
259
+ data-spawner-type="${hasSpawner}"
260
+ data-agent="${agentId}">
261
+ <!-- User Query Parent Row (Clickable) -->
262
+ <div class="userquery-parent"
263
+ onclick="toggleConversationTurn('${turnId}')"
264
+ data-turn-id="${turnId}">
265
+
266
+ <!-- Expand/Collapse Toggle -->
267
+ <span class="expand-toggle-turn" id="toggle-${turnId}">▶</span>
268
+
269
+ <!-- User Prompt Text -->
270
+ <div class="prompt-section">
271
+ <span class="prompt-text" title="${escapeHtml(prompt)}">
272
+ ${escapeHtml(prompt.substring(0, 100))}${prompt.length > 100 ? '...' : ''}
273
+ </span>
274
+ </div>
275
+
276
+ <!-- Stats Badges (initialized to 0) -->
277
+ <div class="turn-stats">
278
+ <span class="stat-badge tool-count" data-value="0" style="display: none;"></span>
279
+ <span class="stat-badge duration" data-value="0">0s</span>
280
+ </div>
281
+
282
+ <!-- Timestamp -->
283
+ <div class="turn-timestamp">
284
+ ${timestamp}
285
+ </div>
286
+ </div>
287
+
288
+ <!-- Child Events Container (Hidden by default) -->
289
+ <div class="turn-children collapsed" id="children-${turnId}">
290
+ <div class="no-children-message">
291
+ <span class="tree-connector">└─</span>
292
+ <span class="text-muted">No child events</span>
293
+ </div>
294
+ </div>
295
+ </div>
296
+ `;
297
+
298
+ // Insert at top of turns list
299
+ const firstTurn = turnsList.firstChild;
300
+ if (firstTurn) {
301
+ firstTurn.insertAdjacentHTML('beforebegin', turnHtml);
302
+ } else {
303
+ turnsList.insertAdjacentHTML('afterbegin', turnHtml);
304
+ }
305
+
306
+ // Auto-expand the new turn
307
+ const childrenContainer = document.getElementById(`children-${turnId}`);
308
+ const toggleButton = document.getElementById(`toggle-${turnId}`);
309
+ if (childrenContainer && toggleButton) {
310
+ childrenContainer.classList.remove('collapsed');
311
+ toggleButton.classList.add('expanded');
312
+ }
313
+
314
+ // Highlight the new turn briefly
315
+ const newTurn = document.querySelector(`[data-turn-id="${turnId}"]`);
316
+ if (newTurn) {
317
+ highlightElement(newTurn.querySelector('.userquery-parent'));
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Find the root conversation turn ID that contains this event in the DOM.
323
+ * Walks up the DOM tree to find the parent conversation-turn element.
324
+ *
325
+ * @param {string} eventId - The event ID to find
326
+ * @returns {string|null} - The root UserQuery event_id or null if not found
327
+ */
328
+ function findRootConversationTurn(eventId) {
329
+ // First, check if this event is already in the DOM
330
+ const eventElement = document.querySelector(`[data-event-id="${eventId}"]`);
331
+ if (!eventElement) {
332
+ return null; // Event not in DOM yet
333
+ }
334
+
335
+ // Walk up the DOM to find the nearest conversation turn container
336
+ let current = eventElement;
337
+ while (current && current.parentElement) {
338
+ current = current.parentElement;
339
+ if (current.classList && current.classList.contains('conversation-turn')) {
340
+ return current.getAttribute('data-turn-id');
341
+ }
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ /**
348
+ * Find or create a children container for an event.
349
+ * If the event is the root conversation turn, use children-${turnId}.
350
+ * If the event is a nested event, create a nested children container.
351
+ *
352
+ * @param {string} parentEventId - The parent event ID
353
+ * @returns {HTMLElement|null} - The children container or null if not found
354
+ */
355
+ function findOrCreateChildrenContainer(parentEventId) {
356
+ // First, check if this is a conversation turn (root level)
357
+ let container = document.getElementById(`children-${parentEventId}`);
358
+ if (container) {
359
+ return container;
360
+ }
361
+
362
+ // Otherwise, look for the parent event in the DOM
363
+ const parentElement = document.querySelector(`[data-event-id="${parentEventId}"]`);
364
+ if (!parentElement) {
365
+ return null; // Parent event not in DOM yet
366
+ }
367
+
368
+ // Check if parent event already has a nested children container
369
+ let nestedContainer = parentElement.querySelector(':scope > .event-children');
370
+ if (!nestedContainer) {
371
+ // Create a nested children container
372
+ nestedContainer = document.createElement('div');
373
+ nestedContainer.className = 'event-children';
374
+ nestedContainer.setAttribute('data-parent-id', parentEventId);
375
+ parentElement.appendChild(nestedContainer);
376
+ }
377
+
378
+ return nestedContainer;
379
+ }
380
+
381
+ /**
382
+ * Calculate the depth of an event based on how many containers separate it from the root turn.
383
+ * Walks up the DOM tree counting .turn-children and .event-children containers.
384
+ *
385
+ * @param {string} parentEventId - The parent event ID
386
+ * @returns {number} - The depth (0 for direct children of a UserQuery turn)
387
+ */
388
+ function calculateEventDepth(parentEventId) {
389
+ let depth = 0;
390
+
391
+ // Start by finding the children container for this parent
392
+ let container = document.getElementById(`children-${parentEventId}`);
393
+ if (!container) {
394
+ const parentElement = document.querySelector(`[data-event-id="${parentEventId}"]`);
395
+ if (parentElement) {
396
+ container = parentElement.querySelector(':scope > .event-children');
397
+ }
398
+ }
399
+
400
+ if (!container) {
401
+ return 0; // Parent not yet in DOM or is a root turn
402
+ }
403
+
404
+ // Walk up the DOM to count nesting levels
405
+ let current = container.parentElement; // Start from parent of container
406
+ while (current) {
407
+ // Check if we're in a .turn-children container (root level)
408
+ if (current.classList && current.classList.contains('turn-children')) {
409
+ return depth; // We've reached the root turn
410
+ }
411
+
412
+ // Check if we're in a child-event-row (nested event) that has children
413
+ if (current.classList && current.classList.contains('child-event-row')) {
414
+ depth++;
415
+ // Move up to find the next ancestor event row
416
+ current = current.parentElement; // Move to .event-children container
417
+ if (current) {
418
+ current = current.parentElement; // Move to parent event row
419
+ }
420
+ } else {
421
+ current = current.parentElement;
422
+ }
423
+ }
424
+
425
+ return depth;
426
+ }
427
+
428
+ /**
429
+ * Insert a child event into its parent (which could be a conversation turn or another event).
430
+ * Handles multi-level nesting for spawner delegations.
431
+ */
432
+ function insertChildEvent(eventData) {
433
+ const parentEventId = eventData.parent_event_id;
434
+ if (!parentEventId) {
435
+ console.warn('Child event has no parent_event_id:', eventData.event_id);
436
+ return;
437
+ }
438
+
439
+ // Find or create the children container for this parent
440
+ const childrenContainer = findOrCreateChildrenContainer(parentEventId);
441
+ if (!childrenContainer) {
442
+ console.warn('Could not find or create children container for parent:', parentEventId);
443
+ return;
444
+ }
445
+
446
+ // Remove "no children" message if it exists
447
+ const noChildrenMsg = childrenContainer.querySelector('.no-children-message');
448
+ if (noChildrenMsg) {
449
+ noChildrenMsg.remove();
450
+ }
451
+
452
+ // Extract event data
453
+ const toolName = eventData.tool_name || 'unknown';
454
+ const summary = eventData.output_summary || eventData.input_summary || eventData.summary || '';
455
+ const duration = eventData.duration_seconds || 0;
456
+ const timestamp = formatTimestamp(eventData.timestamp);
457
+ const agentId = eventData.agent_id || 'Claude Code';
458
+ const model = eventData.context && eventData.context.model ? eventData.context.model : null;
459
+ const spawnerType = eventData.context && eventData.context.spawner_type ? eventData.context.spawner_type : null;
460
+ const spawnedAgent = eventData.context && eventData.context.spawned_agent ? eventData.context.spawned_agent : null;
461
+ const costUsd = eventData.context && eventData.context.cost_usd ? eventData.context.cost_usd : null;
462
+
463
+ // Calculate depth: count how many levels of nesting this event is at
464
+ const depth = calculateEventDepth(parentEventId);
465
+
466
+ // Determine tree connector based on whether this is the last child
467
+ const existingChildren = childrenContainer.querySelectorAll(':scope > .child-event-row');
468
+ const isLastChild = existingChildren.length === 0;
469
+ const hasChildren = eventData.context && eventData.context.has_children;
470
+ const treeConnector = (isLastChild && !hasChildren) ? '└─' : '├─';
471
+
472
+ // Build child event HTML with nested container placeholder
473
+ let childHtml = `
474
+ <div class="child-event-row depth-${depth}"
475
+ data-event-id="${eventData.event_id}"
476
+ data-parent-id="${parentEventId}"
477
+ style="margin-left: ${depth * 20}px;">
478
+
479
+ <!-- Tree Connector -->
480
+ <span class="tree-connector">${treeConnector}</span>
481
+
482
+ <!-- Tool Name -->
483
+ <span class="child-tool-name">${escapeHtml(toolName)}</span>
484
+
485
+ <!-- Summary/Input -->
486
+ <span class="child-summary" title="${escapeHtml(summary)}">
487
+ ${escapeHtml(summary.substring(0, 80))}${summary.length > 80 ? '...' : ''}
488
+ </span>
489
+ `;
490
+
491
+ // Add agent badge with spawner support
492
+ if (spawnerType) {
493
+ // Spawner delegation: show orchestrator → spawned AI
494
+ childHtml += `
495
+ <span class="child-agent-badge agent-${agentId.toLowerCase().replace(/\\s+/g, '-')}">
496
+ ${escapeHtml(agentId)}
497
+ ${model ? `<span class="model-indicator">${escapeHtml(model)}</span>` : ''}
498
+ </span>
499
+ <span class="delegation-arrow">→</span>
500
+ <span class="spawner-badge spawner-${spawnerType.toLowerCase()}">
501
+ ${escapeHtml(spawnedAgent || spawnerType)}
502
+ ${costUsd ? `<span class="cost-badge">$${costUsd.toFixed(2)}</span>` : ''}
503
+ </span>
504
+ `;
505
+ } else {
506
+ // Regular agent: just show agent name + model if available
507
+ childHtml += `
508
+ <span class="child-agent-badge agent-${agentId.toLowerCase().replace(/\\s+/g, '-')}">
509
+ ${escapeHtml(agentId)}
510
+ ${model ? `<span class="model-indicator">${escapeHtml(model)}</span>` : ''}
511
+ </span>
512
+ `;
513
+ }
514
+
515
+ // Add duration and timestamp
516
+ childHtml += `
517
+ <!-- Duration -->
518
+ <span class="child-duration">
519
+ ${duration.toFixed(2)}s
520
+ </span>
521
+
522
+ <!-- Timestamp -->
523
+ <span class="child-timestamp">
524
+ ${timestamp}
525
+ </span>
526
+ </div>
527
+ `;
528
+
529
+ // Insert child event
530
+ childrenContainer.insertAdjacentHTML('beforeend', childHtml);
531
+
532
+ // Update root conversation turn statistics
533
+ const rootTurnId = findRootConversationTurn(eventData.event_id);
534
+ if (rootTurnId) {
535
+ updateParentTurnStats(rootTurnId, eventData);
536
+ }
537
+
538
+ // Highlight the new child event
539
+ const newChild = document.querySelector(`[data-event-id="${eventData.event_id}"]`);
540
+ if (newChild) {
541
+ highlightElement(newChild);
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Update the statistics of a parent conversation turn based on a child event
547
+ */
548
+ function updateParentTurnStats(parentTurnId, childEvent) {
549
+ const turnElement = document.querySelector(`[data-turn-id="${parentTurnId}"]`);
550
+ if (!turnElement) return;
551
+
552
+ const statsContainer = turnElement.querySelector('.turn-stats');
553
+ if (!statsContainer) return;
554
+
555
+ // Get current stats from badges
556
+ let toolCount = parseInt(statsContainer.querySelector('.stat-badge.tool-count')?.getAttribute('data-value') || '0', 10);
557
+ let totalDuration = parseFloat(statsContainer.querySelector('.stat-badge.duration')?.getAttribute('data-value') || '0');
558
+ let successCount = parseInt(statsContainer.querySelector('.stat-badge.success')?.getAttribute('data-value') || '0', 10);
559
+ let errorCount = parseInt(statsContainer.querySelector('.stat-badge.error')?.getAttribute('data-value') || '0', 10);
560
+
561
+ // Update counts based on event
562
+ if (childEvent.tool_name !== 'UserQuery') {
563
+ toolCount++;
564
+ }
565
+
566
+ totalDuration += (childEvent.duration_seconds || 0);
567
+
568
+ // Determine success/error based on status
569
+ const status = childEvent.status || 'completed';
570
+ if (status === 'completed' || status === 'success') {
571
+ successCount++;
572
+ } else if (status === 'error' || status === 'failed') {
573
+ errorCount++;
574
+ }
575
+
576
+ // Update tool count badge
577
+ const toolCountBadge = statsContainer.querySelector('.stat-badge.tool-count');
578
+ if (toolCountBadge) {
579
+ toolCountBadge.setAttribute('data-value', toolCount);
580
+ toolCountBadge.textContent = toolCount;
581
+ toolCountBadge.style.display = toolCount > 0 ? 'inline-block' : 'none';
582
+ }
583
+
584
+ // Update duration badge
585
+ const durationBadge = statsContainer.querySelector('.stat-badge.duration');
586
+ if (durationBadge) {
587
+ durationBadge.setAttribute('data-value', totalDuration.toFixed(2));
588
+ durationBadge.textContent = totalDuration.toFixed(2) + 's';
589
+ }
590
+
591
+ // Update success badge
592
+ let successBadge = statsContainer.querySelector('.stat-badge.success');
593
+ if (successCount > 0) {
594
+ if (!successBadge) {
595
+ const badge = document.createElement('span');
596
+ badge.className = 'stat-badge success';
597
+ statsContainer.appendChild(badge);
598
+ successBadge = badge;
599
+ }
600
+ successBadge.setAttribute('data-value', successCount);
601
+ successBadge.textContent = `✓ ${successCount}`;
602
+ } else if (successBadge) {
603
+ successBadge.remove();
604
+ }
605
+
606
+ // Update error badge
607
+ let errorBadge = statsContainer.querySelector('.stat-badge.error');
608
+ if (errorCount > 0) {
609
+ if (!errorBadge) {
610
+ const badge = document.createElement('span');
611
+ badge.className = 'stat-badge error';
612
+ statsContainer.appendChild(badge);
613
+ errorBadge = badge;
614
+ }
615
+ errorBadge.setAttribute('data-value', errorCount);
616
+ errorBadge.textContent = `✗ ${errorCount}`;
617
+ } else if (errorBadge) {
618
+ errorBadge.remove();
619
+ }
620
+ }
621
+
622
+ /**
623
+ * Format timestamp to readable format (HH:MM:SS)
624
+ */
625
+ function formatTimestamp(timestamp) {
626
+ try {
627
+ const date = new Date(timestamp);
628
+ const hours = String(date.getHours()).padStart(2, '0');
629
+ const minutes = String(date.getMinutes()).padStart(2, '0');
630
+ const seconds = String(date.getSeconds()).padStart(2, '0');
631
+ return `${hours}:${minutes}:${seconds}`;
632
+ } catch (e) {
633
+ return timestamp;
634
+ }
635
+ }
636
+
637
+ /**
638
+ * Escape HTML special characters to prevent XSS
639
+ */
640
+ function escapeHtml(text) {
641
+ if (!text) return '';
642
+ const div = document.createElement('div');
643
+ div.textContent = text;
644
+ return div.innerHTML;
645
+ }
646
+
647
+ /**
648
+ * Highlight an element briefly with a background color animation
649
+ */
650
+ function highlightElement(element) {
651
+ if (!element) return;
652
+ element.style.transition = 'background-color 0.3s ease';
653
+ element.style.backgroundColor = 'rgba(163, 230, 53, 0.2)';
654
+ setTimeout(() => {
655
+ element.style.backgroundColor = '';
656
+ }, 500);
657
+ }
658
+
659
+ function highlightRow(row) {
660
+ if (row) {
661
+ row.classList.add('new-event-highlight');
662
+ setTimeout(() => {
663
+ row.classList.remove('new-event-highlight');
664
+ }, 2000);
665
+ }
666
+ }
667
+
668
+ function createActivityRowHTML(eventData) {
669
+ // Event type emoji mapping
670
+ let eventEmoji = '&#128203;'; // clipboard
671
+ if (eventData.event_type === 'delegation') eventEmoji = '&#128279;'; // link
672
+ else if (eventData.event_type === 'tool_call') eventEmoji = '&#128296;'; // hammer
673
+ else if (eventData.event_type === 'completion') eventEmoji = '&#127881;'; // party
674
+ else if (eventData.event_type === 'tool_result') eventEmoji = '&#9989;'; // check
675
+ else if (eventData.event_type === 'error') eventEmoji = '&#10060;'; // x
676
+
677
+ const inputSummary = eventData.input_summary ? eventData.input_summary.substring(0, 150) : '';
678
+ const inputTruncated = eventData.input_summary && eventData.input_summary.length > 150 ? '...' : '';
679
+ const outputSummary = eventData.output_summary ? eventData.output_summary.substring(0, 150) : '';
680
+ const outputTruncated = eventData.output_summary && eventData.output_summary.length > 150 ? '...' : '';
681
+
682
+ const isChild = !!eventData.parent_event_id;
683
+ const rowClass = isChild ? 'child-row hidden' : 'parent-row';
684
+ const borderStyle = isChild ? 'border-left: 4px solid var(--text-muted);' : 'border-left: 4px solid var(--accent-lime);';
685
+
686
+ // New column order: Agent | Tool | Input | Output | Status | Timestamp (no ID column)
687
+ const html = `
688
+ <tr class="activity-row ${rowClass} event-${eventData.status || 'pending'}"
689
+ data-event-id="${escapeHtml(eventData.event_id)}"
690
+ ${isChild ? `data-parent="${escapeHtml(eventData.parent_event_id)}"` : ''}
691
+ style="${borderStyle}">
692
+ <td class="col-agent">
693
+ ${isChild ? '<span class="child-indicator">&#8627;</span>' : ''}
694
+ <span class="agent-badge agent-${escapeHtml(eventData.agent_id.toLowerCase())}">${escapeHtml(eventData.agent_id)}</span>
695
+ </td>
696
+ <td class="col-tool">
697
+ <span class="event-type-badge" title="${escapeHtml(eventData.event_type)}">
698
+ ${eventEmoji}
699
+ </span>
700
+ ${eventData.tool_name ? `<code class="tool-name">${escapeHtml(eventData.tool_name)}</code>` : '<span class="text-muted">-</span>'}
701
+ </td>
702
+ <td class="col-input">
703
+ ${inputSummary ? `<span class="truncate" title="${escapeHtml(eventData.input_summary)}">${escapeHtml(inputSummary)}${inputTruncated}</span>` : '<span class="text-muted">-</span>'}
704
+ </td>
705
+ <td class="col-output">
706
+ ${outputSummary ? `<span class="truncate" title="${escapeHtml(eventData.output_summary)}">${escapeHtml(outputSummary)}${outputTruncated}</span>` : '<span class="text-muted">-</span>'}
707
+ </td>
708
+ <td class="col-status">
709
+ <span class="status-badge status-${eventData.status || 'pending'}">${escapeHtml(eventData.status || 'pending')}</span>
710
+ </td>
711
+ <td class="col-timestamp">
712
+ <span class="timestamp-text" data-utc-time="${escapeHtml(eventData.timestamp)}">${escapeHtml(eventData.timestamp)}</span>
713
+ </td>
714
+ </tr>
715
+ `;
716
+ return html;
717
+ }
718
+
719
+ function convertTimestampsToLocal() {
720
+ const timestampElements = document.querySelectorAll('[data-utc-time]');
721
+ timestampElements.forEach(element => {
722
+ const utcTime = element.getAttribute('data-utc-time');
723
+ if (utcTime) {
724
+ try {
725
+ const date = new Date(utcTime.replace(' ', 'T') + 'Z');
726
+ const localTime = new Intl.DateTimeFormat('en-US', {
727
+ year: 'numeric',
728
+ month: '2-digit',
729
+ day: '2-digit',
730
+ hour: '2-digit',
731
+ minute: '2-digit',
732
+ second: '2-digit',
733
+ hour12: false,
734
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
735
+ }).format(date);
736
+ element.textContent = localTime;
737
+ element.setAttribute('title', `UTC: ${utcTime} | Local: ${localTime}`);
738
+ } catch (err) {
739
+ console.warn('Failed to convert timestamp:', utcTime, err);
740
+ }
741
+ }
742
+ });
743
+ }
744
+
745
+ // Convert timestamps after HTMX loads content
746
+ document.body.addEventListener('htmx:afterSettle', function(evt) {
747
+ if (evt.detail.target.id === 'content-area') {
748
+ if (typeof convertTimestampsToLocal === 'function') {
749
+ convertTimestampsToLocal();
750
+ }
751
+ }
752
+ });
753
+
754
+ // Toggle child rows visibility (for expandable tracing)
755
+ function toggleChildren(parentRow) {
756
+ const eventId = parentRow.dataset.eventId;
757
+ const children = document.querySelectorAll(`[data-parent="${eventId}"]`);
758
+ const expandIcon = parentRow.querySelector('.expand-icon');
759
+
760
+ children.forEach(child => {
761
+ child.classList.toggle('hidden');
762
+ });
763
+
764
+ if (expandIcon) {
765
+ expandIcon.classList.toggle('expanded');
766
+ }
767
+ }
768
+
769
+ // Expand all parent rows
770
+ function expandAll() {
771
+ document.querySelectorAll('.parent-row.has-children').forEach(row => {
772
+ const eventId = row.dataset.eventId;
773
+ const children = document.querySelectorAll(`[data-parent="${eventId}"]`);
774
+ const expandIcon = row.querySelector('.expand-icon');
775
+
776
+ children.forEach(child => {
777
+ child.classList.remove('hidden');
778
+ });
779
+
780
+ if (expandIcon) {
781
+ expandIcon.classList.add('expanded');
782
+ }
783
+ });
784
+ }
785
+
786
+ // Collapse all parent rows
787
+ function collapseAll() {
788
+ document.querySelectorAll('.child-row').forEach(child => {
789
+ child.classList.add('hidden');
790
+ });
791
+ document.querySelectorAll('.expand-icon').forEach(icon => {
792
+ icon.classList.remove('expanded');
793
+ });
794
+ }
795
+
796
+ // ============================================
797
+ // Jaeger-Style Trace Interactivity Functions
798
+ // (Must be global for HTMX-loaded partials)
799
+ // ============================================
800
+
801
+ // Toggle expand/collapse with animation
802
+ function toggleTrace(id, event) {
803
+ if (event) event.stopPropagation();
804
+
805
+ const children = document.querySelectorAll(`[data-parent="${id}"]`);
806
+ const toggle = document.querySelector(`[data-id="${id}"] .expand-toggle`);
807
+
808
+ children.forEach(child => {
809
+ child.classList.toggle('collapsed');
810
+ });
811
+
812
+ if (toggle) {
813
+ toggle.classList.toggle('expanded');
814
+ }
815
+
816
+ // Update breadcrumbs if drilling into a trace
817
+ updateBreadcrumbs(id);
818
+ }
819
+
820
+ // Highlight ancestor path on hover (Jaeger pattern)
821
+ function highlightAncestors(row) {
822
+ clearAncestorHighlight();
823
+
824
+ let parentId = row.dataset.parent;
825
+ while (parentId) {
826
+ const parent = document.querySelector(`[data-id="${parentId}"]`);
827
+ if (parent) {
828
+ parent.classList.add('ancestor-highlight');
829
+ parentId = parent.dataset.parent;
830
+ } else {
831
+ break;
832
+ }
833
+ }
834
+ }
835
+
836
+ // Clear all ancestor highlights
837
+ function clearAncestorHighlight() {
838
+ document.querySelectorAll('.ancestor-highlight').forEach(el => {
839
+ el.classList.remove('ancestor-highlight');
840
+ });
841
+ }
842
+
843
+ // Expand all traces
844
+ function expandAllTraces() {
845
+ document.querySelectorAll('.child-row').forEach(child => {
846
+ child.classList.remove('collapsed');
847
+ });
848
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
849
+ toggle.classList.add('expanded');
850
+ });
851
+ }
852
+
853
+ // Collapse all traces
854
+ function collapseAllTraces() {
855
+ document.querySelectorAll('.child-row').forEach(child => {
856
+ child.classList.add('collapsed');
857
+ });
858
+ document.querySelectorAll('.expand-toggle').forEach(toggle => {
859
+ toggle.classList.remove('expanded');
860
+ });
861
+ }
862
+
863
+ // Breadcrumb management
864
+ let breadcrumbStack = ['root'];
865
+
866
+ function updateBreadcrumbs(id) {
867
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
868
+ if (!breadcrumbsContainer) return;
869
+
870
+ const row = document.querySelector(`[data-id="${id}"]`);
871
+ if (!row) return;
872
+
873
+ // Only show breadcrumbs when we have nested navigation
874
+ const depth = parseInt(row.dataset.depth || '0');
875
+ if (depth > 0 || breadcrumbStack.length > 1) {
876
+ breadcrumbsContainer.style.display = 'flex';
877
+ }
878
+
879
+ // Get tool name or operation for breadcrumb label
880
+ const toolName = row.querySelector('.tool-name');
881
+ const label = toolName ? toolName.textContent : `Trace ${id.substring(0, 8)}`;
882
+
883
+ // Add to breadcrumb stack if not already present
884
+ if (!breadcrumbStack.includes(id)) {
885
+ breadcrumbStack.push(id);
886
+
887
+ const separator = document.createElement('span');
888
+ separator.className = 'separator';
889
+ separator.textContent = '>';
890
+
891
+ const crumb = document.createElement('span');
892
+ crumb.className = 'breadcrumb active';
893
+ crumb.dataset.id = id;
894
+ crumb.textContent = label;
895
+ crumb.onclick = () => navigateToBreadcrumb(id);
896
+
897
+ // Remove active class from previous breadcrumbs
898
+ breadcrumbsContainer.querySelectorAll('.breadcrumb').forEach(b => {
899
+ b.classList.remove('active');
900
+ });
901
+
902
+ breadcrumbsContainer.appendChild(separator);
903
+ breadcrumbsContainer.appendChild(crumb);
904
+ }
905
+ }
906
+
907
+ function navigateToBreadcrumb(id) {
908
+ // Find position in stack and remove everything after
909
+ const index = breadcrumbStack.indexOf(id);
910
+ if (index === -1) return;
911
+
912
+ // Remove breadcrumbs after this one
913
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
914
+ if (!breadcrumbsContainer) return;
915
+
916
+ const allCrumbs = breadcrumbsContainer.querySelectorAll('.breadcrumb, .separator');
917
+
918
+ let removing = false;
919
+ allCrumbs.forEach(el => {
920
+ if (removing) {
921
+ el.remove();
922
+ }
923
+ if (el.dataset && el.dataset.id === id) {
924
+ el.classList.add('active');
925
+ removing = true;
926
+ }
927
+ });
928
+
929
+ // Update stack
930
+ breadcrumbStack = breadcrumbStack.slice(0, index + 1);
931
+
932
+ // Hide breadcrumbs if back to root
933
+ if (breadcrumbStack.length <= 1) {
934
+ breadcrumbsContainer.style.display = 'none';
935
+ }
936
+ }
937
+
938
+ function resetBreadcrumbs() {
939
+ const breadcrumbsContainer = document.getElementById('trace-breadcrumbs');
940
+ if (!breadcrumbsContainer) return;
941
+
942
+ breadcrumbsContainer.innerHTML = '<span class="breadcrumb" data-id="root" onclick="resetBreadcrumbs()">Session</span>';
943
+ breadcrumbsContainer.style.display = 'none';
944
+ breadcrumbStack = ['root'];
945
+
946
+ // Collapse all traces when resetting
947
+ collapseAllTraces();
948
+ }
949
+ </script>
950
+
951
+ <style>
952
+ .new-event-highlight {
953
+ animation: highlightPulse 2s ease-out;
954
+ }
955
+
956
+ @keyframes highlightPulse {
957
+ 0% {
958
+ background-color: rgba(205, 255, 0, 0.1);
959
+ border-left-color: #CDFF00 !important;
960
+ }
961
+ 100% {
962
+ background-color: transparent;
963
+ border-left-color: transparent !important;
964
+ }
965
+ }
966
+
967
+ /* Activity Feed Table Styles */
968
+ .activity-table {
969
+ width: 100%;
970
+ border-collapse: separate;
971
+ border-spacing: 0;
972
+ background: var(--bg-card);
973
+ border: 1px solid var(--border-subtle);
974
+ }
975
+
976
+ .activity-table thead {
977
+ background: var(--bg-darker);
978
+ }
979
+
980
+ .activity-table th {
981
+ padding: var(--spacing-lg);
982
+ text-align: left;
983
+ color: var(--accent-lime);
984
+ font-weight: 700;
985
+ font-size: 0.85rem;
986
+ text-transform: uppercase;
987
+ letter-spacing: 0.05em;
988
+ border-bottom: 1px solid var(--border-subtle);
989
+ }
990
+
991
+ .activity-table td {
992
+ padding: var(--spacing-md) var(--spacing-lg);
993
+ border-bottom: 1px solid var(--border-subtle);
994
+ color: var(--text-primary);
995
+ }
996
+
997
+ .activity-row:hover {
998
+ background: var(--bg-hover);
999
+ }
1000
+
1001
+ .activity-row.parent-row {
1002
+ font-weight: 500;
1003
+ }
1004
+
1005
+ .activity-row.child-row {
1006
+ background: rgba(0, 0, 0, 0.2);
1007
+ font-size: 0.95rem;
1008
+ }
1009
+
1010
+ .event-type-badge {
1011
+ margin-right: var(--spacing-sm);
1012
+ font-size: 1.1rem;
1013
+ }
1014
+
1015
+ .agent-badge {
1016
+ display: inline-flex;
1017
+ align-items: center;
1018
+ padding: var(--spacing-xs) var(--spacing-sm);
1019
+ background: rgba(205, 255, 0, 0.1);
1020
+ border: 1px solid rgba(205, 255, 0, 0.3);
1021
+ border-radius: 2px;
1022
+ font-size: 0.8rem;
1023
+ font-weight: 600;
1024
+ text-transform: uppercase;
1025
+ letter-spacing: 0.05em;
1026
+ color: var(--accent-lime);
1027
+ }
1028
+
1029
+ .agent-badge.agent-claude {
1030
+ background: rgba(139, 92, 246, 0.1);
1031
+ border-color: rgba(139, 92, 246, 0.3);
1032
+ color: var(--agent-claude);
1033
+ }
1034
+
1035
+ .agent-badge.agent-gemini {
1036
+ background: rgba(59, 130, 246, 0.1);
1037
+ border-color: rgba(59, 130, 246, 0.3);
1038
+ color: var(--agent-gemini);
1039
+ }
1040
+
1041
+ .child-indicator {
1042
+ margin-left: var(--spacing-md);
1043
+ color: var(--text-muted);
1044
+ }
1045
+
1046
+ .tool-name {
1047
+ color: var(--accent-lime);
1048
+ font-size: 0.9rem;
1049
+ }
1050
+
1051
+ .status-badge {
1052
+ display: inline-block;
1053
+ padding: var(--spacing-xs) var(--spacing-sm);
1054
+ border-radius: 2px;
1055
+ font-size: 0.75rem;
1056
+ font-weight: 600;
1057
+ text-transform: uppercase;
1058
+ letter-spacing: 0.05em;
1059
+ }
1060
+
1061
+ .status-badge.success {
1062
+ background: rgba(16, 185, 129, 0.15);
1063
+ color: var(--status-success);
1064
+ border: 1px solid var(--status-success);
1065
+ }
1066
+
1067
+ .status-badge.progress {
1068
+ background: rgba(59, 130, 246, 0.15);
1069
+ color: var(--status-progress);
1070
+ border: 1px solid var(--status-progress);
1071
+ }
1072
+
1073
+ .status-badge.blocked {
1074
+ background: rgba(239, 68, 68, 0.15);
1075
+ color: var(--status-blocked);
1076
+ border: 1px solid var(--status-blocked);
1077
+ }
1078
+
1079
+ .status-badge.todo {
1080
+ background: rgba(107, 114, 128, 0.15);
1081
+ color: var(--status-todo);
1082
+ border: 1px solid var(--status-todo);
1083
+ }
1084
+
1085
+ .status-badge.pending {
1086
+ background: rgba(99, 102, 241, 0.15);
1087
+ color: #6366F1;
1088
+ border: 1px solid #6366F1;
1089
+ }
1090
+
1091
+ .status-badge.done {
1092
+ background: rgba(139, 92, 246, 0.15);
1093
+ color: var(--status-done);
1094
+ border: 1px solid var(--status-done);
1095
+ }
1096
+
1097
+ .event-id-code {
1098
+ color: var(--accent-lime);
1099
+ font-size: 0.8rem;
1100
+ font-weight: 600;
1101
+ }
1102
+
1103
+ .text-muted {
1104
+ color: var(--text-muted);
1105
+ }
1106
+
1107
+ .truncate {
1108
+ overflow: hidden;
1109
+ text-overflow: ellipsis;
1110
+ white-space: nowrap;
1111
+ display: inline-block;
1112
+ max-width: 200px;
1113
+ }
1114
+
1115
+ .empty-state {
1116
+ display: flex;
1117
+ flex-direction: column;
1118
+ align-items: center;
1119
+ justify-content: center;
1120
+ height: 300px;
1121
+ color: var(--text-muted);
1122
+ text-align: center;
1123
+ }
1124
+
1125
+ .empty-state p {
1126
+ font-size: 1.1rem;
1127
+ margin-bottom: var(--spacing-md);
1128
+ }
1129
+
1130
+ .empty-state small {
1131
+ color: var(--text-secondary);
1132
+ }
1133
+
1134
+ /* New column widths - Input/Output are flexible, others fixed */
1135
+ .col-agent { width: 120px; }
1136
+ .col-tool { width: 100px; }
1137
+ .col-input { width: auto; }
1138
+ .col-output { width: auto; }
1139
+ .col-status { width: 100px; }
1140
+ .col-timestamp { width: 140px; }
1141
+
1142
+ /* Make table use fixed layout for predictable column widths */
1143
+ .activity-table {
1144
+ table-layout: fixed;
1145
+ }
1146
+
1147
+ /* Expandable tracing structure */
1148
+ .expand-icon {
1149
+ cursor: pointer;
1150
+ display: inline-block;
1151
+ margin-right: 0.5rem;
1152
+ transition: transform 0.2s ease;
1153
+ font-size: 0.7rem;
1154
+ color: var(--text-muted);
1155
+ }
1156
+
1157
+ .expand-icon.expanded {
1158
+ transform: rotate(90deg);
1159
+ }
1160
+
1161
+ .parent-row.has-children {
1162
+ cursor: pointer;
1163
+ }
1164
+
1165
+ .child-count {
1166
+ font-size: 0.75rem;
1167
+ color: var(--text-muted);
1168
+ margin-left: 0.5rem;
1169
+ }
1170
+
1171
+ .child-row.hidden {
1172
+ display: none;
1173
+ }
1174
+
1175
+ /* Nested event children container styles */
1176
+ .event-children {
1177
+ display: flex;
1178
+ flex-direction: column;
1179
+ margin-top: 0;
1180
+ border-left: 1px solid var(--border-subtle);
1181
+ padding-left: var(--spacing-sm);
1182
+ }
1183
+
1184
+ .child-event-row {
1185
+ display: flex;
1186
+ align-items: center;
1187
+ gap: var(--spacing-sm);
1188
+ padding: var(--spacing-sm) var(--spacing-md);
1189
+ border-radius: 4px;
1190
+ background: rgba(0, 0, 0, 0.1);
1191
+ font-size: 0.9rem;
1192
+ color: var(--text-primary);
1193
+ border: 1px solid transparent;
1194
+ transition: all 0.2s ease;
1195
+ }
1196
+
1197
+ .child-event-row:hover {
1198
+ background: rgba(0, 0, 0, 0.15);
1199
+ border-color: var(--border-subtle);
1200
+ }
1201
+
1202
+ .child-event-row.depth-0 {
1203
+ margin-left: 0;
1204
+ }
1205
+
1206
+ .child-event-row.depth-1 {
1207
+ margin-left: 20px;
1208
+ }
1209
+
1210
+ .child-event-row.depth-2 {
1211
+ margin-left: 40px;
1212
+ }
1213
+
1214
+ .child-event-row.depth-3 {
1215
+ margin-left: 60px;
1216
+ }
1217
+
1218
+ .child-event-row.depth-4 {
1219
+ margin-left: 80px;
1220
+ }
1221
+
1222
+ .child-event-row.depth-5 {
1223
+ margin-left: 100px;
1224
+ }
1225
+
1226
+ .tree-connector {
1227
+ color: var(--text-muted);
1228
+ font-size: 0.85rem;
1229
+ font-family: monospace;
1230
+ flex-shrink: 0;
1231
+ }
1232
+
1233
+ .child-tool-name {
1234
+ font-weight: 600;
1235
+ color: var(--accent-lime);
1236
+ flex-shrink: 0;
1237
+ }
1238
+
1239
+ .child-summary {
1240
+ flex: 1;
1241
+ overflow: hidden;
1242
+ text-overflow: ellipsis;
1243
+ white-space: nowrap;
1244
+ color: var(--text-secondary);
1245
+ font-size: 0.85rem;
1246
+ }
1247
+
1248
+ .child-agent-badge {
1249
+ display: inline-flex;
1250
+ align-items: center;
1251
+ gap: var(--spacing-xs);
1252
+ padding: var(--spacing-xs) var(--spacing-sm);
1253
+ background: rgba(205, 255, 0, 0.1);
1254
+ border: 1px solid rgba(205, 255, 0, 0.3);
1255
+ border-radius: 3px;
1256
+ font-size: 0.75rem;
1257
+ font-weight: 600;
1258
+ color: var(--accent-lime);
1259
+ flex-shrink: 0;
1260
+ }
1261
+
1262
+ .child-agent-badge.agent-claude {
1263
+ background: rgba(139, 92, 246, 0.1);
1264
+ border-color: rgba(139, 92, 246, 0.3);
1265
+ color: var(--agent-claude);
1266
+ }
1267
+
1268
+ .child-agent-badge.agent-gemini {
1269
+ background: rgba(59, 130, 246, 0.1);
1270
+ border-color: rgba(59, 130, 246, 0.3);
1271
+ color: var(--agent-gemini);
1272
+ }
1273
+
1274
+ .model-indicator {
1275
+ font-size: 0.7rem;
1276
+ opacity: 0.8;
1277
+ }
1278
+
1279
+ .delegation-arrow {
1280
+ color: var(--text-muted);
1281
+ font-weight: bold;
1282
+ flex-shrink: 0;
1283
+ }
1284
+
1285
+ .spawner-badge {
1286
+ display: inline-flex;
1287
+ align-items: center;
1288
+ gap: var(--spacing-xs);
1289
+ padding: var(--spacing-xs) var(--spacing-sm);
1290
+ background: rgba(139, 92, 246, 0.15);
1291
+ border: 1px solid rgba(139, 92, 246, 0.4);
1292
+ border-radius: 3px;
1293
+ font-size: 0.75rem;
1294
+ font-weight: 600;
1295
+ color: #8B5CF6;
1296
+ flex-shrink: 0;
1297
+ }
1298
+
1299
+ .spawner-badge.spawner-gemini {
1300
+ background: rgba(59, 130, 246, 0.15);
1301
+ border-color: rgba(59, 130, 246, 0.4);
1302
+ color: #3B82F6;
1303
+ }
1304
+
1305
+ .spawner-badge.spawner-codex {
1306
+ background: rgba(34, 197, 94, 0.15);
1307
+ border-color: rgba(34, 197, 94, 0.4);
1308
+ color: #22C55E;
1309
+ }
1310
+
1311
+ .spawner-badge.spawner-copilot {
1312
+ background: rgba(251, 146, 60, 0.15);
1313
+ border-color: rgba(251, 146, 60, 0.4);
1314
+ color: #FB923C;
1315
+ }
1316
+
1317
+ .cost-badge {
1318
+ font-size: 0.7rem;
1319
+ opacity: 0.9;
1320
+ margin-left: 2px;
1321
+ }
1322
+
1323
+ .child-duration {
1324
+ font-size: 0.8rem;
1325
+ color: var(--text-secondary);
1326
+ flex-shrink: 0;
1327
+ font-family: monospace;
1328
+ }
1329
+
1330
+ .child-timestamp {
1331
+ font-size: 0.8rem;
1332
+ color: var(--text-muted);
1333
+ flex-shrink: 0;
1334
+ font-family: monospace;
1335
+ }
1336
+
1337
+ @media (max-width: 1024px) {
1338
+ .col-agent { width: 100px; }
1339
+ .col-tool { width: 90px; }
1340
+ .col-status { width: 90px; }
1341
+ .col-timestamp { width: 120px; }
1342
+
1343
+ .activity-table th,
1344
+ .activity-table td {
1345
+ padding: var(--spacing-md) var(--spacing-sm);
1346
+ font-size: 0.9rem;
1347
+ }
1348
+
1349
+ .truncate {
1350
+ max-width: 100%;
1351
+ }
1352
+
1353
+ .child-event-row {
1354
+ flex-wrap: wrap;
1355
+ gap: var(--spacing-xs);
1356
+ padding: var(--spacing-sm);
1357
+ }
1358
+
1359
+ .child-summary {
1360
+ flex-basis: 100%;
1361
+ order: 3;
1362
+ }
1363
+ }
1364
+ </style>
1365
+ </body>
1366
+ </html>