htmlgraph 0.20.1__py3-none-any.whl → 0.27.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. htmlgraph/.htmlgraph/.session-warning-state.json +6 -0
  2. htmlgraph/.htmlgraph/agents.json +72 -0
  3. htmlgraph/.htmlgraph/htmlgraph.db +0 -0
  4. htmlgraph/__init__.py +51 -1
  5. htmlgraph/__init__.pyi +123 -0
  6. htmlgraph/agent_detection.py +26 -10
  7. htmlgraph/agent_registry.py +2 -1
  8. htmlgraph/analytics/__init__.py +8 -1
  9. htmlgraph/analytics/cli.py +86 -20
  10. htmlgraph/analytics/cost_analyzer.py +391 -0
  11. htmlgraph/analytics/cost_monitor.py +664 -0
  12. htmlgraph/analytics/cost_reporter.py +675 -0
  13. htmlgraph/analytics/cross_session.py +617 -0
  14. htmlgraph/analytics/dependency.py +10 -6
  15. htmlgraph/analytics/pattern_learning.py +771 -0
  16. htmlgraph/analytics/session_graph.py +707 -0
  17. htmlgraph/analytics/strategic/__init__.py +80 -0
  18. htmlgraph/analytics/strategic/cost_optimizer.py +611 -0
  19. htmlgraph/analytics/strategic/pattern_detector.py +876 -0
  20. htmlgraph/analytics/strategic/preference_manager.py +709 -0
  21. htmlgraph/analytics/strategic/suggestion_engine.py +747 -0
  22. htmlgraph/analytics/work_type.py +67 -27
  23. htmlgraph/analytics_index.py +53 -20
  24. htmlgraph/api/__init__.py +3 -0
  25. htmlgraph/api/cost_alerts_websocket.py +416 -0
  26. htmlgraph/api/main.py +2498 -0
  27. htmlgraph/api/static/htmx.min.js +1 -0
  28. htmlgraph/api/static/style-redesign.css +1344 -0
  29. htmlgraph/api/static/style.css +1079 -0
  30. htmlgraph/api/templates/dashboard-redesign.html +1366 -0
  31. htmlgraph/api/templates/dashboard.html +794 -0
  32. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  33. htmlgraph/api/templates/partials/activity-feed.html +1100 -0
  34. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  35. htmlgraph/api/templates/partials/agents.html +317 -0
  36. htmlgraph/api/templates/partials/event-traces.html +373 -0
  37. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  38. htmlgraph/api/templates/partials/features.html +578 -0
  39. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  40. htmlgraph/api/templates/partials/metrics.html +346 -0
  41. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  42. htmlgraph/api/templates/partials/orchestration.html +198 -0
  43. htmlgraph/api/templates/partials/spawners.html +375 -0
  44. htmlgraph/api/templates/partials/work-items.html +613 -0
  45. htmlgraph/api/websocket.py +538 -0
  46. htmlgraph/archive/__init__.py +24 -0
  47. htmlgraph/archive/bloom.py +234 -0
  48. htmlgraph/archive/fts.py +297 -0
  49. htmlgraph/archive/manager.py +583 -0
  50. htmlgraph/archive/search.py +244 -0
  51. htmlgraph/atomic_ops.py +560 -0
  52. htmlgraph/attribute_index.py +2 -1
  53. htmlgraph/bounded_paths.py +539 -0
  54. htmlgraph/builders/base.py +57 -2
  55. htmlgraph/builders/bug.py +19 -3
  56. htmlgraph/builders/chore.py +19 -3
  57. htmlgraph/builders/epic.py +19 -3
  58. htmlgraph/builders/feature.py +27 -3
  59. htmlgraph/builders/insight.py +2 -1
  60. htmlgraph/builders/metric.py +2 -1
  61. htmlgraph/builders/pattern.py +2 -1
  62. htmlgraph/builders/phase.py +19 -3
  63. htmlgraph/builders/spike.py +29 -3
  64. htmlgraph/builders/track.py +42 -1
  65. htmlgraph/cigs/__init__.py +81 -0
  66. htmlgraph/cigs/autonomy.py +385 -0
  67. htmlgraph/cigs/cost.py +475 -0
  68. htmlgraph/cigs/messages_basic.py +472 -0
  69. htmlgraph/cigs/messaging.py +365 -0
  70. htmlgraph/cigs/models.py +771 -0
  71. htmlgraph/cigs/pattern_storage.py +427 -0
  72. htmlgraph/cigs/patterns.py +503 -0
  73. htmlgraph/cigs/posttool_analyzer.py +234 -0
  74. htmlgraph/cigs/reporter.py +818 -0
  75. htmlgraph/cigs/tracker.py +317 -0
  76. htmlgraph/cli/.htmlgraph/.session-warning-state.json +6 -0
  77. htmlgraph/cli/.htmlgraph/agents.json +72 -0
  78. htmlgraph/cli/.htmlgraph/htmlgraph.db +0 -0
  79. htmlgraph/cli/__init__.py +42 -0
  80. htmlgraph/cli/__main__.py +6 -0
  81. htmlgraph/cli/analytics.py +1424 -0
  82. htmlgraph/cli/base.py +685 -0
  83. htmlgraph/cli/constants.py +206 -0
  84. htmlgraph/cli/core.py +954 -0
  85. htmlgraph/cli/main.py +147 -0
  86. htmlgraph/cli/models.py +475 -0
  87. htmlgraph/cli/templates/__init__.py +1 -0
  88. htmlgraph/cli/templates/cost_dashboard.py +399 -0
  89. htmlgraph/cli/work/__init__.py +239 -0
  90. htmlgraph/cli/work/browse.py +115 -0
  91. htmlgraph/cli/work/features.py +568 -0
  92. htmlgraph/cli/work/orchestration.py +676 -0
  93. htmlgraph/cli/work/report.py +728 -0
  94. htmlgraph/cli/work/sessions.py +466 -0
  95. htmlgraph/cli/work/snapshot.py +559 -0
  96. htmlgraph/cli/work/tracks.py +486 -0
  97. htmlgraph/cli_commands/__init__.py +1 -0
  98. htmlgraph/cli_commands/feature.py +195 -0
  99. htmlgraph/cli_framework.py +115 -0
  100. htmlgraph/collections/__init__.py +2 -0
  101. htmlgraph/collections/base.py +197 -14
  102. htmlgraph/collections/bug.py +2 -1
  103. htmlgraph/collections/chore.py +2 -1
  104. htmlgraph/collections/epic.py +2 -1
  105. htmlgraph/collections/feature.py +2 -1
  106. htmlgraph/collections/insight.py +2 -1
  107. htmlgraph/collections/metric.py +2 -1
  108. htmlgraph/collections/pattern.py +2 -1
  109. htmlgraph/collections/phase.py +2 -1
  110. htmlgraph/collections/session.py +194 -0
  111. htmlgraph/collections/spike.py +13 -2
  112. htmlgraph/collections/task_delegation.py +241 -0
  113. htmlgraph/collections/todo.py +14 -1
  114. htmlgraph/collections/traces.py +487 -0
  115. htmlgraph/config/cost_models.json +56 -0
  116. htmlgraph/config.py +190 -0
  117. htmlgraph/context_analytics.py +2 -1
  118. htmlgraph/converter.py +116 -7
  119. htmlgraph/cost_analysis/__init__.py +5 -0
  120. htmlgraph/cost_analysis/analyzer.py +438 -0
  121. htmlgraph/dashboard.html +2246 -248
  122. htmlgraph/dashboard.html.backup +6592 -0
  123. htmlgraph/dashboard.html.bak +7181 -0
  124. htmlgraph/dashboard.html.bak2 +7231 -0
  125. htmlgraph/dashboard.html.bak3 +7232 -0
  126. htmlgraph/db/__init__.py +38 -0
  127. htmlgraph/db/queries.py +790 -0
  128. htmlgraph/db/schema.py +1788 -0
  129. htmlgraph/decorators.py +317 -0
  130. htmlgraph/dependency_models.py +2 -1
  131. htmlgraph/deploy.py +26 -27
  132. htmlgraph/docs/API_REFERENCE.md +841 -0
  133. htmlgraph/docs/HTTP_API.md +750 -0
  134. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  135. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +717 -0
  136. htmlgraph/docs/README.md +532 -0
  137. htmlgraph/docs/__init__.py +77 -0
  138. htmlgraph/docs/docs_version.py +55 -0
  139. htmlgraph/docs/metadata.py +93 -0
  140. htmlgraph/docs/migrations.py +232 -0
  141. htmlgraph/docs/template_engine.py +143 -0
  142. htmlgraph/docs/templates/_sections/cli_reference.md.j2 +52 -0
  143. htmlgraph/docs/templates/_sections/core_concepts.md.j2 +29 -0
  144. htmlgraph/docs/templates/_sections/sdk_basics.md.j2 +69 -0
  145. htmlgraph/docs/templates/base_agents.md.j2 +78 -0
  146. htmlgraph/docs/templates/example_user_override.md.j2 +47 -0
  147. htmlgraph/docs/version_check.py +163 -0
  148. htmlgraph/edge_index.py +2 -1
  149. htmlgraph/error_handler.py +544 -0
  150. htmlgraph/event_log.py +86 -37
  151. htmlgraph/event_migration.py +2 -1
  152. htmlgraph/file_watcher.py +12 -8
  153. htmlgraph/find_api.py +2 -1
  154. htmlgraph/git_events.py +67 -9
  155. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  156. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  157. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  158. htmlgraph/hooks/__init__.py +8 -0
  159. htmlgraph/hooks/bootstrap.py +169 -0
  160. htmlgraph/hooks/cigs_pretool_enforcer.py +354 -0
  161. htmlgraph/hooks/concurrent_sessions.py +208 -0
  162. htmlgraph/hooks/context.py +350 -0
  163. htmlgraph/hooks/drift_handler.py +525 -0
  164. htmlgraph/hooks/event_tracker.py +790 -99
  165. htmlgraph/hooks/git_commands.py +175 -0
  166. htmlgraph/hooks/installer.py +5 -1
  167. htmlgraph/hooks/orchestrator.py +327 -76
  168. htmlgraph/hooks/orchestrator_reflector.py +31 -4
  169. htmlgraph/hooks/post_tool_use_failure.py +32 -7
  170. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  171. htmlgraph/hooks/posttooluse.py +92 -19
  172. htmlgraph/hooks/pretooluse.py +527 -7
  173. htmlgraph/hooks/prompt_analyzer.py +637 -0
  174. htmlgraph/hooks/session_handler.py +668 -0
  175. htmlgraph/hooks/session_summary.py +395 -0
  176. htmlgraph/hooks/state_manager.py +504 -0
  177. htmlgraph/hooks/subagent_detection.py +202 -0
  178. htmlgraph/hooks/subagent_stop.py +369 -0
  179. htmlgraph/hooks/task_enforcer.py +99 -4
  180. htmlgraph/hooks/validator.py +212 -91
  181. htmlgraph/ids.py +2 -1
  182. htmlgraph/learning.py +125 -100
  183. htmlgraph/mcp_server.py +2 -1
  184. htmlgraph/models.py +217 -18
  185. htmlgraph/operations/README.md +62 -0
  186. htmlgraph/operations/__init__.py +79 -0
  187. htmlgraph/operations/analytics.py +339 -0
  188. htmlgraph/operations/bootstrap.py +289 -0
  189. htmlgraph/operations/events.py +244 -0
  190. htmlgraph/operations/fastapi_server.py +231 -0
  191. htmlgraph/operations/hooks.py +350 -0
  192. htmlgraph/operations/initialization.py +597 -0
  193. htmlgraph/operations/initialization.py.backup +228 -0
  194. htmlgraph/operations/server.py +303 -0
  195. htmlgraph/orchestration/__init__.py +58 -0
  196. htmlgraph/orchestration/claude_launcher.py +179 -0
  197. htmlgraph/orchestration/command_builder.py +72 -0
  198. htmlgraph/orchestration/headless_spawner.py +281 -0
  199. htmlgraph/orchestration/live_events.py +377 -0
  200. htmlgraph/orchestration/model_selection.py +327 -0
  201. htmlgraph/orchestration/plugin_manager.py +140 -0
  202. htmlgraph/orchestration/prompts.py +137 -0
  203. htmlgraph/orchestration/spawner_event_tracker.py +383 -0
  204. htmlgraph/orchestration/spawners/__init__.py +16 -0
  205. htmlgraph/orchestration/spawners/base.py +194 -0
  206. htmlgraph/orchestration/spawners/claude.py +173 -0
  207. htmlgraph/orchestration/spawners/codex.py +435 -0
  208. htmlgraph/orchestration/spawners/copilot.py +294 -0
  209. htmlgraph/orchestration/spawners/gemini.py +471 -0
  210. htmlgraph/orchestration/subprocess_runner.py +36 -0
  211. htmlgraph/{orchestration.py → orchestration/task_coordination.py} +16 -8
  212. htmlgraph/orchestration.md +563 -0
  213. htmlgraph/orchestrator-system-prompt-optimized.txt +863 -0
  214. htmlgraph/orchestrator.py +2 -1
  215. htmlgraph/orchestrator_config.py +357 -0
  216. htmlgraph/orchestrator_mode.py +115 -4
  217. htmlgraph/parallel.py +2 -1
  218. htmlgraph/parser.py +86 -6
  219. htmlgraph/path_query.py +608 -0
  220. htmlgraph/pattern_matcher.py +636 -0
  221. htmlgraph/pydantic_models.py +476 -0
  222. htmlgraph/quality_gates.py +350 -0
  223. htmlgraph/query_builder.py +2 -1
  224. htmlgraph/query_composer.py +509 -0
  225. htmlgraph/reflection.py +443 -0
  226. htmlgraph/refs.py +344 -0
  227. htmlgraph/repo_hash.py +512 -0
  228. htmlgraph/repositories/__init__.py +292 -0
  229. htmlgraph/repositories/analytics_repository.py +455 -0
  230. htmlgraph/repositories/analytics_repository_standard.py +628 -0
  231. htmlgraph/repositories/feature_repository.py +581 -0
  232. htmlgraph/repositories/feature_repository_htmlfile.py +668 -0
  233. htmlgraph/repositories/feature_repository_memory.py +607 -0
  234. htmlgraph/repositories/feature_repository_sqlite.py +858 -0
  235. htmlgraph/repositories/filter_service.py +620 -0
  236. htmlgraph/repositories/filter_service_standard.py +445 -0
  237. htmlgraph/repositories/shared_cache.py +621 -0
  238. htmlgraph/repositories/shared_cache_memory.py +395 -0
  239. htmlgraph/repositories/track_repository.py +552 -0
  240. htmlgraph/repositories/track_repository_htmlfile.py +619 -0
  241. htmlgraph/repositories/track_repository_memory.py +508 -0
  242. htmlgraph/repositories/track_repository_sqlite.py +711 -0
  243. htmlgraph/sdk/__init__.py +398 -0
  244. htmlgraph/sdk/__init__.pyi +14 -0
  245. htmlgraph/sdk/analytics/__init__.py +19 -0
  246. htmlgraph/sdk/analytics/engine.py +155 -0
  247. htmlgraph/sdk/analytics/helpers.py +178 -0
  248. htmlgraph/sdk/analytics/registry.py +109 -0
  249. htmlgraph/sdk/base.py +484 -0
  250. htmlgraph/sdk/constants.py +216 -0
  251. htmlgraph/sdk/core.pyi +308 -0
  252. htmlgraph/sdk/discovery.py +120 -0
  253. htmlgraph/sdk/help/__init__.py +12 -0
  254. htmlgraph/sdk/help/mixin.py +699 -0
  255. htmlgraph/sdk/mixins/__init__.py +15 -0
  256. htmlgraph/sdk/mixins/attribution.py +113 -0
  257. htmlgraph/sdk/mixins/mixin.py +410 -0
  258. htmlgraph/sdk/operations/__init__.py +12 -0
  259. htmlgraph/sdk/operations/mixin.py +427 -0
  260. htmlgraph/sdk/orchestration/__init__.py +17 -0
  261. htmlgraph/sdk/orchestration/coordinator.py +203 -0
  262. htmlgraph/sdk/orchestration/spawner.py +204 -0
  263. htmlgraph/sdk/planning/__init__.py +19 -0
  264. htmlgraph/sdk/planning/bottlenecks.py +93 -0
  265. htmlgraph/sdk/planning/mixin.py +211 -0
  266. htmlgraph/sdk/planning/parallel.py +186 -0
  267. htmlgraph/sdk/planning/queue.py +210 -0
  268. htmlgraph/sdk/planning/recommendations.py +87 -0
  269. htmlgraph/sdk/planning/smart_planning.py +319 -0
  270. htmlgraph/sdk/session/__init__.py +19 -0
  271. htmlgraph/sdk/session/continuity.py +57 -0
  272. htmlgraph/sdk/session/handoff.py +110 -0
  273. htmlgraph/sdk/session/info.py +309 -0
  274. htmlgraph/sdk/session/manager.py +103 -0
  275. htmlgraph/sdk/strategic/__init__.py +26 -0
  276. htmlgraph/sdk/strategic/mixin.py +563 -0
  277. htmlgraph/server.py +295 -107
  278. htmlgraph/session_hooks.py +300 -0
  279. htmlgraph/session_manager.py +285 -3
  280. htmlgraph/session_registry.py +587 -0
  281. htmlgraph/session_state.py +436 -0
  282. htmlgraph/session_warning.py +2 -1
  283. htmlgraph/sessions/__init__.py +23 -0
  284. htmlgraph/sessions/handoff.py +756 -0
  285. htmlgraph/system_prompts.py +450 -0
  286. htmlgraph/templates/orchestration-view.html +350 -0
  287. htmlgraph/track_builder.py +33 -1
  288. htmlgraph/track_manager.py +38 -0
  289. htmlgraph/transcript.py +18 -5
  290. htmlgraph/validation.py +115 -0
  291. htmlgraph/watch.py +2 -1
  292. htmlgraph/work_type_utils.py +2 -1
  293. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/dashboard.html +2246 -248
  294. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/METADATA +95 -64
  295. htmlgraph-0.27.5.dist-info/RECORD +337 -0
  296. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/entry_points.txt +1 -1
  297. htmlgraph/cli.py +0 -4839
  298. htmlgraph/sdk.py +0 -2359
  299. htmlgraph-0.20.1.dist-info/RECORD +0 -118
  300. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/styles.css +0 -0
  301. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  302. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  303. {htmlgraph-0.20.1.data → htmlgraph-0.27.5.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  304. {htmlgraph-0.20.1.dist-info → htmlgraph-0.27.5.dist-info}/WHEEL +0 -0
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 -->
@@ -2732,11 +3845,233 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2732
3845
  ]);
2733
3846
  }
2734
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>
4011
+
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('');
4019
+
4020
+ containerEl.innerHTML = barsHTML;
4021
+ }
4022
+
4023
+ function renderAgentCostLegend(costs) {
4024
+ const legendEl = document.getElementById('cost-legend');
4025
+ if (!legendEl || costs.byAgent.length === 0) return;
4026
+
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('');
4036
+
4037
+ legendEl.innerHTML = legendItems;
4038
+ }
4039
+
4040
+ async function loadAndRenderAgentCosts() {
4041
+ const container = document.getElementById('agent-costs');
4042
+ if (!container) return;
4043
+
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
+ }
4052
+
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
+ }
4067
+ }
4068
+
2735
4069
  async function ensureAnalyticsLoaded(force = false) {
2736
4070
  const stale = (Date.now() - analyticsLoadedAt) > 60_000;
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
  }
@@ -3240,7 +4575,9 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3240
4575
  // Sort tracks by feature completion (incomplete first), then by priority
3241
4576
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
3242
4577
  const sortedTracks = Array.from(tracked.values())
3243
- .filter(t => t.track !== null)
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)
3244
4581
  .sort((a, b) => {
3245
4582
  // Calculate completion percentage for each track
3246
4583
  const aTotal = a.features.length;
@@ -3257,7 +4594,10 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3257
4594
  }
3258
4595
 
3259
4596
  // Within same completion status, sort by priority
3260
- return priorityOrder[a.track.priority] - priorityOrder[b.track.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);
3261
4601
  });
3262
4602
 
3263
4603
  // Render
@@ -3426,18 +4766,51 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3426
4766
  <div class="track-column-cards">
3427
4767
  ${byStatus[status].length === 0
3428
4768
  ? '<div class="empty-column">No items</div>'
3429
- : 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 `
3430
4792
  <div class="card priority-${f.priority}"
3431
4793
  data-collection="${f._collection}"
3432
- 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>
3433
4798
  <div class="card-title">${f.title}</div>
3434
4799
  <div class="card-meta">
3435
4800
  <span class="badge priority-${f.priority}">${f.priority}</span>
3436
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}
3437
4804
  <span class="card-path">${f._collection}/${f.id}</span>
3438
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>
3439
4812
  </div>
3440
- `).join('')}
4813
+ `}).join('')}
3441
4814
  </div>
3442
4815
  </div>
3443
4816
  `).join('')}
@@ -3731,6 +5104,24 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3731
5104
 
3732
5105
  let bodyHtml = '';
3733
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
+
3734
5125
  // Meta section
3735
5126
  bodyHtml += `
3736
5127
  <div class="panel-section">
@@ -3739,7 +5130,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3739
5130
  <span class="badge priority-${node.priority}">${node.priority}</span>
3740
5131
  <span class="badge type">${node.type}</span>
3741
5132
  <span class="badge">${node.status}</span>
3742
- ${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>` : ''}
3743
5134
  </div>
3744
5135
  </div>
3745
5136
  `;
@@ -3768,6 +5159,43 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3768
5159
  `;
3769
5160
  }
3770
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
+
3771
5199
  // Edges section (excluding implemented-in which gets special handling)
3772
5200
  const edgeTypes = Object.keys(node.edges || {}).filter(t => t !== 'implemented-in');
3773
5201
  if (edgeTypes.length > 0) {
@@ -4083,219 +5511,771 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4083
5511
  return div.innerHTML;
4084
5512
  }
4085
5513
 
4086
- async function refreshDashboard() {
4087
- const { status, nodes } = await loadData();
4088
- await renderKanban(nodes);
4089
- 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();
4090
5844
  }
4091
5845
 
4092
5846
  // =====================================================================
4093
- // Graph Visualization
5847
+ // Agent Skills Analysis
4094
5848
  // =====================================================================
4095
5849
 
4096
- let simulation = null;
4097
-
4098
- function getNodeColor(node) {
4099
- const colors = {
4100
- 'done': getComputedStyle(document.documentElement).getPropertyValue('--status-done').trim(),
4101
- 'in-progress': getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(),
4102
- 'blocked': getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(),
4103
- 'todo': getComputedStyle(document.documentElement).getPropertyValue('--status-todo').trim()
4104
- };
4105
- 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 };
4106
5889
  }
4107
5890
 
4108
- function getNodeRadius(node) {
4109
- const edgeCount = Object.values(node.edges || {})
4110
- .reduce((sum, edges) => sum + edges.length, 0);
4111
- return Math.min(45 + edgeCount * 3, 60);
5891
+ function getProficiencyColor(level) {
5892
+ return `proficiency-${Math.round(level)}`;
4112
5893
  }
4113
5894
 
4114
- function wrapText(text, maxCharsPerLine = 10) {
4115
- const words = text.split(/\s+/);
4116
- const lines = [];
4117
- let currentLine = '';
5895
+ function getProficiencyLabel(level) {
5896
+ const labels = ['', 'Novice', 'Beginner', 'Intermediate', 'Advanced', 'Expert'];
5897
+ return labels[Math.round(level)] || 'Expert';
5898
+ }
4118
5899
 
4119
- for (const word of words) {
4120
- const testLine = currentLine ? currentLine + ' ' + word : word;
4121
- if (testLine.length <= maxCharsPerLine) {
4122
- currentLine = testLine;
4123
- } else {
4124
- if (currentLine) lines.push(currentLine);
4125
- currentLine = word.length > maxCharsPerLine
4126
- ? word.substring(0, maxCharsPerLine - 1) + '…'
4127
- : word;
4128
- }
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>`;
4129
5916
  }
4130
- if (currentLine) lines.push(currentLine);
5917
+ html += '</div>';
5918
+ return html;
5919
+ }
4131
5920
 
4132
- if (lines.length > 3) {
4133
- lines.length = 3;
4134
- 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>`;
4135
5942
  }
4136
-
4137
- return lines;
4138
5943
  }
4139
5944
 
4140
- function buildGraphData(nodes) {
4141
- const graphNodes = nodes.map(n => ({
4142
- id: n.id,
4143
- title: n.title,
4144
- status: n.status,
4145
- type: n.type,
4146
- priority: n.priority,
4147
- edges: n.edges || {},
4148
- _collection: n._collection,
4149
- x: null,
4150
- y: null
4151
- }));
5945
+ // =====================================================================
5946
+ // View Toggle
5947
+ // =====================================================================
4152
5948
 
4153
- const nodeIds = new Set(nodes.map(n => n.id));
4154
- 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');
4155
5956
 
4156
- nodes.forEach(node => {
4157
- Object.entries(node.edges || {}).forEach(([edgeType, edges]) => {
4158
- edges.forEach(edge => {
4159
- if (nodeIds.has(edge.target_id)) {
4160
- graphEdges.push({
4161
- source: node.id,
4162
- target: edge.target_id,
4163
- type: edgeType
4164
- });
4165
- }
4166
- });
4167
- });
5957
+ buttons.forEach(btn => {
5958
+ btn.classList.toggle('active', btn.dataset.view === view);
4168
5959
  });
4169
5960
 
4170
- 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
+ }
4171
5997
  }
4172
5998
 
4173
- function renderGraph(nodes) {
4174
- const svg = document.getElementById('graph-svg');
4175
- const edgesGroup = document.getElementById('graph-edges');
4176
- const nodesGroup = document.getElementById('graph-nodes');
4177
5999
 
4178
- edgesGroup.innerHTML = '';
4179
- nodesGroup.innerHTML = '';
6000
+ // =====================================================================
6001
+ // Init
6002
+ // =====================================================================
4180
6003
 
4181
- if (nodes.length === 0) return;
6004
+ document.getElementById('panel-close').addEventListener('click', closePanel);
6005
+ document.getElementById('panel-overlay').addEventListener('click', closePanel);
4182
6006
 
4183
- const rect = svg.getBoundingClientRect();
4184
- const width = rect.width || 800;
4185
- const height = rect.height || 500;
6007
+ document.querySelectorAll('.view-btn').forEach(btn => {
6008
+ btn.addEventListener('click', () => switchView(btn.dataset.view));
6009
+ });
4186
6010
 
4187
- const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
4188
- const nodeById = new Map(graphNodes.map(n => [n.id, n]));
6011
+ document.getElementById('analytics-refresh').addEventListener('click', () => {
6012
+ ensureAnalyticsLoaded(true);
6013
+ });
4189
6014
 
4190
- graphNodes.forEach((n, i) => {
4191
- const angle = (2 * Math.PI * i) / graphNodes.length;
4192
- n.x = width / 2 + Math.cos(angle) * 150;
4193
- n.y = height / 2 + Math.sin(angle) * 150;
4194
- });
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
+ });
4195
6020
 
4196
- 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);
4197
6029
 
4198
- simulation = d3.forceSimulation(graphNodes)
4199
- .force('link', d3.forceLink(graphEdges)
4200
- .id(d => d.id)
4201
- .distance(120)
4202
- .strength(0.5))
4203
- .force('charge', d3.forceManyBody()
4204
- .strength(-400))
4205
- .force('center', d3.forceCenter(width / 2, height / 2))
4206
- .force('collision', d3.forceCollide().radius(70))
4207
- .on('tick', updatePositions);
6030
+ // Session comparison modal
6031
+ document.getElementById('comparison-close').addEventListener('click', closeComparison);
6032
+ document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4208
6033
 
4209
- graphEdges.forEach(edge => {
4210
- const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
4211
- line.classList.add('graph-edge', edge.type);
4212
- line.setAttribute('marker-end', 'url(#arrowhead)');
4213
- line.dataset.source = edge.source.id || edge.source;
4214
- line.dataset.target = edge.target.id || edge.target;
4215
- edgesGroup.appendChild(line);
4216
- });
6034
+ // Graph filter event listeners
6035
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6036
+ cb.addEventListener('change', applyGraphFilters);
6037
+ });
4217
6038
 
4218
- graphNodes.forEach(node => {
4219
- const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
4220
- g.classList.add('graph-node');
4221
- g.dataset.id = node.id;
4222
- g.dataset.collection = node._collection;
6039
+ const graphSearchEl = document.getElementById('graph-search');
6040
+ if (graphSearchEl) {
6041
+ graphSearchEl.addEventListener('input', applyGraphFilters);
6042
+ }
4223
6043
 
4224
- const radius = getNodeRadius(node);
4225
- const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
4226
- circle.setAttribute('r', radius);
4227
- circle.setAttribute('fill', getNodeColor(node));
6044
+ const graphResetBtn = document.getElementById('graph-reset');
6045
+ if (graphResetBtn) {
6046
+ graphResetBtn.addEventListener('click', resetGraphView);
6047
+ }
4228
6048
 
4229
- const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
4230
- const lines = wrapText(node.title);
4231
- const lineHeight = 11;
4232
- const startY = -((lines.length - 1) * lineHeight) / 2;
4233
-
4234
- lines.forEach((line, i) => {
4235
- const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
4236
- tspan.setAttribute('x', '0');
4237
- tspan.setAttribute('dy', i === 0 ? `${startY}px` : `${lineHeight}px`);
4238
- tspan.textContent = line;
4239
- text.appendChild(tspan);
4240
- });
6049
+ const graphShowAllBtn = document.getElementById('graph-show-all');
6050
+ if (graphShowAllBtn) {
6051
+ graphShowAllBtn.addEventListener('click', showAllNodes);
6052
+ }
4241
6053
 
4242
- g.appendChild(circle);
4243
- g.appendChild(text);
4244
- nodesGroup.appendChild(g);
6054
+ document.addEventListener('keydown', (e) => {
6055
+ if (e.key === 'Escape') closePanel();
6056
+ });
4245
6057
 
4246
- g.addEventListener('click', () => {
4247
- openPanel(node._collection, node.id);
4248
- });
6058
+ loadData().then(async ({ status, nodes }) => {
6059
+ await renderKanban(nodes);
6060
+ updateKanbanGrid();
6061
+ }).catch(err => {
6062
+ console.error('Error loading dashboard data:', err);
6063
+ });
4249
6064
 
4250
- let isDragging = false;
4251
- 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
+ }
4252
6072
 
4253
- g.addEventListener('mousedown', (e) => {
4254
- isDragging = true;
4255
- dragOffset = { x: e.clientX - node.x, y: e.clientY - node.y };
4256
- simulation.alphaTarget(0.3).restart();
4257
- e.preventDefault();
4258
- });
6073
+ function renderGraph(nodes) {
6074
+ if (nodes.length === 0) {
6075
+ if (visNetwork) visNetwork.destroy();
6076
+ visNetwork = null;
6077
+ return;
6078
+ }
4259
6079
 
4260
- document.addEventListener('mousemove', (e) => {
4261
- if (isDragging) {
4262
- node.fx = e.clientX - dragOffset.x;
4263
- 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()
4264
6096
  }
4265
- });
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
+ }
4266
6128
 
4267
- document.addEventListener('mouseup', () => {
4268
- if (isDragging) {
4269
- isDragging = false;
4270
- node.fx = null;
4271
- node.fy = null;
4272
- 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
4273
6162
  }
4274
- });
4275
- });
6163
+ }
6164
+ };
4276
6165
 
4277
- function updatePositions() {
4278
- const nodeElements = nodesGroup.querySelectorAll('.graph-node');
4279
- nodeElements.forEach(g => {
4280
- 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);
4281
6173
  if (node) {
4282
- node.x = Math.max(30, Math.min(width - 30, node.x));
4283
- node.y = Math.max(30, Math.min(height - 30, node.y));
4284
- g.setAttribute('transform', `translate(${node.x}, ${node.y})`);
6174
+ openPanel(node._collection, node.id);
4285
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]));
4286
6223
  });
6224
+ });
6225
+ return { agents, skillProfiles };
6226
+ }
4287
6227
 
4288
- const edgeElements = edgesGroup.querySelectorAll('.graph-edge');
4289
- edgeElements.forEach(line => {
4290
- const source = nodeById.get(line.dataset.source);
4291
- const target = nodeById.get(line.dataset.target);
4292
- if (source && target) {
4293
- line.setAttribute('x1', source.x);
4294
- line.setAttribute('y1', source.y);
4295
- line.setAttribute('x2', target.x);
4296
- line.setAttribute('y2', target.y);
4297
- }
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>`;
4298
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>`;
4299
6279
  }
4300
6280
  }
4301
6281
 
@@ -4307,6 +6287,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4307
6287
  const kanban = document.getElementById('kanban');
4308
6288
  const graph = document.getElementById('graph-container');
4309
6289
  const analytics = document.getElementById('analytics');
6290
+ const agents = document.getElementById('agents');
4310
6291
  const sessions = document.getElementById('sessions');
4311
6292
  const buttons = document.querySelectorAll('.view-btn');
4312
6293
 
@@ -4318,29 +6299,41 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4318
6299
  kanban.classList.add('active');
4319
6300
  graph.classList.remove('active');
4320
6301
  analytics.classList.remove('active');
6302
+ agents.classList.remove('active');
4321
6303
  sessions.classList.remove('active');
4322
6304
  renderKanban(allNodes);
4323
6305
  } else if (view === 'graph') {
4324
6306
  kanban.classList.remove('active');
4325
6307
  graph.classList.add('active');
4326
6308
  analytics.classList.remove('active');
6309
+ agents.classList.remove('active');
4327
6310
  sessions.classList.remove('active');
4328
6311
  renderGraph(allNodes);
4329
6312
  } else if (view === 'analytics') {
4330
6313
  kanban.classList.remove('active');
4331
6314
  graph.classList.remove('active');
4332
6315
  analytics.classList.add('active');
6316
+ agents.classList.remove('active');
4333
6317
  sessions.classList.remove('active');
4334
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();
4335
6326
  } else if (view === 'sessions') {
4336
6327
  kanban.classList.remove('active');
4337
6328
  graph.classList.remove('active');
4338
6329
  analytics.classList.remove('active');
6330
+ agents.classList.remove('active');
4339
6331
  sessions.classList.add('active');
4340
6332
  loadAndRenderSessions();
4341
6333
  }
4342
6334
  }
4343
6335
 
6336
+
4344
6337
  // =====================================================================
4345
6338
  // Init
4346
6339
  // =====================================================================
@@ -4375,6 +6368,11 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4375
6368
  document.getElementById('comparison-close').addEventListener('click', closeComparison);
4376
6369
  document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4377
6370
 
6371
+ // Graph filter event listeners
6372
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6373
+ cb.addEventListener('change', applyGraphFilters);
6374
+ });
6375
+
4378
6376
  document.addEventListener('keydown', (e) => {
4379
6377
  if (e.key === 'Escape') closePanel();
4380
6378
  });