htmlgraph 0.9.3__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 (331) 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 +173 -17
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +127 -0
  7. htmlgraph/agent_registry.py +45 -30
  8. htmlgraph/agents.py +160 -107
  9. htmlgraph/analytics/__init__.py +9 -2
  10. htmlgraph/analytics/cli.py +190 -51
  11. htmlgraph/analytics/cost_analyzer.py +391 -0
  12. htmlgraph/analytics/cost_monitor.py +664 -0
  13. htmlgraph/analytics/cost_reporter.py +675 -0
  14. htmlgraph/analytics/cross_session.py +617 -0
  15. htmlgraph/analytics/dependency.py +192 -100
  16. htmlgraph/analytics/pattern_learning.py +771 -0
  17. htmlgraph/analytics/session_graph.py +707 -0
  18. htmlgraph/analytics/strategic/__init__.py +80 -0
  19. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  20. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  21. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  22. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  23. htmlgraph/analytics/work_type.py +190 -14
  24. htmlgraph/analytics_index.py +135 -51
  25. htmlgraph/api/__init__.py +3 -0
  26. htmlgraph/api/cost_alerts_websocket.py +416 -0
  27. htmlgraph/api/main.py +2498 -0
  28. htmlgraph/api/static/htmx.min.js +1 -0
  29. htmlgraph/api/static/style-redesign.css +1344 -0
  30. htmlgraph/api/static/style.css +1079 -0
  31. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  32. htmlgraph/api/templates/dashboard.html +794 -0
  33. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  34. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  35. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  36. htmlgraph/api/templates/partials/agents.html +317 -0
  37. htmlgraph/api/templates/partials/event-traces.html +373 -0
  38. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  39. htmlgraph/api/templates/partials/features.html +578 -0
  40. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  41. htmlgraph/api/templates/partials/metrics.html +346 -0
  42. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  43. htmlgraph/api/templates/partials/orchestration.html +198 -0
  44. htmlgraph/api/templates/partials/spawners.html +375 -0
  45. htmlgraph/api/templates/partials/work-items.html +613 -0
  46. htmlgraph/api/websocket.py +538 -0
  47. htmlgraph/archive/__init__.py +24 -0
  48. htmlgraph/archive/bloom.py +234 -0
  49. htmlgraph/archive/fts.py +297 -0
  50. htmlgraph/archive/manager.py +583 -0
  51. htmlgraph/archive/search.py +244 -0
  52. htmlgraph/atomic_ops.py +560 -0
  53. htmlgraph/attribute_index.py +208 -0
  54. htmlgraph/bounded_paths.py +539 -0
  55. htmlgraph/builders/__init__.py +14 -0
  56. htmlgraph/builders/base.py +118 -29
  57. htmlgraph/builders/bug.py +150 -0
  58. htmlgraph/builders/chore.py +119 -0
  59. htmlgraph/builders/epic.py +150 -0
  60. htmlgraph/builders/feature.py +31 -6
  61. htmlgraph/builders/insight.py +195 -0
  62. htmlgraph/builders/metric.py +217 -0
  63. htmlgraph/builders/pattern.py +202 -0
  64. htmlgraph/builders/phase.py +162 -0
  65. htmlgraph/builders/spike.py +52 -19
  66. htmlgraph/builders/track.py +148 -72
  67. htmlgraph/cigs/__init__.py +81 -0
  68. htmlgraph/cigs/autonomy.py +385 -0
  69. htmlgraph/cigs/cost.py +475 -0
  70. htmlgraph/cigs/messages_basic.py +472 -0
  71. htmlgraph/cigs/messaging.py +365 -0
  72. htmlgraph/cigs/models.py +771 -0
  73. htmlgraph/cigs/pattern_storage.py +427 -0
  74. htmlgraph/cigs/patterns.py +503 -0
  75. htmlgraph/cigs/posttool_analyzer.py +234 -0
  76. htmlgraph/cigs/reporter.py +818 -0
  77. htmlgraph/cigs/tracker.py +317 -0
  78. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  79. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  80. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  81. htmlgraph/cli/__init__.py +42 -0
  82. htmlgraph/cli/__main__.py +6 -0
  83. htmlgraph/cli/analytics.py +1424 -0
  84. htmlgraph/cli/base.py +685 -0
  85. htmlgraph/cli/constants.py +206 -0
  86. htmlgraph/cli/core.py +954 -0
  87. htmlgraph/cli/main.py +147 -0
  88. htmlgraph/cli/models.py +475 -0
  89. htmlgraph/cli/templates/__init__.py +1 -0
  90. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  91. htmlgraph/cli/work/__init__.py +239 -0
  92. htmlgraph/cli/work/browse.py +115 -0
  93. htmlgraph/cli/work/features.py +568 -0
  94. htmlgraph/cli/work/orchestration.py +676 -0
  95. htmlgraph/cli/work/report.py +728 -0
  96. htmlgraph/cli/work/sessions.py +466 -0
  97. htmlgraph/cli/work/snapshot.py +559 -0
  98. htmlgraph/cli/work/tracks.py +486 -0
  99. htmlgraph/cli_commands/__init__.py +1 -0
  100. htmlgraph/cli_commands/feature.py +195 -0
  101. htmlgraph/cli_framework.py +115 -0
  102. htmlgraph/collections/__init__.py +18 -0
  103. htmlgraph/collections/base.py +415 -98
  104. htmlgraph/collections/bug.py +53 -0
  105. htmlgraph/collections/chore.py +53 -0
  106. htmlgraph/collections/epic.py +53 -0
  107. htmlgraph/collections/feature.py +12 -26
  108. htmlgraph/collections/insight.py +100 -0
  109. htmlgraph/collections/metric.py +92 -0
  110. htmlgraph/collections/pattern.py +97 -0
  111. htmlgraph/collections/phase.py +53 -0
  112. htmlgraph/collections/session.py +194 -0
  113. htmlgraph/collections/spike.py +56 -16
  114. htmlgraph/collections/task_delegation.py +241 -0
  115. htmlgraph/collections/todo.py +511 -0
  116. htmlgraph/collections/traces.py +487 -0
  117. htmlgraph/config/cost_models.json +56 -0
  118. htmlgraph/config.py +190 -0
  119. htmlgraph/context_analytics.py +344 -0
  120. htmlgraph/converter.py +216 -28
  121. htmlgraph/cost_analysis/__init__.py +5 -0
  122. htmlgraph/cost_analysis/analyzer.py +438 -0
  123. htmlgraph/dashboard.html +2406 -307
  124. htmlgraph/dashboard.html.backup +6592 -0
  125. htmlgraph/dashboard.html.bak +7181 -0
  126. htmlgraph/dashboard.html.bak2 +7231 -0
  127. htmlgraph/dashboard.html.bak3 +7232 -0
  128. htmlgraph/db/__init__.py +38 -0
  129. htmlgraph/db/queries.py +790 -0
  130. htmlgraph/db/schema.py +1788 -0
  131. htmlgraph/decorators.py +317 -0
  132. htmlgraph/dependency_models.py +19 -2
  133. htmlgraph/deploy.py +142 -125
  134. htmlgraph/deployment_models.py +474 -0
  135. htmlgraph/docs/API_REFERENCE.md +841 -0
  136. htmlgraph/docs/HTTP_API.md +750 -0
  137. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  138. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  139. htmlgraph/docs/README.md +532 -0
  140. htmlgraph/docs/__init__.py +77 -0
  141. htmlgraph/docs/docs_version.py +55 -0
  142. htmlgraph/docs/metadata.py +93 -0
  143. htmlgraph/docs/migrations.py +232 -0
  144. htmlgraph/docs/template_engine.py +143 -0
  145. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  146. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  147. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  148. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  149. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  150. htmlgraph/docs/version_check.py +163 -0
  151. htmlgraph/edge_index.py +182 -27
  152. htmlgraph/error_handler.py +544 -0
  153. htmlgraph/event_log.py +100 -52
  154. htmlgraph/event_migration.py +13 -4
  155. htmlgraph/exceptions.py +49 -0
  156. htmlgraph/file_watcher.py +101 -28
  157. htmlgraph/find_api.py +75 -63
  158. htmlgraph/git_events.py +145 -63
  159. htmlgraph/graph.py +1122 -106
  160. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  161. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  162. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  163. htmlgraph/hooks/__init__.py +45 -0
  164. htmlgraph/hooks/bootstrap.py +169 -0
  165. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  166. htmlgraph/hooks/concurrent_sessions.py +208 -0
  167. htmlgraph/hooks/context.py +350 -0
  168. htmlgraph/hooks/drift_handler.py +525 -0
  169. htmlgraph/hooks/event_tracker.py +1314 -0
  170. htmlgraph/hooks/git_commands.py +175 -0
  171. htmlgraph/hooks/hooks-config.example.json +12 -0
  172. htmlgraph/hooks/installer.py +343 -0
  173. htmlgraph/hooks/orchestrator.py +674 -0
  174. htmlgraph/hooks/orchestrator_reflector.py +223 -0
  175. htmlgraph/hooks/post-checkout.sh +28 -0
  176. htmlgraph/hooks/post-commit.sh +24 -0
  177. htmlgraph/hooks/post-merge.sh +26 -0
  178. htmlgraph/hooks/post_tool_use_failure.py +273 -0
  179. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  180. htmlgraph/hooks/posttooluse.py +408 -0
  181. htmlgraph/hooks/pre-commit.sh +94 -0
  182. htmlgraph/hooks/pre-push.sh +28 -0
  183. htmlgraph/hooks/pretooluse.py +819 -0
  184. htmlgraph/hooks/prompt_analyzer.py +637 -0
  185. htmlgraph/hooks/session_handler.py +668 -0
  186. htmlgraph/hooks/session_summary.py +395 -0
  187. htmlgraph/hooks/state_manager.py +504 -0
  188. htmlgraph/hooks/subagent_detection.py +202 -0
  189. htmlgraph/hooks/subagent_stop.py +369 -0
  190. htmlgraph/hooks/task_enforcer.py +255 -0
  191. htmlgraph/hooks/task_validator.py +177 -0
  192. htmlgraph/hooks/validator.py +628 -0
  193. htmlgraph/ids.py +41 -27
  194. htmlgraph/index.d.ts +286 -0
  195. htmlgraph/learning.py +767 -0
  196. htmlgraph/mcp_server.py +69 -23
  197. htmlgraph/models.py +1586 -87
  198. htmlgraph/operations/README.md +62 -0
  199. htmlgraph/operations/__init__.py +79 -0
  200. htmlgraph/operations/analytics.py +339 -0
  201. htmlgraph/operations/bootstrap.py +289 -0
  202. htmlgraph/operations/events.py +244 -0
  203. htmlgraph/operations/fastapi_server.py +231 -0
  204. htmlgraph/operations/hooks.py +350 -0
  205. htmlgraph/operations/initialization.py +597 -0
  206. htmlgraph/operations/initialization.py.backup +228 -0
  207. htmlgraph/operations/server.py +303 -0
  208. htmlgraph/orchestration/__init__.py +58 -0
  209. htmlgraph/orchestration/claude_launcher.py +179 -0
  210. htmlgraph/orchestration/command_builder.py +72 -0
  211. htmlgraph/orchestration/headless_spawner.py +281 -0
  212. htmlgraph/orchestration/live_events.py +377 -0
  213. htmlgraph/orchestration/model_selection.py +327 -0
  214. htmlgraph/orchestration/plugin_manager.py +140 -0
  215. htmlgraph/orchestration/prompts.py +137 -0
  216. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  217. htmlgraph/orchestration/spawners/__init__.py +16 -0
  218. htmlgraph/orchestration/spawners/base.py +194 -0
  219. htmlgraph/orchestration/spawners/claude.py +173 -0
  220. htmlgraph/orchestration/spawners/codex.py +435 -0
  221. htmlgraph/orchestration/spawners/copilot.py +294 -0
  222. htmlgraph/orchestration/spawners/gemini.py +471 -0
  223. htmlgraph/orchestration/subprocess_runner.py +36 -0
  224. htmlgraph/orchestration/task_coordination.py +343 -0
  225. htmlgraph/orchestration.md +563 -0
  226. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  227. htmlgraph/orchestrator.py +669 -0
  228. htmlgraph/orchestrator_config.py +357 -0
  229. htmlgraph/orchestrator_mode.py +328 -0
  230. htmlgraph/orchestrator_validator.py +133 -0
  231. htmlgraph/parallel.py +646 -0
  232. htmlgraph/parser.py +160 -35
  233. htmlgraph/path_query.py +608 -0
  234. htmlgraph/pattern_matcher.py +636 -0
  235. htmlgraph/planning.py +147 -52
  236. htmlgraph/pydantic_models.py +476 -0
  237. htmlgraph/quality_gates.py +350 -0
  238. htmlgraph/query_builder.py +109 -72
  239. htmlgraph/query_composer.py +509 -0
  240. htmlgraph/reflection.py +443 -0
  241. htmlgraph/refs.py +344 -0
  242. htmlgraph/repo_hash.py +512 -0
  243. htmlgraph/repositories/__init__.py +292 -0
  244. htmlgraph/repositories/analytics_repository.py +455 -0
  245. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  246. htmlgraph/repositories/feature_repository.py +581 -0
  247. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  248. htmlgraph/repositories/feature_repository_memory.py +607 -0
  249. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  250. htmlgraph/repositories/filter_service.py +620 -0
  251. htmlgraph/repositories/filter_service_standard.py +445 -0
  252. htmlgraph/repositories/shared_cache.py +621 -0
  253. htmlgraph/repositories/shared_cache_memory.py +395 -0
  254. htmlgraph/repositories/track_repository.py +552 -0
  255. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  256. htmlgraph/repositories/track_repository_memory.py +508 -0
  257. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  258. htmlgraph/routing.py +8 -19
  259. htmlgraph/scripts/deploy.py +1 -2
  260. htmlgraph/sdk/__init__.py +398 -0
  261. htmlgraph/sdk/__init__.pyi +14 -0
  262. htmlgraph/sdk/analytics/__init__.py +19 -0
  263. htmlgraph/sdk/analytics/engine.py +155 -0
  264. htmlgraph/sdk/analytics/helpers.py +178 -0
  265. htmlgraph/sdk/analytics/registry.py +109 -0
  266. htmlgraph/sdk/base.py +484 -0
  267. htmlgraph/sdk/constants.py +216 -0
  268. htmlgraph/sdk/core.pyi +308 -0
  269. htmlgraph/sdk/discovery.py +120 -0
  270. htmlgraph/sdk/help/__init__.py +12 -0
  271. htmlgraph/sdk/help/mixin.py +699 -0
  272. htmlgraph/sdk/mixins/__init__.py +15 -0
  273. htmlgraph/sdk/mixins/attribution.py +113 -0
  274. htmlgraph/sdk/mixins/mixin.py +410 -0
  275. htmlgraph/sdk/operations/__init__.py +12 -0
  276. htmlgraph/sdk/operations/mixin.py +427 -0
  277. htmlgraph/sdk/orchestration/__init__.py +17 -0
  278. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  279. htmlgraph/sdk/orchestration/spawner.py +204 -0
  280. htmlgraph/sdk/planning/__init__.py +19 -0
  281. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  282. htmlgraph/sdk/planning/mixin.py +211 -0
  283. htmlgraph/sdk/planning/parallel.py +186 -0
  284. htmlgraph/sdk/planning/queue.py +210 -0
  285. htmlgraph/sdk/planning/recommendations.py +87 -0
  286. htmlgraph/sdk/planning/smart_planning.py +319 -0
  287. htmlgraph/sdk/session/__init__.py +19 -0
  288. htmlgraph/sdk/session/continuity.py +57 -0
  289. htmlgraph/sdk/session/handoff.py +110 -0
  290. htmlgraph/sdk/session/info.py +309 -0
  291. htmlgraph/sdk/session/manager.py +103 -0
  292. htmlgraph/sdk/strategic/__init__.py +26 -0
  293. htmlgraph/sdk/strategic/mixin.py +563 -0
  294. htmlgraph/server.py +685 -180
  295. htmlgraph/services/__init__.py +10 -0
  296. htmlgraph/services/claiming.py +199 -0
  297. htmlgraph/session_hooks.py +300 -0
  298. htmlgraph/session_manager.py +1392 -175
  299. htmlgraph/session_registry.py +587 -0
  300. htmlgraph/session_state.py +436 -0
  301. htmlgraph/session_warning.py +201 -0
  302. htmlgraph/sessions/__init__.py +23 -0
  303. htmlgraph/sessions/handoff.py +756 -0
  304. htmlgraph/setup.py +34 -17
  305. htmlgraph/spike_index.py +143 -0
  306. htmlgraph/sync_docs.py +12 -15
  307. htmlgraph/system_prompts.py +450 -0
  308. htmlgraph/templates/AGENTS.md.template +366 -0
  309. htmlgraph/templates/CLAUDE.md.template +97 -0
  310. htmlgraph/templates/GEMINI.md.template +87 -0
  311. htmlgraph/templates/orchestration-view.html +350 -0
  312. htmlgraph/track_builder.py +146 -15
  313. htmlgraph/track_manager.py +69 -21
  314. htmlgraph/transcript.py +890 -0
  315. htmlgraph/transcript_analytics.py +699 -0
  316. htmlgraph/types.py +323 -0
  317. htmlgraph/validation.py +115 -0
  318. htmlgraph/watch.py +8 -5
  319. htmlgraph/work_type_utils.py +3 -2
  320. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2406 -307
  321. htmlgraph-0.27.5.data/data/htmlgraph/templates/AGENTS.md.template +366 -0
  322. htmlgraph-0.27.5.data/data/htmlgraph/templates/CLAUDE.md.template +97 -0
  323. htmlgraph-0.27.5.data/data/htmlgraph/templates/GEMINI.md.template +87 -0
  324. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +97 -64
  325. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  326. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  327. htmlgraph/cli.py +0 -2688
  328. htmlgraph/sdk.py +0 -709
  329. htmlgraph-0.9.3.dist-info/RECORD +0 -61
  330. {htmlgraph-0.9.3.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  331. {htmlgraph-0.9.3.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
htmlgraph/dashboard.html CHANGED
@@ -4,11 +4,9 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>HtmlGraph Dashboard</title>
7
- <!-- d3-force for graph layout simulation -->
8
- <script src="https://d3js.org/d3-dispatch.v3.min.js"></script>
9
- <script src="https://d3js.org/d3-quadtree.v3.min.js"></script>
10
- <script src="https://d3js.org/d3-timer.v3.min.js"></script>
11
- <script src="https://d3js.org/d3-force.v3.min.js"></script>
7
+ <!-- Vis.js for graph visualization -->
8
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
9
+ <link href="https://unpkg.com/vis-network/styles/vis-network.min.css" rel="stylesheet" type="text/css" />
12
10
  <!-- Typography: JetBrains Mono + Outfit -->
13
11
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
12
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
@@ -106,9 +104,11 @@
106
104
  background: var(--bg-primary);
107
105
  color: var(--text-primary);
108
106
  line-height: 1.5;
109
- min-height: 100vh;
107
+ height: 100vh;
110
108
  position: relative;
111
- overflow-x: hidden;
109
+ overflow: hidden;
110
+ display: flex;
111
+ flex-direction: column;
112
112
  }
113
113
 
114
114
  /* Subtle grain texture overlay */
@@ -126,6 +126,12 @@
126
126
  max-width: 1440px;
127
127
  margin: 0 auto;
128
128
  padding: 2rem;
129
+ display: flex;
130
+ flex-direction: column;
131
+ flex: 1;
132
+ min-height: 0;
133
+ width: 100%;
134
+ overflow: hidden;
129
135
  }
130
136
 
131
137
  /* ================================================================
@@ -211,6 +217,7 @@
211
217
  margin-bottom: 1rem;
212
218
  border: 2px solid var(--border-strong);
213
219
  width: fit-content;
220
+ flex-shrink: 0;
214
221
  }
215
222
 
216
223
  .view-btn {
@@ -252,8 +259,11 @@
252
259
  }
253
260
 
254
261
  .kanban.active {
255
- display: block;
262
+ display: grid;
256
263
  width: 100%;
264
+ flex: 1;
265
+ min-height: 0;
266
+ overflow: auto;
257
267
  }
258
268
 
259
269
  /* Dynamic grid: expanded columns grow, collapsed stay fixed */
@@ -871,6 +881,53 @@
871
881
  .badge.priority-high { background: var(--priority-high); color: white; border-color: var(--priority-high); }
872
882
  .badge.type { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
873
883
 
884
+ /* Agent attribution badges - color-coded by agent type */
885
+ .badge.agent {
886
+ padding: 0.375rem 0.625rem;
887
+ font-weight: 600;
888
+ display: inline-flex;
889
+ align-items: center;
890
+ gap: 0.3rem;
891
+ position: relative;
892
+ transition: all 0.2s ease;
893
+ cursor: help;
894
+ }
895
+
896
+ .badge.agent::before {
897
+ content: '';
898
+ display: inline-block;
899
+ width: 0.5rem;
900
+ height: 0.5rem;
901
+ border-radius: 50%;
902
+ background: currentColor;
903
+ opacity: 0.8;
904
+ }
905
+
906
+ .badge.agent:hover {
907
+ transform: translateY(-2px);
908
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
909
+ }
910
+
911
+ /* Primary agents - Requested color system */
912
+ .badge.agent-claude { background: #2979FF; color: white; border-color: #2979FF; }
913
+ .badge.agent-codex { background: #00C853; color: white; border-color: #00C853; }
914
+ .badge.agent-orchestrator { background: #7C4DFF; color: white; border-color: #7C4DFF; }
915
+ .badge.agent-gemini { background: #FBC02D; color: #000; border-color: #FBC02D; }
916
+ .badge.agent-gemini-2 { background: #FF9100; color: white; border-color: #FF9100; }
917
+
918
+ /* Secondary agents - Backward compatibility */
919
+ .badge.agent-analyst { background: #7C3AED; color: white; border-color: #7C3AED; }
920
+ .badge.agent-developer { background: #00C853; color: white; border-color: #00C853; }
921
+ .badge.agent-researcher { background: #FF6D00; color: white; border-color: #FF6D00; }
922
+ .badge.agent-debugger { background: #E91E63; color: white; border-color: #E91E63; }
923
+ .badge.agent-default { background: #78909C; color: white; border-color: #78909C; }
924
+
925
+ /* Delegation badges */
926
+ .badge.delegation { padding: 0.25rem 0.5rem; font-size: 0.55rem; }
927
+ .badge.delegation-external { background: #00C853; color: white; }
928
+ .badge.delegation-fallback { background: #FF9100; color: white; }
929
+ .badge.delegation-direct { background: #2979FF; color: white; }
930
+
874
931
  .card-path {
875
932
  font-family: 'JetBrains Mono', monospace;
876
933
  font-size: 0.625rem;
@@ -895,18 +952,127 @@
895
952
  background: var(--bg-secondary);
896
953
  border: 2px solid var(--border-strong);
897
954
  box-shadow: var(--shadow-md);
955
+ flex-direction: column;
956
+ flex: 1;
957
+ min-height: 0;
958
+ overflow: hidden;
898
959
  }
899
960
 
900
961
  .graph-container.active {
901
- display: block;
962
+ display: flex;
963
+ }
964
+
965
+ /* Graph Controls */
966
+ .graph-controls {
967
+ display: flex;
968
+ gap: 1.5rem;
969
+ align-items: center;
970
+ padding: 1rem;
971
+ border-bottom: 2px solid var(--border-strong);
972
+ background: var(--bg-tertiary);
973
+ flex-wrap: wrap;
974
+ }
975
+
976
+ .graph-control-group {
977
+ display: flex;
978
+ align-items: center;
979
+ gap: 0.75rem;
980
+ }
981
+
982
+ .graph-filter-label {
983
+ font-family: 'JetBrains Mono', monospace;
984
+ font-size: 0.75rem;
985
+ text-transform: uppercase;
986
+ letter-spacing: 0.1em;
987
+ color: var(--text-muted);
988
+ font-weight: 600;
989
+ white-space: nowrap;
990
+ }
991
+
992
+ .graph-filters {
993
+ display: flex;
994
+ gap: 1rem;
995
+ flex-wrap: wrap;
996
+ }
997
+
998
+ .graph-filter-checkbox {
999
+ display: flex;
1000
+ align-items: center;
1001
+ gap: 0.4rem;
1002
+ cursor: pointer;
1003
+ font-size: 0.875rem;
1004
+ user-select: none;
1005
+ }
1006
+
1007
+ .graph-filter-checkbox input {
1008
+ cursor: pointer;
1009
+ width: 16px;
1010
+ height: 16px;
1011
+ accent-color: var(--accent);
1012
+ }
1013
+
1014
+ .graph-search {
1015
+ padding: 0.5rem 0.75rem;
1016
+ border: 2px solid var(--border-strong);
1017
+ background: var(--bg-secondary);
1018
+ color: var(--text-primary);
1019
+ font-family: 'JetBrains Mono', monospace;
1020
+ font-size: 0.875rem;
1021
+ border-radius: 0;
1022
+ min-width: 150px;
1023
+ }
1024
+
1025
+ .graph-search::placeholder {
1026
+ color: var(--text-muted);
1027
+ }
1028
+
1029
+ .graph-search:focus {
1030
+ outline: none;
1031
+ box-shadow: inset 0 0 0 2px var(--accent);
1032
+ }
1033
+
1034
+ .graph-control-buttons {
1035
+ margin-left: auto;
1036
+ }
1037
+
1038
+ /* Graph Viewport */
1039
+ .graph-viewport {
1040
+ flex: 1;
1041
+ min-height: 0;
1042
+ position: relative;
1043
+ overflow: hidden;
1044
+ background: var(--bg-secondary);
1045
+ width: 100%;
902
1046
  }
903
1047
 
904
1048
  .graph-svg {
905
1049
  width: 100%;
906
- height: 600px;
1050
+ height: 100%;
907
1051
  display: block;
908
1052
  }
909
1053
 
1054
+ /* Vis.js Network Container */
1055
+ .graph-network {
1056
+ width: 100%;
1057
+ height: 100%;
1058
+ border: none;
1059
+ }
1060
+
1061
+ /* Vis.js Node Styling */
1062
+ .vis-network {
1063
+ border: none;
1064
+ background: var(--bg-secondary);
1065
+ }
1066
+
1067
+ /* Vis.js Label Styling */
1068
+ .vis-label {
1069
+ color: white;
1070
+ font-family: 'JetBrains Mono', monospace;
1071
+ font-size: 12px;
1072
+ font-weight: 500;
1073
+ }
1074
+
1075
+ /* Node Status Classes */
910
1076
  .graph-node {
911
1077
  cursor: pointer;
912
1078
  }
@@ -915,10 +1081,39 @@
915
1081
  stroke: var(--border-strong);
916
1082
  stroke-width: 2;
917
1083
  transition: all 0.2s var(--ease-out-expo);
1084
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
918
1085
  }
919
1086
 
920
1087
  .graph-node:hover circle {
921
1088
  stroke-width: 4;
1089
+ filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
1090
+ }
1091
+
1092
+ .graph-node.node-hidden {
1093
+ display: none;
1094
+ }
1095
+
1096
+ .graph-node.node-search-highlight circle {
1097
+ stroke: var(--accent);
1098
+ stroke-width: 3;
1099
+ }
1100
+
1101
+ /* Status-based sizing */
1102
+ .graph-node.node-done circle {
1103
+ opacity: 0.6;
1104
+ r: 15;
1105
+ }
1106
+
1107
+ .graph-node.node-in-progress circle {
1108
+ r: 25;
1109
+ }
1110
+
1111
+ .graph-node.node-todo circle {
1112
+ r: 22;
1113
+ }
1114
+
1115
+ .graph-node.node-blocked circle {
1116
+ r: 22;
922
1117
  }
923
1118
 
924
1119
  .graph-node text {
@@ -932,8 +1127,9 @@
932
1127
 
933
1128
  .graph-edge {
934
1129
  stroke: var(--border);
935
- stroke-width: 2;
1130
+ stroke-width: 1.5;
936
1131
  fill: none;
1132
+ transition: stroke-width 0.2s var(--ease-out-expo);
937
1133
  }
938
1134
 
939
1135
  .graph-edge.blocked_by {
@@ -945,17 +1141,47 @@
945
1141
  stroke: var(--status-active);
946
1142
  }
947
1143
 
1144
+ .graph-edge.hidden {
1145
+ display: none;
1146
+ }
1147
+
948
1148
  .graph-arrowhead {
949
1149
  fill: var(--border);
950
1150
  }
951
1151
 
952
- .graph-legend {
1152
+ .graph-edge.blocked_by .graph-arrowhead {
1153
+ fill: var(--status-blocked);
1154
+ }
1155
+
1156
+ .graph-edge.related .graph-arrowhead {
1157
+ fill: var(--status-active);
1158
+ }
1159
+
1160
+ /* Graph Footer */
1161
+ .graph-footer {
953
1162
  display: flex;
954
- gap: 2rem;
955
- justify-content: center;
1163
+ justify-content: space-between;
1164
+ align-items: center;
956
1165
  padding: 1rem;
957
1166
  border-top: 2px solid var(--border-strong);
958
1167
  background: var(--bg-tertiary);
1168
+ flex-wrap: wrap;
1169
+ gap: 1rem;
1170
+ }
1171
+
1172
+ .graph-stats {
1173
+ display: flex;
1174
+ gap: 1.5rem;
1175
+ font-family: 'JetBrains Mono', monospace;
1176
+ font-size: 0.875rem;
1177
+ color: var(--text-secondary);
1178
+ }
1179
+
1180
+ .graph-legend {
1181
+ display: flex;
1182
+ gap: 1.5rem;
1183
+ justify-content: center;
1184
+ flex-wrap: wrap;
959
1185
  }
960
1186
 
961
1187
  .graph-legend-item {
@@ -963,20 +1189,29 @@
963
1189
  align-items: center;
964
1190
  gap: 0.5rem;
965
1191
  font-family: 'JetBrains Mono', monospace;
966
- font-size: 0.6875rem;
1192
+ font-size: 0.75rem;
967
1193
  text-transform: uppercase;
968
1194
  letter-spacing: 0.1em;
969
1195
  color: var(--text-muted);
970
1196
  }
971
1197
 
972
1198
  .graph-legend-item span {
973
- width: 24px;
1199
+ width: 20px;
974
1200
  height: 3px;
975
1201
  }
976
1202
 
1203
+ .graph-legend-item span.legend-color {
1204
+ width: 12px;
1205
+ height: 12px;
1206
+ border-radius: 2px;
1207
+ }
1208
+
1209
+ .legend-done { background: var(--status-done); }
1210
+ .legend-active { background: var(--status-active); }
1211
+ .legend-todo { background: var(--status-todo); }
977
1212
  .legend-blocked { background: var(--status-blocked); }
978
1213
  .legend-related { background: var(--status-active); }
979
- .legend-default { background: var(--border); }
1214
+ .legend-blocked-edge { background: var(--status-blocked); }
980
1215
 
981
1216
  /* ================================================================
982
1217
  ANALYTICS VIEW
@@ -987,7 +1222,11 @@
987
1222
  }
988
1223
 
989
1224
  .analytics.active {
990
- display: block;
1225
+ display: flex;
1226
+ flex-direction: column;
1227
+ flex: 1;
1228
+ min-height: 0;
1229
+ overflow: auto;
991
1230
  }
992
1231
 
993
1232
  /* ================================================================
@@ -999,84 +1238,805 @@
999
1238
  }
1000
1239
 
1001
1240
  .sessions.active {
1002
- display: block;
1241
+ display: flex;
1242
+ flex-direction: column;
1243
+ flex: 1;
1244
+ min-height: 0;
1245
+ overflow: auto;
1003
1246
  }
1004
1247
 
1005
1248
  /* ================================================================
1006
- TRACKS VIEW
1249
+ AGENTS VIEW - Multi-Agent Work Attribution
1007
1250
  ================================================================ */
1008
- .tracks {
1251
+ .agents {
1009
1252
  display: none;
1253
+ margin-top: 1.5rem;
1254
+ padding: 0 1.5rem 1.5rem 1.5rem;
1010
1255
  }
1011
1256
 
1012
- .tracks.active {
1013
- display: block;
1257
+ .agents.active {
1258
+ display: flex;
1259
+ flex-direction: column;
1260
+ flex: 1;
1261
+ min-height: 0;
1262
+ overflow: auto;
1014
1263
  }
1015
1264
 
1016
- .sessions-table {
1017
- width: 100%;
1018
- border-collapse: collapse;
1019
- font-family: 'JetBrains Mono', monospace;
1020
- font-size: 0.875rem;
1021
- background: var(--bg-secondary);
1022
- border: 2px solid var(--border-strong);
1023
- box-shadow: var(--shadow-md);
1265
+ .agent-stats-grid {
1266
+ display: grid;
1267
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1268
+ gap: 1rem;
1269
+ margin: 1rem 0;
1024
1270
  }
1025
1271
 
1026
- .sessions-table th,
1027
- .sessions-table td {
1028
- border-bottom: 1px solid var(--border);
1029
- padding: 0.875rem 1rem;
1030
- text-align: left;
1031
- vertical-align: middle;
1272
+ .agent-stat-card {
1273
+ background: var(--bg-secondary);
1274
+ border: 2px solid var(--border);
1275
+ border-radius: 8px;
1276
+ padding: 1.5rem;
1277
+ text-align: center;
1278
+ box-shadow: var(--shadow-sm);
1032
1279
  }
1033
1280
 
1034
- .sessions-table th {
1281
+ .agent-stat-card h4 {
1035
1282
  color: var(--text-muted);
1036
- font-weight: 600;
1283
+ font-size: 0.75rem;
1037
1284
  text-transform: uppercase;
1038
1285
  letter-spacing: 0.08em;
1039
- font-size: 0.625rem;
1040
- background: var(--bg-tertiary);
1041
- border-bottom: 2px solid var(--border-strong);
1286
+ font-weight: 600;
1287
+ margin-bottom: 0.5rem;
1042
1288
  }
1043
1289
 
1044
- .sessions-table tr:hover td {
1045
- background: var(--bg-tertiary);
1290
+ .agent-stat-value {
1291
+ font-size: 1.75rem;
1292
+ font-weight: 700;
1293
+ color: var(--text-primary);
1294
+ font-family: 'JetBrains Mono', monospace;
1046
1295
  }
1047
1296
 
1048
- .sessions-table .session-id {
1297
+ .agent-stat-unit {
1298
+ font-size: 0.75rem;
1299
+ color: var(--text-muted);
1300
+ margin-top: 0.25rem;
1301
+ }
1302
+
1303
+ /* ================================================================
1304
+ WORKLOAD DISTRIBUTION CHART
1305
+ ================================================================ */
1306
+ .workload-chart-container {
1307
+ background: var(--bg-secondary);
1308
+ border: 2px solid var(--border);
1309
+ border-radius: 8px;
1310
+ padding: 1.5rem;
1311
+ box-shadow: var(--shadow-sm);
1312
+ margin-top: 1rem;
1313
+ }
1314
+
1315
+ .workload-chart-header {
1316
+ margin-bottom: 1.5rem;
1317
+ }
1318
+
1319
+ .workload-chart-header h3 {
1320
+ font-family: 'JetBrains Mono', monospace;
1321
+ font-size: 0.875rem;
1322
+ text-transform: uppercase;
1323
+ letter-spacing: 0.08em;
1324
+ color: var(--text-muted);
1325
+ margin-bottom: 0.25rem;
1049
1326
  font-weight: 600;
1050
- color: var(--status-active);
1051
- cursor: pointer;
1052
- text-decoration: underline;
1053
- text-underline-offset: 2px;
1054
1327
  }
1055
1328
 
1056
- .sessions-table .session-id:hover {
1057
- color: var(--accent);
1329
+ .workload-chart-header p {
1330
+ color: var(--text-secondary);
1331
+ font-size: 0.875rem;
1332
+ margin: 0;
1058
1333
  }
1059
1334
 
1060
- /* Session Filters */
1061
- .session-filters {
1335
+ .workload-bars {
1062
1336
  display: flex;
1337
+ flex-direction: column;
1063
1338
  gap: 1rem;
1064
- padding: 1rem 1.5rem;
1065
- background: var(--bg-secondary);
1066
- border: 2px solid var(--border-strong);
1067
- box-shadow: var(--shadow-md);
1068
- margin-bottom: 1rem;
1069
- flex-wrap: wrap;
1070
- align-items: flex-end;
1339
+ max-height: 600px;
1340
+ overflow-y: auto;
1071
1341
  }
1072
1342
 
1073
- .filter-group {
1343
+ .workload-bar-group {
1074
1344
  display: flex;
1075
1345
  flex-direction: column;
1076
1346
  gap: 0.375rem;
1077
1347
  }
1078
1348
 
1079
- .filter-group label {
1349
+ .workload-bar-label {
1350
+ display: flex;
1351
+ justify-content: space-between;
1352
+ align-items: center;
1353
+ font-size: 0.875rem;
1354
+ font-weight: 500;
1355
+ color: var(--text-primary);
1356
+ margin-bottom: 0.25rem;
1357
+ }
1358
+
1359
+ .workload-bar-label-name {
1360
+ display: flex;
1361
+ align-items: center;
1362
+ gap: 0.5rem;
1363
+ flex: 1;
1364
+ }
1365
+
1366
+ .workload-agent-badge {
1367
+ display: inline-flex;
1368
+ align-items: center;
1369
+ justify-content: center;
1370
+ width: 24px;
1371
+ height: 24px;
1372
+ border-radius: 50%;
1373
+ font-size: 0.7rem;
1374
+ font-weight: 600;
1375
+ color: white;
1376
+ flex-shrink: 0;
1377
+ }
1378
+
1379
+ .workload-bar-value {
1380
+ font-family: 'JetBrains Mono', monospace;
1381
+ font-size: 0.8rem;
1382
+ color: var(--text-muted);
1383
+ white-space: nowrap;
1384
+ }
1385
+
1386
+ .workload-bar-container {
1387
+ position: relative;
1388
+ width: 100%;
1389
+ height: 32px;
1390
+ background: var(--bg-tertiary);
1391
+ border: 1px solid var(--border);
1392
+ border-radius: 4px;
1393
+ overflow: hidden;
1394
+ }
1395
+
1396
+ .workload-bar-fill {
1397
+ height: 100%;
1398
+ display: flex;
1399
+ align-items: center;
1400
+ padding: 0 0.75rem;
1401
+ transition: all 0.3s var(--ease-out-expo);
1402
+ position: relative;
1403
+ justify-content: flex-start;
1404
+ }
1405
+
1406
+ .workload-bar-fill::after {
1407
+ content: '';
1408
+ position: absolute;
1409
+ inset: 0;
1410
+ background: linear-gradient(90deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);
1411
+ pointer-events: none;
1412
+ }
1413
+
1414
+ .workload-bar-text {
1415
+ position: absolute;
1416
+ right: 0.75rem;
1417
+ top: 50%;
1418
+ transform: translateY(-50%);
1419
+ color: white;
1420
+ font-size: 0.75rem;
1421
+ font-weight: 600;
1422
+ font-family: 'JetBrains Mono', monospace;
1423
+ white-space: nowrap;
1424
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
1425
+ pointer-events: none;
1426
+ z-index: 2;
1427
+ }
1428
+
1429
+ .workload-bar-hover-info {
1430
+ position: absolute;
1431
+ bottom: 100%;
1432
+ left: 50%;
1433
+ transform: translateX(-50%);
1434
+ background: var(--bg-tertiary);
1435
+ border: 1px solid var(--border-strong);
1436
+ border-radius: 4px;
1437
+ padding: 0.75rem;
1438
+ font-size: 0.75rem;
1439
+ white-space: nowrap;
1440
+ pointer-events: none;
1441
+ opacity: 0;
1442
+ visibility: hidden;
1443
+ transition: all 0.2s;
1444
+ z-index: 100;
1445
+ margin-bottom: 0.5rem;
1446
+ box-shadow: var(--shadow-md);
1447
+ }
1448
+
1449
+ .workload-bar-container:hover .workload-bar-hover-info {
1450
+ opacity: 1;
1451
+ visibility: visible;
1452
+ }
1453
+
1454
+ .workload-bar-hover-info::after {
1455
+ content: '';
1456
+ position: absolute;
1457
+ top: 100%;
1458
+ left: 50%;
1459
+ transform: translateX(-50%);
1460
+ border: 6px solid transparent;
1461
+ border-top-color: var(--border-strong);
1462
+ }
1463
+
1464
+ .workload-chart-legend {
1465
+ display: flex;
1466
+ flex-wrap: wrap;
1467
+ gap: 1.5rem;
1468
+ margin-top: 1.5rem;
1469
+ padding-top: 1.5rem;
1470
+ border-top: 1px solid var(--border);
1471
+ font-size: 0.875rem;
1472
+ }
1473
+
1474
+ .workload-legend-item {
1475
+ display: flex;
1476
+ align-items: center;
1477
+ gap: 0.5rem;
1478
+ }
1479
+
1480
+ .workload-legend-color {
1481
+ width: 16px;
1482
+ height: 16px;
1483
+ border-radius: 3px;
1484
+ flex-shrink: 0;
1485
+ }
1486
+
1487
+ .workload-legend-label {
1488
+ color: var(--text-secondary);
1489
+ }
1490
+
1491
+ /* Agent Color System */
1492
+ .agent-claude { background: linear-gradient(135deg, #6366f1, #818cf8); }
1493
+ .agent-codex { background: linear-gradient(135deg, #10b981, #34d399); }
1494
+ .agent-orchestrator { background: linear-gradient(135deg, #f59e0b, #fbbf24); }
1495
+ .agent-gemini-2 { background: linear-gradient(135deg, #8b5cf6, #a78bfa); }
1496
+ .agent-gemini { background: linear-gradient(135deg, #ec4899, #f472b6); }
1497
+ .agent-analyst { background: linear-gradient(135deg, #0ea5e9, #38bdf8); }
1498
+ .agent-developer { background: linear-gradient(135deg, #06b6d4, #22d3ee); }
1499
+
1500
+ /* ================================================================
1501
+ AGENT COST VISUALIZATION
1502
+ ================================================================ */
1503
+ .cost-breakdown-container {
1504
+ background: var(--bg-secondary);
1505
+ border: 2px solid var(--border);
1506
+ border-radius: 8px;
1507
+ padding: 1.5rem;
1508
+ box-shadow: var(--shadow-sm);
1509
+ margin-top: 1rem;
1510
+ }
1511
+
1512
+ .cost-breakdown-header {
1513
+ margin-bottom: 1.5rem;
1514
+ }
1515
+
1516
+ .cost-breakdown-header h3 {
1517
+ font-family: 'JetBrains Mono', monospace;
1518
+ font-size: 0.875rem;
1519
+ text-transform: uppercase;
1520
+ letter-spacing: 0.08em;
1521
+ color: var(--text-muted);
1522
+ margin-bottom: 0.25rem;
1523
+ font-weight: 600;
1524
+ }
1525
+
1526
+ .cost-breakdown-header p {
1527
+ color: var(--text-secondary);
1528
+ font-size: 0.875rem;
1529
+ margin: 0;
1530
+ }
1531
+
1532
+ .cost-summary-metrics {
1533
+ display: grid;
1534
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1535
+ gap: 1rem;
1536
+ margin-bottom: 1.5rem;
1537
+ }
1538
+
1539
+ .cost-metric {
1540
+ background: var(--bg-tertiary);
1541
+ border: 1px solid var(--border);
1542
+ border-radius: 4px;
1543
+ padding: 1rem;
1544
+ text-align: center;
1545
+ }
1546
+
1547
+ .cost-metric-label {
1548
+ font-size: 0.7rem;
1549
+ text-transform: uppercase;
1550
+ letter-spacing: 0.08em;
1551
+ color: var(--text-muted);
1552
+ margin-bottom: 0.5rem;
1553
+ font-weight: 600;
1554
+ }
1555
+
1556
+ .cost-metric-value {
1557
+ font-family: 'JetBrains Mono', monospace;
1558
+ font-size: 1.5rem;
1559
+ font-weight: 700;
1560
+ color: var(--text-primary);
1561
+ }
1562
+
1563
+ .cost-metric-unit {
1564
+ font-size: 0.65rem;
1565
+ color: var(--text-muted);
1566
+ margin-top: 0.25rem;
1567
+ }
1568
+
1569
+ .cost-bars {
1570
+ display: flex;
1571
+ flex-direction: column;
1572
+ gap: 1.25rem;
1573
+ max-height: 600px;
1574
+ overflow-y: auto;
1575
+ }
1576
+
1577
+ .cost-bar-group {
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ gap: 0.375rem;
1581
+ }
1582
+
1583
+ .cost-bar-label {
1584
+ display: flex;
1585
+ justify-content: space-between;
1586
+ align-items: center;
1587
+ font-size: 0.875rem;
1588
+ font-weight: 500;
1589
+ color: var(--text-primary);
1590
+ margin-bottom: 0.375rem;
1591
+ }
1592
+
1593
+ .cost-bar-label-name {
1594
+ display: flex;
1595
+ align-items: center;
1596
+ gap: 0.5rem;
1597
+ flex: 1;
1598
+ }
1599
+
1600
+ .cost-agent-badge {
1601
+ display: inline-flex;
1602
+ align-items: center;
1603
+ justify-content: center;
1604
+ width: 24px;
1605
+ height: 24px;
1606
+ border-radius: 50%;
1607
+ font-size: 0.7rem;
1608
+ font-weight: 600;
1609
+ color: white;
1610
+ flex-shrink: 0;
1611
+ }
1612
+
1613
+ .cost-bar-stats {
1614
+ display: flex;
1615
+ gap: 1rem;
1616
+ font-family: 'JetBrains Mono', monospace;
1617
+ font-size: 0.75rem;
1618
+ }
1619
+
1620
+ .cost-bar-stat {
1621
+ display: flex;
1622
+ flex-direction: column;
1623
+ gap: 0.125rem;
1624
+ }
1625
+
1626
+ .cost-bar-stat-label {
1627
+ color: var(--text-muted);
1628
+ font-size: 0.65rem;
1629
+ text-transform: uppercase;
1630
+ letter-spacing: 0.05em;
1631
+ }
1632
+
1633
+ .cost-bar-stat-value {
1634
+ color: var(--text-primary);
1635
+ font-weight: 600;
1636
+ }
1637
+
1638
+ .cost-bar-container {
1639
+ position: relative;
1640
+ width: 100%;
1641
+ height: 40px;
1642
+ background: var(--bg-tertiary);
1643
+ border: 1px solid var(--border);
1644
+ border-radius: 4px;
1645
+ overflow: hidden;
1646
+ }
1647
+
1648
+ .cost-bar-stacked {
1649
+ display: flex;
1650
+ height: 100%;
1651
+ width: 100%;
1652
+ position: relative;
1653
+ overflow: hidden;
1654
+ }
1655
+
1656
+ .cost-bar-segment {
1657
+ height: 100%;
1658
+ display: flex;
1659
+ align-items: center;
1660
+ justify-content: center;
1661
+ position: relative;
1662
+ transition: all 0.3s var(--ease-out-expo);
1663
+ flex-grow: 1;
1664
+ min-width: 2px;
1665
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
1666
+ }
1667
+
1668
+ .cost-bar-segment:last-child {
1669
+ border-right: none;
1670
+ }
1671
+
1672
+ .cost-bar-segment::after {
1673
+ content: '';
1674
+ position: absolute;
1675
+ inset: 0;
1676
+ background: linear-gradient(90deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 100%);
1677
+ pointer-events: none;
1678
+ }
1679
+
1680
+ .cost-bar-segment-label {
1681
+ position: relative;
1682
+ z-index: 2;
1683
+ font-size: 0.65rem;
1684
+ font-weight: 600;
1685
+ font-family: 'JetBrains Mono', monospace;
1686
+ color: white;
1687
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
1688
+ white-space: nowrap;
1689
+ padding: 0 0.3rem;
1690
+ pointer-events: none;
1691
+ }
1692
+
1693
+ .cost-bar-tooltip {
1694
+ position: absolute;
1695
+ bottom: 100%;
1696
+ left: 50%;
1697
+ transform: translateX(-50%);
1698
+ background: var(--bg-tertiary);
1699
+ border: 1px solid var(--border-strong);
1700
+ border-radius: 4px;
1701
+ padding: 0.75rem;
1702
+ font-size: 0.75rem;
1703
+ pointer-events: none;
1704
+ opacity: 0;
1705
+ visibility: hidden;
1706
+ transition: all 0.2s;
1707
+ z-index: 100;
1708
+ margin-bottom: 0.5rem;
1709
+ box-shadow: var(--shadow-md);
1710
+ white-space: nowrap;
1711
+ }
1712
+
1713
+ .cost-bar-segment:hover ~ .cost-bar-tooltip,
1714
+ .cost-bar-container:hover .cost-bar-tooltip {
1715
+ opacity: 1;
1716
+ visibility: visible;
1717
+ }
1718
+
1719
+ .cost-bar-tooltip::after {
1720
+ content: '';
1721
+ position: absolute;
1722
+ top: 100%;
1723
+ left: 50%;
1724
+ transform: translateX(-50%);
1725
+ border: 6px solid transparent;
1726
+ border-top-color: var(--border-strong);
1727
+ }
1728
+
1729
+ .cost-range-indicator {
1730
+ display: flex;
1731
+ align-items: center;
1732
+ gap: 0.5rem;
1733
+ font-size: 0.7rem;
1734
+ color: var(--text-muted);
1735
+ margin-top: 0.25rem;
1736
+ }
1737
+
1738
+ .cost-range-dot {
1739
+ width: 8px;
1740
+ height: 8px;
1741
+ border-radius: 50%;
1742
+ flex-shrink: 0;
1743
+ }
1744
+
1745
+ .cost-range-dot.low { background: #10b981; }
1746
+ .cost-range-dot.medium { background: #f59e0b; }
1747
+ .cost-range-dot.high { background: #ef4444; }
1748
+
1749
+ .cost-breakdown-legend {
1750
+ display: flex;
1751
+ flex-wrap: wrap;
1752
+ gap: 1.5rem;
1753
+ margin-top: 1.5rem;
1754
+ padding-top: 1.5rem;
1755
+ border-top: 1px solid var(--border);
1756
+ font-size: 0.875rem;
1757
+ }
1758
+
1759
+ .cost-legend-item {
1760
+ display: flex;
1761
+ align-items: center;
1762
+ gap: 0.5rem;
1763
+ }
1764
+
1765
+ .cost-legend-color {
1766
+ width: 16px;
1767
+ height: 16px;
1768
+ border-radius: 3px;
1769
+ flex-shrink: 0;
1770
+ }
1771
+
1772
+ .cost-legend-label {
1773
+ color: var(--text-secondary);
1774
+ }
1775
+
1776
+ /* Agent cost colors - match agent system */
1777
+ .cost-claude { background: #2979FF; }
1778
+ .cost-codex { background: #00C853; }
1779
+ .cost-orchestrator { background: #7C4DFF; }
1780
+ .cost-gemini { background: #FBC02D; }
1781
+ .cost-gemini-2 { background: #FF9100; }
1782
+ .agent-researcher { background: linear-gradient(135deg, #d946ef, #e879f9); }
1783
+ .agent-debugger { background: linear-gradient(135deg, #ef4444, #f87171); }
1784
+ .agent-default { background: linear-gradient(135deg, #6b7280, #9ca3af); }
1785
+
1786
+ .workload-empty-state {
1787
+ text-align: center;
1788
+ padding: 2rem;
1789
+ color: var(--text-muted);
1790
+ }
1791
+
1792
+ .workload-empty-state svg {
1793
+ width: 48px;
1794
+ height: 48px;
1795
+ margin: 0 auto 1rem;
1796
+ opacity: 0.5;
1797
+ }
1798
+
1799
+ .workload-chart-responsive {
1800
+ max-height: 500px;
1801
+ overflow-y: auto;
1802
+ }
1803
+
1804
+ @media (max-width: 768px) {
1805
+ .workload-bar-group {
1806
+ gap: 0.25rem;
1807
+ }
1808
+
1809
+ .workload-bar-label {
1810
+ font-size: 0.8rem;
1811
+ }
1812
+
1813
+ .workload-bar-text {
1814
+ font-size: 0.65rem;
1815
+ }
1816
+
1817
+ .workload-chart-legend {
1818
+ gap: 1rem;
1819
+ }
1820
+
1821
+ .workload-bars {
1822
+ max-height: 400px;
1823
+ }
1824
+ }
1825
+
1826
+ /* Skills Matrix Styles */
1827
+ .skills-matrix-container {
1828
+ background: var(--bg-secondary);
1829
+ border: 2px solid var(--border);
1830
+ border-radius: 8px;
1831
+ padding: 1.5rem;
1832
+ box-shadow: var(--shadow-sm);
1833
+ overflow-x: auto;
1834
+ }
1835
+
1836
+ .skills-matrix {
1837
+ display: grid;
1838
+ grid-template-columns: 150px repeat(auto-fit, minmax(100px, 1fr));
1839
+ gap: 0;
1840
+ min-width: 600px;
1841
+ }
1842
+
1843
+ .skills-matrix-cell {
1844
+ display: flex;
1845
+ align-items: center;
1846
+ justify-content: center;
1847
+ padding: 0.75rem;
1848
+ border: 1px solid var(--border);
1849
+ font-size: 0.85rem;
1850
+ min-height: 50px;
1851
+ }
1852
+
1853
+ .skills-matrix-header-row {
1854
+ position: sticky;
1855
+ top: 0;
1856
+ background: var(--bg-tertiary);
1857
+ font-weight: 600;
1858
+ text-transform: uppercase;
1859
+ letter-spacing: 0.05em;
1860
+ color: var(--text-muted);
1861
+ font-size: 0.75rem;
1862
+ z-index: 10;
1863
+ }
1864
+
1865
+ .skills-matrix-agent-name {
1866
+ position: sticky;
1867
+ left: 0;
1868
+ background: var(--bg-tertiary);
1869
+ font-weight: 600;
1870
+ color: var(--text-primary);
1871
+ text-align: left;
1872
+ z-index: 11;
1873
+ }
1874
+
1875
+ .skills-matrix-skill-label {
1876
+ writing-mode: horizontal-tb;
1877
+ white-space: nowrap;
1878
+ }
1879
+
1880
+ .proficiency-dot {
1881
+ display: inline-flex;
1882
+ align-items: center;
1883
+ justify-content: center;
1884
+ width: 28px;
1885
+ height: 28px;
1886
+ border-radius: 50%;
1887
+ font-size: 0.7rem;
1888
+ font-weight: 600;
1889
+ transition: transform 0.2s, box-shadow 0.2s;
1890
+ cursor: help;
1891
+ }
1892
+
1893
+ .proficiency-dot:hover {
1894
+ transform: scale(1.15);
1895
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
1896
+ }
1897
+
1898
+ .proficiency-1 {
1899
+ background: #fee5e5;
1900
+ color: #8b0000;
1901
+ border: 1px solid #d4a5a5;
1902
+ }
1903
+
1904
+ .proficiency-2 {
1905
+ background: #ffcccb;
1906
+ color: #660000;
1907
+ border: 1px solid #c97c7c;
1908
+ }
1909
+
1910
+ .proficiency-3 {
1911
+ background: #ffb366;
1912
+ color: #5a3a00;
1913
+ border: 1px solid #cc8844;
1914
+ }
1915
+
1916
+ .proficiency-4 {
1917
+ background: #99ff99;
1918
+ color: #1a4d1a;
1919
+ border: 1px solid #66cc66;
1920
+ }
1921
+
1922
+ .proficiency-5 {
1923
+ background: #00cc00;
1924
+ color: #ffffff;
1925
+ border: 1px solid #009900;
1926
+ }
1927
+
1928
+ .skill-category-legend {
1929
+ display: flex;
1930
+ gap: 1.5rem;
1931
+ flex-wrap: wrap;
1932
+ margin-top: 1.5rem;
1933
+ padding-top: 1.5rem;
1934
+ border-top: 1px solid var(--border);
1935
+ }
1936
+
1937
+ .skill-category-item {
1938
+ display: flex;
1939
+ align-items: center;
1940
+ gap: 0.5rem;
1941
+ font-size: 0.875rem;
1942
+ }
1943
+
1944
+ .skill-category-icon {
1945
+ display: inline-block;
1946
+ width: 16px;
1947
+ height: 16px;
1948
+ border-radius: 3px;
1949
+ background: var(--accent);
1950
+ }
1951
+
1952
+ .skill-tooltip {
1953
+ position: absolute;
1954
+ background: var(--bg-tertiary);
1955
+ border: 1px solid var(--border-strong);
1956
+ border-radius: 4px;
1957
+ padding: 0.5rem 0.75rem;
1958
+ font-size: 0.75rem;
1959
+ white-space: nowrap;
1960
+ pointer-events: none;
1961
+ z-index: 1000;
1962
+ box-shadow: var(--shadow-md);
1963
+ }
1964
+
1965
+ /* ================================================================
1966
+ TRACKS VIEW
1967
+ ================================================================ */
1968
+ .tracks {
1969
+ display: none;
1970
+ }
1971
+
1972
+ .tracks.active {
1973
+ display: block;
1974
+ }
1975
+
1976
+ .sessions-table {
1977
+ width: 100%;
1978
+ border-collapse: collapse;
1979
+ font-family: 'JetBrains Mono', monospace;
1980
+ font-size: 0.875rem;
1981
+ background: var(--bg-secondary);
1982
+ border: 2px solid var(--border-strong);
1983
+ box-shadow: var(--shadow-md);
1984
+ }
1985
+
1986
+ .sessions-table th,
1987
+ .sessions-table td {
1988
+ border-bottom: 1px solid var(--border);
1989
+ padding: 0.875rem 1rem;
1990
+ text-align: left;
1991
+ vertical-align: middle;
1992
+ }
1993
+
1994
+ .sessions-table th {
1995
+ color: var(--text-muted);
1996
+ font-weight: 600;
1997
+ text-transform: uppercase;
1998
+ letter-spacing: 0.08em;
1999
+ font-size: 0.625rem;
2000
+ background: var(--bg-tertiary);
2001
+ border-bottom: 2px solid var(--border-strong);
2002
+ }
2003
+
2004
+ .sessions-table tr:hover td {
2005
+ background: var(--bg-tertiary);
2006
+ }
2007
+
2008
+ .sessions-table .session-id {
2009
+ font-weight: 600;
2010
+ color: var(--status-active);
2011
+ cursor: pointer;
2012
+ text-decoration: underline;
2013
+ text-underline-offset: 2px;
2014
+ }
2015
+
2016
+ .sessions-table .session-id:hover {
2017
+ color: var(--accent);
2018
+ }
2019
+
2020
+ /* Session Filters */
2021
+ .session-filters {
2022
+ display: flex;
2023
+ gap: 1rem;
2024
+ padding: 1rem 1.5rem;
2025
+ background: var(--bg-secondary);
2026
+ border: 2px solid var(--border-strong);
2027
+ box-shadow: var(--shadow-md);
2028
+ margin-bottom: 1rem;
2029
+ flex-wrap: wrap;
2030
+ align-items: flex-end;
2031
+ }
2032
+
2033
+ .filter-group {
2034
+ display: flex;
2035
+ flex-direction: column;
2036
+ gap: 0.375rem;
2037
+ }
2038
+
2039
+ .filter-group label {
1080
2040
  font-size: 0.75rem;
1081
2041
  font-weight: 600;
1082
2042
  color: var(--text-muted);
@@ -1592,6 +2552,47 @@
1592
2552
  font-family: 'JetBrains Mono', monospace;
1593
2553
  }
1594
2554
 
2555
+ /* Delegations */
2556
+ .delegations-list {
2557
+ display: flex;
2558
+ flex-direction: column;
2559
+ gap: 0.75rem;
2560
+ }
2561
+
2562
+ .delegation-item {
2563
+ padding: 0.75rem;
2564
+ border: 1px solid var(--border);
2565
+ background: var(--bg-tertiary);
2566
+ border-radius: 2px;
2567
+ }
2568
+
2569
+ .delegation-meta {
2570
+ display: flex;
2571
+ gap: 0.5rem;
2572
+ flex-wrap: wrap;
2573
+ align-items: center;
2574
+ margin-bottom: 0.5rem;
2575
+ }
2576
+
2577
+ .delegation-task {
2578
+ font-family: 'JetBrains Mono', monospace;
2579
+ font-size: 0.75rem;
2580
+ color: var(--text-secondary);
2581
+ margin-bottom: 0.375rem;
2582
+ }
2583
+
2584
+ .delegation-time {
2585
+ font-family: 'JetBrains Mono', monospace;
2586
+ font-size: 0.65rem;
2587
+ color: var(--text-muted);
2588
+ }
2589
+
2590
+ .mono {
2591
+ font-family: 'JetBrains Mono', monospace;
2592
+ font-size: 0.75rem;
2593
+ color: var(--text-secondary);
2594
+ }
2595
+
1595
2596
  /* Session Activity Preview */
1596
2597
  .session-preview {
1597
2598
  background: var(--bg-tertiary);
@@ -2007,6 +3008,7 @@
2007
3008
  <button class="view-btn active" data-view="kanban">Work</button>
2008
3009
  <button class="view-btn" data-view="graph">Graph</button>
2009
3010
  <button class="view-btn" data-view="analytics">Analytics</button>
3011
+ <button class="view-btn" data-view="agents">Agents</button>
2010
3012
  <button class="view-btn" data-view="sessions">Sessions</button>
2011
3013
  </div>
2012
3014
 
@@ -2015,19 +3017,58 @@
2015
3017
 
2016
3018
  <!-- Graph View -->
2017
3019
  <div class="graph-container" id="graph-container">
2018
- <svg class="graph-svg" id="graph-svg">
2019
- <defs>
2020
- <marker id="arrowhead" markerWidth="10" markerHeight="7" refX="55" refY="3.5" orient="auto">
2021
- <polygon points="0 0, 10 3.5, 0 7" class="graph-arrowhead"/>
2022
- </marker>
2023
- </defs>
2024
- <g id="graph-edges"></g>
2025
- <g id="graph-nodes"></g>
2026
- </svg>
2027
- <div class="graph-legend">
2028
- <div class="graph-legend-item"><span class="legend-blocked"></span> Blocked by</div>
2029
- <div class="graph-legend-item"><span class="legend-related"></span> Related</div>
2030
- <div class="graph-legend-item"><span class="legend-default"></span> Other</div>
3020
+ <!-- Graph Controls -->
3021
+ <div class="graph-controls">
3022
+ <div class="graph-control-group">
3023
+ <label class="graph-filter-label">Filter by Status:</label>
3024
+ <div class="graph-filters">
3025
+ <label class="graph-filter-checkbox">
3026
+ <input type="checkbox" data-status="todo" checked>
3027
+ <span>To Do</span>
3028
+ </label>
3029
+ <label class="graph-filter-checkbox">
3030
+ <input type="checkbox" data-status="in-progress" checked>
3031
+ <span>In Progress</span>
3032
+ </label>
3033
+ <label class="graph-filter-checkbox">
3034
+ <input type="checkbox" data-status="blocked" checked>
3035
+ <span>Blocked</span>
3036
+ </label>
3037
+ <label class="graph-filter-checkbox">
3038
+ <input type="checkbox" data-status="done">
3039
+ <span>Done</span>
3040
+ </label>
3041
+ </div>
3042
+ </div>
3043
+
3044
+ <div class="graph-control-group">
3045
+ <input type="text" id="graph-search" class="graph-search" placeholder="Search nodes..." />
3046
+ </div>
3047
+
3048
+ <div class="graph-control-group graph-control-buttons">
3049
+ <button id="graph-reset" class="btn btn-secondary">Reset View</button>
3050
+ <button id="graph-show-all" class="btn btn-secondary">Show All</button>
3051
+ </div>
3052
+ </div>
3053
+
3054
+ <!-- Graph Container for Vis.js -->
3055
+ <div class="graph-viewport">
3056
+ <div id="graph-network" class="graph-network"></div>
3057
+ </div>
3058
+
3059
+ <div class="graph-footer">
3060
+ <div class="graph-stats">
3061
+ <span id="graph-node-count">0 nodes</span>
3062
+ <span id="graph-edge-count">0 edges</span>
3063
+ </div>
3064
+ <div class="graph-legend">
3065
+ <div class="graph-legend-item"><span class="legend-done"></span> Done</div>
3066
+ <div class="graph-legend-item"><span class="legend-active"></span> In Progress</div>
3067
+ <div class="graph-legend-item"><span class="legend-todo"></span> To Do</div>
3068
+ <div class="graph-legend-item"><span class="legend-blocked"></span> Blocked</div>
3069
+ <div class="graph-legend-item"><span class="legend-related"></span> Related edge</div>
3070
+ <div class="graph-legend-item"><span class="legend-blocked-edge"></span> Blocked by</div>
3071
+ </div>
2031
3072
  </div>
2032
3073
  </div>
2033
3074
 
@@ -2115,6 +3156,78 @@
2115
3156
  <div id="sessions-list" class="loading">Loading sessions...</div>
2116
3157
  </div>
2117
3158
 
3159
+ <!-- Agents View - Multi-Agent Work Attribution -->
3160
+ <div class="agents" id="agents">
3161
+ <div class="analytics-header">
3162
+ <div>
3163
+ <h2>Multi-Agent Work Attribution</h2>
3164
+ <p>Track which agents completed work items and monitor delegation performance.</p>
3165
+ </div>
3166
+ <div style="display:flex; gap:0.5rem; align-items:center;">
3167
+ <button class="btn btn-primary" id="agents-refresh">Refresh</button>
3168
+ </div>
3169
+ </div>
3170
+
3171
+ <!-- Agent Skills Matrix -->
3172
+ <div class="analytics-card analytics-card-wide" id="agent-skills-matrix">
3173
+ <h3>Agent Specializations & Skills Matrix</h3>
3174
+ <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">
3175
+ Proficiency levels based on work history analysis. Color intensity indicates expertise level (1=novice, 5=expert).
3176
+ </p>
3177
+ <div class="skills-matrix-container" id="skills-matrix-content">
3178
+ <div class="loading">Analyzing agent work history...</div>
3179
+ </div>
3180
+ </div>
3181
+
3182
+ <!-- Agent Stats Summary -->
3183
+ <div class="analytics-card" id="agent-summary">
3184
+ <div class="loading">Loading agent statistics...</div>
3185
+ </div>
3186
+
3187
+ <!-- Workload Distribution Chart -->
3188
+ <div class="analytics-card analytics-card-wide">
3189
+ <div class="workload-chart-container" id="workload-chart">
3190
+ <div class="workload-chart-header">
3191
+ <h3>Agent Workload Distribution</h3>
3192
+ <p>Horizontal bar chart showing work completion by agent</p>
3193
+ </div>
3194
+ <div id="workload-chart-content" class="loading">Loading workload data...</div>
3195
+ </div>
3196
+ </div>
3197
+
3198
+ <!-- Agent Work Table -->
3199
+ <div class="analytics-card analytics-card-wide" id="agent-work-table">
3200
+ <div class="loading">Loading agent work data...</div>
3201
+ </div>
3202
+
3203
+ <!-- Agent Performance Metrics -->
3204
+ <div class="analytics-card" id="agent-performance">
3205
+ <div class="loading">Loading agent performance metrics...</div>
3206
+ </div>
3207
+
3208
+ <!-- Agent Cost Breakdown -->
3209
+ <div class="analytics-card analytics-card-wide" id="agent-costs">
3210
+ <div class="cost-breakdown-container">
3211
+ <div class="cost-breakdown-header">
3212
+ <h3>Agent Cost Breakdown</h3>
3213
+ <p>Token costs aggregated by agent type with visual distribution</p>
3214
+ </div>
3215
+
3216
+ <div class="cost-summary-metrics" id="cost-metrics">
3217
+ <!-- Populated by JavaScript -->
3218
+ </div>
3219
+
3220
+ <div class="cost-bars" id="cost-bars-container">
3221
+ <!-- Populated by JavaScript -->
3222
+ </div>
3223
+
3224
+ <div class="cost-breakdown-legend" id="cost-legend">
3225
+ <!-- Populated by JavaScript -->
3226
+ </div>
3227
+ </div>
3228
+ </div>
3229
+ </div>
3230
+
2118
3231
  </div>
2119
3232
 
2120
3233
  <!-- Detail Panel -->
@@ -2663,73 +3776,294 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2663
3776
  }
2664
3777
  }
2665
3778
 
2666
- async function loadAndRenderCommitDag(featureId) {
2667
- const data = await fetchAnalytics('commit-graph', { feature_id: featureId });
2668
- renderCommitDag(featureId, data.graph || {});
2669
- }
3779
+ async function loadAndRenderCommitDag(featureId) {
3780
+ const data = await fetchAnalytics('commit-graph', { feature_id: featureId });
3781
+ renderCommitDag(featureId, data.graph || {});
3782
+ }
3783
+
3784
+ function renderAnalyticsError(err) {
3785
+ const msg = err && err.message ? err.message : String(err);
3786
+ const overview = document.getElementById('analytics-overview');
3787
+ const tools = document.getElementById('analytics-tools');
3788
+ const features = document.getElementById('analytics-features');
3789
+ const continuity = document.getElementById('analytics-continuity');
3790
+ const commits = document.getElementById('analytics-commits');
3791
+ const commitDag = document.getElementById('analytics-commit-dag');
3792
+ const hint = window.location.protocol === 'file:'
3793
+ ? 'You opened this dashboard as a local file. Run <span class="mono">htmlgraph serve</span> and open <span class="mono">http://localhost:8080</span>, or pass <span class="mono">?api=http://localhost:8080/api</span>.'
3794
+ : 'Ensure <span class="mono">htmlgraph serve</span> is running and accessible from this page.';
3795
+
3796
+ const html = `
3797
+ <h3>Analytics Unavailable</h3>
3798
+ <p class="analytics-note">${msg}</p>
3799
+ <p class="analytics-note">${hint}</p>
3800
+ <p class="analytics-note">Build the index:</p>
3801
+ <pre class="mono">PYTHONPATH=src/python .venv/bin/python -m htmlgraph.cli events export-sessions -g .htmlgraph\nPYTHONPATH=src/python .venv/bin/python -m htmlgraph.cli index rebuild -g .htmlgraph</pre>
3802
+ `;
3803
+ overview.innerHTML = html;
3804
+ tools.innerHTML = html;
3805
+ features.innerHTML = html;
3806
+ continuity.innerHTML = html;
3807
+ commits.innerHTML = html;
3808
+ if (commitDag) commitDag.innerHTML = html;
3809
+ }
3810
+
3811
+ async function loadAndRenderAnalyticsBase() {
3812
+ const [overview, features, transitions] = await Promise.all([
3813
+ fetchAnalytics('overview'),
3814
+ fetchAnalytics('features', { limit: 50 }).then(r => r.features),
3815
+ fetchAnalytics('transitions', { limit: 25 }).then(r => r.transitions),
3816
+ ]);
3817
+
3818
+ analyticsCache.overview = overview;
3819
+ analyticsCache.features = features;
3820
+ analyticsCache.transitions = transitions;
3821
+ analyticsLoadedAt = Date.now();
3822
+
3823
+ renderAnalyticsOverview(overview);
3824
+ renderAnalyticsTools(transitions);
3825
+ renderAnalyticsFeatures(features);
3826
+ }
3827
+
3828
+ async function loadAndRenderContinuity(featureId) {
3829
+ analyticsCache.selectedFeatureId = featureId;
3830
+ const data = await fetchAnalytics('continuity', { feature_id: featureId, limit: 200 });
3831
+ renderAnalyticsContinuity(featureId, data.sessions || []);
3832
+ }
3833
+
3834
+ async function loadAndRenderCommits(featureId) {
3835
+ const data = await fetchAnalytics('commits', { feature_id: featureId, limit: 200 });
3836
+ renderAnalyticsCommits(featureId, data.commits || []);
3837
+ }
3838
+
3839
+ async function loadFeatureAnalytics(featureId) {
3840
+ analyticsCache.selectedFeatureId = featureId;
3841
+ await Promise.all([
3842
+ loadAndRenderContinuity(featureId),
3843
+ loadAndRenderCommits(featureId),
3844
+ loadAndRenderCommitDag(featureId)
3845
+ ]);
3846
+ }
3847
+
3848
+ // =====================================================================
3849
+ // Agent Cost Visualization
3850
+ // =====================================================================
3851
+
3852
+ const AGENT_COLORS = {
3853
+ 'claude': '#2979FF',
3854
+ 'codex': '#00C853',
3855
+ 'orchestrator': '#7C4DFF',
3856
+ 'gemini': '#FBC02D',
3857
+ 'gemini-2': '#FF9100',
3858
+ 'analyst': '#0ea5e9',
3859
+ 'developer': '#06b6d4',
3860
+ 'researcher': '#d946ef',
3861
+ 'debugger': '#ef4444',
3862
+ 'default': '#78909C'
3863
+ };
3864
+
3865
+ function getAgentColor(agent) {
3866
+ const normalized = (agent || 'default').toLowerCase();
3867
+ return AGENT_COLORS[normalized] || AGENT_COLORS['default'];
3868
+ }
3869
+
3870
+ function formatCost(tokens) {
3871
+ // Approximate cost: $0.003 per 1K input tokens
3872
+ const cost = (tokens / 1000) * 0.003;
3873
+ return cost.toFixed(4);
3874
+ }
3875
+
3876
+ function formatCostDisplay(tokens) {
3877
+ const cost = parseFloat(formatCost(tokens));
3878
+ if (cost === 0) return '$0.00';
3879
+ return `$${cost.toFixed(2)}`;
3880
+ }
3881
+
3882
+ function getCostRange(cost) {
3883
+ // Define ranges: low (< $0.01), medium ($0.01-$0.05), high (> $0.05)
3884
+ const numCost = parseFloat(formatCost(cost));
3885
+ if (numCost < 0.01) return 'low';
3886
+ if (numCost < 0.05) return 'medium';
3887
+ return 'high';
3888
+ }
3889
+
3890
+ function aggregateAgentCosts(sessions) {
3891
+ const costsByAgent = {};
3892
+ let totalCost = 0;
3893
+
3894
+ sessions.forEach(session => {
3895
+ const agent = session.properties?.agent || 'unknown';
3896
+ const tokens = parseInt(session.properties?.total_tokens) || 0;
3897
+
3898
+ if (!costsByAgent[agent]) {
3899
+ costsByAgent[agent] = {
3900
+ agent,
3901
+ totalTokens: 0,
3902
+ operationCount: 0,
3903
+ sessionCount: 0
3904
+ };
3905
+ }
3906
+
3907
+ costsByAgent[agent].totalTokens += tokens;
3908
+ costsByAgent[agent].operationCount += parseInt(session.properties?.event_count) || 0;
3909
+ costsByAgent[agent].sessionCount += 1;
3910
+ totalCost += tokens;
3911
+ });
3912
+
3913
+ return {
3914
+ byAgent: Object.values(costsByAgent).sort((a, b) => b.totalTokens - a.totalTokens),
3915
+ totalTokens: totalCost
3916
+ };
3917
+ }
3918
+
3919
+ function renderAgentCostMetrics(costs) {
3920
+ const metricsEl = document.getElementById('cost-metrics');
3921
+ if (!metricsEl || costs.byAgent.length === 0) return;
3922
+
3923
+ const avgCostPerAgent = costs.totalTokens / costs.byAgent.length;
3924
+ const totalCostUSD = formatCostDisplay(costs.totalTokens);
3925
+ const avgCostUSD = formatCostDisplay(avgCostPerAgent);
3926
+
3927
+ const topAgent = costs.byAgent[0];
3928
+ const topAgentPercent = ((topAgent.totalTokens / costs.totalTokens) * 100).toFixed(1);
3929
+
3930
+ metricsEl.innerHTML = `
3931
+ <div class="cost-metric">
3932
+ <div class="cost-metric-label">Total Cost</div>
3933
+ <div class="cost-metric-value">${totalCostUSD}</div>
3934
+ <div class="cost-metric-unit">${costs.totalTokens.toLocaleString()} tokens</div>
3935
+ </div>
3936
+ <div class="cost-metric">
3937
+ <div class="cost-metric-label">Average Per Agent</div>
3938
+ <div class="cost-metric-value">${avgCostUSD}</div>
3939
+ <div class="cost-metric-unit">~${Math.round(avgCostPerAgent).toLocaleString()} tokens</div>
3940
+ </div>
3941
+ <div class="cost-metric">
3942
+ <div class="cost-metric-label">Top Agent</div>
3943
+ <div class="cost-metric-value">${topAgent.agent}</div>
3944
+ <div class="cost-metric-unit">${topAgentPercent}% of total</div>
3945
+ </div>
3946
+ <div class="cost-metric">
3947
+ <div class="cost-metric-label">Agent Count</div>
3948
+ <div class="cost-metric-value">${costs.byAgent.length}</div>
3949
+ <div class="cost-metric-unit">unique agents</div>
3950
+ </div>
3951
+ `;
3952
+ }
3953
+
3954
+ function renderAgentCostBars(costs) {
3955
+ const containerEl = document.getElementById('cost-bars-container');
3956
+ if (!containerEl || costs.byAgent.length === 0) {
3957
+ if (containerEl) containerEl.innerHTML = '<div class="loading">No cost data available</div>';
3958
+ return;
3959
+ }
3960
+
3961
+ const maxCost = Math.max(...costs.byAgent.map(a => a.totalTokens));
3962
+
3963
+ const barsHTML = costs.byAgent.map(agent => {
3964
+ const percentOfTotal = (agent.totalTokens / costs.totalTokens) * 100;
3965
+ const costUSD = formatCostDisplay(agent.totalTokens);
3966
+ const costRange = getCostRange(agent.totalTokens);
3967
+ const color = getAgentColor(agent.agent);
3968
+ const avgPerSession = Math.round(agent.totalTokens / agent.sessionCount);
3969
+
3970
+ return `
3971
+ <div class="cost-bar-group">
3972
+ <div class="cost-bar-label">
3973
+ <div class="cost-bar-label-name">
3974
+ <div class="cost-agent-badge" style="background: ${color};">
3975
+ ${agent.agent.substring(0, 1).toUpperCase()}
3976
+ </div>
3977
+ <span>${agent.agent}</span>
3978
+ </div>
3979
+ <div class="cost-bar-stats">
3980
+ <div class="cost-bar-stat">
3981
+ <div class="cost-bar-stat-label">Cost</div>
3982
+ <div class="cost-bar-stat-value">${costUSD}</div>
3983
+ </div>
3984
+ <div class="cost-bar-stat">
3985
+ <div class="cost-bar-stat-label">%</div>
3986
+ <div class="cost-bar-stat-value">${percentOfTotal.toFixed(1)}%</div>
3987
+ </div>
3988
+ <div class="cost-bar-stat">
3989
+ <div class="cost-bar-stat-label">Tokens</div>
3990
+ <div class="cost-bar-stat-value">${agent.totalTokens.toLocaleString()}</div>
3991
+ </div>
3992
+ </div>
3993
+ </div>
3994
+
3995
+ <div class="cost-bar-container">
3996
+ <div class="cost-bar-stacked">
3997
+ <div class="cost-bar-segment" style="
3998
+ width: 100%;
3999
+ background: ${color};
4000
+ opacity: 0.85;
4001
+ " title="${agent.agent}: ${costUSD}">
4002
+ <span class="cost-bar-segment-label">
4003
+ ${percentOfTotal.toFixed(0)}%
4004
+ </span>
4005
+ </div>
4006
+ </div>
4007
+ <div class="cost-bar-tooltip">
4008
+ Sessions: ${agent.sessionCount} | Avg/Session: ${avgPerSession.toLocaleString()} tokens
4009
+ </div>
4010
+ </div>
2670
4011
 
2671
- function renderAnalyticsError(err) {
2672
- const msg = err && err.message ? err.message : String(err);
2673
- const overview = document.getElementById('analytics-overview');
2674
- const tools = document.getElementById('analytics-tools');
2675
- const features = document.getElementById('analytics-features');
2676
- const continuity = document.getElementById('analytics-continuity');
2677
- const commits = document.getElementById('analytics-commits');
2678
- const commitDag = document.getElementById('analytics-commit-dag');
2679
- const hint = window.location.protocol === 'file:'
2680
- ? 'You opened this dashboard as a local file. Run <span class="mono">htmlgraph serve</span> and open <span class="mono">http://localhost:8080</span>, or pass <span class="mono">?api=http://localhost:8080/api</span>.'
2681
- : 'Ensure <span class="mono">htmlgraph serve</span> is running and accessible from this page.';
4012
+ <div class="cost-range-indicator">
4013
+ <div class="cost-range-dot ${costRange}"></div>
4014
+ <span>${costRange === 'low' ? 'Low' : costRange === 'medium' ? 'Medium' : 'High'} cost</span>
4015
+ </div>
4016
+ </div>
4017
+ `;
4018
+ }).join('');
2682
4019
 
2683
- const html = `
2684
- <h3>Analytics Unavailable</h3>
2685
- <p class="analytics-note">${msg}</p>
2686
- <p class="analytics-note">${hint}</p>
2687
- <p class="analytics-note">Build the index:</p>
2688
- <pre class="mono">PYTHONPATH=src/python .venv/bin/python -m htmlgraph.cli events export-sessions -g .htmlgraph\nPYTHONPATH=src/python .venv/bin/python -m htmlgraph.cli index rebuild -g .htmlgraph</pre>
2689
- `;
2690
- overview.innerHTML = html;
2691
- tools.innerHTML = html;
2692
- features.innerHTML = html;
2693
- continuity.innerHTML = html;
2694
- commits.innerHTML = html;
2695
- if (commitDag) commitDag.innerHTML = html;
4020
+ containerEl.innerHTML = barsHTML;
2696
4021
  }
2697
4022
 
2698
- async function loadAndRenderAnalyticsBase() {
2699
- const [overview, features, transitions] = await Promise.all([
2700
- fetchAnalytics('overview'),
2701
- fetchAnalytics('features', { limit: 50 }).then(r => r.features),
2702
- fetchAnalytics('transitions', { limit: 25 }).then(r => r.transitions),
2703
- ]);
4023
+ function renderAgentCostLegend(costs) {
4024
+ const legendEl = document.getElementById('cost-legend');
4025
+ if (!legendEl || costs.byAgent.length === 0) return;
2704
4026
 
2705
- analyticsCache.overview = overview;
2706
- analyticsCache.features = features;
2707
- analyticsCache.transitions = transitions;
2708
- analyticsLoadedAt = Date.now();
4027
+ const legendItems = costs.byAgent.map(agent => {
4028
+ const color = getAgentColor(agent.agent);
4029
+ return `
4030
+ <div class="cost-legend-item">
4031
+ <div class="cost-legend-color" style="background: ${color};"></div>
4032
+ <span class="cost-legend-label">${agent.agent}</span>
4033
+ </div>
4034
+ `;
4035
+ }).join('');
2709
4036
 
2710
- renderAnalyticsOverview(overview);
2711
- renderAnalyticsTools(transitions);
2712
- renderAnalyticsFeatures(features);
4037
+ legendEl.innerHTML = legendItems;
2713
4038
  }
2714
4039
 
2715
- async function loadAndRenderContinuity(featureId) {
2716
- analyticsCache.selectedFeatureId = featureId;
2717
- const data = await fetchAnalytics('continuity', { feature_id: featureId, limit: 200 });
2718
- renderAnalyticsContinuity(featureId, data.sessions || []);
2719
- }
4040
+ async function loadAndRenderAgentCosts() {
4041
+ const container = document.getElementById('agent-costs');
4042
+ if (!container) return;
2720
4043
 
2721
- async function loadAndRenderCommits(featureId) {
2722
- const data = await fetchAnalytics('commits', { feature_id: featureId, limit: 200 });
2723
- renderAnalyticsCommits(featureId, data.commits || []);
2724
- }
4044
+ try {
4045
+ // Fetch all sessions to aggregate costs
4046
+ if (allSessions.length === 0) {
4047
+ const response = await fetch(`${API}/sessions`);
4048
+ if (!response.ok) throw new Error('Failed to load sessions');
4049
+ const data = await response.json();
4050
+ allSessions = data.nodes || [];
4051
+ }
2725
4052
 
2726
- async function loadFeatureAnalytics(featureId) {
2727
- analyticsCache.selectedFeatureId = featureId;
2728
- await Promise.all([
2729
- loadAndRenderContinuity(featureId),
2730
- loadAndRenderCommits(featureId),
2731
- loadAndRenderCommitDag(featureId)
2732
- ]);
4053
+ if (allSessions.length === 0) {
4054
+ return;
4055
+ }
4056
+
4057
+ // Aggregate costs by agent
4058
+ const costs = aggregateAgentCosts(allSessions);
4059
+
4060
+ // Render visualization
4061
+ renderAgentCostMetrics(costs);
4062
+ renderAgentCostBars(costs);
4063
+ renderAgentCostLegend(costs);
4064
+ } catch (err) {
4065
+ console.error('Error loading agent costs:', err);
4066
+ }
2733
4067
  }
2734
4068
 
2735
4069
  async function ensureAnalyticsLoaded(force = false) {
@@ -2737,6 +4071,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2737
4071
  if (!force && analyticsCache.overview && !stale) return;
2738
4072
  try {
2739
4073
  await loadAndRenderAnalyticsBase();
4074
+ await loadAndRenderAgentCosts();
2740
4075
  if (!analyticsCache.selectedFeatureId && analyticsCache.features && analyticsCache.features.length) {
2741
4076
  await loadFeatureAnalytics(analyticsCache.features[0].feature_id);
2742
4077
  }
@@ -2814,6 +4149,8 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2814
4149
  const created = new Date(s.created).toLocaleString();
2815
4150
  const updated = new Date(s.updated).toLocaleString();
2816
4151
  const statusBadge = s.status === 'active' ? 'status-active' : 'status-done';
4152
+ const hasTranscript = s.properties?.transcript_id ? true : false;
4153
+ const transcriptIcon = hasTranscript ? '<span title="Transcript linked" style="cursor: help;">📝</span>' : '<span style="color: var(--text-muted);">—</span>';
2817
4154
 
2818
4155
  return `
2819
4156
  <tr>
@@ -2827,6 +4164,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2827
4164
  </td>
2828
4165
  <td><span class="badge ${statusBadge}">${s.status}</span></td>
2829
4166
  <td>${s.properties?.event_count || 0}</td>
4167
+ <td style="text-align: center;">${transcriptIcon}</td>
2830
4168
  <td>${s.properties?.agent || '—'}</td>
2831
4169
  <td class="mono">${created}</td>
2832
4170
  <td class="mono">${updated}</td>
@@ -2842,6 +4180,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2842
4180
  <th>Session ID</th>
2843
4181
  <th>Status</th>
2844
4182
  <th>Events</th>
4183
+ <th title="Claude Code Transcript">Tx</th>
2845
4184
  <th>Agent</th>
2846
4185
  <th>Created</th>
2847
4186
  <th>Updated</th>
@@ -3233,11 +4572,33 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3233
4572
  }
3234
4573
  });
3235
4574
 
3236
- // Sort tracks by priority (critical > high > medium > low)
4575
+ // Sort tracks by feature completion (incomplete first), then by priority
3237
4576
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
3238
4577
  const sortedTracks = Array.from(tracked.values())
3239
- .filter(t => t.track !== null)
3240
- .sort((a, b) => priorityOrder[a.track.priority] - priorityOrder[b.track.priority]);
4578
+ // FIXED: Do NOT filter out tracks with null metadata - they still have features to show!
4579
+ // Only filter if there are no features for this track
4580
+ .filter(t => t.features && t.features.length > 0)
4581
+ .sort((a, b) => {
4582
+ // Calculate completion percentage for each track
4583
+ const aTotal = a.features.length;
4584
+ const aDone = a.features.filter(f => f.status === 'done').length;
4585
+ const aComplete = aTotal > 0 ? (aDone === aTotal) : false;
4586
+
4587
+ const bTotal = b.features.length;
4588
+ const bDone = b.features.filter(f => f.status === 'done').length;
4589
+ const bComplete = bTotal > 0 ? (bDone === bTotal) : false;
4590
+
4591
+ // Completed tracks go to bottom (1), incomplete stay at top (0)
4592
+ if (aComplete !== bComplete) {
4593
+ return aComplete ? 1 : -1;
4594
+ }
4595
+
4596
+ // Within same completion status, sort by priority
4597
+ // Use track priority if available, otherwise default to 'medium'
4598
+ const aPriority = (a.track && a.track.priority) || 'medium';
4599
+ const bPriority = (b.track && b.track.priority) || 'medium';
4600
+ return (priorityOrder[aPriority] || 2) - (priorityOrder[bPriority] || 2);
4601
+ });
3241
4602
 
3242
4603
  // Render
3243
4604
  renderTrackSections(sortedTracks);
@@ -3405,18 +4766,51 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3405
4766
  <div class="track-column-cards">
3406
4767
  ${byStatus[status].length === 0
3407
4768
  ? '<div class="empty-column">No items</div>'
3408
- : byStatus[status].map(f => `
4769
+ : byStatus[status].map(f => {
4770
+ // Determine agent badge color based on agent name
4771
+ let agentClass = 'agent-default';
4772
+ if (f.agent_assigned) {
4773
+ const agentName = f.agent_assigned.toLowerCase();
4774
+ // Primary agents
4775
+ if (agentName.includes('claude')) agentClass = 'agent-claude';
4776
+ else if (agentName.includes('codex')) agentClass = 'agent-codex';
4777
+ else if (agentName.includes('orchestrator')) agentClass = 'agent-orchestrator';
4778
+ else if (agentName.includes('gemini-2') || agentName.includes('gemini 2')) agentClass = 'agent-gemini-2';
4779
+ else if (agentName.includes('gemini')) agentClass = 'agent-gemini';
4780
+ // Secondary agents (backward compatibility)
4781
+ else if (agentName.includes('analyst')) agentClass = 'agent-analyst';
4782
+ else if (agentName.includes('developer')) agentClass = 'agent-developer';
4783
+ else if (agentName.includes('researcher')) agentClass = 'agent-researcher';
4784
+ else if (agentName.includes('debugger')) agentClass = 'agent-debugger';
4785
+ }
4786
+ // Check if feature has delegations
4787
+ const delegations = (f.properties && f.properties.delegations) || [];
4788
+ const delegationBadges = delegations.length > 0
4789
+ ? `<span class="badge delegation" title="Delegated to ${delegations.length} spawner(s)">Delegated: ${delegations.length}</span>`
4790
+ : '';
4791
+ return `
3409
4792
  <div class="card priority-${f.priority}"
3410
4793
  data-collection="${f._collection}"
3411
- data-id="${f.id}">
4794
+ data-id="${f.id}"
4795
+ data-agent="${f.agent_assigned || ''}"
4796
+ onclick="toggleCardTimeline(event)">
4797
+ <button class="card-expand-btn" onclick="toggleCardTimeline(event)" title="Toggle agent timeline">▼</button>
3412
4798
  <div class="card-title">${f.title}</div>
3413
4799
  <div class="card-meta">
3414
4800
  <span class="badge priority-${f.priority}">${f.priority}</span>
3415
4801
  ${f.type !== 'feature' ? `<span class="badge type">${f.type}</span>` : ''}
4802
+ ${f.agent_assigned ? `<span class="badge agent ${agentClass}">${f.agent_assigned}</span>` : ''}
4803
+ ${delegationBadges}
3416
4804
  <span class="card-path">${f._collection}/${f.id}</span>
3417
4805
  </div>
4806
+ <div class="card-timeline" data-feature-id="${f.id}">
4807
+ <div class="timeline-header">Agent Timeline</div>
4808
+ <div class="timeline-list" data-loading="true">
4809
+ <div class="timeline-empty">Loading timeline...</div>
4810
+ </div>
4811
+ </div>
3418
4812
  </div>
3419
- `).join('')}
4813
+ `}).join('')}
3420
4814
  </div>
3421
4815
  </div>
3422
4816
  `).join('')}
@@ -3622,6 +5016,77 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3622
5016
  }
3623
5017
  }
3624
5018
 
5019
+ async function fetchTranscriptStats(sessionId) {
5020
+ const container = document.getElementById('transcript-stats-container');
5021
+ if (!container) return;
5022
+
5023
+ try {
5024
+ const response = await fetch(`${API}/sessions/${sessionId}?transcript=true`);
5025
+ if (!response.ok) throw new Error('Failed to load transcript stats');
5026
+
5027
+ const data = await response.json();
5028
+
5029
+ if (!data.transcript_linked) {
5030
+ container.innerHTML = `
5031
+ <div class="transcript-not-linked">
5032
+ <span style="color: var(--text-muted);">No Claude Code transcript linked</span>
5033
+ <div style="font-size: 0.75rem; margin-top: 0.5rem; color: var(--text-muted);">
5034
+ Transcripts are linked automatically on work item completion
5035
+ </div>
5036
+ </div>
5037
+ `;
5038
+ return;
5039
+ }
5040
+
5041
+ // Format duration
5042
+ let durationStr = '—';
5043
+ if (data.duration_seconds) {
5044
+ const mins = Math.floor(data.duration_seconds / 60);
5045
+ const secs = Math.floor(data.duration_seconds % 60);
5046
+ durationStr = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
5047
+ }
5048
+
5049
+ // Build tool breakdown
5050
+ let toolBreakdownHtml = '';
5051
+ if (data.tool_breakdown && Object.keys(data.tool_breakdown).length > 0) {
5052
+ const sortedTools = Object.entries(data.tool_breakdown)
5053
+ .sort((a, b) => b[1] - a[1])
5054
+ .slice(0, 8);
5055
+ toolBreakdownHtml = `
5056
+ <div class="transcript-tools" style="margin-top: 0.75rem;">
5057
+ <div style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 0.25rem;">Tool Usage:</div>
5058
+ <div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
5059
+ ${sortedTools.map(([tool, count]) => `
5060
+ <span class="badge" style="font-size: 0.7rem;">${tool}: ${count}</span>
5061
+ `).join('')}
5062
+ </div>
5063
+ </div>
5064
+ `;
5065
+ }
5066
+
5067
+ container.innerHTML = `
5068
+ <div class="transcript-stats">
5069
+ <div class="transcript-meta" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem; margin-bottom: 0.75rem;">
5070
+ <div><strong>User Messages:</strong> ${data.user_messages || 0}</div>
5071
+ <div><strong>Tool Calls:</strong> ${data.tool_calls || 0}</div>
5072
+ <div><strong>Duration:</strong> ${durationStr}</div>
5073
+ <div><strong>Total Entries:</strong> ${data.entry_count || 0}</div>
5074
+ </div>
5075
+ <div class="transcript-badges" style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
5076
+ ${data.has_thinking_traces ? '<span class="badge" style="background: var(--accent);">Has Thinking Traces</span>' : ''}
5077
+ ${data.git_branch ? `<span class="badge">Branch: ${data.git_branch}</span>` : ''}
5078
+ </div>
5079
+ ${toolBreakdownHtml}
5080
+ <div class="transcript-id" style="margin-top: 0.75rem; font-size: 0.7rem; color: var(--text-muted);">
5081
+ Transcript ID: <code style="font-family: monospace;">${data.transcript_id || '—'}</code>
5082
+ </div>
5083
+ </div>
5084
+ `;
5085
+ } catch (err) {
5086
+ container.innerHTML = `<div class="loading">Error loading transcript: ${err.message}</div>`;
5087
+ }
5088
+ }
5089
+
3625
5090
  function renderPanelLoading() {
3626
5091
  document.getElementById('panel-title').textContent = 'Loading...';
3627
5092
  document.getElementById('panel-body').innerHTML = '<div class="loading">Loading...</div>';
@@ -3639,6 +5104,24 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3639
5104
 
3640
5105
  let bodyHtml = '';
3641
5106
 
5107
+ // Helper function to get agent badge class
5108
+ function getAgentClass(agentName) {
5109
+ if (!agentName) return 'agent-default';
5110
+ const name = agentName.toLowerCase();
5111
+ // Primary agents
5112
+ if (name.includes('claude')) return 'agent-claude';
5113
+ if (name.includes('codex')) return 'agent-codex';
5114
+ if (name.includes('orchestrator')) return 'agent-orchestrator';
5115
+ if (name.includes('gemini-2') || name.includes('gemini 2')) return 'agent-gemini-2';
5116
+ if (name.includes('gemini')) return 'agent-gemini';
5117
+ // Secondary agents (backward compatibility)
5118
+ if (name.includes('analyst')) return 'agent-analyst';
5119
+ if (name.includes('developer')) return 'agent-developer';
5120
+ if (name.includes('researcher')) return 'agent-researcher';
5121
+ if (name.includes('debugger')) return 'agent-debugger';
5122
+ return 'agent-default';
5123
+ }
5124
+
3642
5125
  // Meta section
3643
5126
  bodyHtml += `
3644
5127
  <div class="panel-section">
@@ -3647,7 +5130,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3647
5130
  <span class="badge priority-${node.priority}">${node.priority}</span>
3648
5131
  <span class="badge type">${node.type}</span>
3649
5132
  <span class="badge">${node.status}</span>
3650
- ${node.agent_assigned ? `<span class="badge">Agent: ${node.agent_assigned}</span>` : ''}
5133
+ ${node.agent_assigned ? `<span class="badge agent ${getAgentClass(node.agent_assigned)}">Agent: ${node.agent_assigned}</span>` : ''}
3651
5134
  </div>
3652
5135
  </div>
3653
5136
  `;
@@ -3676,6 +5159,43 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3676
5159
  `;
3677
5160
  }
3678
5161
 
5162
+ // Delegation information section (if delegation data exists in properties)
5163
+ if (node.properties && (node.properties.delegated_tasks || node.properties.delegations)) {
5164
+ const delegations = node.properties.delegations || node.properties.delegated_tasks || [];
5165
+ if (delegations && delegations.length > 0) {
5166
+ bodyHtml += `
5167
+ <div class="panel-section">
5168
+ <h3>Delegations (${delegations.length})</h3>
5169
+ <div class="delegations-list">
5170
+ ${delegations.map((d, idx) => {
5171
+ const spawner = d.spawner || d.executor || 'unknown';
5172
+ const executorType = d.executor_type || 'direct';
5173
+ let executorBadge = 'delegation-direct';
5174
+ if (executorType === 'external_cli') executorBadge = 'delegation-external';
5175
+ else if (executorType === 'fallback') executorBadge = 'delegation-fallback';
5176
+
5177
+ const tokens = d.tokens_used ? ` (${d.tokens_used} tokens)` : '';
5178
+ const cost = d.cost ? ` - $${d.cost.toFixed(2)}` : '';
5179
+
5180
+ return `
5181
+ <div class="delegation-item">
5182
+ <div class="delegation-meta">
5183
+ <span class="badge delegation ${executorBadge}">${spawner}</span>
5184
+ <span class="badge delegation">${executorType}</span>
5185
+ ${tokens ? `<span class="mono">${tokens}</span>` : ''}
5186
+ ${cost ? `<span class="mono">${cost}</span>` : ''}
5187
+ </div>
5188
+ ${d.task_id ? `<div class="delegation-task">Task: ${d.task_id}</div>` : ''}
5189
+ ${d.timestamp ? `<div class="delegation-time">${new Date(d.timestamp).toLocaleString()}</div>` : ''}
5190
+ </div>
5191
+ `;
5192
+ }).join('')}
5193
+ </div>
5194
+ </div>
5195
+ `;
5196
+ }
5197
+ }
5198
+
3679
5199
  // Edges section (excluding implemented-in which gets special handling)
3680
5200
  const edgeTypes = Object.keys(node.edges || {}).filter(t => t !== 'implemented-in');
3681
5201
  if (edgeTypes.length > 0) {
@@ -3753,6 +5273,14 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3753
5273
  <div id="activity-log-container" class="loading">Loading activity log...</div>
3754
5274
  </div>
3755
5275
  `;
5276
+
5277
+ // Transcript section (for sessions only)
5278
+ bodyHtml += `
5279
+ <div class="panel-section">
5280
+ <h3>Claude Code Transcript</h3>
5281
+ <div id="transcript-stats-container" class="loading">Loading transcript data...</div>
5282
+ </div>
5283
+ `;
3756
5284
  }
3757
5285
 
3758
5286
  // Timestamps
@@ -3768,9 +5296,10 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3768
5296
 
3769
5297
  document.getElementById('panel-body').innerHTML = bodyHtml;
3770
5298
 
3771
- // Load activity log for sessions
5299
+ // Load activity log and transcript stats for sessions
3772
5300
  if (node.type === 'session') {
3773
5301
  fetchActivityLog(node._collection || 'sessions', node.id);
5302
+ fetchTranscriptStats(node.id);
3774
5303
  }
3775
5304
 
3776
5305
  // Actions
@@ -3982,219 +5511,771 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3982
5511
  return div.innerHTML;
3983
5512
  }
3984
5513
 
3985
- async function refreshDashboard() {
3986
- const { status, nodes } = await loadData();
3987
- await renderKanban(nodes);
3988
- updateKanbanGrid();
5514
+ async function refreshDashboard() {
5515
+ const { status, nodes } = await loadData();
5516
+ await renderKanban(nodes);
5517
+ updateKanbanGrid();
5518
+ }
5519
+
5520
+ // =====================================================================
5521
+ // Graph Visualization
5522
+ // =====================================================================
5523
+
5524
+ let visNetwork = null; // Vis.js network instance
5525
+
5526
+ function getNodeColor(node) {
5527
+ const colors = {
5528
+ 'done': getComputedStyle(document.documentElement).getPropertyValue('--status-done').trim(),
5529
+ 'in-progress': getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(),
5530
+ 'blocked': getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(),
5531
+ 'todo': getComputedStyle(document.documentElement).getPropertyValue('--status-todo').trim()
5532
+ };
5533
+ return colors[node.status] || colors['todo'];
5534
+ }
5535
+
5536
+ function getNodeRadius(node) {
5537
+ const statusSizes = {
5538
+ 'done': 20,
5539
+ 'in-progress': 35,
5540
+ 'blocked': 30,
5541
+ 'todo': 28
5542
+ };
5543
+ return statusSizes[node.status] || 25;
5544
+ }
5545
+
5546
+ function wrapText(text, maxCharsPerLine = 10) {
5547
+ const words = text.split(/\s+/);
5548
+ const lines = [];
5549
+ let currentLine = '';
5550
+
5551
+ for (const word of words) {
5552
+ const testLine = currentLine ? currentLine + ' ' + word : word;
5553
+ if (testLine.length <= maxCharsPerLine) {
5554
+ currentLine = testLine;
5555
+ } else {
5556
+ if (currentLine) lines.push(currentLine);
5557
+ currentLine = word.length > maxCharsPerLine
5558
+ ? word.substring(0, maxCharsPerLine - 1) + '…'
5559
+ : word;
5560
+ }
5561
+ }
5562
+ if (currentLine) lines.push(currentLine);
5563
+
5564
+ if (lines.length > 3) {
5565
+ lines.length = 3;
5566
+ lines[2] = lines[2].substring(0, lines[2].length - 1) + '…';
5567
+ }
5568
+
5569
+ return lines.join('\n');
5570
+ }
5571
+
5572
+ function buildGraphData(nodes) {
5573
+ const graphNodes = nodes.map(n => ({
5574
+ id: n.id,
5575
+ title: n.title,
5576
+ status: n.status,
5577
+ type: n.type,
5578
+ priority: n.priority,
5579
+ edges: n.edges || {},
5580
+ _collection: n._collection
5581
+ }));
5582
+
5583
+ const nodeIds = new Set(nodes.map(n => n.id));
5584
+ const graphEdges = [];
5585
+
5586
+ nodes.forEach(node => {
5587
+ Object.entries(node.edges || {}).forEach(([edgeType, edges]) => {
5588
+ edges.forEach(edge => {
5589
+ if (nodeIds.has(edge.target_id)) {
5590
+ graphEdges.push({
5591
+ from: node.id,
5592
+ to: edge.target_id,
5593
+ type: edgeType
5594
+ });
5595
+ }
5596
+ });
5597
+ });
5598
+ });
5599
+
5600
+ return { nodes: graphNodes, edges: graphEdges };
5601
+ }
5602
+
5603
+ // Graph State Management
5604
+ let graphState = {
5605
+ allNodes: [],
5606
+ allEdges: [],
5607
+ visibleNodeIds: new Set(),
5608
+ searchQuery: '',
5609
+ filters: {
5610
+ todo: true,
5611
+ 'in-progress': true,
5612
+ blocked: true,
5613
+ done: false
5614
+ }
5615
+ };
5616
+
5617
+ function applyGraphFilters() {
5618
+ const filters = {};
5619
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
5620
+ filters[cb.dataset.status] = cb.checked;
5621
+ });
5622
+
5623
+ graphState.filters = filters;
5624
+ graphState.searchQuery = (document.getElementById('graph-search')?.value || '').toLowerCase();
5625
+
5626
+ // Determine visible nodes
5627
+ graphState.visibleNodeIds = new Set();
5628
+ graphState.allNodes.forEach(node => {
5629
+ const statusMatch = filters[node.status] || false;
5630
+ const searchMatch = !graphState.searchQuery || node.title.toLowerCase().includes(graphState.searchQuery);
5631
+ if (statusMatch && searchMatch) {
5632
+ graphState.visibleNodeIds.add(node.id);
5633
+ }
5634
+ });
5635
+
5636
+ // Update Vis.js network with visible nodes and edges
5637
+ if (visNetwork) {
5638
+ const visibleNodes = graphState.allNodes.filter(n => graphState.visibleNodeIds.has(n.id));
5639
+ const visibleEdges = graphState.allEdges.filter(e =>
5640
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
5641
+ );
5642
+
5643
+ const nodesDataset = new vis.DataSet(visibleNodes.map(n => ({
5644
+ id: n.id,
5645
+ label: wrapText(n.title),
5646
+ title: n.title + '\nStatus: ' + n.status,
5647
+ color: {
5648
+ background: getNodeColor(n),
5649
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
5650
+ highlight: {
5651
+ background: getNodeColor(n),
5652
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
5653
+ }
5654
+ },
5655
+ size: getNodeRadius(n),
5656
+ font: {
5657
+ size: 12,
5658
+ face: "'JetBrains Mono', monospace",
5659
+ color: 'white',
5660
+ strokeWidth: 0
5661
+ },
5662
+ physics: true,
5663
+ borderWidth: 2,
5664
+ status: n.status,
5665
+ _collection: n._collection,
5666
+ x: undefined, // Let physics handle positioning
5667
+ y: undefined
5668
+ })));
5669
+
5670
+ const edgesDataset = new vis.DataSet(visibleEdges.map(e => ({
5671
+ from: e.from,
5672
+ to: e.to,
5673
+ arrows: 'to',
5674
+ smooth: { type: 'continuous' },
5675
+ color: e.type === 'blocked_by'
5676
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
5677
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
5678
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
5679
+ width: 1.5
5680
+ })));
5681
+
5682
+ visNetwork.setData({ nodes: nodesDataset, edges: edgesDataset });
5683
+ }
5684
+
5685
+ updateGraphStats();
5686
+ localStorage.setItem('graphFilters', JSON.stringify(graphState.filters));
5687
+ }
5688
+
5689
+ function updateGraphStats() {
5690
+ const visibleNodeCount = graphState.visibleNodeIds.size;
5691
+ const visibleEdgeCount = graphState.allEdges.filter(e =>
5692
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
5693
+ ).length;
5694
+ const nodeCountEl = document.getElementById('graph-node-count');
5695
+ const edgeCountEl = document.getElementById('graph-edge-count');
5696
+ if (nodeCountEl) nodeCountEl.textContent = `${visibleNodeCount} nodes`;
5697
+ if (edgeCountEl) edgeCountEl.textContent = `${visibleEdgeCount} edges`;
5698
+ }
5699
+
5700
+ function resetGraphView() {
5701
+ const searchEl = document.getElementById('graph-search');
5702
+ if (searchEl) searchEl.value = '';
5703
+ graphState.searchQuery = '';
5704
+ applyGraphFilters();
5705
+ if (visNetwork) visNetwork.fit();
5706
+ }
5707
+
5708
+ function showAllNodes() {
5709
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
5710
+ cb.checked = true;
5711
+ });
5712
+ graphState.filters = { todo: true, 'in-progress': true, blocked: true, done: true };
5713
+ applyGraphFilters();
5714
+ if (visNetwork) visNetwork.fit();
5715
+ }
5716
+
5717
+ function renderGraph(nodes) {
5718
+ if (nodes.length === 0) {
5719
+ if (visNetwork) visNetwork.destroy();
5720
+ visNetwork = null;
5721
+ return;
5722
+ }
5723
+
5724
+ const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
5725
+
5726
+ graphState.allNodes = graphNodes;
5727
+ graphState.allEdges = graphEdges;
5728
+
5729
+ // FILTER FIRST: Apply default filters before rendering
5730
+ // Default: show todo, in-progress, blocked (exclude done items)
5731
+ graphState.visibleNodeIds = new Set();
5732
+ graphState.allNodes.forEach(node => {
5733
+ if (graphState.filters[node.status] !== false) {
5734
+ graphState.visibleNodeIds.add(node.id);
5735
+ }
5736
+ });
5737
+
5738
+ // Only render visible nodes and edges
5739
+ const visibleNodes = graphState.allNodes.filter(n => graphState.visibleNodeIds.has(n.id));
5740
+ const visibleEdges = graphState.allEdges.filter(e =>
5741
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
5742
+ );
5743
+
5744
+ // Create Vis.js nodes dataset with FILTERED nodes only
5745
+ const nodesData = new vis.DataSet(visibleNodes.map(n => ({
5746
+ id: n.id,
5747
+ label: wrapText(n.title),
5748
+ title: n.title + '\nStatus: ' + n.status,
5749
+ color: {
5750
+ background: getNodeColor(n),
5751
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
5752
+ highlight: {
5753
+ background: getNodeColor(n),
5754
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
5755
+ }
5756
+ },
5757
+ size: getNodeRadius(n),
5758
+ font: {
5759
+ size: 12,
5760
+ face: "'JetBrains Mono', monospace",
5761
+ color: 'white',
5762
+ strokeWidth: 0
5763
+ },
5764
+ physics: true,
5765
+ borderWidth: 2,
5766
+ status: n.status,
5767
+ _collection: n._collection
5768
+ })));
5769
+
5770
+ // Create Vis.js edges dataset with FILTERED edges only
5771
+ const edgesData = new vis.DataSet(visibleEdges.map(e => ({
5772
+ from: e.from,
5773
+ to: e.to,
5774
+ arrows: 'to',
5775
+ smooth: { type: 'continuous' },
5776
+ color: e.type === 'blocked_by'
5777
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
5778
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
5779
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
5780
+ width: 1.5
5781
+ })));
5782
+
5783
+ // Destroy existing network if it exists
5784
+ if (visNetwork) {
5785
+ visNetwork.destroy();
5786
+ }
5787
+
5788
+ // Create new Vis.js network
5789
+ const container = document.getElementById('graph-network');
5790
+ const data = {
5791
+ nodes: nodesData,
5792
+ edges: edgesData
5793
+ };
5794
+
5795
+ // Optimize physics based on node count
5796
+ const nodeCount = visibleNodes.length;
5797
+ const stabilizationIterations = nodeCount > 300 ? 100 : (nodeCount > 150 ? 150 : 200);
5798
+
5799
+ const options = {
5800
+ physics: {
5801
+ enabled: true,
5802
+ stabilization: {
5803
+ iterations: stabilizationIterations,
5804
+ fit: true
5805
+ },
5806
+ barnesHut: {
5807
+ gravitationalConstant: -30000,
5808
+ centralGravity: 0.3,
5809
+ springLength: 200,
5810
+ springConstant: 0.04
5811
+ },
5812
+ maxVelocity: 50
5813
+ },
5814
+ interaction: {
5815
+ navigationButtons: true,
5816
+ keyboard: true,
5817
+ zoomView: true,
5818
+ dragView: true
5819
+ },
5820
+ nodes: {
5821
+ shape: 'circle',
5822
+ scaling: {
5823
+ min: 10,
5824
+ max: 50
5825
+ }
5826
+ }
5827
+ };
5828
+
5829
+ visNetwork = new vis.Network(container, data, options);
5830
+
5831
+ // Handle node clicks
5832
+ visNetwork.on('click', (params) => {
5833
+ if (params.nodes.length > 0) {
5834
+ const nodeId = params.nodes[0];
5835
+ const node = graphState.allNodes.find(n => n.id === nodeId);
5836
+ if (node) {
5837
+ openPanel(node._collection, node.id);
5838
+ }
5839
+ }
5840
+ });
5841
+
5842
+ // Apply filters after network is initialized
5843
+ applyGraphFilters();
3989
5844
  }
3990
5845
 
3991
5846
  // =====================================================================
3992
- // Graph Visualization
5847
+ // Agent Skills Analysis
3993
5848
  // =====================================================================
3994
5849
 
3995
- let simulation = null;
3996
-
3997
- function getNodeColor(node) {
3998
- const colors = {
3999
- 'done': getComputedStyle(document.documentElement).getPropertyValue('--status-done').trim(),
4000
- 'in-progress': getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(),
4001
- 'blocked': getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(),
4002
- 'todo': getComputedStyle(document.documentElement).getPropertyValue('--status-todo').trim()
4003
- };
4004
- return colors[node.status] || colors['todo'];
5850
+ function analyzeAgentSkills(sessions) {
5851
+ const skillProfiles = {};
5852
+ const agents = [...new Set(sessions.map(s => s.properties?.agent).filter(Boolean))];
5853
+ agents.forEach(agent => {
5854
+ skillProfiles[agent] = {Implementation: 0, Analysis: 0, Testing: 0, Documentation: 0, Coordination: 0};
5855
+ });
5856
+ sessions.forEach(session => {
5857
+ const agent = session.properties?.agent;
5858
+ if (!agent) return;
5859
+ const desc = (session.name || session.id || '').toLowerCase();
5860
+ const cnt = session.properties?.event_count || 0;
5861
+ if (desc.includes('test') || desc.includes('validate')) skillProfiles[agent].Testing += Math.min(cnt / 10, 2);
5862
+ if (desc.includes('implement') || desc.includes('code') || desc.includes('build')) skillProfiles[agent].Implementation += Math.min(cnt / 10, 2);
5863
+ if (desc.includes('analyze') || desc.includes('research')) skillProfiles[agent].Analysis += Math.min(cnt / 10, 2);
5864
+ if (desc.includes('document') || desc.includes('explain')) skillProfiles[agent].Documentation += Math.min(cnt / 10, 2);
5865
+ if (desc.includes('coordinate') || desc.includes('delegate')) skillProfiles[agent].Coordination += Math.min(cnt / 10, 2);
5866
+ if (agent.includes('Claude')) {
5867
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
5868
+ skillProfiles[agent].Documentation = Math.max(skillProfiles[agent].Documentation, 4);
5869
+ }
5870
+ if (agent.includes('Codex')) {
5871
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 5);
5872
+ skillProfiles[agent].Testing = Math.max(skillProfiles[agent].Testing, 4);
5873
+ }
5874
+ if (agent.includes('Orchestrator')) {
5875
+ skillProfiles[agent].Coordination = Math.max(skillProfiles[agent].Coordination, 5);
5876
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4);
5877
+ }
5878
+ if (agent.includes('Gemini')) {
5879
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
5880
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 3);
5881
+ }
5882
+ });
5883
+ agents.forEach(agent => {
5884
+ Object.keys(skillProfiles[agent]).forEach(skill => {
5885
+ skillProfiles[agent][skill] = Math.min(5, Math.max(1, skillProfiles[agent][skill]));
5886
+ });
5887
+ });
5888
+ return { agents, skillProfiles };
4005
5889
  }
4006
5890
 
4007
- function getNodeRadius(node) {
4008
- const edgeCount = Object.values(node.edges || {})
4009
- .reduce((sum, edges) => sum + edges.length, 0);
4010
- return Math.min(45 + edgeCount * 3, 60);
5891
+ function getProficiencyColor(level) {
5892
+ return `proficiency-${Math.round(level)}`;
4011
5893
  }
4012
5894
 
4013
- function wrapText(text, maxCharsPerLine = 10) {
4014
- const words = text.split(/\s+/);
4015
- const lines = [];
4016
- let currentLine = '';
5895
+ function getProficiencyLabel(level) {
5896
+ const labels = ['', 'Novice', 'Beginner', 'Intermediate', 'Advanced', 'Expert'];
5897
+ return labels[Math.round(level)] || 'Expert';
5898
+ }
4017
5899
 
4018
- for (const word of words) {
4019
- const testLine = currentLine ? currentLine + ' ' + word : word;
4020
- if (testLine.length <= maxCharsPerLine) {
4021
- currentLine = testLine;
4022
- } else {
4023
- if (currentLine) lines.push(currentLine);
4024
- currentLine = word.length > maxCharsPerLine
4025
- ? word.substring(0, maxCharsPerLine - 1) + '…'
4026
- : word;
4027
- }
5900
+ function renderSkillsMatrix(agents, skillProfiles) {
5901
+ const skills = ['Implementation', 'Analysis', 'Testing', 'Documentation', 'Coordination'];
5902
+ let html = '<div class="skills-matrix">';
5903
+ html += '<div class="skills-matrix-cell skills-matrix-header-row">AGENT</div>';
5904
+ skills.forEach(skill => html += `<div class="skills-matrix-cell skills-matrix-header-row">${skill}</div>`);
5905
+ agents.forEach(agent => {
5906
+ html += `<div class="skills-matrix-cell skills-matrix-agent-name">${agent}</div>`;
5907
+ skills.forEach(skill => {
5908
+ const level = skillProfiles[agent][skill];
5909
+ const rnd = Math.round(level);
5910
+ html += `<div class="skills-matrix-cell"><div class="proficiency-dot ${getProficiencyColor(level)}" title="${getProficiencyLabel(level)} (${rnd}/5)">${rnd}</div></div>`;
5911
+ });
5912
+ });
5913
+ html += '</div><div class="skill-category-legend"><div style="font-weight: 600; width: 100%; margin-bottom: 0.5rem;">Proficiency Scale:</div>';
5914
+ for (let i = 1; i <= 5; i++) {
5915
+ html += `<div class="skill-category-item"><span class="proficiency-dot proficiency-${i}">${i}</span> ${getProficiencyLabel(i)}</div>`;
4028
5916
  }
4029
- if (currentLine) lines.push(currentLine);
5917
+ html += '</div>';
5918
+ return html;
5919
+ }
4030
5920
 
4031
- if (lines.length > 3) {
4032
- lines.length = 3;
4033
- lines[2] = lines[2].substring(0, lines[2].length - 1) + '…';
5921
+ async function loadAndRenderAgents() {
5922
+ const el = document.getElementById('skills-matrix-content');
5923
+ try {
5924
+ let sessions = allSessions;
5925
+ if (!sessions.length) {
5926
+ const r = await fetch(`${API}/sessions`);
5927
+ if (!r.ok) throw new Error('Failed to load');
5928
+ sessions = (await r.json()).nodes || [];
5929
+ }
5930
+ if (!sessions.length) {
5931
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents found</div>';
5932
+ return;
5933
+ }
5934
+ const { agents, skillProfiles } = analyzeAgentSkills(sessions);
5935
+ if (!agents.length) {
5936
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents</div>';
5937
+ return;
5938
+ }
5939
+ el.innerHTML = renderSkillsMatrix(agents, skillProfiles);
5940
+ } catch (e) {
5941
+ el.innerHTML = `<div style="padding: 2rem; color: red;">Error: ${e.message}</div>`;
4034
5942
  }
4035
-
4036
- return lines;
4037
5943
  }
4038
5944
 
4039
- function buildGraphData(nodes) {
4040
- const graphNodes = nodes.map(n => ({
4041
- id: n.id,
4042
- title: n.title,
4043
- status: n.status,
4044
- type: n.type,
4045
- priority: n.priority,
4046
- edges: n.edges || {},
4047
- _collection: n._collection,
4048
- x: null,
4049
- y: null
4050
- }));
5945
+ // =====================================================================
5946
+ // View Toggle
5947
+ // =====================================================================
4051
5948
 
4052
- const nodeIds = new Set(nodes.map(n => n.id));
4053
- const graphEdges = [];
5949
+ function switchView(view) {
5950
+ const kanban = document.getElementById('kanban');
5951
+ const graph = document.getElementById('graph-container');
5952
+ const analytics = document.getElementById('analytics');
5953
+ const agents = document.getElementById('agents');
5954
+ const sessions = document.getElementById('sessions');
5955
+ const buttons = document.querySelectorAll('.view-btn');
4054
5956
 
4055
- nodes.forEach(node => {
4056
- Object.entries(node.edges || {}).forEach(([edgeType, edges]) => {
4057
- edges.forEach(edge => {
4058
- if (nodeIds.has(edge.target_id)) {
4059
- graphEdges.push({
4060
- source: node.id,
4061
- target: edge.target_id,
4062
- type: edgeType
4063
- });
4064
- }
4065
- });
4066
- });
5957
+ buttons.forEach(btn => {
5958
+ btn.classList.toggle('active', btn.dataset.view === view);
4067
5959
  });
4068
5960
 
4069
- return { nodes: graphNodes, edges: graphEdges };
5961
+ if (view === 'kanban') {
5962
+ kanban.classList.add('active');
5963
+ graph.classList.remove('active');
5964
+ analytics.classList.remove('active');
5965
+ agents.classList.remove('active');
5966
+ sessions.classList.remove('active');
5967
+ renderKanban(allNodes);
5968
+ } else if (view === 'graph') {
5969
+ kanban.classList.remove('active');
5970
+ graph.classList.add('active');
5971
+ analytics.classList.remove('active');
5972
+ agents.classList.remove('active');
5973
+ sessions.classList.remove('active');
5974
+ renderGraph(allNodes);
5975
+ } else if (view === 'analytics') {
5976
+ kanban.classList.remove('active');
5977
+ graph.classList.remove('active');
5978
+ analytics.classList.add('active');
5979
+ agents.classList.remove('active');
5980
+ sessions.classList.remove('active');
5981
+ ensureAnalyticsLoaded(false);
5982
+ } else if (view === 'agents') {
5983
+ kanban.classList.remove('active');
5984
+ graph.classList.remove('active');
5985
+ analytics.classList.remove('active');
5986
+ agents.classList.add('active');
5987
+ sessions.classList.remove('active');
5988
+ loadAndRenderAgents();
5989
+ } else if (view === 'sessions') {
5990
+ kanban.classList.remove('active');
5991
+ graph.classList.remove('active');
5992
+ analytics.classList.remove('active');
5993
+ agents.classList.remove('active');
5994
+ sessions.classList.add('active');
5995
+ loadAndRenderSessions();
5996
+ }
4070
5997
  }
4071
5998
 
4072
- function renderGraph(nodes) {
4073
- const svg = document.getElementById('graph-svg');
4074
- const edgesGroup = document.getElementById('graph-edges');
4075
- const nodesGroup = document.getElementById('graph-nodes');
4076
5999
 
4077
- edgesGroup.innerHTML = '';
4078
- nodesGroup.innerHTML = '';
6000
+ // =====================================================================
6001
+ // Init
6002
+ // =====================================================================
4079
6003
 
4080
- if (nodes.length === 0) return;
6004
+ document.getElementById('panel-close').addEventListener('click', closePanel);
6005
+ document.getElementById('panel-overlay').addEventListener('click', closePanel);
4081
6006
 
4082
- const rect = svg.getBoundingClientRect();
4083
- const width = rect.width || 800;
4084
- const height = rect.height || 500;
6007
+ document.querySelectorAll('.view-btn').forEach(btn => {
6008
+ btn.addEventListener('click', () => switchView(btn.dataset.view));
6009
+ });
4085
6010
 
4086
- const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
4087
- const nodeById = new Map(graphNodes.map(n => [n.id, n]));
6011
+ document.getElementById('analytics-refresh').addEventListener('click', () => {
6012
+ ensureAnalyticsLoaded(true);
6013
+ });
4088
6014
 
4089
- graphNodes.forEach((n, i) => {
4090
- const angle = (2 * Math.PI * i) / graphNodes.length;
4091
- n.x = width / 2 + Math.cos(angle) * 150;
4092
- n.y = height / 2 + Math.sin(angle) * 150;
4093
- });
6015
+ document.getElementById('analytics-features').addEventListener('click', (e) => {
6016
+ const btn = e.target.closest && e.target.closest('button[data-feature]');
6017
+ if (!btn) return;
6018
+ loadFeatureAnalytics(btn.dataset.feature).catch(err => renderAnalyticsError(err));
6019
+ });
4094
6020
 
4095
- if (simulation) simulation.stop();
6021
+ // Session filter event listeners
6022
+ document.getElementById('filter-status').addEventListener('change', applySessionFilters);
6023
+ document.getElementById('filter-agent').addEventListener('change', applySessionFilters);
6024
+ document.getElementById('filter-search').addEventListener('input', applySessionFilters);
6025
+ document.getElementById('filter-date-from').addEventListener('change', applySessionFilters);
6026
+ document.getElementById('filter-date-to').addEventListener('change', applySessionFilters);
6027
+ document.getElementById('filter-clear').addEventListener('click', clearSessionFilters);
6028
+ document.getElementById('compare-sessions-btn').addEventListener('click', compareSessions);
4096
6029
 
4097
- simulation = d3.forceSimulation(graphNodes)
4098
- .force('link', d3.forceLink(graphEdges)
4099
- .id(d => d.id)
4100
- .distance(120)
4101
- .strength(0.5))
4102
- .force('charge', d3.forceManyBody()
4103
- .strength(-400))
4104
- .force('center', d3.forceCenter(width / 2, height / 2))
4105
- .force('collision', d3.forceCollide().radius(70))
4106
- .on('tick', updatePositions);
6030
+ // Session comparison modal
6031
+ document.getElementById('comparison-close').addEventListener('click', closeComparison);
6032
+ document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4107
6033
 
4108
- graphEdges.forEach(edge => {
4109
- const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
4110
- line.classList.add('graph-edge', edge.type);
4111
- line.setAttribute('marker-end', 'url(#arrowhead)');
4112
- line.dataset.source = edge.source.id || edge.source;
4113
- line.dataset.target = edge.target.id || edge.target;
4114
- edgesGroup.appendChild(line);
4115
- });
6034
+ // Graph filter event listeners
6035
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6036
+ cb.addEventListener('change', applyGraphFilters);
6037
+ });
4116
6038
 
4117
- graphNodes.forEach(node => {
4118
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
4119
- g.classList.add('graph-node');
4120
- g.dataset.id = node.id;
4121
- g.dataset.collection = node._collection;
6039
+ const graphSearchEl = document.getElementById('graph-search');
6040
+ if (graphSearchEl) {
6041
+ graphSearchEl.addEventListener('input', applyGraphFilters);
6042
+ }
4122
6043
 
4123
- const radius = getNodeRadius(node);
4124
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4125
- circle.setAttribute('r', radius);
4126
- circle.setAttribute('fill', getNodeColor(node));
6044
+ const graphResetBtn = document.getElementById('graph-reset');
6045
+ if (graphResetBtn) {
6046
+ graphResetBtn.addEventListener('click', resetGraphView);
6047
+ }
4127
6048
 
4128
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
4129
- const lines = wrapText(node.title);
4130
- const lineHeight = 11;
4131
- const startY = -((lines.length - 1) * lineHeight) / 2;
4132
-
4133
- lines.forEach((line, i) => {
4134
- const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
4135
- tspan.setAttribute('x', '0');
4136
- tspan.setAttribute('dy', i === 0 ? `${startY}px` : `${lineHeight}px`);
4137
- tspan.textContent = line;
4138
- text.appendChild(tspan);
4139
- });
6049
+ const graphShowAllBtn = document.getElementById('graph-show-all');
6050
+ if (graphShowAllBtn) {
6051
+ graphShowAllBtn.addEventListener('click', showAllNodes);
6052
+ }
4140
6053
 
4141
- g.appendChild(circle);
4142
- g.appendChild(text);
4143
- nodesGroup.appendChild(g);
6054
+ document.addEventListener('keydown', (e) => {
6055
+ if (e.key === 'Escape') closePanel();
6056
+ });
4144
6057
 
4145
- g.addEventListener('click', () => {
4146
- openPanel(node._collection, node.id);
4147
- });
6058
+ loadData().then(async ({ status, nodes }) => {
6059
+ await renderKanban(nodes);
6060
+ updateKanbanGrid();
6061
+ }).catch(err => {
6062
+ console.error('Error loading dashboard data:', err);
6063
+ });
4148
6064
 
4149
- let isDragging = false;
4150
- let dragOffset = { x: 0, y: 0 };
6065
+ function showAllNodes() {
6066
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6067
+ cb.checked = true;
6068
+ });
6069
+ graphState.filters = { todo: true, 'in-progress': true, blocked: true, done: true };
6070
+ applyGraphFilters();
6071
+ }
4151
6072
 
4152
- g.addEventListener('mousedown', (e) => {
4153
- isDragging = true;
4154
- dragOffset = { x: e.clientX - node.x, y: e.clientY - node.y };
4155
- simulation.alphaTarget(0.3).restart();
4156
- e.preventDefault();
4157
- });
6073
+ function renderGraph(nodes) {
6074
+ if (nodes.length === 0) {
6075
+ if (visNetwork) visNetwork.destroy();
6076
+ visNetwork = null;
6077
+ return;
6078
+ }
4158
6079
 
4159
- document.addEventListener('mousemove', (e) => {
4160
- if (isDragging) {
4161
- node.fx = e.clientX - dragOffset.x;
4162
- node.fy = e.clientY - dragOffset.y;
6080
+ const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
6081
+
6082
+ graphState.allNodes = graphNodes;
6083
+ graphState.allEdges = graphEdges;
6084
+
6085
+ // Create Vis.js nodes dataset
6086
+ const nodesData = new vis.DataSet(graphNodes.map(n => ({
6087
+ id: n.id,
6088
+ label: wrapText(n.title),
6089
+ title: n.title + '\nStatus: ' + n.status,
6090
+ color: {
6091
+ background: getNodeColor(n),
6092
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
6093
+ highlight: {
6094
+ background: getNodeColor(n),
6095
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
4163
6096
  }
4164
- });
6097
+ },
6098
+ size: getNodeRadius(n),
6099
+ font: {
6100
+ size: 12,
6101
+ face: "'JetBrains Mono', monospace",
6102
+ color: 'white',
6103
+ strokeWidth: 0
6104
+ },
6105
+ physics: true,
6106
+ borderWidth: 2,
6107
+ status: n.status,
6108
+ _collection: n._collection
6109
+ })));
6110
+
6111
+ // Create Vis.js edges dataset
6112
+ const edgesData = new vis.DataSet(graphEdges.map(e => ({
6113
+ from: e.from,
6114
+ to: e.to,
6115
+ arrows: 'to',
6116
+ smooth: { type: 'continuous' },
6117
+ color: e.type === 'blocked_by'
6118
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
6119
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
6120
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
6121
+ width: 1.5
6122
+ })));
6123
+
6124
+ // Destroy existing network if it exists
6125
+ if (visNetwork) {
6126
+ visNetwork.destroy();
6127
+ }
4165
6128
 
4166
- document.addEventListener('mouseup', () => {
4167
- if (isDragging) {
4168
- isDragging = false;
4169
- node.fx = null;
4170
- node.fy = null;
4171
- simulation.alphaTarget(0);
6129
+ // Create new Vis.js network
6130
+ const container = document.getElementById('graph-network');
6131
+ const data = {
6132
+ nodes: nodesData,
6133
+ edges: edgesData
6134
+ };
6135
+
6136
+ const options = {
6137
+ physics: {
6138
+ enabled: true,
6139
+ stabilization: {
6140
+ iterations: 200,
6141
+ fit: true
6142
+ },
6143
+ barnesHut: {
6144
+ gravitationalConstant: -30000,
6145
+ centralGravity: 0.3,
6146
+ springLength: 200,
6147
+ springConstant: 0.04
6148
+ },
6149
+ maxVelocity: 50
6150
+ },
6151
+ interaction: {
6152
+ navigationButtons: true,
6153
+ keyboard: true,
6154
+ zoomView: true,
6155
+ dragView: true
6156
+ },
6157
+ nodes: {
6158
+ shape: 'circle',
6159
+ scaling: {
6160
+ min: 10,
6161
+ max: 50
4172
6162
  }
4173
- });
4174
- });
6163
+ }
6164
+ };
4175
6165
 
4176
- function updatePositions() {
4177
- const nodeElements = nodesGroup.querySelectorAll('.graph-node');
4178
- nodeElements.forEach(g => {
4179
- const node = nodeById.get(g.dataset.id);
6166
+ visNetwork = new vis.Network(container, data, options);
6167
+
6168
+ // Handle node clicks
6169
+ visNetwork.on('click', (params) => {
6170
+ if (params.nodes.length > 0) {
6171
+ const nodeId = params.nodes[0];
6172
+ const node = graphState.allNodes.find(n => n.id === nodeId);
4180
6173
  if (node) {
4181
- node.x = Math.max(30, Math.min(width - 30, node.x));
4182
- node.y = Math.max(30, Math.min(height - 30, node.y));
4183
- g.setAttribute('transform', `translate(${node.x}, ${node.y})`);
6174
+ openPanel(node._collection, node.id);
4184
6175
  }
6176
+ }
6177
+ });
6178
+
6179
+ // Apply filters after network is initialized
6180
+ applyGraphFilters();
6181
+ }
6182
+
6183
+ // =====================================================================
6184
+ // Agent Skills Analysis
6185
+ // =====================================================================
6186
+
6187
+ function analyzeAgentSkills(sessions) {
6188
+ const skillProfiles = {};
6189
+ const agents = [...new Set(sessions.map(s => s.properties?.agent).filter(Boolean))];
6190
+ agents.forEach(agent => {
6191
+ skillProfiles[agent] = {Implementation: 0, Analysis: 0, Testing: 0, Documentation: 0, Coordination: 0};
6192
+ });
6193
+ sessions.forEach(session => {
6194
+ const agent = session.properties?.agent;
6195
+ if (!agent) return;
6196
+ const desc = (session.name || session.id || '').toLowerCase();
6197
+ const cnt = session.properties?.event_count || 0;
6198
+ if (desc.includes('test') || desc.includes('validate')) skillProfiles[agent].Testing += Math.min(cnt / 10, 2);
6199
+ if (desc.includes('implement') || desc.includes('code') || desc.includes('build')) skillProfiles[agent].Implementation += Math.min(cnt / 10, 2);
6200
+ if (desc.includes('analyze') || desc.includes('research')) skillProfiles[agent].Analysis += Math.min(cnt / 10, 2);
6201
+ if (desc.includes('document') || desc.includes('explain')) skillProfiles[agent].Documentation += Math.min(cnt / 10, 2);
6202
+ if (desc.includes('coordinate') || desc.includes('delegate')) skillProfiles[agent].Coordination += Math.min(cnt / 10, 2);
6203
+ if (agent.includes('Claude')) {
6204
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
6205
+ skillProfiles[agent].Documentation = Math.max(skillProfiles[agent].Documentation, 4);
6206
+ }
6207
+ if (agent.includes('Codex')) {
6208
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 5);
6209
+ skillProfiles[agent].Testing = Math.max(skillProfiles[agent].Testing, 4);
6210
+ }
6211
+ if (agent.includes('Orchestrator')) {
6212
+ skillProfiles[agent].Coordination = Math.max(skillProfiles[agent].Coordination, 5);
6213
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4);
6214
+ }
6215
+ if (agent.includes('Gemini')) {
6216
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
6217
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 3);
6218
+ }
6219
+ });
6220
+ agents.forEach(agent => {
6221
+ Object.keys(skillProfiles[agent]).forEach(skill => {
6222
+ skillProfiles[agent][skill] = Math.min(5, Math.max(1, skillProfiles[agent][skill]));
4185
6223
  });
6224
+ });
6225
+ return { agents, skillProfiles };
6226
+ }
4186
6227
 
4187
- const edgeElements = edgesGroup.querySelectorAll('.graph-edge');
4188
- edgeElements.forEach(line => {
4189
- const source = nodeById.get(line.dataset.source);
4190
- const target = nodeById.get(line.dataset.target);
4191
- if (source && target) {
4192
- line.setAttribute('x1', source.x);
4193
- line.setAttribute('y1', source.y);
4194
- line.setAttribute('x2', target.x);
4195
- line.setAttribute('y2', target.y);
4196
- }
6228
+ function getProficiencyColor(level) {
6229
+ return `proficiency-${Math.round(level)}`;
6230
+ }
6231
+
6232
+ function getProficiencyLabel(level) {
6233
+ const labels = ['', 'Novice', 'Beginner', 'Intermediate', 'Advanced', 'Expert'];
6234
+ return labels[Math.round(level)] || 'Expert';
6235
+ }
6236
+
6237
+ function renderSkillsMatrix(agents, skillProfiles) {
6238
+ const skills = ['Implementation', 'Analysis', 'Testing', 'Documentation', 'Coordination'];
6239
+ let html = '<div class="skills-matrix">';
6240
+ html += '<div class="skills-matrix-cell skills-matrix-header-row">AGENT</div>';
6241
+ skills.forEach(skill => html += `<div class="skills-matrix-cell skills-matrix-header-row">${skill}</div>`);
6242
+ agents.forEach(agent => {
6243
+ html += `<div class="skills-matrix-cell skills-matrix-agent-name">${agent}</div>`;
6244
+ skills.forEach(skill => {
6245
+ const level = skillProfiles[agent][skill];
6246
+ const rnd = Math.round(level);
6247
+ html += `<div class="skills-matrix-cell"><div class="proficiency-dot ${getProficiencyColor(level)}" title="${getProficiencyLabel(level)} (${rnd}/5)">${rnd}</div></div>`;
4197
6248
  });
6249
+ });
6250
+ html += '</div><div class="skill-category-legend"><div style="font-weight: 600; width: 100%; margin-bottom: 0.5rem;">Proficiency Scale:</div>';
6251
+ for (let i = 1; i <= 5; i++) {
6252
+ html += `<div class="skill-category-item"><span class="proficiency-dot proficiency-${i}">${i}</span> ${getProficiencyLabel(i)}</div>`;
6253
+ }
6254
+ html += '</div>';
6255
+ return html;
6256
+ }
6257
+
6258
+ async function loadAndRenderAgents() {
6259
+ const el = document.getElementById('skills-matrix-content');
6260
+ try {
6261
+ let sessions = allSessions;
6262
+ if (!sessions.length) {
6263
+ const r = await fetch(`${API}/sessions`);
6264
+ if (!r.ok) throw new Error('Failed to load');
6265
+ sessions = (await r.json()).nodes || [];
6266
+ }
6267
+ if (!sessions.length) {
6268
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents found</div>';
6269
+ return;
6270
+ }
6271
+ const { agents, skillProfiles } = analyzeAgentSkills(sessions);
6272
+ if (!agents.length) {
6273
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents</div>';
6274
+ return;
6275
+ }
6276
+ el.innerHTML = renderSkillsMatrix(agents, skillProfiles);
6277
+ } catch (e) {
6278
+ el.innerHTML = `<div style="padding: 2rem; color: red;">Error: ${e.message}</div>`;
4198
6279
  }
4199
6280
  }
4200
6281
 
@@ -4206,6 +6287,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4206
6287
  const kanban = document.getElementById('kanban');
4207
6288
  const graph = document.getElementById('graph-container');
4208
6289
  const analytics = document.getElementById('analytics');
6290
+ const agents = document.getElementById('agents');
4209
6291
  const sessions = document.getElementById('sessions');
4210
6292
  const buttons = document.querySelectorAll('.view-btn');
4211
6293
 
@@ -4217,29 +6299,41 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4217
6299
  kanban.classList.add('active');
4218
6300
  graph.classList.remove('active');
4219
6301
  analytics.classList.remove('active');
6302
+ agents.classList.remove('active');
4220
6303
  sessions.classList.remove('active');
4221
6304
  renderKanban(allNodes);
4222
6305
  } else if (view === 'graph') {
4223
6306
  kanban.classList.remove('active');
4224
6307
  graph.classList.add('active');
4225
6308
  analytics.classList.remove('active');
6309
+ agents.classList.remove('active');
4226
6310
  sessions.classList.remove('active');
4227
6311
  renderGraph(allNodes);
4228
6312
  } else if (view === 'analytics') {
4229
6313
  kanban.classList.remove('active');
4230
6314
  graph.classList.remove('active');
4231
6315
  analytics.classList.add('active');
6316
+ agents.classList.remove('active');
4232
6317
  sessions.classList.remove('active');
4233
6318
  ensureAnalyticsLoaded(false);
6319
+ } else if (view === 'agents') {
6320
+ kanban.classList.remove('active');
6321
+ graph.classList.remove('active');
6322
+ analytics.classList.remove('active');
6323
+ agents.classList.add('active');
6324
+ sessions.classList.remove('active');
6325
+ loadAndRenderAgents();
4234
6326
  } else if (view === 'sessions') {
4235
6327
  kanban.classList.remove('active');
4236
6328
  graph.classList.remove('active');
4237
6329
  analytics.classList.remove('active');
6330
+ agents.classList.remove('active');
4238
6331
  sessions.classList.add('active');
4239
6332
  loadAndRenderSessions();
4240
6333
  }
4241
6334
  }
4242
6335
 
6336
+
4243
6337
  // =====================================================================
4244
6338
  // Init
4245
6339
  // =====================================================================
@@ -4274,6 +6368,11 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4274
6368
  document.getElementById('comparison-close').addEventListener('click', closeComparison);
4275
6369
  document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4276
6370
 
6371
+ // Graph filter event listeners
6372
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6373
+ cb.addEventListener('change', applyGraphFilters);
6374
+ });
6375
+
4277
6376
  document.addEventListener('keydown', (e) => {
4278
6377
  if (e.key === 'Escape') closePanel();
4279
6378
  });