htmlgraph 0.24.2__py3-none-any.whl → 0.26.1__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 (112) hide show
  1. htmlgraph/__init__.py +20 -1
  2. htmlgraph/agent_detection.py +26 -10
  3. htmlgraph/analytics/cross_session.py +4 -3
  4. htmlgraph/analytics/work_type.py +52 -16
  5. htmlgraph/analytics_index.py +51 -19
  6. htmlgraph/api/__init__.py +3 -0
  7. htmlgraph/api/main.py +2263 -0
  8. htmlgraph/api/static/htmx.min.js +1 -0
  9. htmlgraph/api/static/style-redesign.css +1344 -0
  10. htmlgraph/api/static/style.css +1079 -0
  11. htmlgraph/api/templates/dashboard-redesign.html +812 -0
  12. htmlgraph/api/templates/dashboard.html +794 -0
  13. htmlgraph/api/templates/partials/activity-feed-hierarchical.html +326 -0
  14. htmlgraph/api/templates/partials/activity-feed.html +1020 -0
  15. htmlgraph/api/templates/partials/agents-redesign.html +317 -0
  16. htmlgraph/api/templates/partials/agents.html +317 -0
  17. htmlgraph/api/templates/partials/event-traces.html +373 -0
  18. htmlgraph/api/templates/partials/features-kanban-redesign.html +509 -0
  19. htmlgraph/api/templates/partials/features.html +509 -0
  20. htmlgraph/api/templates/partials/metrics-redesign.html +346 -0
  21. htmlgraph/api/templates/partials/metrics.html +346 -0
  22. htmlgraph/api/templates/partials/orchestration-redesign.html +443 -0
  23. htmlgraph/api/templates/partials/orchestration.html +163 -0
  24. htmlgraph/api/templates/partials/spawners.html +375 -0
  25. htmlgraph/atomic_ops.py +560 -0
  26. htmlgraph/builders/base.py +55 -1
  27. htmlgraph/builders/bug.py +17 -2
  28. htmlgraph/builders/chore.py +17 -2
  29. htmlgraph/builders/epic.py +17 -2
  30. htmlgraph/builders/feature.py +25 -2
  31. htmlgraph/builders/phase.py +17 -2
  32. htmlgraph/builders/spike.py +27 -2
  33. htmlgraph/builders/track.py +14 -0
  34. htmlgraph/cigs/__init__.py +4 -0
  35. htmlgraph/cigs/reporter.py +818 -0
  36. htmlgraph/cli.py +1427 -401
  37. htmlgraph/cli_commands/__init__.py +1 -0
  38. htmlgraph/cli_commands/feature.py +195 -0
  39. htmlgraph/cli_framework.py +115 -0
  40. htmlgraph/collections/__init__.py +2 -0
  41. htmlgraph/collections/base.py +21 -0
  42. htmlgraph/collections/session.py +189 -0
  43. htmlgraph/collections/spike.py +7 -1
  44. htmlgraph/collections/task_delegation.py +236 -0
  45. htmlgraph/collections/traces.py +482 -0
  46. htmlgraph/config.py +113 -0
  47. htmlgraph/converter.py +41 -0
  48. htmlgraph/cost_analysis/__init__.py +5 -0
  49. htmlgraph/cost_analysis/analyzer.py +438 -0
  50. htmlgraph/dashboard.html +3356 -492
  51. htmlgraph-0.24.2.data/data/htmlgraph/dashboard.html → htmlgraph/dashboard.html.backup +2246 -248
  52. htmlgraph/dashboard.html.bak +7181 -0
  53. htmlgraph/dashboard.html.bak2 +7231 -0
  54. htmlgraph/dashboard.html.bak3 +7232 -0
  55. htmlgraph/db/__init__.py +38 -0
  56. htmlgraph/db/queries.py +790 -0
  57. htmlgraph/db/schema.py +1584 -0
  58. htmlgraph/deploy.py +26 -27
  59. htmlgraph/docs/API_REFERENCE.md +841 -0
  60. htmlgraph/docs/HTTP_API.md +750 -0
  61. htmlgraph/docs/INTEGRATION_GUIDE.md +752 -0
  62. htmlgraph/docs/ORCHESTRATION_PATTERNS.md +710 -0
  63. htmlgraph/docs/README.md +533 -0
  64. htmlgraph/docs/version_check.py +3 -1
  65. htmlgraph/error_handler.py +544 -0
  66. htmlgraph/event_log.py +2 -0
  67. htmlgraph/hooks/.htmlgraph/.session-warning-state.json +6 -0
  68. htmlgraph/hooks/.htmlgraph/agents.json +72 -0
  69. htmlgraph/hooks/.htmlgraph/index.sqlite +0 -0
  70. htmlgraph/hooks/__init__.py +8 -0
  71. htmlgraph/hooks/bootstrap.py +169 -0
  72. htmlgraph/hooks/cigs_pretool_enforcer.py +2 -2
  73. htmlgraph/hooks/concurrent_sessions.py +208 -0
  74. htmlgraph/hooks/context.py +318 -0
  75. htmlgraph/hooks/drift_handler.py +525 -0
  76. htmlgraph/hooks/event_tracker.py +496 -79
  77. htmlgraph/hooks/orchestrator.py +6 -4
  78. htmlgraph/hooks/orchestrator_reflector.py +4 -4
  79. htmlgraph/hooks/post_tool_use_handler.py +257 -0
  80. htmlgraph/hooks/pretooluse.py +473 -6
  81. htmlgraph/hooks/prompt_analyzer.py +637 -0
  82. htmlgraph/hooks/session_handler.py +637 -0
  83. htmlgraph/hooks/state_manager.py +504 -0
  84. htmlgraph/hooks/subagent_stop.py +309 -0
  85. htmlgraph/hooks/task_enforcer.py +39 -0
  86. htmlgraph/hooks/validator.py +15 -11
  87. htmlgraph/models.py +111 -15
  88. htmlgraph/operations/fastapi_server.py +230 -0
  89. htmlgraph/orchestration/headless_spawner.py +344 -29
  90. htmlgraph/orchestration/live_events.py +377 -0
  91. htmlgraph/pydantic_models.py +476 -0
  92. htmlgraph/quality_gates.py +350 -0
  93. htmlgraph/repo_hash.py +511 -0
  94. htmlgraph/sdk.py +348 -10
  95. htmlgraph/server.py +194 -0
  96. htmlgraph/session_hooks.py +300 -0
  97. htmlgraph/session_manager.py +131 -1
  98. htmlgraph/session_registry.py +587 -0
  99. htmlgraph/session_state.py +436 -0
  100. htmlgraph/system_prompts.py +449 -0
  101. htmlgraph/templates/orchestration-view.html +350 -0
  102. htmlgraph/track_builder.py +19 -0
  103. htmlgraph/validation.py +115 -0
  104. htmlgraph-0.26.1.data/data/htmlgraph/dashboard.html +7458 -0
  105. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/METADATA +91 -64
  106. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/RECORD +112 -46
  107. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/styles.css +0 -0
  108. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/AGENTS.md.template +0 -0
  109. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/CLAUDE.md.template +0 -0
  110. {htmlgraph-0.24.2.data → htmlgraph-0.26.1.data}/data/htmlgraph/templates/GEMINI.md.template +0 -0
  111. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/WHEEL +0 -0
  112. {htmlgraph-0.24.2.dist-info → htmlgraph-0.26.1.dist-info}/entry_points.txt +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,14 @@
252
259
  }
253
260
 
254
261
  .kanban.active {
255
- display: block;
262
+ display: grid;
256
263
  width: 100%;
264
+ height: 100%;
265
+ flex: 1;
266
+ min-height: 0;
267
+ overflow: auto;
268
+ grid-auto-rows: minmax(0, 1fr);
269
+ grid-auto-flow: dense;
257
270
  }
258
271
 
259
272
  /* Dynamic grid: expanded columns grow, collapsed stay fixed */
@@ -524,6 +537,7 @@
524
537
  gap: 1rem;
525
538
  width: 100%;
526
539
  max-width: 100%;
540
+ grid-column: 1 / -1;
527
541
  }
528
542
 
529
543
  .track-section {
@@ -732,6 +746,11 @@
732
746
  border-radius: 8px;
733
747
  overflow: hidden;
734
748
  box-shadow: var(--shadow-md);
749
+ display: flex;
750
+ flex-direction: column;
751
+ grid-column: 1 / -1;
752
+ align-self: stretch;
753
+ min-height: 0;
735
754
  }
736
755
 
737
756
  .untracked-header {
@@ -777,6 +796,7 @@
777
796
  max-height: 0;
778
797
  overflow: hidden;
779
798
  transition: max-height 0.4s var(--ease-out-expo);
799
+ flex-grow: 0;
780
800
  }
781
801
 
782
802
  .untracked-content.collapsed {
@@ -784,7 +804,9 @@
784
804
  }
785
805
 
786
806
  .untracked-section.expanded .untracked-content {
787
- max-height: 2000px;
807
+ max-height: none;
808
+ flex-grow: 1;
809
+ overflow-y: auto;
788
810
  padding: 1rem;
789
811
  }
790
812
 
@@ -871,6 +893,94 @@
871
893
  .badge.priority-high { background: var(--priority-high); color: white; border-color: var(--priority-high); }
872
894
  .badge.type { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
873
895
 
896
+ /* Agent attribution badges - color-coded by agent type */
897
+ .badge.agent {
898
+ padding: 0.375rem 0.625rem;
899
+ font-weight: 600;
900
+ display: inline-flex;
901
+ align-items: center;
902
+ gap: 0.3rem;
903
+ position: relative;
904
+ transition: all 0.2s ease;
905
+ cursor: help;
906
+ }
907
+
908
+ .badge.agent::before {
909
+ content: '';
910
+ display: inline-block;
911
+ width: 0.5rem;
912
+ height: 0.5rem;
913
+ border-radius: 50%;
914
+ background: currentColor;
915
+ opacity: 0.8;
916
+ }
917
+
918
+ .badge.agent:hover {
919
+ transform: translateY(-2px);
920
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
921
+ }
922
+
923
+ /* Primary agents - Requested color system */
924
+ .badge.agent-claude { background: #2979FF; color: white; border-color: #2979FF; }
925
+ .badge.agent-codex { background: #00C853; color: white; border-color: #00C853; }
926
+ .badge.agent-orchestrator { background: #7C4DFF; color: white; border-color: #7C4DFF; }
927
+ .badge.agent-gemini { background: #FBC02D; color: #000; border-color: #FBC02D; }
928
+ .badge.agent-gemini-2 { background: #FF9100; color: white; border-color: #FF9100; }
929
+
930
+ /* Secondary agents - Backward compatibility */
931
+ .badge.agent-analyst { background: #7C3AED; color: white; border-color: #7C3AED; }
932
+ .badge.agent-developer { background: #00C853; color: white; border-color: #00C853; }
933
+ .badge.agent-researcher { background: #FF6D00; color: white; border-color: #FF6D00; }
934
+ .badge.agent-debugger { background: #E91E63; color: white; border-color: #E91E63; }
935
+ .badge.agent-default { background: #78909C; color: white; border-color: #78909C; }
936
+
937
+ /* Delegation badges */
938
+ .badge.delegation { padding: 0.25rem 0.5rem; font-size: 0.55rem; }
939
+ .badge.delegation-external { background: #00C853; color: white; }
940
+ .badge.delegation-fallback { background: #FF9100; color: white; }
941
+ .badge.delegation-direct { background: #2979FF; color: white; }
942
+
943
+ /* Event source badges - Color-coded by data source */
944
+ .badge.source { padding: 0.25rem 0.5rem; font-size: 0.55rem; font-weight: 600; }
945
+ .badge.source-hook { background: #2979FF; color: white; border-color: #2979FF; }
946
+ .badge.source-subagent { background: #7C4DFF; color: white; border-color: #7C4DFF; }
947
+ .badge.source-spike { background: #00C853; color: white; border-color: #00C853; }
948
+ .badge.source-delegation { background: #FF9100; color: white; border-color: #FF9100; }
949
+
950
+ /* Event type indicators */
951
+ .event-source-indicator {
952
+ display: inline-flex;
953
+ align-items: center;
954
+ gap: 0.25rem;
955
+ font-size: 0.65rem;
956
+ color: var(--text-muted);
957
+ }
958
+ .event-source-indicator::before {
959
+ content: '';
960
+ display: inline-block;
961
+ width: 6px;
962
+ height: 6px;
963
+ border-radius: 50%;
964
+ }
965
+ .event-source-indicator.hook::before { background: #2979FF; }
966
+ .event-source-indicator.subagent::before { background: #7C4DFF; }
967
+ .event-source-indicator.spike::before { background: #00C853; }
968
+ .event-source-indicator.delegation::before { background: #FF9100; }
969
+
970
+ /* Model tracking badges - Display which AI model executed the event */
971
+ .badge.model {
972
+ padding: 0.25rem 0.5rem;
973
+ font-size: 0.55rem;
974
+ font-weight: 600;
975
+ display: inline-flex;
976
+ align-items: center;
977
+ gap: 0.3rem;
978
+ }
979
+ .badge.model-haiku { background: #00BCD4; color: white; border-color: #00BCD4; } /* Cyan for Haiku */
980
+ .badge.model-sonnet { background: #9C27B0; color: white; border-color: #9C27B0; } /* Purple for Sonnet */
981
+ .badge.model-opus { background: #FFC107; color: #000; border-color: #FFC107; } /* Amber/Gold for Opus */
982
+ .badge.model-default { background: #9E9E9E; color: white; border-color: #9E9E9E; } /* Gray for unknown */
983
+
874
984
  .card-path {
875
985
  font-family: 'JetBrains Mono', monospace;
876
986
  font-size: 0.625rem;
@@ -887,97 +997,6 @@
887
997
  margin: 1rem;
888
998
  }
889
999
 
890
- /* ================================================================
891
- GRAPH VIEW
892
- ================================================================ */
893
- .graph-container {
894
- display: none;
895
- background: var(--bg-secondary);
896
- border: 2px solid var(--border-strong);
897
- box-shadow: var(--shadow-md);
898
- }
899
-
900
- .graph-container.active {
901
- display: block;
902
- }
903
-
904
- .graph-svg {
905
- width: 100%;
906
- height: 600px;
907
- display: block;
908
- }
909
-
910
- .graph-node {
911
- cursor: pointer;
912
- }
913
-
914
- .graph-node circle {
915
- stroke: var(--border-strong);
916
- stroke-width: 2;
917
- transition: all 0.2s var(--ease-out-expo);
918
- }
919
-
920
- .graph-node:hover circle {
921
- stroke-width: 4;
922
- }
923
-
924
- .graph-node text {
925
- font-family: 'JetBrains Mono', monospace;
926
- font-size: 8px;
927
- font-weight: 500;
928
- fill: white;
929
- text-anchor: middle;
930
- pointer-events: none;
931
- }
932
-
933
- .graph-edge {
934
- stroke: var(--border);
935
- stroke-width: 2;
936
- fill: none;
937
- }
938
-
939
- .graph-edge.blocked_by {
940
- stroke: var(--status-blocked);
941
- stroke-dasharray: 6, 4;
942
- }
943
-
944
- .graph-edge.related {
945
- stroke: var(--status-active);
946
- }
947
-
948
- .graph-arrowhead {
949
- fill: var(--border);
950
- }
951
-
952
- .graph-legend {
953
- display: flex;
954
- gap: 2rem;
955
- justify-content: center;
956
- padding: 1rem;
957
- border-top: 2px solid var(--border-strong);
958
- background: var(--bg-tertiary);
959
- }
960
-
961
- .graph-legend-item {
962
- display: flex;
963
- align-items: center;
964
- gap: 0.5rem;
965
- font-family: 'JetBrains Mono', monospace;
966
- font-size: 0.6875rem;
967
- text-transform: uppercase;
968
- letter-spacing: 0.1em;
969
- color: var(--text-muted);
970
- }
971
-
972
- .graph-legend-item span {
973
- width: 24px;
974
- height: 3px;
975
- }
976
-
977
- .legend-blocked { background: var(--status-blocked); }
978
- .legend-related { background: var(--status-active); }
979
- .legend-default { background: var(--border); }
980
-
981
1000
  /* ================================================================
982
1001
  ANALYTICS VIEW
983
1002
  ================================================================ */
@@ -987,7 +1006,11 @@
987
1006
  }
988
1007
 
989
1008
  .analytics.active {
990
- display: block;
1009
+ display: flex;
1010
+ flex-direction: column;
1011
+ flex: 1;
1012
+ min-height: 0;
1013
+ overflow: auto;
991
1014
  }
992
1015
 
993
1016
  /* ================================================================
@@ -999,171 +1022,1127 @@
999
1022
  }
1000
1023
 
1001
1024
  .sessions.active {
1002
- display: block;
1025
+ display: flex;
1026
+ flex-direction: column;
1027
+ flex: 1;
1028
+ min-height: 0;
1029
+ overflow: auto;
1003
1030
  }
1004
1031
 
1005
1032
  /* ================================================================
1006
- TRACKS VIEW
1033
+ AGENTS VIEW - Multi-Agent Work Attribution
1007
1034
  ================================================================ */
1008
- .tracks {
1035
+ .agents {
1009
1036
  display: none;
1037
+ margin-top: 1.5rem;
1038
+ padding: 0 1.5rem 1.5rem 1.5rem;
1010
1039
  }
1011
1040
 
1012
- .tracks.active {
1013
- display: block;
1041
+ .agents.active {
1042
+ display: flex;
1043
+ flex-direction: column;
1044
+ flex: 1;
1045
+ min-height: 0;
1046
+ overflow: auto;
1014
1047
  }
1015
1048
 
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);
1049
+ .agent-stats-grid {
1050
+ display: grid;
1051
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
1052
+ gap: 1rem;
1053
+ margin: 1rem 0;
1024
1054
  }
1025
1055
 
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;
1056
+ .agent-stat-card {
1057
+ background: var(--bg-secondary);
1058
+ border: 2px solid var(--border);
1059
+ border-radius: 8px;
1060
+ padding: 1.5rem;
1061
+ text-align: center;
1062
+ box-shadow: var(--shadow-sm);
1032
1063
  }
1033
1064
 
1034
- .sessions-table th {
1065
+ .agent-stat-card h4 {
1035
1066
  color: var(--text-muted);
1036
- font-weight: 600;
1067
+ font-size: 0.75rem;
1037
1068
  text-transform: uppercase;
1038
1069
  letter-spacing: 0.08em;
1039
- font-size: 0.625rem;
1040
- background: var(--bg-tertiary);
1041
- border-bottom: 2px solid var(--border-strong);
1042
- }
1043
-
1044
- .sessions-table tr:hover td {
1045
- background: var(--bg-tertiary);
1046
- }
1047
-
1048
- .sessions-table .session-id {
1049
- font-weight: 600;
1050
- color: var(--status-active);
1051
- cursor: pointer;
1052
- text-decoration: underline;
1053
- text-underline-offset: 2px;
1054
- }
1055
-
1056
- .sessions-table .session-id:hover {
1057
- color: var(--accent);
1058
- }
1059
-
1060
- /* Session Filters */
1061
- .session-filters {
1062
- display: flex;
1063
- 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;
1071
- }
1072
-
1073
- .filter-group {
1074
- display: flex;
1075
- flex-direction: column;
1076
- gap: 0.375rem;
1077
- }
1078
-
1079
- .filter-group label {
1080
- font-size: 0.75rem;
1081
1070
  font-weight: 600;
1082
- color: var(--text-muted);
1083
- text-transform: uppercase;
1084
- letter-spacing: 0.05em;
1071
+ margin-bottom: 0.5rem;
1085
1072
  }
1086
1073
 
1087
- .filter-select,
1088
- .filter-input {
1089
- padding: 0.5rem 0.75rem;
1090
- border: 1px solid var(--border);
1091
- background: var(--bg-primary);
1074
+ .agent-stat-value {
1075
+ font-size: 1.75rem;
1076
+ font-weight: 700;
1092
1077
  color: var(--text-primary);
1093
- border-radius: 4px;
1094
- font-size: 0.875rem;
1095
1078
  font-family: 'JetBrains Mono', monospace;
1096
- min-width: 150px;
1097
- }
1098
-
1099
- .filter-select:focus,
1100
- .filter-input:focus {
1101
- outline: none;
1102
- border-color: var(--accent);
1103
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
1104
1079
  }
1105
1080
 
1106
- .filter-input::placeholder {
1081
+ .agent-stat-unit {
1082
+ font-size: 0.75rem;
1107
1083
  color: var(--text-muted);
1108
- opacity: 0.6;
1084
+ margin-top: 0.25rem;
1109
1085
  }
1110
1086
 
1111
- .analytics-header {
1112
- display: flex;
1113
- align-items: flex-start;
1114
- justify-content: space-between;
1115
- gap: 1rem;
1116
- padding: 1.25rem 1.5rem;
1087
+ /* ================================================================
1088
+ WORKLOAD DISTRIBUTION CHART
1089
+ ================================================================ */
1090
+ .workload-chart-container {
1117
1091
  background: var(--bg-secondary);
1118
- border: 2px solid var(--border-strong);
1119
- box-shadow: var(--shadow-md);
1120
- margin-bottom: 1rem;
1092
+ border: 2px solid var(--border);
1093
+ border-radius: 8px;
1094
+ padding: 1.5rem;
1095
+ box-shadow: var(--shadow-sm);
1096
+ margin-top: 1rem;
1121
1097
  }
1122
1098
 
1123
- .analytics-header h2 {
1099
+ .workload-chart-header {
1100
+ margin-bottom: 1.5rem;
1101
+ }
1102
+
1103
+ .workload-chart-header h3 {
1124
1104
  font-family: 'JetBrains Mono', monospace;
1125
- font-size: 1rem;
1105
+ font-size: 0.875rem;
1126
1106
  text-transform: uppercase;
1127
1107
  letter-spacing: 0.08em;
1108
+ color: var(--text-muted);
1128
1109
  margin-bottom: 0.25rem;
1110
+ font-weight: 600;
1129
1111
  }
1130
1112
 
1131
- .analytics-header p {
1132
- color: var(--text-muted);
1113
+ .workload-chart-header p {
1114
+ color: var(--text-secondary);
1133
1115
  font-size: 0.875rem;
1134
1116
  margin: 0;
1135
1117
  }
1136
1118
 
1137
- .analytics-grid {
1138
- display: grid;
1139
- grid-template-columns: 1fr 1fr;
1119
+ .workload-bars {
1120
+ display: flex;
1121
+ flex-direction: column;
1140
1122
  gap: 1rem;
1123
+ max-height: 600px;
1124
+ overflow-y: auto;
1141
1125
  }
1142
1126
 
1143
- @media (max-width: 1100px) {
1144
- .analytics-grid {
1145
- grid-template-columns: 1fr;
1146
- }
1127
+ .workload-bar-group {
1128
+ display: flex;
1129
+ flex-direction: column;
1130
+ gap: 0.375rem;
1147
1131
  }
1148
1132
 
1149
- .analytics-card {
1150
- background: var(--bg-secondary);
1151
- border: 2px solid var(--border-strong);
1152
- box-shadow: var(--shadow-md);
1153
- padding: 1rem 1.25rem;
1154
- }
1133
+ .workload-bar-label {
1134
+ display: flex;
1135
+ justify-content: space-between;
1136
+ align-items: center;
1137
+ font-size: 0.875rem;
1138
+ font-weight: 500;
1139
+ color: var(--text-primary);
1140
+ margin-bottom: 0.25rem;
1141
+ }
1142
+
1143
+ .workload-bar-label-name {
1144
+ display: flex;
1145
+ align-items: center;
1146
+ gap: 0.5rem;
1147
+ flex: 1;
1148
+ }
1149
+
1150
+ .workload-agent-badge {
1151
+ display: inline-flex;
1152
+ align-items: center;
1153
+ justify-content: center;
1154
+ width: 24px;
1155
+ height: 24px;
1156
+ border-radius: 50%;
1157
+ font-size: 0.7rem;
1158
+ font-weight: 600;
1159
+ color: white;
1160
+ flex-shrink: 0;
1161
+ }
1162
+
1163
+ .workload-bar-value {
1164
+ font-family: 'JetBrains Mono', monospace;
1165
+ font-size: 0.8rem;
1166
+ color: var(--text-muted);
1167
+ white-space: nowrap;
1168
+ }
1169
+
1170
+ .workload-bar-container {
1171
+ position: relative;
1172
+ width: 100%;
1173
+ height: 32px;
1174
+ background: var(--bg-tertiary);
1175
+ border: 1px solid var(--border);
1176
+ border-radius: 4px;
1177
+ overflow: hidden;
1178
+ }
1179
+
1180
+ .workload-bar-fill {
1181
+ height: 100%;
1182
+ display: flex;
1183
+ align-items: center;
1184
+ padding: 0 0.75rem;
1185
+ transition: all 0.3s var(--ease-out-expo);
1186
+ position: relative;
1187
+ justify-content: flex-start;
1188
+ }
1189
+
1190
+ .workload-bar-fill::after {
1191
+ content: '';
1192
+ position: absolute;
1193
+ inset: 0;
1194
+ background: linear-gradient(90deg, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);
1195
+ pointer-events: none;
1196
+ }
1197
+
1198
+ .workload-bar-text {
1199
+ position: absolute;
1200
+ right: 0.75rem;
1201
+ top: 50%;
1202
+ transform: translateY(-50%);
1203
+ color: white;
1204
+ font-size: 0.75rem;
1205
+ font-weight: 600;
1206
+ font-family: 'JetBrains Mono', monospace;
1207
+ white-space: nowrap;
1208
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
1209
+ pointer-events: none;
1210
+ z-index: 2;
1211
+ }
1212
+
1213
+ .workload-bar-hover-info {
1214
+ position: absolute;
1215
+ bottom: 100%;
1216
+ left: 50%;
1217
+ transform: translateX(-50%);
1218
+ background: var(--bg-tertiary);
1219
+ border: 1px solid var(--border-strong);
1220
+ border-radius: 4px;
1221
+ padding: 0.75rem;
1222
+ font-size: 0.75rem;
1223
+ white-space: nowrap;
1224
+ pointer-events: none;
1225
+ opacity: 0;
1226
+ visibility: hidden;
1227
+ transition: all 0.2s;
1228
+ z-index: 100;
1229
+ margin-bottom: 0.5rem;
1230
+ box-shadow: var(--shadow-md);
1231
+ }
1232
+
1233
+ .workload-bar-container:hover .workload-bar-hover-info {
1234
+ opacity: 1;
1235
+ visibility: visible;
1236
+ }
1237
+
1238
+ .workload-bar-hover-info::after {
1239
+ content: '';
1240
+ position: absolute;
1241
+ top: 100%;
1242
+ left: 50%;
1243
+ transform: translateX(-50%);
1244
+ border: 6px solid transparent;
1245
+ border-top-color: var(--border-strong);
1246
+ }
1247
+
1248
+ .workload-chart-legend {
1249
+ display: flex;
1250
+ flex-wrap: wrap;
1251
+ gap: 1.5rem;
1252
+ margin-top: 1.5rem;
1253
+ padding-top: 1.5rem;
1254
+ border-top: 1px solid var(--border);
1255
+ font-size: 0.875rem;
1256
+ }
1257
+
1258
+ .workload-legend-item {
1259
+ display: flex;
1260
+ align-items: center;
1261
+ gap: 0.5rem;
1262
+ }
1263
+
1264
+ .workload-legend-color {
1265
+ width: 16px;
1266
+ height: 16px;
1267
+ border-radius: 3px;
1268
+ flex-shrink: 0;
1269
+ }
1270
+
1271
+ .workload-legend-label {
1272
+ color: var(--text-secondary);
1273
+ }
1274
+
1275
+ /* Agent Color System */
1276
+ .agent-claude { background: linear-gradient(135deg, #6366f1, #818cf8); }
1277
+ .agent-codex { background: linear-gradient(135deg, #10b981, #34d399); }
1278
+ .agent-orchestrator { background: linear-gradient(135deg, #f59e0b, #fbbf24); }
1279
+ .agent-gemini-2 { background: linear-gradient(135deg, #8b5cf6, #a78bfa); }
1280
+ .agent-gemini { background: linear-gradient(135deg, #ec4899, #f472b6); }
1281
+ .agent-analyst { background: linear-gradient(135deg, #0ea5e9, #38bdf8); }
1282
+ .agent-developer { background: linear-gradient(135deg, #06b6d4, #22d3ee); }
1283
+
1284
+ /* ================================================================
1285
+ AGENT COST VISUALIZATION
1286
+ ================================================================ */
1287
+ .cost-breakdown-container {
1288
+ background: var(--bg-secondary);
1289
+ border: 2px solid var(--border);
1290
+ border-radius: 8px;
1291
+ padding: 1.5rem;
1292
+ box-shadow: var(--shadow-sm);
1293
+ margin-top: 1rem;
1294
+ }
1295
+
1296
+ .cost-breakdown-header {
1297
+ margin-bottom: 1.5rem;
1298
+ }
1299
+
1300
+ .cost-breakdown-header h3 {
1301
+ font-family: 'JetBrains Mono', monospace;
1302
+ font-size: 0.875rem;
1303
+ text-transform: uppercase;
1304
+ letter-spacing: 0.08em;
1305
+ color: var(--text-muted);
1306
+ margin-bottom: 0.25rem;
1307
+ font-weight: 600;
1308
+ }
1309
+
1310
+ .cost-breakdown-header p {
1311
+ color: var(--text-secondary);
1312
+ font-size: 0.875rem;
1313
+ margin: 0;
1314
+ }
1315
+
1316
+ .cost-summary-metrics {
1317
+ display: grid;
1318
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1319
+ gap: 1rem;
1320
+ margin-bottom: 1.5rem;
1321
+ }
1322
+
1323
+ .cost-metric {
1324
+ background: var(--bg-tertiary);
1325
+ border: 1px solid var(--border);
1326
+ border-radius: 4px;
1327
+ padding: 1rem;
1328
+ text-align: center;
1329
+ }
1330
+
1331
+ .cost-metric-label {
1332
+ font-size: 0.7rem;
1333
+ text-transform: uppercase;
1334
+ letter-spacing: 0.08em;
1335
+ color: var(--text-muted);
1336
+ margin-bottom: 0.5rem;
1337
+ font-weight: 600;
1338
+ }
1339
+
1340
+ .cost-metric-value {
1341
+ font-family: 'JetBrains Mono', monospace;
1342
+ font-size: 1.5rem;
1343
+ font-weight: 700;
1344
+ color: var(--text-primary);
1345
+ }
1346
+
1347
+ .cost-metric-unit {
1348
+ font-size: 0.65rem;
1349
+ color: var(--text-muted);
1350
+ margin-top: 0.25rem;
1351
+ }
1352
+
1353
+ .cost-bars {
1354
+ display: flex;
1355
+ flex-direction: column;
1356
+ gap: 1.25rem;
1357
+ max-height: 600px;
1358
+ overflow-y: auto;
1359
+ }
1360
+
1361
+ .cost-bar-group {
1362
+ display: flex;
1363
+ flex-direction: column;
1364
+ gap: 0.375rem;
1365
+ }
1366
+
1367
+ .cost-bar-label {
1368
+ display: flex;
1369
+ justify-content: space-between;
1370
+ align-items: center;
1371
+ font-size: 0.875rem;
1372
+ font-weight: 500;
1373
+ color: var(--text-primary);
1374
+ margin-bottom: 0.375rem;
1375
+ }
1376
+
1377
+ .cost-bar-label-name {
1378
+ display: flex;
1379
+ align-items: center;
1380
+ gap: 0.5rem;
1381
+ flex: 1;
1382
+ }
1383
+
1384
+ .cost-agent-badge {
1385
+ display: inline-flex;
1386
+ align-items: center;
1387
+ justify-content: center;
1388
+ width: 24px;
1389
+ height: 24px;
1390
+ border-radius: 50%;
1391
+ font-size: 0.7rem;
1392
+ font-weight: 600;
1393
+ color: white;
1394
+ flex-shrink: 0;
1395
+ }
1396
+
1397
+ .cost-bar-stats {
1398
+ display: flex;
1399
+ gap: 1rem;
1400
+ font-family: 'JetBrains Mono', monospace;
1401
+ font-size: 0.75rem;
1402
+ }
1403
+
1404
+ .cost-bar-stat {
1405
+ display: flex;
1406
+ flex-direction: column;
1407
+ gap: 0.125rem;
1408
+ }
1409
+
1410
+ .cost-bar-stat-label {
1411
+ color: var(--text-muted);
1412
+ font-size: 0.65rem;
1413
+ text-transform: uppercase;
1414
+ letter-spacing: 0.05em;
1415
+ }
1416
+
1417
+ .cost-bar-stat-value {
1418
+ color: var(--text-primary);
1419
+ font-weight: 600;
1420
+ }
1421
+
1422
+ .cost-bar-container {
1423
+ position: relative;
1424
+ width: 100%;
1425
+ height: 40px;
1426
+ background: var(--bg-tertiary);
1427
+ border: 1px solid var(--border);
1428
+ border-radius: 4px;
1429
+ overflow: hidden;
1430
+ }
1431
+
1432
+ .cost-bar-stacked {
1433
+ display: flex;
1434
+ height: 100%;
1435
+ width: 100%;
1436
+ position: relative;
1437
+ overflow: hidden;
1438
+ }
1439
+
1440
+ .cost-bar-segment {
1441
+ height: 100%;
1442
+ display: flex;
1443
+ align-items: center;
1444
+ justify-content: center;
1445
+ position: relative;
1446
+ transition: all 0.3s var(--ease-out-expo);
1447
+ flex-grow: 1;
1448
+ min-width: 2px;
1449
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
1450
+ }
1451
+
1452
+ .cost-bar-segment:last-child {
1453
+ border-right: none;
1454
+ }
1455
+
1456
+ .cost-bar-segment::after {
1457
+ content: '';
1458
+ position: absolute;
1459
+ inset: 0;
1460
+ background: linear-gradient(90deg, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 100%);
1461
+ pointer-events: none;
1462
+ }
1463
+
1464
+ .cost-bar-segment-label {
1465
+ position: relative;
1466
+ z-index: 2;
1467
+ font-size: 0.65rem;
1468
+ font-weight: 600;
1469
+ font-family: 'JetBrains Mono', monospace;
1470
+ color: white;
1471
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.3);
1472
+ white-space: nowrap;
1473
+ padding: 0 0.3rem;
1474
+ pointer-events: none;
1475
+ }
1476
+
1477
+ .cost-bar-tooltip {
1478
+ position: absolute;
1479
+ bottom: 100%;
1480
+ left: 50%;
1481
+ transform: translateX(-50%);
1482
+ background: var(--bg-tertiary);
1483
+ border: 1px solid var(--border-strong);
1484
+ border-radius: 4px;
1485
+ padding: 0.75rem;
1486
+ font-size: 0.75rem;
1487
+ pointer-events: none;
1488
+ opacity: 0;
1489
+ visibility: hidden;
1490
+ transition: all 0.2s;
1491
+ z-index: 100;
1492
+ margin-bottom: 0.5rem;
1493
+ box-shadow: var(--shadow-md);
1494
+ white-space: nowrap;
1495
+ }
1496
+
1497
+ .cost-bar-segment:hover ~ .cost-bar-tooltip,
1498
+ .cost-bar-container:hover .cost-bar-tooltip {
1499
+ opacity: 1;
1500
+ visibility: visible;
1501
+ }
1502
+
1503
+ .cost-bar-tooltip::after {
1504
+ content: '';
1505
+ position: absolute;
1506
+ top: 100%;
1507
+ left: 50%;
1508
+ transform: translateX(-50%);
1509
+ border: 6px solid transparent;
1510
+ border-top-color: var(--border-strong);
1511
+ }
1512
+
1513
+ .cost-range-indicator {
1514
+ display: flex;
1515
+ align-items: center;
1516
+ gap: 0.5rem;
1517
+ font-size: 0.7rem;
1518
+ color: var(--text-muted);
1519
+ margin-top: 0.25rem;
1520
+ }
1521
+
1522
+ .cost-range-dot {
1523
+ width: 8px;
1524
+ height: 8px;
1525
+ border-radius: 50%;
1526
+ flex-shrink: 0;
1527
+ }
1528
+
1529
+ .cost-range-dot.low { background: #10b981; }
1530
+ .cost-range-dot.medium { background: #f59e0b; }
1531
+ .cost-range-dot.high { background: #ef4444; }
1532
+
1533
+ .cost-breakdown-legend {
1534
+ display: flex;
1535
+ flex-wrap: wrap;
1536
+ gap: 1.5rem;
1537
+ margin-top: 1.5rem;
1538
+ padding-top: 1.5rem;
1539
+ border-top: 1px solid var(--border);
1540
+ font-size: 0.875rem;
1541
+ }
1542
+
1543
+ .cost-legend-item {
1544
+ display: flex;
1545
+ align-items: center;
1546
+ gap: 0.5rem;
1547
+ }
1548
+
1549
+ .cost-legend-color {
1550
+ width: 16px;
1551
+ height: 16px;
1552
+ border-radius: 3px;
1553
+ flex-shrink: 0;
1554
+ }
1555
+
1556
+ .cost-legend-label {
1557
+ color: var(--text-secondary);
1558
+ }
1559
+
1560
+ /* Agent cost colors - match agent system */
1561
+ .cost-claude { background: #2979FF; }
1562
+ .cost-codex { background: #00C853; }
1563
+ .cost-orchestrator { background: #7C4DFF; }
1564
+ .cost-gemini { background: #FBC02D; }
1565
+ .cost-gemini-2 { background: #FF9100; }
1566
+ .agent-researcher { background: linear-gradient(135deg, #d946ef, #e879f9); }
1567
+ .agent-debugger { background: linear-gradient(135deg, #ef4444, #f87171); }
1568
+ .agent-default { background: linear-gradient(135deg, #6b7280, #9ca3af); }
1569
+
1570
+ .workload-empty-state {
1571
+ text-align: center;
1572
+ padding: 2rem;
1573
+ color: var(--text-muted);
1574
+ }
1575
+
1576
+ .workload-empty-state svg {
1577
+ width: 48px;
1578
+ height: 48px;
1579
+ margin: 0 auto 1rem;
1580
+ opacity: 0.5;
1581
+ }
1582
+
1583
+ .workload-chart-responsive {
1584
+ max-height: 500px;
1585
+ overflow-y: auto;
1586
+ }
1587
+
1588
+ @media (max-width: 768px) {
1589
+ .workload-bar-group {
1590
+ gap: 0.25rem;
1591
+ }
1592
+
1593
+ .workload-bar-label {
1594
+ font-size: 0.8rem;
1595
+ }
1596
+
1597
+ .workload-bar-text {
1598
+ font-size: 0.65rem;
1599
+ }
1600
+
1601
+ .workload-chart-legend {
1602
+ gap: 1rem;
1603
+ }
1604
+
1605
+ .workload-bars {
1606
+ max-height: 400px;
1607
+ }
1608
+ }
1609
+
1610
+ /* Skills Matrix Styles */
1611
+ .skills-matrix-container {
1612
+ background: var(--bg-secondary);
1613
+ border: 2px solid var(--border);
1614
+ border-radius: 8px;
1615
+ padding: 1.5rem;
1616
+ box-shadow: var(--shadow-sm);
1617
+ overflow-x: auto;
1618
+ }
1619
+
1620
+ .skills-matrix {
1621
+ display: grid;
1622
+ grid-template-columns: 150px repeat(auto-fit, minmax(100px, 1fr));
1623
+ gap: 0;
1624
+ min-width: 600px;
1625
+ }
1626
+
1627
+ .skills-matrix-cell {
1628
+ display: flex;
1629
+ align-items: center;
1630
+ justify-content: center;
1631
+ padding: 0.75rem;
1632
+ border: 1px solid var(--border);
1633
+ font-size: 0.85rem;
1634
+ min-height: 50px;
1635
+ }
1636
+
1637
+ .skills-matrix-header-row {
1638
+ position: sticky;
1639
+ top: 0;
1640
+ background: var(--bg-tertiary);
1641
+ font-weight: 600;
1642
+ text-transform: uppercase;
1643
+ letter-spacing: 0.05em;
1644
+ color: var(--text-muted);
1645
+ font-size: 0.75rem;
1646
+ z-index: 10;
1647
+ }
1648
+
1649
+ .skills-matrix-agent-name {
1650
+ position: sticky;
1651
+ left: 0;
1652
+ background: var(--bg-tertiary);
1653
+ font-weight: 600;
1654
+ color: var(--text-primary);
1655
+ text-align: left;
1656
+ z-index: 11;
1657
+ }
1658
+
1659
+ .skills-matrix-skill-label {
1660
+ writing-mode: horizontal-tb;
1661
+ white-space: nowrap;
1662
+ }
1663
+
1664
+ .proficiency-dot {
1665
+ display: inline-flex;
1666
+ align-items: center;
1667
+ justify-content: center;
1668
+ width: 28px;
1669
+ height: 28px;
1670
+ border-radius: 50%;
1671
+ font-size: 0.7rem;
1672
+ font-weight: 600;
1673
+ transition: transform 0.2s, box-shadow 0.2s;
1674
+ cursor: help;
1675
+ }
1676
+
1677
+ .proficiency-dot:hover {
1678
+ transform: scale(1.15);
1679
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
1680
+ }
1681
+
1682
+ .proficiency-1 {
1683
+ background: #fee5e5;
1684
+ color: #8b0000;
1685
+ border: 1px solid #d4a5a5;
1686
+ }
1687
+
1688
+ .proficiency-2 {
1689
+ background: #ffcccb;
1690
+ color: #660000;
1691
+ border: 1px solid #c97c7c;
1692
+ }
1693
+
1694
+ .proficiency-3 {
1695
+ background: #ffb366;
1696
+ color: #5a3a00;
1697
+ border: 1px solid #cc8844;
1698
+ }
1699
+
1700
+ .proficiency-4 {
1701
+ background: #99ff99;
1702
+ color: #1a4d1a;
1703
+ border: 1px solid #66cc66;
1704
+ }
1705
+
1706
+ .proficiency-5 {
1707
+ background: #00cc00;
1708
+ color: #ffffff;
1709
+ border: 1px solid #009900;
1710
+ }
1711
+
1712
+ .skill-category-legend {
1713
+ display: flex;
1714
+ gap: 1.5rem;
1715
+ flex-wrap: wrap;
1716
+ margin-top: 1.5rem;
1717
+ padding-top: 1.5rem;
1718
+ border-top: 1px solid var(--border);
1719
+ }
1720
+
1721
+ .skill-category-item {
1722
+ display: flex;
1723
+ align-items: center;
1724
+ gap: 0.5rem;
1725
+ font-size: 0.875rem;
1726
+ }
1727
+
1728
+ .skill-category-icon {
1729
+ display: inline-block;
1730
+ width: 16px;
1731
+ height: 16px;
1732
+ border-radius: 3px;
1733
+ background: var(--accent);
1734
+ }
1735
+
1736
+ .skill-tooltip {
1737
+ position: absolute;
1738
+ background: var(--bg-tertiary);
1739
+ border: 1px solid var(--border-strong);
1740
+ border-radius: 4px;
1741
+ padding: 0.5rem 0.75rem;
1742
+ font-size: 0.75rem;
1743
+ white-space: nowrap;
1744
+ pointer-events: none;
1745
+ z-index: 1000;
1746
+ box-shadow: var(--shadow-md);
1747
+ }
1748
+
1749
+ /* ================================================================
1750
+ TRACKS VIEW
1751
+ ================================================================ */
1752
+ .tracks {
1753
+ display: none;
1754
+ }
1755
+
1756
+ .tracks.active {
1757
+ display: block;
1758
+ }
1759
+
1760
+ .sessions-table {
1761
+ width: 100%;
1762
+ border-collapse: collapse;
1763
+ font-family: 'JetBrains Mono', monospace;
1764
+ font-size: 0.875rem;
1765
+ background: var(--bg-secondary);
1766
+ border: 2px solid var(--border-strong);
1767
+ box-shadow: var(--shadow-md);
1768
+ }
1769
+
1770
+ .sessions-table th,
1771
+ .sessions-table td {
1772
+ border-bottom: 1px solid var(--border);
1773
+ padding: 0.875rem 1rem;
1774
+ text-align: left;
1775
+ vertical-align: middle;
1776
+ }
1777
+
1778
+ .sessions-table th {
1779
+ color: var(--text-muted);
1780
+ font-weight: 600;
1781
+ text-transform: uppercase;
1782
+ letter-spacing: 0.08em;
1783
+ font-size: 0.625rem;
1784
+ background: var(--bg-tertiary);
1785
+ border-bottom: 2px solid var(--border-strong);
1786
+ }
1787
+
1788
+ .sessions-table tr:hover td {
1789
+ background: var(--bg-tertiary);
1790
+ }
1791
+
1792
+ .sessions-table .session-id {
1793
+ font-weight: 600;
1794
+ color: var(--status-active);
1795
+ cursor: pointer;
1796
+ text-decoration: underline;
1797
+ text-underline-offset: 2px;
1798
+ }
1799
+
1800
+ .sessions-table .session-id:hover {
1801
+ color: var(--accent);
1802
+ }
1803
+
1804
+ /* Session Filters */
1805
+ .session-filters {
1806
+ display: flex;
1807
+ gap: 1rem;
1808
+ padding: 1rem 1.5rem;
1809
+ background: var(--bg-secondary);
1810
+ border: 2px solid var(--border-strong);
1811
+ box-shadow: var(--shadow-md);
1812
+ margin-bottom: 1rem;
1813
+ flex-wrap: wrap;
1814
+ align-items: flex-end;
1815
+ }
1816
+
1817
+ .filter-group {
1818
+ display: flex;
1819
+ flex-direction: column;
1820
+ gap: 0.375rem;
1821
+ }
1822
+
1823
+ .filter-group label {
1824
+ font-size: 0.75rem;
1825
+ font-weight: 600;
1826
+ color: var(--text-muted);
1827
+ text-transform: uppercase;
1828
+ letter-spacing: 0.05em;
1829
+ }
1830
+
1831
+ .filter-select,
1832
+ .filter-input {
1833
+ padding: 0.5rem 0.75rem;
1834
+ border: 1px solid var(--border);
1835
+ background: var(--bg-primary);
1836
+ color: var(--text-primary);
1837
+ border-radius: 4px;
1838
+ font-size: 0.875rem;
1839
+ font-family: 'JetBrains Mono', monospace;
1840
+ min-width: 150px;
1841
+ }
1842
+
1843
+ .filter-select:focus,
1844
+ .filter-input:focus {
1845
+ outline: none;
1846
+ border-color: var(--accent);
1847
+ box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
1848
+ }
1849
+
1850
+ .filter-input::placeholder {
1851
+ color: var(--text-muted);
1852
+ opacity: 0.6;
1853
+ }
1854
+
1855
+ .analytics-header {
1856
+ display: flex;
1857
+ align-items: flex-start;
1858
+ justify-content: space-between;
1859
+ gap: 1rem;
1860
+ padding: 1.25rem 1.5rem;
1861
+ background: var(--bg-secondary);
1862
+ border: 2px solid var(--border-strong);
1863
+ box-shadow: var(--shadow-md);
1864
+ margin-bottom: 1rem;
1865
+ }
1866
+
1867
+ .analytics-header h2 {
1868
+ font-family: 'JetBrains Mono', monospace;
1869
+ font-size: 1rem;
1870
+ text-transform: uppercase;
1871
+ letter-spacing: 0.08em;
1872
+ margin-bottom: 0.25rem;
1873
+ }
1874
+
1875
+ .analytics-header p {
1876
+ color: var(--text-muted);
1877
+ font-size: 0.875rem;
1878
+ margin: 0;
1879
+ }
1880
+
1881
+ .analytics-grid {
1882
+ display: grid;
1883
+ grid-template-columns: 1fr;
1884
+ gap: 1.5rem;
1885
+ }
1886
+
1887
+ /* Timeline is always full width and first */
1888
+ .analytics-grid > #analytics-timeline {
1889
+ grid-column: 1 / -1;
1890
+ order: -10;
1891
+ }
1892
+
1893
+ /* Summary metrics grid: 3 columns */
1894
+ .analytics-grid > #analytics-summary {
1895
+ grid-column: 1 / -1;
1896
+ order: -9;
1897
+ }
1898
+
1899
+ /* Collaboration metrics: 2 columns */
1900
+ .analytics-grid > #analytics-collaboration {
1901
+ grid-column: 1 / -1;
1902
+ order: -8;
1903
+ }
1904
+
1905
+ /* Feature and tool analysis: 2 columns each */
1906
+ .analytics-grid > #analytics-features,
1907
+ .analytics-grid > #analytics-tools {
1908
+ order: -7;
1909
+ }
1910
+
1911
+ /* Commit DAG: full width */
1912
+ .analytics-grid > #analytics-commit-dag {
1913
+ grid-column: 1 / -1;
1914
+ order: -6;
1915
+ }
1916
+
1917
+ @media (max-width: 1200px) {
1918
+ .analytics-grid {
1919
+ grid-template-columns: 1fr;
1920
+ }
1921
+ }
1922
+
1923
+ @media (max-width: 768px) {
1924
+ .analytics-grid {
1925
+ gap: 1rem;
1926
+ }
1927
+ }
1928
+
1929
+ .analytics-card {
1930
+ background: var(--bg-secondary);
1931
+ border: 2px solid var(--border-strong);
1932
+ box-shadow: var(--shadow-md);
1933
+ padding: 1rem 1.25rem;
1934
+ }
1935
+
1936
+ .analytics-card h3 {
1937
+ font-family: 'JetBrains Mono', monospace;
1938
+ font-size: 0.75rem;
1939
+ text-transform: uppercase;
1940
+ letter-spacing: 0.1em;
1941
+ color: var(--text-muted);
1942
+ margin-bottom: 0.75rem;
1943
+ }
1944
+
1945
+ .analytics-card-wide {
1946
+ grid-column: 1 / -1;
1947
+ }
1948
+
1949
+ /* Timeline Section */
1950
+ .analytics-timeline {
1951
+ background: var(--bg-secondary);
1952
+ border: 2px solid var(--border-strong);
1953
+ box-shadow: var(--shadow-md);
1954
+ padding: 1.5rem;
1955
+ position: relative;
1956
+ }
1957
+
1958
+ .timeline-header {
1959
+ display: flex;
1960
+ align-items: center;
1961
+ justify-content: space-between;
1962
+ margin-bottom: 1.5rem;
1963
+ padding-bottom: 1rem;
1964
+ border-bottom: 1px solid var(--border);
1965
+ }
1966
+
1967
+ .timeline-header h3 {
1968
+ font-family: 'JetBrains Mono', monospace;
1969
+ font-size: 0.875rem;
1970
+ text-transform: uppercase;
1971
+ letter-spacing: 0.1em;
1972
+ color: var(--text-muted);
1973
+ }
1974
+
1975
+ .timeline-header .timeline-legend {
1976
+ display: flex;
1977
+ gap: 1.5rem;
1978
+ font-size: 0.75rem;
1979
+ color: var(--text-secondary);
1980
+ }
1981
+
1982
+ .timeline-legend-item {
1983
+ display: flex;
1984
+ align-items: center;
1985
+ gap: 0.5rem;
1986
+ }
1987
+
1988
+ .timeline-legend-dot {
1989
+ width: 10px;
1990
+ height: 10px;
1991
+ border-radius: 50%;
1992
+ }
1993
+
1994
+ .timeline-container {
1995
+ display: flex;
1996
+ flex-direction: column;
1997
+ gap: 1rem;
1998
+ max-height: 400px;
1999
+ overflow-y: auto;
2000
+ }
2001
+
2002
+ .timeline-entry {
2003
+ display: flex;
2004
+ gap: 1rem;
2005
+ padding: 0.875rem;
2006
+ background: var(--bg-tertiary);
2007
+ border: 1px solid var(--border);
2008
+ border-radius: 4px;
2009
+ transition: all 0.2s var(--ease-out-expo);
2010
+ cursor: pointer;
2011
+ }
2012
+
2013
+ .timeline-entry:hover {
2014
+ background: var(--bg-secondary);
2015
+ border-color: var(--accent);
2016
+ box-shadow: 0 0 0 2px rgba(205, 255, 0, 0.1);
2017
+ }
2018
+
2019
+ .timeline-entry-marker {
2020
+ flex-shrink: 0;
2021
+ width: 12px;
2022
+ height: 12px;
2023
+ border-radius: 50%;
2024
+ margin-top: 3px;
2025
+ border: 2px solid var(--border-strong);
2026
+ }
2027
+
2028
+ .timeline-entry-marker.session { background: var(--status-active); }
2029
+ .timeline-entry-marker.feature { background: var(--accent); }
2030
+ .timeline-entry-marker.commit { background: var(--status-done); }
2031
+ .timeline-entry-marker.error { background: var(--status-blocked); }
2032
+
2033
+ .timeline-entry-content {
2034
+ flex: 1;
2035
+ min-width: 0;
2036
+ }
2037
+
2038
+ .timeline-entry-title {
2039
+ font-size: 0.875rem;
2040
+ font-weight: 600;
2041
+ color: var(--text-primary);
2042
+ margin-bottom: 0.25rem;
2043
+ white-space: nowrap;
2044
+ overflow: hidden;
2045
+ text-overflow: ellipsis;
2046
+ }
2047
+
2048
+ .timeline-entry-meta {
2049
+ display: flex;
2050
+ gap: 1rem;
2051
+ font-size: 0.75rem;
2052
+ color: var(--text-muted);
2053
+ font-family: 'JetBrains Mono', monospace;
2054
+ }
2055
+
2056
+ /* Summary/Health Indicators */
2057
+ .analytics-summary {
2058
+ display: grid;
2059
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
2060
+ gap: 1rem;
2061
+ }
2062
+
2063
+ .health-card {
2064
+ background: var(--bg-secondary);
2065
+ border: 2px solid var(--border-strong);
2066
+ box-shadow: var(--shadow-md);
2067
+ padding: 1.5rem;
2068
+ position: relative;
2069
+ }
2070
+
2071
+ .health-card::before {
2072
+ content: '';
2073
+ position: absolute;
2074
+ top: 0;
2075
+ left: 0;
2076
+ right: 0;
2077
+ height: 4px;
2078
+ }
2079
+
2080
+ .health-card.health-good::before { background: var(--status-done); }
2081
+ .health-card.health-ok::before { background: var(--priority-high); }
2082
+ .health-card.health-poor::before { background: var(--status-blocked); }
2083
+
2084
+ .health-label {
2085
+ font-family: 'JetBrains Mono', monospace;
2086
+ font-size: 0.625rem;
2087
+ text-transform: uppercase;
2088
+ letter-spacing: 0.1em;
2089
+ color: var(--text-muted);
2090
+ margin-bottom: 0.5rem;
2091
+ }
2092
+
2093
+ .health-value {
2094
+ font-size: 2rem;
2095
+ font-weight: 700;
2096
+ color: var(--text-primary);
2097
+ font-family: 'JetBrains Mono', monospace;
2098
+ }
2099
+
2100
+ .health-detail {
2101
+ font-size: 0.75rem;
2102
+ color: var(--text-secondary);
2103
+ margin-top: 0.75rem;
2104
+ padding-top: 0.75rem;
2105
+ border-top: 1px solid var(--border);
2106
+ }
2107
+
2108
+ /* Collaboration Metrics */
2109
+ .collaboration-metrics {
2110
+ background: var(--bg-secondary);
2111
+ border: 2px solid var(--border-strong);
2112
+ box-shadow: var(--shadow-md);
2113
+ padding: 1.5rem;
2114
+ }
2115
+
2116
+ .collaboration-grid {
2117
+ display: grid;
2118
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2119
+ gap: 1rem;
2120
+ margin-bottom: 1.5rem;
2121
+ }
2122
+
2123
+ .collab-stat {
2124
+ display: flex;
2125
+ flex-direction: column;
2126
+ gap: 0.5rem;
2127
+ padding: 1rem;
2128
+ background: var(--bg-tertiary);
2129
+ border: 1px solid var(--border);
2130
+ border-radius: 4px;
2131
+ }
1155
2132
 
1156
- .analytics-card h3 {
2133
+ .collab-stat-value {
1157
2134
  font-family: 'JetBrains Mono', monospace;
1158
- font-size: 0.75rem;
1159
- text-transform: uppercase;
1160
- letter-spacing: 0.1em;
1161
- color: var(--text-muted);
1162
- margin-bottom: 0.75rem;
2135
+ font-size: 1.5rem;
2136
+ font-weight: 700;
2137
+ color: var(--text-primary);
1163
2138
  }
1164
2139
 
1165
- .analytics-card-wide {
1166
- grid-column: 1 / -1;
2140
+ .collab-stat-label {
2141
+ font-family: 'JetBrains Mono', monospace;
2142
+ font-size: 0.65rem;
2143
+ text-transform: uppercase;
2144
+ letter-spacing: 0.08em;
2145
+ color: var(--text-muted);
1167
2146
  }
1168
2147
 
1169
2148
  /* Commit DAG Visualization */
@@ -1272,26 +2251,44 @@
1272
2251
 
1273
2252
  .analytics-kpis {
1274
2253
  display: grid;
1275
- grid-template-columns: repeat(3, 1fr);
1276
- gap: 0.75rem;
2254
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2255
+ gap: 1rem;
1277
2256
  }
1278
2257
 
1279
2258
  @media (max-width: 700px) {
1280
2259
  .analytics-kpis {
1281
- grid-template-columns: 1fr;
2260
+ grid-template-columns: repeat(2, 1fr);
1282
2261
  }
1283
2262
  }
1284
2263
 
1285
2264
  .kpi {
1286
- border: 1px solid var(--border);
1287
- background: var(--bg-tertiary);
1288
- padding: 0.75rem;
2265
+ border: 2px solid var(--border-strong);
2266
+ background: var(--bg-secondary);
2267
+ padding: 1.25rem;
2268
+ box-shadow: var(--shadow-sm);
2269
+ position: relative;
2270
+ overflow: hidden;
1289
2271
  }
1290
2272
 
2273
+ .kpi::before {
2274
+ content: '';
2275
+ position: absolute;
2276
+ top: 0;
2277
+ left: 0;
2278
+ right: 0;
2279
+ height: 3px;
2280
+ background: var(--accent);
2281
+ }
2282
+
2283
+ .kpi.healthy::before { background: var(--status-done); }
2284
+ .kpi.warning::before { background: var(--priority-high); }
2285
+ .kpi.critical::before { background: var(--priority-critical); }
2286
+
1291
2287
  .kpi .kpi-value {
1292
2288
  font-family: 'JetBrains Mono', monospace;
1293
- font-size: 1.25rem;
2289
+ font-size: 2rem;
1294
2290
  font-weight: 700;
2291
+ color: var(--text-primary);
1295
2292
  }
1296
2293
 
1297
2294
  .kpi .kpi-label {
@@ -1300,7 +2297,7 @@
1300
2297
  text-transform: uppercase;
1301
2298
  letter-spacing: 0.1em;
1302
2299
  color: var(--text-muted);
1303
- margin-top: 0.25rem;
2300
+ margin-top: 0.5rem;
1304
2301
  }
1305
2302
 
1306
2303
  .table {
@@ -1532,6 +2529,38 @@
1532
2529
  border-bottom: none;
1533
2530
  }
1534
2531
 
2532
+ /* Parent events with children */
2533
+ .activity-item.parent-event {
2534
+ cursor: pointer;
2535
+ font-weight: 500;
2536
+ }
2537
+
2538
+ .activity-item.parent-event .expand-icon {
2539
+ display: inline-block;
2540
+ margin-right: 0.5rem;
2541
+ transition: transform 0.2s;
2542
+ font-size: 0.75rem;
2543
+ }
2544
+
2545
+ .activity-item.parent-event .expand-icon::before {
2546
+ content: '▶';
2547
+ color: var(--text-muted);
2548
+ }
2549
+
2550
+ .activity-item.parent-event.expanded .expand-icon::before {
2551
+ content: '▼';
2552
+ }
2553
+
2554
+ /* Child events styling */
2555
+ .activity-item.child-event {
2556
+ background-color: rgba(205, 255, 0, 0.02);
2557
+ margin-left: 0;
2558
+ }
2559
+
2560
+ .activity-item.child-event:hover {
2561
+ background-color: rgba(205, 255, 0, 0.05);
2562
+ }
2563
+
1535
2564
  .activity-meta {
1536
2565
  display: flex;
1537
2566
  align-items: center;
@@ -1592,6 +2621,47 @@
1592
2621
  font-family: 'JetBrains Mono', monospace;
1593
2622
  }
1594
2623
 
2624
+ /* Delegations */
2625
+ .delegations-list {
2626
+ display: flex;
2627
+ flex-direction: column;
2628
+ gap: 0.75rem;
2629
+ }
2630
+
2631
+ .delegation-item {
2632
+ padding: 0.75rem;
2633
+ border: 1px solid var(--border);
2634
+ background: var(--bg-tertiary);
2635
+ border-radius: 2px;
2636
+ }
2637
+
2638
+ .delegation-meta {
2639
+ display: flex;
2640
+ gap: 0.5rem;
2641
+ flex-wrap: wrap;
2642
+ align-items: center;
2643
+ margin-bottom: 0.5rem;
2644
+ }
2645
+
2646
+ .delegation-task {
2647
+ font-family: 'JetBrains Mono', monospace;
2648
+ font-size: 0.75rem;
2649
+ color: var(--text-secondary);
2650
+ margin-bottom: 0.375rem;
2651
+ }
2652
+
2653
+ .delegation-time {
2654
+ font-family: 'JetBrains Mono', monospace;
2655
+ font-size: 0.65rem;
2656
+ color: var(--text-muted);
2657
+ }
2658
+
2659
+ .mono {
2660
+ font-family: 'JetBrains Mono', monospace;
2661
+ font-size: 0.75rem;
2662
+ color: var(--text-secondary);
2663
+ }
2664
+
1595
2665
  /* Session Activity Preview */
1596
2666
  .session-preview {
1597
2667
  background: var(--bg-tertiary);
@@ -1637,47 +2707,355 @@
1637
2707
  display: none;
1638
2708
  }
1639
2709
 
1640
- .activity-entry {
1641
- display: flex;
1642
- gap: 0.5rem;
1643
- padding: 0.375rem 0.75rem;
2710
+ .activity-entry {
2711
+ display: flex;
2712
+ gap: 0.5rem;
2713
+ padding: 0.375rem 0.75rem;
2714
+ font-size: 0.75rem;
2715
+ border-bottom: 1px solid var(--border);
2716
+ }
2717
+
2718
+ .activity-entry:last-child {
2719
+ border-bottom: none;
2720
+ }
2721
+
2722
+ .activity-tool {
2723
+ font-family: 'JetBrains Mono', monospace;
2724
+ font-weight: 600;
2725
+ color: var(--status-active);
2726
+ min-width: 70px;
2727
+ }
2728
+
2729
+ .activity-tool.Edit { color: var(--priority-high); }
2730
+ .activity-tool.Bash { color: var(--status-done); }
2731
+ .activity-tool.Read { color: var(--text-muted); }
2732
+ .activity-tool.UserQuery { color: var(--priority-critical); }
2733
+
2734
+ .activity-summary {
2735
+ color: var(--text-secondary);
2736
+ flex: 1;
2737
+ }
2738
+
2739
+ .activity-time {
2740
+ font-family: 'JetBrains Mono', monospace;
2741
+ font-size: 0.625rem;
2742
+ color: var(--text-muted);
2743
+ }
2744
+
2745
+ .drift-warning {
2746
+ color: var(--priority-high);
2747
+ font-size: 0.625rem;
2748
+ font-size: 0.6875rem;
2749
+ color: var(--text-muted);
2750
+ margin-left: 0.5rem;
2751
+ }
2752
+
2753
+ /* Activity Feed Table View */
2754
+ .activity-list.table-view {
2755
+ flex: 1;
2756
+ min-height: 0;
2757
+ overflow: auto;
2758
+ background: var(--bg-secondary);
2759
+ border: 1px solid var(--border);
2760
+ }
2761
+
2762
+ .activity-table {
2763
+ width: 100%;
2764
+ border-collapse: collapse;
2765
+ font-size: 0.8125rem;
2766
+ background: var(--bg-secondary);
2767
+ }
2768
+
2769
+ .activity-table thead {
2770
+ position: sticky;
2771
+ top: 0;
2772
+ background: var(--bg-tertiary);
2773
+ border-bottom: 2px solid var(--border-strong);
2774
+ z-index: 10;
2775
+ }
2776
+
2777
+ .activity-table th {
2778
+ padding: 0.875rem 1rem;
2779
+ text-align: left;
2780
+ font-family: 'JetBrains Mono', monospace;
2781
+ font-weight: 600;
2782
+ font-size: 0.6875rem;
2783
+ text-transform: uppercase;
2784
+ letter-spacing: 0.05em;
2785
+ color: var(--text-secondary);
2786
+ white-space: nowrap;
2787
+ border-right: 1px solid var(--border);
2788
+ user-select: none;
2789
+ }
2790
+
2791
+ .activity-table th:last-child {
2792
+ border-right: none;
2793
+ }
2794
+
2795
+ .activity-table th.col-timestamp {
2796
+ cursor: pointer;
2797
+ transition: background 0.2s;
2798
+ }
2799
+
2800
+ .activity-table th.col-timestamp:hover {
2801
+ background: var(--bg-secondary);
2802
+ color: var(--accent);
2803
+ }
2804
+
2805
+ .activity-table tbody tr {
2806
+ border-bottom: 1px solid var(--border);
2807
+ transition: background-color 0.2s;
2808
+ }
2809
+
2810
+ .activity-table tbody tr:hover {
2811
+ background-color: var(--bg-tertiary);
2812
+ }
2813
+
2814
+ /* Alternating row colors for readability */
2815
+ .activity-table tbody tr:nth-child(even) {
2816
+ background-color: var(--bg-primary);
2817
+ }
2818
+
2819
+ .activity-table tbody tr:nth-child(even):hover {
2820
+ background-color: var(--bg-tertiary);
2821
+ }
2822
+
2823
+ /* Parent rows slightly emphasized */
2824
+ .activity-table tbody tr.parent-row {
2825
+ font-weight: 500;
2826
+ border-left: 3px solid var(--accent);
2827
+ cursor: pointer;
2828
+ }
2829
+
2830
+ /* Expand icon for parent rows */
2831
+ .activity-table tbody tr.parent-row .expand-icon {
2832
+ display: inline-block;
2833
+ margin-right: 0.5rem;
2834
+ transition: transform 0.2s;
2835
+ }
2836
+
2837
+ .activity-table tbody tr.parent-row .expand-icon::before {
2838
+ content: '▶';
2839
+ color: var(--text-muted);
2840
+ }
2841
+
2842
+ .activity-table tbody tr.parent-row.expanded .expand-icon::before {
2843
+ content: '▼';
2844
+ }
2845
+
2846
+ /* Child rows with visual indent marker */
2847
+ .activity-table tbody tr.child-row {
2848
+ background-color: rgba(205, 255, 0, 0.02);
2849
+ border-left: 3px solid var(--text-muted);
2850
+ display: none;
2851
+ }
2852
+
2853
+ .activity-table tbody tr.child-row.visible {
2854
+ display: table-row;
2855
+ }
2856
+
2857
+ .activity-table tbody tr.child-row:hover {
2858
+ background-color: rgba(205, 255, 0, 0.05);
2859
+ }
2860
+
2861
+ /* Indent child row content */
2862
+ .activity-table tbody tr.child-row td:first-child {
2863
+ padding-left: 2.5rem;
2864
+ }
2865
+
2866
+ /* Status-based row coloring */
2867
+ .activity-table tbody tr.event-recorded {
2868
+ border-left-color: var(--status-done);
2869
+ }
2870
+
2871
+ .activity-table tbody tr.event-pending {
2872
+ border-left-color: var(--priority-medium);
2873
+ }
2874
+
2875
+ .activity-table tbody tr.event-error {
2876
+ border-left-color: var(--status-blocked);
2877
+ }
2878
+
2879
+ .activity-table td {
2880
+ padding: 0.75rem 1rem;
2881
+ color: var(--text-secondary);
2882
+ border-right: 1px solid var(--border);
2883
+ vertical-align: top;
2884
+ }
2885
+
2886
+ .activity-table td:last-child {
2887
+ border-right: none;
2888
+ }
2889
+
2890
+ /* Column widths */
2891
+ .activity-table .col-timestamp {
2892
+ width: 180px;
2893
+ min-width: 180px;
2894
+ }
2895
+
2896
+ .activity-table .col-agent {
2897
+ width: 120px;
2898
+ min-width: 120px;
2899
+ }
2900
+
2901
+ .activity-table .col-tool {
2902
+ width: 100px;
2903
+ min-width: 100px;
2904
+ }
2905
+
2906
+ .activity-table .col-input {
2907
+ width: 180px;
2908
+ min-width: 180px;
2909
+ }
2910
+
2911
+ .activity-table .col-output {
2912
+ width: 180px;
2913
+ min-width: 180px;
2914
+ }
2915
+
2916
+ .activity-table .col-status {
2917
+ width: 90px;
2918
+ min-width: 90px;
2919
+ }
2920
+
2921
+ .activity-table .col-id {
2922
+ width: 80px;
2923
+ min-width: 80px;
2924
+ }
2925
+
2926
+ /* Table cell content styling */
2927
+ .activity-table .event-type-badge {
2928
+ font-size: 1rem;
2929
+ margin-right: 0.5rem;
2930
+ display: inline-block;
2931
+ vertical-align: middle;
2932
+ }
2933
+
2934
+ .activity-table .timestamp-text {
2935
+ font-family: 'JetBrains Mono', monospace;
2936
+ font-size: 0.75rem;
2937
+ color: var(--text-muted);
2938
+ white-space: nowrap;
2939
+ }
2940
+
2941
+ .activity-table .agent-badge {
2942
+ font-family: 'JetBrains Mono', monospace;
2943
+ font-size: 0.75rem;
2944
+ background: var(--bg-tertiary);
2945
+ padding: 0.25rem 0.5rem;
2946
+ border-radius: 2px;
2947
+ margin-right: 0.25rem;
2948
+ }
2949
+
2950
+ .activity-table .parent-indicator {
2951
+ font-size: 0.875rem;
2952
+ margin-left: 0.25rem;
2953
+ opacity: 0.7;
2954
+ }
2955
+
2956
+ .activity-table .child-indicator {
2957
+ font-size: 0.875rem;
2958
+ margin-left: 0.25rem;
2959
+ opacity: 0.6;
2960
+ color: var(--text-muted);
2961
+ }
2962
+
2963
+ .activity-table .tool-name {
2964
+ font-family: 'JetBrains Mono', monospace;
2965
+ font-size: 0.75rem;
2966
+ background: var(--bg-tertiary);
2967
+ padding: 0.25rem 0.5rem;
2968
+ border-radius: 2px;
2969
+ color: var(--status-active);
2970
+ word-break: break-all;
2971
+ }
2972
+
2973
+ .activity-table .truncate {
2974
+ display: inline-block;
2975
+ max-width: 100%;
2976
+ white-space: nowrap;
2977
+ overflow: hidden;
2978
+ text-overflow: ellipsis;
2979
+ font-family: 'JetBrains Mono', monospace;
1644
2980
  font-size: 0.75rem;
1645
- border-bottom: 1px solid var(--border);
1646
2981
  }
1647
2982
 
1648
- .activity-entry:last-child {
1649
- border-bottom: none;
2983
+ .activity-table .text-muted {
2984
+ color: var(--text-muted);
2985
+ font-style: italic;
1650
2986
  }
1651
2987
 
1652
- .activity-tool {
2988
+ .activity-table .status-badge {
2989
+ display: inline-block;
2990
+ padding: 0.375rem 0.625rem;
2991
+ border-radius: 2px;
1653
2992
  font-family: 'JetBrains Mono', monospace;
2993
+ font-size: 0.65rem;
1654
2994
  font-weight: 600;
1655
- color: var(--status-active);
1656
- min-width: 70px;
2995
+ text-transform: uppercase;
2996
+ letter-spacing: 0.05em;
1657
2997
  }
1658
2998
 
1659
- .activity-tool.Edit { color: var(--priority-high); }
1660
- .activity-tool.Bash { color: var(--status-done); }
1661
- .activity-tool.Read { color: var(--text-muted); }
1662
- .activity-tool.UserQuery { color: var(--priority-critical); }
2999
+ .activity-table .status-badge.status-recorded {
3000
+ background: rgba(0, 200, 83, 0.15);
3001
+ color: var(--status-done);
3002
+ border: 1px solid var(--status-done);
3003
+ }
1663
3004
 
1664
- .activity-summary {
1665
- color: var(--text-secondary);
1666
- flex: 1;
3005
+ .activity-table .status-badge.status-pending {
3006
+ background: rgba(41, 121, 255, 0.15);
3007
+ color: var(--priority-medium);
3008
+ border: 1px solid var(--priority-medium);
1667
3009
  }
1668
3010
 
1669
- .activity-time {
3011
+ .activity-table .status-badge.status-error {
3012
+ background: rgba(255, 23, 68, 0.15);
3013
+ color: var(--status-blocked);
3014
+ border: 1px solid var(--status-blocked);
3015
+ }
3016
+
3017
+ .activity-table .event-id-code {
1670
3018
  font-family: 'JetBrains Mono', monospace;
1671
- font-size: 0.625rem;
3019
+ font-size: 0.65rem;
1672
3020
  color: var(--text-muted);
3021
+ cursor: pointer;
3022
+ transition: color 0.2s;
3023
+ word-break: break-all;
1673
3024
  }
1674
3025
 
1675
- .drift-warning {
1676
- color: var(--priority-high);
1677
- font-size: 0.625rem;
1678
- font-size: 0.6875rem;
1679
- color: var(--text-muted);
1680
- margin-left: 0.5rem;
3026
+ .activity-table .event-id-code:hover {
3027
+ color: var(--accent);
3028
+ }
3029
+
3030
+ /* Responsive table scrolling */
3031
+ @media (max-width: 1200px) {
3032
+ .activity-table .col-input,
3033
+ .activity-table .col-output {
3034
+ width: 120px;
3035
+ min-width: 120px;
3036
+ }
3037
+ }
3038
+
3039
+ @media (max-width: 768px) {
3040
+ .activity-table {
3041
+ font-size: 0.75rem;
3042
+ }
3043
+
3044
+ .activity-table th,
3045
+ .activity-table td {
3046
+ padding: 0.5rem 0.75rem;
3047
+ }
3048
+
3049
+ .activity-table .col-timestamp {
3050
+ width: 140px;
3051
+ min-width: 140px;
3052
+ }
3053
+
3054
+ .activity-table .col-input,
3055
+ .activity-table .col-output {
3056
+ width: 100px;
3057
+ min-width: 100px;
3058
+ }
1681
3059
  }
1682
3060
 
1683
3061
  /* Content */
@@ -2005,32 +3383,14 @@
2005
3383
  <!-- View Toggle -->
2006
3384
  <div class="view-toggle">
2007
3385
  <button class="view-btn active" data-view="kanban">Work</button>
2008
- <button class="view-btn" data-view="graph">Graph</button>
2009
3386
  <button class="view-btn" data-view="analytics">Analytics</button>
3387
+ <button class="view-btn" data-view="agents">Agents</button>
2010
3388
  <button class="view-btn" data-view="sessions">Sessions</button>
2011
3389
  </div>
2012
3390
 
2013
3391
  <!-- Features View (Track-Grouped Kanban) -->
2014
3392
  <div class="kanban active" id="kanban"></div>
2015
3393
 
2016
- <!-- Graph View -->
2017
- <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>
2031
- </div>
2032
- </div>
2033
-
2034
3394
  <!-- Analytics View -->
2035
3395
  <div class="analytics" id="analytics">
2036
3396
  <div class="analytics-header">
@@ -2044,22 +3404,76 @@
2044
3404
  </div>
2045
3405
 
2046
3406
  <div class="analytics-grid">
3407
+ <!-- Timeline: Primary Focus -->
3408
+ <div class="analytics-timeline" id="analytics-timeline">
3409
+ <div class="timeline-header">
3410
+ <h3>Activity Timeline</h3>
3411
+ <div class="timeline-legend">
3412
+ <div class="timeline-legend-item">
3413
+ <div class="timeline-legend-dot" style="background: var(--status-active);"></div>
3414
+ <span>Sessions</span>
3415
+ </div>
3416
+ <div class="timeline-legend-item">
3417
+ <div class="timeline-legend-dot" style="background: var(--accent);"></div>
3418
+ <span>Features</span>
3419
+ </div>
3420
+ <div class="timeline-legend-item">
3421
+ <div class="timeline-legend-dot" style="background: var(--status-done);"></div>
3422
+ <span>Commits</span>
3423
+ </div>
3424
+ </div>
3425
+ </div>
3426
+ <div class="timeline-container" id="timeline-content">
3427
+ <div class="loading">Loading timeline…</div>
3428
+ </div>
3429
+ </div>
3430
+
3431
+ <!-- Summary Section: Key Metrics & Health -->
3432
+ <div class="analytics-card analytics-card-wide" id="analytics-summary">
3433
+ <h3>Summary & Health</h3>
3434
+ <div class="analytics-summary" id="summary-content">
3435
+ <div class="loading">Loading summary…</div>
3436
+ </div>
3437
+ </div>
3438
+
3439
+ <!-- Collaboration Metrics -->
3440
+ <div class="analytics-card analytics-card-wide" id="analytics-collaboration">
3441
+ <div class="collaboration-metrics">
3442
+ <h3>Collaboration & Handoff Metrics</h3>
3443
+ <div class="collaboration-grid" id="collaboration-content">
3444
+ <div class="loading">Loading collaboration metrics…</div>
3445
+ </div>
3446
+ </div>
3447
+ </div>
3448
+
3449
+ <!-- Overview KPIs -->
2047
3450
  <div class="analytics-card" id="analytics-overview">
2048
- <div class="loading">Loading overview…</div>
3451
+ <h3>Overview</h3>
3452
+ <div id="overview-kpis" class="loading">Loading overview…</div>
2049
3453
  </div>
3454
+
3455
+ <!-- Tool Patterns -->
2050
3456
  <div class="analytics-card" id="analytics-tools">
2051
3457
  <div class="loading">Loading tool patterns…</div>
2052
3458
  </div>
3459
+
3460
+ <!-- Feature Analysis -->
2053
3461
  <div class="analytics-card" id="analytics-features">
2054
3462
  <div class="loading">Loading top features…</div>
2055
3463
  </div>
2056
- <div class="analytics-card" id="analytics-continuity">
3464
+
3465
+ <!-- Feature Continuity (hidden until feature selected) -->
3466
+ <div class="analytics-card analytics-card-wide" id="analytics-continuity" style="display: none;">
2057
3467
  <div class="loading">Select a feature to see continuity…</div>
2058
3468
  </div>
2059
- <div class="analytics-card" id="analytics-commits">
3469
+
3470
+ <!-- Feature Commits (hidden until feature selected) -->
3471
+ <div class="analytics-card analytics-card-wide" id="analytics-commits" style="display: none;">
2060
3472
  <div class="loading">Select a feature to see commits…</div>
2061
3473
  </div>
2062
- <div class="analytics-card analytics-card-wide" id="analytics-commit-dag">
3474
+
3475
+ <!-- Commit DAG (hidden until feature selected) -->
3476
+ <div class="analytics-card analytics-card-wide" id="analytics-commit-dag" style="display: none;">
2063
3477
  <div class="loading">Select a feature to see commit graph…</div>
2064
3478
  </div>
2065
3479
  </div>
@@ -2115,6 +3529,89 @@
2115
3529
  <div id="sessions-list" class="loading">Loading sessions...</div>
2116
3530
  </div>
2117
3531
 
3532
+ <!-- Agents View - Multi-Agent Work Attribution -->
3533
+ <div class="agents" id="agents">
3534
+ <div class="analytics-header">
3535
+ <div>
3536
+ <h2>Multi-Agent Work Attribution</h2>
3537
+ <p>Track which agents completed work items and monitor delegation performance.</p>
3538
+ </div>
3539
+ <div style="display:flex; gap:0.5rem; align-items:center;">
3540
+ <button class="btn btn-primary" id="agents-refresh">Refresh</button>
3541
+ </div>
3542
+ </div>
3543
+
3544
+ <!-- Agent Skills Matrix -->
3545
+ <div class="analytics-card analytics-card-wide" id="agent-skills-matrix">
3546
+ <h3>Agent Specializations & Skills Matrix</h3>
3547
+ <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">
3548
+ Proficiency levels based on work history analysis. Color intensity indicates expertise level (1=novice, 5=expert).
3549
+ </p>
3550
+ <div class="skills-matrix-container" id="skills-matrix-content">
3551
+ <div class="loading">Analyzing agent work history...</div>
3552
+ </div>
3553
+ </div>
3554
+
3555
+ <!-- Orchestration & Delegations -->
3556
+ <div class="analytics-card analytics-card-wide" id="orchestration-view">
3557
+ <h3>Orchestration & Delegations</h3>
3558
+ <p style="color: var(--text-muted); font-size: 0.875rem; margin-bottom: 1.5rem;">
3559
+ Track agent-to-agent task delegations and coordination patterns.
3560
+ </p>
3561
+ <div id="orchestration-content">
3562
+ <div class="loading">Loading delegation data...</div>
3563
+ </div>
3564
+ </div>
3565
+
3566
+ <!-- Agent Stats Summary -->
3567
+ <div class="analytics-card" id="agent-summary">
3568
+ <div class="loading">Loading agent statistics...</div>
3569
+ </div>
3570
+
3571
+ <!-- Workload Distribution Chart -->
3572
+ <div class="analytics-card analytics-card-wide">
3573
+ <div class="workload-chart-container" id="workload-chart">
3574
+ <div class="workload-chart-header">
3575
+ <h3>Agent Workload Distribution</h3>
3576
+ <p>Horizontal bar chart showing work completion by agent</p>
3577
+ </div>
3578
+ <div id="workload-chart-content" class="loading">Loading workload data...</div>
3579
+ </div>
3580
+ </div>
3581
+
3582
+ <!-- Agent Work Table -->
3583
+ <div class="analytics-card analytics-card-wide" id="agent-work-table">
3584
+ <div class="loading">Loading agent work data...</div>
3585
+ </div>
3586
+
3587
+ <!-- Agent Performance Metrics -->
3588
+ <div class="analytics-card" id="agent-performance">
3589
+ <div class="loading">Loading agent performance metrics...</div>
3590
+ </div>
3591
+
3592
+ <!-- Agent Cost Breakdown -->
3593
+ <div class="analytics-card analytics-card-wide" id="agent-costs">
3594
+ <div class="cost-breakdown-container">
3595
+ <div class="cost-breakdown-header">
3596
+ <h3>Agent Cost Breakdown</h3>
3597
+ <p>Token costs aggregated by agent type with visual distribution</p>
3598
+ </div>
3599
+
3600
+ <div class="cost-summary-metrics" id="cost-metrics">
3601
+ <!-- Populated by JavaScript -->
3602
+ </div>
3603
+
3604
+ <div class="cost-bars" id="cost-bars-container">
3605
+ <!-- Populated by JavaScript -->
3606
+ </div>
3607
+
3608
+ <div class="cost-breakdown-legend" id="cost-legend">
3609
+ <!-- Populated by JavaScript -->
3610
+ </div>
3611
+ </div>
3612
+ </div>
3613
+ </div>
3614
+
2118
3615
  </div>
2119
3616
 
2120
3617
  <!-- Detail Panel -->
@@ -2228,11 +3725,13 @@
2228
3725
  // =====================================================================
2229
3726
 
2230
3727
  async function loadData() {
3728
+ console.log('[Dashboard] Loading data from:', API);
2231
3729
  const [status, query] = await Promise.all([
2232
3730
  fetch(`${API}/status`).then(r => r.json()),
2233
3731
  fetch(`${API}/query`).then(r => r.json())
2234
3732
  ]);
2235
3733
  allNodes = query.nodes;
3734
+ console.log('[Dashboard] Loaded nodes:', allNodes.length, 'Status:', status);
2236
3735
  return { status, nodes: query.nodes };
2237
3736
  }
2238
3737
 
@@ -2287,6 +3786,131 @@
2287
3786
  return Number(value).toFixed(2);
2288
3787
  }
2289
3788
 
3789
+ function renderTimeline(overview, features, transitions) {
3790
+ const el = document.getElementById('timeline-content');
3791
+ if (!overview) {
3792
+ el.innerHTML = '<p class="analytics-note">No timeline data available.</p>';
3793
+ return;
3794
+ }
3795
+
3796
+ // Build timeline entries from overview data
3797
+ const entries = [];
3798
+
3799
+ // Add overview entry (summary event)
3800
+ if (overview.events > 0) {
3801
+ entries.push({
3802
+ type: 'session',
3803
+ title: `${overview.events} events recorded`,
3804
+ meta: `Failure rate: ${formatPercent(overview.failure_rate)}, Avg drift: ${formatFloat(overview.avg_drift)}`
3805
+ });
3806
+ }
3807
+
3808
+ // Add top features as timeline entries
3809
+ if (features && features.length > 0) {
3810
+ features.slice(0, 5).forEach(f => {
3811
+ entries.push({
3812
+ type: 'feature',
3813
+ title: f.feature_id,
3814
+ meta: `${f.count} events, ${f.failures} failures`
3815
+ });
3816
+ });
3817
+ }
3818
+
3819
+ // Add top transitions as timeline entries
3820
+ if (transitions && transitions.length > 0) {
3821
+ transitions.slice(0, 3).forEach(t => {
3822
+ entries.push({
3823
+ type: 'session',
3824
+ title: `${t.tool} → ${t.next_tool}`,
3825
+ meta: `${t.count} transitions`
3826
+ });
3827
+ });
3828
+ }
3829
+
3830
+ if (entries.length === 0) {
3831
+ el.innerHTML = '<p class="analytics-note">No timeline events to display.</p>';
3832
+ return;
3833
+ }
3834
+
3835
+ el.innerHTML = entries.map(e => `
3836
+ <div class="timeline-entry" title="${escapeHtml(e.meta)}">
3837
+ <div class="timeline-entry-marker ${e.type}"></div>
3838
+ <div class="timeline-entry-content">
3839
+ <div class="timeline-entry-title">${escapeHtml(e.title)}</div>
3840
+ <div class="timeline-entry-meta">${escapeHtml(e.meta)}</div>
3841
+ </div>
3842
+ </div>
3843
+ `).join('');
3844
+ }
3845
+
3846
+ function renderAnalyticsSummary(overview) {
3847
+ const el = document.getElementById('summary-content');
3848
+ if (!overview) {
3849
+ el.innerHTML = '<p class="analytics-note">No summary data available.</p>';
3850
+ return;
3851
+ }
3852
+
3853
+ const failureRate = overview.failure_rate || 0;
3854
+ const avgDrift = overview.avg_drift || 0;
3855
+ const eventCount = overview.events || 0;
3856
+
3857
+ // Determine health status
3858
+ const failureHealth = failureRate < 0.05 ? 'health-good' : failureRate < 0.15 ? 'health-ok' : 'health-poor';
3859
+ const driftHealth = avgDrift < 1.0 ? 'health-good' : avgDrift < 2.0 ? 'health-ok' : 'health-poor';
3860
+ const activityHealth = eventCount > 100 ? 'health-good' : eventCount > 20 ? 'health-ok' : 'health-poor';
3861
+
3862
+ el.innerHTML = `
3863
+ <div class="health-card ${failureHealth}">
3864
+ <div class="health-label">Failure Rate</div>
3865
+ <div class="health-value">${formatPercent(failureRate)}</div>
3866
+ <div class="health-detail">${failureRate < 0.05 ? '✓ Excellent' : failureRate < 0.15 ? '⚠ Acceptable' : '✗ Needs attention'}</div>
3867
+ </div>
3868
+ <div class="health-card ${driftHealth}">
3869
+ <div class="health-label">Avg Context Drift</div>
3870
+ <div class="health-value">${formatFloat(avgDrift)}</div>
3871
+ <div class="health-detail">${avgDrift < 1.0 ? '✓ Clean' : avgDrift < 2.0 ? '⚠ Acceptable' : '✗ High drift'}</div>
3872
+ </div>
3873
+ <div class="health-card ${activityHealth}">
3874
+ <div class="health-label">Total Events</div>
3875
+ <div class="health-value">${eventCount}</div>
3876
+ <div class="health-detail">${eventCount > 100 ? '✓ Active' : eventCount > 20 ? '⚠ Some data' : '✗ Minimal activity'}</div>
3877
+ </div>
3878
+ `;
3879
+ }
3880
+
3881
+ function renderCollaborationMetrics(overview) {
3882
+ const el = document.getElementById('collaboration-content');
3883
+ if (!overview) {
3884
+ el.innerHTML = '<p class="analytics-note">No collaboration data available.</p>';
3885
+ return;
3886
+ }
3887
+
3888
+ // Extract or calculate collaboration metrics
3889
+ const handoffCount = overview.handoffs || 0;
3890
+ const parallelWorkPct = overview.parallel_work_pct || 0;
3891
+ const agentCount = overview.agent_count || 1;
3892
+ const avgSessionDuration = overview.avg_session_duration || 0;
3893
+
3894
+ el.innerHTML = `
3895
+ <div class="collab-stat">
3896
+ <div class="collab-stat-value">${handoffCount}</div>
3897
+ <div class="collab-stat-label">Handoffs</div>
3898
+ </div>
3899
+ <div class="collab-stat">
3900
+ <div class="collab-stat-value">${formatPercent(parallelWorkPct)}</div>
3901
+ <div class="collab-stat-label">Parallel Work</div>
3902
+ </div>
3903
+ <div class="collab-stat">
3904
+ <div class="collab-stat-value">${agentCount}</div>
3905
+ <div class="collab-stat-label">Active Agents</div>
3906
+ </div>
3907
+ <div class="collab-stat">
3908
+ <div class="collab-stat-value">${formatFloat(avgSessionDuration)}</div>
3909
+ <div class="collab-stat-label">Avg Session (min)</div>
3910
+ </div>
3911
+ `;
3912
+ }
3913
+
2290
3914
  function renderAnalyticsOverview(overview) {
2291
3915
  const el = document.getElementById('analytics-overview');
2292
3916
  el.innerHTML = `
@@ -2696,17 +4320,25 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2696
4320
  }
2697
4321
 
2698
4322
  async function loadAndRenderAnalyticsBase() {
4323
+ console.log('[Dashboard] Loading analytics from:', `${API}/analytics/`);
2699
4324
  const [overview, features, transitions] = await Promise.all([
2700
- fetchAnalytics('overview'),
2701
- fetchAnalytics('features', { limit: 50 }).then(r => r.features),
2702
- fetchAnalytics('transitions', { limit: 25 }).then(r => r.transitions),
4325
+ fetchAnalytics('overview').catch(e => { console.error('[Analytics] overview failed:', e); return null; }),
4326
+ fetchAnalytics('features', { limit: 50 }).then(r => r.features).catch(e => { console.error('[Analytics] features failed:', e); return null; }),
4327
+ fetchAnalytics('transitions', { limit: 25 }).then(r => r.transitions).catch(e => { console.error('[Analytics] transitions failed:', e); return null; }),
2703
4328
  ]);
2704
4329
 
2705
4330
  analyticsCache.overview = overview;
2706
4331
  analyticsCache.features = features;
2707
4332
  analyticsCache.transitions = transitions;
2708
4333
  analyticsLoadedAt = Date.now();
4334
+ console.log('[Analytics] Loaded - overview:', !!overview, 'features:', features?.length || 0, 'transitions:', transitions?.length || 0);
2709
4335
 
4336
+ // Render primary analytics views (new card-based layout)
4337
+ renderTimeline(overview, features, transitions);
4338
+ renderAnalyticsSummary(overview);
4339
+ renderCollaborationMetrics(overview);
4340
+
4341
+ // Render detail cards
2710
4342
  renderAnalyticsOverview(overview);
2711
4343
  renderAnalyticsTools(transitions);
2712
4344
  renderAnalyticsFeatures(features);
@@ -2718,18 +4350,245 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2718
4350
  renderAnalyticsContinuity(featureId, data.sessions || []);
2719
4351
  }
2720
4352
 
2721
- async function loadAndRenderCommits(featureId) {
2722
- const data = await fetchAnalytics('commits', { feature_id: featureId, limit: 200 });
2723
- renderAnalyticsCommits(featureId, data.commits || []);
2724
- }
4353
+ async function loadAndRenderCommits(featureId) {
4354
+ const data = await fetchAnalytics('commits', { feature_id: featureId, limit: 200 });
4355
+ renderAnalyticsCommits(featureId, data.commits || []);
4356
+ }
4357
+
4358
+ async function loadFeatureAnalytics(featureId) {
4359
+ analyticsCache.selectedFeatureId = featureId;
4360
+
4361
+ // Show the detail sections for selected feature
4362
+ document.getElementById('analytics-continuity').style.display = 'block';
4363
+ document.getElementById('analytics-commits').style.display = 'block';
4364
+ document.getElementById('analytics-commit-dag').style.display = 'block';
4365
+
4366
+ await Promise.all([
4367
+ loadAndRenderContinuity(featureId),
4368
+ loadAndRenderCommits(featureId),
4369
+ loadAndRenderCommitDag(featureId)
4370
+ ]);
4371
+ }
4372
+
4373
+ // =====================================================================
4374
+ // Agent Cost Visualization
4375
+ // =====================================================================
4376
+
4377
+ const AGENT_COLORS = {
4378
+ 'claude': '#2979FF',
4379
+ 'codex': '#00C853',
4380
+ 'orchestrator': '#7C4DFF',
4381
+ 'gemini': '#FBC02D',
4382
+ 'gemini-2': '#FF9100',
4383
+ 'analyst': '#0ea5e9',
4384
+ 'developer': '#06b6d4',
4385
+ 'researcher': '#d946ef',
4386
+ 'debugger': '#ef4444',
4387
+ 'default': '#78909C'
4388
+ };
4389
+
4390
+ function getAgentColor(agent) {
4391
+ const normalized = (agent || 'default').toLowerCase();
4392
+ return AGENT_COLORS[normalized] || AGENT_COLORS['default'];
4393
+ }
4394
+
4395
+ function formatCost(tokens) {
4396
+ // Approximate cost: $0.003 per 1K input tokens
4397
+ const cost = (tokens / 1000) * 0.003;
4398
+ return cost.toFixed(4);
4399
+ }
4400
+
4401
+ function formatCostDisplay(tokens) {
4402
+ const cost = parseFloat(formatCost(tokens));
4403
+ if (cost === 0) return '$0.00';
4404
+ return `$${cost.toFixed(2)}`;
4405
+ }
4406
+
4407
+ function getCostRange(cost) {
4408
+ // Define ranges: low (< $0.01), medium ($0.01-$0.05), high (> $0.05)
4409
+ const numCost = parseFloat(formatCost(cost));
4410
+ if (numCost < 0.01) return 'low';
4411
+ if (numCost < 0.05) return 'medium';
4412
+ return 'high';
4413
+ }
4414
+
4415
+ function aggregateAgentCosts(sessions) {
4416
+ const costsByAgent = {};
4417
+ let totalCost = 0;
4418
+
4419
+ sessions.forEach(session => {
4420
+ const agent = session.properties?.agent || 'unknown';
4421
+ const tokens = parseInt(session.properties?.total_tokens) || 0;
4422
+
4423
+ if (!costsByAgent[agent]) {
4424
+ costsByAgent[agent] = {
4425
+ agent,
4426
+ totalTokens: 0,
4427
+ operationCount: 0,
4428
+ sessionCount: 0
4429
+ };
4430
+ }
4431
+
4432
+ costsByAgent[agent].totalTokens += tokens;
4433
+ costsByAgent[agent].operationCount += parseInt(session.properties?.event_count) || 0;
4434
+ costsByAgent[agent].sessionCount += 1;
4435
+ totalCost += tokens;
4436
+ });
4437
+
4438
+ return {
4439
+ byAgent: Object.values(costsByAgent).sort((a, b) => b.totalTokens - a.totalTokens),
4440
+ totalTokens: totalCost
4441
+ };
4442
+ }
4443
+
4444
+ function renderAgentCostMetrics(costs) {
4445
+ const metricsEl = document.getElementById('cost-metrics');
4446
+ if (!metricsEl || costs.byAgent.length === 0) return;
4447
+
4448
+ const avgCostPerAgent = costs.totalTokens / costs.byAgent.length;
4449
+ const totalCostUSD = formatCostDisplay(costs.totalTokens);
4450
+ const avgCostUSD = formatCostDisplay(avgCostPerAgent);
4451
+
4452
+ const topAgent = costs.byAgent[0];
4453
+ const topAgentPercent = ((topAgent.totalTokens / costs.totalTokens) * 100).toFixed(1);
4454
+
4455
+ metricsEl.innerHTML = `
4456
+ <div class="cost-metric">
4457
+ <div class="cost-metric-label">Total Cost</div>
4458
+ <div class="cost-metric-value">${totalCostUSD}</div>
4459
+ <div class="cost-metric-unit">${costs.totalTokens.toLocaleString()} tokens</div>
4460
+ </div>
4461
+ <div class="cost-metric">
4462
+ <div class="cost-metric-label">Average Per Agent</div>
4463
+ <div class="cost-metric-value">${avgCostUSD}</div>
4464
+ <div class="cost-metric-unit">~${Math.round(avgCostPerAgent).toLocaleString()} tokens</div>
4465
+ </div>
4466
+ <div class="cost-metric">
4467
+ <div class="cost-metric-label">Top Agent</div>
4468
+ <div class="cost-metric-value">${topAgent.agent}</div>
4469
+ <div class="cost-metric-unit">${topAgentPercent}% of total</div>
4470
+ </div>
4471
+ <div class="cost-metric">
4472
+ <div class="cost-metric-label">Agent Count</div>
4473
+ <div class="cost-metric-value">${costs.byAgent.length}</div>
4474
+ <div class="cost-metric-unit">unique agents</div>
4475
+ </div>
4476
+ `;
4477
+ }
4478
+
4479
+ function renderAgentCostBars(costs) {
4480
+ const containerEl = document.getElementById('cost-bars-container');
4481
+ if (!containerEl || costs.byAgent.length === 0) {
4482
+ if (containerEl) containerEl.innerHTML = '<div class="loading">No cost data available</div>';
4483
+ return;
4484
+ }
4485
+
4486
+ const maxCost = Math.max(...costs.byAgent.map(a => a.totalTokens));
4487
+
4488
+ const barsHTML = costs.byAgent.map(agent => {
4489
+ const percentOfTotal = (agent.totalTokens / costs.totalTokens) * 100;
4490
+ const costUSD = formatCostDisplay(agent.totalTokens);
4491
+ const costRange = getCostRange(agent.totalTokens);
4492
+ const color = getAgentColor(agent.agent);
4493
+ const avgPerSession = Math.round(agent.totalTokens / agent.sessionCount);
4494
+
4495
+ return `
4496
+ <div class="cost-bar-group">
4497
+ <div class="cost-bar-label">
4498
+ <div class="cost-bar-label-name">
4499
+ <div class="cost-agent-badge" style="background: ${color};">
4500
+ ${agent.agent.substring(0, 1).toUpperCase()}
4501
+ </div>
4502
+ <span>${agent.agent}</span>
4503
+ </div>
4504
+ <div class="cost-bar-stats">
4505
+ <div class="cost-bar-stat">
4506
+ <div class="cost-bar-stat-label">Cost</div>
4507
+ <div class="cost-bar-stat-value">${costUSD}</div>
4508
+ </div>
4509
+ <div class="cost-bar-stat">
4510
+ <div class="cost-bar-stat-label">%</div>
4511
+ <div class="cost-bar-stat-value">${percentOfTotal.toFixed(1)}%</div>
4512
+ </div>
4513
+ <div class="cost-bar-stat">
4514
+ <div class="cost-bar-stat-label">Tokens</div>
4515
+ <div class="cost-bar-stat-value">${agent.totalTokens.toLocaleString()}</div>
4516
+ </div>
4517
+ </div>
4518
+ </div>
4519
+
4520
+ <div class="cost-bar-container">
4521
+ <div class="cost-bar-stacked">
4522
+ <div class="cost-bar-segment" style="
4523
+ width: 100%;
4524
+ background: ${color};
4525
+ opacity: 0.85;
4526
+ " title="${agent.agent}: ${costUSD}">
4527
+ <span class="cost-bar-segment-label">
4528
+ ${percentOfTotal.toFixed(0)}%
4529
+ </span>
4530
+ </div>
4531
+ </div>
4532
+ <div class="cost-bar-tooltip">
4533
+ Sessions: ${agent.sessionCount} | Avg/Session: ${avgPerSession.toLocaleString()} tokens
4534
+ </div>
4535
+ </div>
4536
+
4537
+ <div class="cost-range-indicator">
4538
+ <div class="cost-range-dot ${costRange}"></div>
4539
+ <span>${costRange === 'low' ? 'Low' : costRange === 'medium' ? 'Medium' : 'High'} cost</span>
4540
+ </div>
4541
+ </div>
4542
+ `;
4543
+ }).join('');
4544
+
4545
+ containerEl.innerHTML = barsHTML;
4546
+ }
4547
+
4548
+ function renderAgentCostLegend(costs) {
4549
+ const legendEl = document.getElementById('cost-legend');
4550
+ if (!legendEl || costs.byAgent.length === 0) return;
4551
+
4552
+ const legendItems = costs.byAgent.map(agent => {
4553
+ const color = getAgentColor(agent.agent);
4554
+ return `
4555
+ <div class="cost-legend-item">
4556
+ <div class="cost-legend-color" style="background: ${color};"></div>
4557
+ <span class="cost-legend-label">${agent.agent}</span>
4558
+ </div>
4559
+ `;
4560
+ }).join('');
4561
+
4562
+ legendEl.innerHTML = legendItems;
4563
+ }
4564
+
4565
+ async function loadAndRenderAgentCosts() {
4566
+ const container = document.getElementById('agent-costs');
4567
+ if (!container) return;
4568
+
4569
+ try {
4570
+ // Fetch all sessions to aggregate costs
4571
+ if (allSessions.length === 0) {
4572
+ const response = await fetch(`${API}/sessions`);
4573
+ if (!response.ok) throw new Error('Failed to load sessions');
4574
+ const data = await response.json();
4575
+ allSessions = data.nodes || [];
4576
+ }
4577
+
4578
+ if (allSessions.length === 0) {
4579
+ return;
4580
+ }
2725
4581
 
2726
- async function loadFeatureAnalytics(featureId) {
2727
- analyticsCache.selectedFeatureId = featureId;
2728
- await Promise.all([
2729
- loadAndRenderContinuity(featureId),
2730
- loadAndRenderCommits(featureId),
2731
- loadAndRenderCommitDag(featureId)
2732
- ]);
4582
+ // Aggregate costs by agent
4583
+ const costs = aggregateAgentCosts(allSessions);
4584
+
4585
+ // Render visualization
4586
+ renderAgentCostMetrics(costs);
4587
+ renderAgentCostBars(costs);
4588
+ renderAgentCostLegend(costs);
4589
+ } catch (err) {
4590
+ console.error('Error loading agent costs:', err);
4591
+ }
2733
4592
  }
2734
4593
 
2735
4594
  async function ensureAnalyticsLoaded(force = false) {
@@ -2737,6 +4596,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2737
4596
  if (!force && analyticsCache.overview && !stale) return;
2738
4597
  try {
2739
4598
  await loadAndRenderAnalyticsBase();
4599
+ await loadAndRenderAgentCosts();
2740
4600
  if (!analyticsCache.selectedFeatureId && analyticsCache.features && analyticsCache.features.length) {
2741
4601
  await loadFeatureAnalytics(analyticsCache.features[0].feature_id);
2742
4602
  }
@@ -2753,11 +4613,13 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
2753
4613
  try {
2754
4614
  // Fetch all sessions from the API (only on first load)
2755
4615
  if (allSessions.length === 0) {
4616
+ console.log('[Sessions] Fetching from:', `${API}/sessions`);
2756
4617
  const response = await fetch(`${API}/sessions`);
2757
- if (!response.ok) throw new Error('Failed to load sessions');
4618
+ if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
2758
4619
 
2759
4620
  const data = await response.json();
2760
4621
  allSessions = data.nodes || [];
4622
+ console.log('[Sessions] Loaded', allSessions.length, 'sessions from API response keys:', Object.keys(data));
2761
4623
 
2762
4624
  // Populate agent dropdown with unique agents
2763
4625
  const agents = [...new Set(allSessions.map(s => s.properties?.agent).filter(Boolean))];
@@ -3240,7 +5102,9 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3240
5102
  // Sort tracks by feature completion (incomplete first), then by priority
3241
5103
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
3242
5104
  const sortedTracks = Array.from(tracked.values())
3243
- .filter(t => t.track !== null)
5105
+ // FIXED: Do NOT filter out tracks with null metadata - they still have features to show!
5106
+ // Only filter if there are no features for this track
5107
+ .filter(t => t.features && t.features.length > 0)
3244
5108
  .sort((a, b) => {
3245
5109
  // Calculate completion percentage for each track
3246
5110
  const aTotal = a.features.length;
@@ -3257,7 +5121,10 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3257
5121
  }
3258
5122
 
3259
5123
  // Within same completion status, sort by priority
3260
- return priorityOrder[a.track.priority] - priorityOrder[b.track.priority];
5124
+ // Use track priority if available, otherwise default to 'medium'
5125
+ const aPriority = (a.track && a.track.priority) || 'medium';
5126
+ const bPriority = (b.track && b.track.priority) || 'medium';
5127
+ return (priorityOrder[aPriority] || 2) - (priorityOrder[bPriority] || 2);
3261
5128
  });
3262
5129
 
3263
5130
  // Render
@@ -3426,18 +5293,51 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3426
5293
  <div class="track-column-cards">
3427
5294
  ${byStatus[status].length === 0
3428
5295
  ? '<div class="empty-column">No items</div>'
3429
- : byStatus[status].map(f => `
5296
+ : byStatus[status].map(f => {
5297
+ // Determine agent badge color based on agent name
5298
+ let agentClass = 'agent-default';
5299
+ if (f.agent_assigned) {
5300
+ const agentName = f.agent_assigned.toLowerCase();
5301
+ // Primary agents
5302
+ if (agentName.includes('claude')) agentClass = 'agent-claude';
5303
+ else if (agentName.includes('codex')) agentClass = 'agent-codex';
5304
+ else if (agentName.includes('orchestrator')) agentClass = 'agent-orchestrator';
5305
+ else if (agentName.includes('gemini-2') || agentName.includes('gemini 2')) agentClass = 'agent-gemini-2';
5306
+ else if (agentName.includes('gemini')) agentClass = 'agent-gemini';
5307
+ // Secondary agents (backward compatibility)
5308
+ else if (agentName.includes('analyst')) agentClass = 'agent-analyst';
5309
+ else if (agentName.includes('developer')) agentClass = 'agent-developer';
5310
+ else if (agentName.includes('researcher')) agentClass = 'agent-researcher';
5311
+ else if (agentName.includes('debugger')) agentClass = 'agent-debugger';
5312
+ }
5313
+ // Check if feature has delegations
5314
+ const delegations = (f.properties && f.properties.delegations) || [];
5315
+ const delegationBadges = delegations.length > 0
5316
+ ? `<span class="badge delegation" title="Delegated to ${delegations.length} spawner(s)">Delegated: ${delegations.length}</span>`
5317
+ : '';
5318
+ return `
3430
5319
  <div class="card priority-${f.priority}"
3431
5320
  data-collection="${f._collection}"
3432
- data-id="${f.id}">
5321
+ data-id="${f.id}"
5322
+ data-agent="${f.agent_assigned || ''}"
5323
+ onclick="toggleCardTimeline(event)">
5324
+ <button class="card-expand-btn" onclick="toggleCardTimeline(event)" title="Toggle agent timeline">▼</button>
3433
5325
  <div class="card-title">${f.title}</div>
3434
5326
  <div class="card-meta">
3435
5327
  <span class="badge priority-${f.priority}">${f.priority}</span>
3436
5328
  ${f.type !== 'feature' ? `<span class="badge type">${f.type}</span>` : ''}
5329
+ ${f.agent_assigned ? `<span class="badge agent ${agentClass}">${f.agent_assigned}</span>` : ''}
5330
+ ${delegationBadges}
3437
5331
  <span class="card-path">${f._collection}/${f.id}</span>
3438
5332
  </div>
5333
+ <div class="card-timeline" data-feature-id="${f.id}">
5334
+ <div class="timeline-header">Agent Timeline</div>
5335
+ <div class="timeline-list" data-loading="true">
5336
+ <div class="timeline-empty">Loading timeline...</div>
5337
+ </div>
5338
+ </div>
3439
5339
  </div>
3440
- `).join('')}
5340
+ `}).join('')}
3441
5341
  </div>
3442
5342
  </div>
3443
5343
  `).join('')}
@@ -3573,64 +5473,54 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3573
5473
  const container = document.getElementById('activity-log-container');
3574
5474
 
3575
5475
  try {
3576
- // Fetch the HTML file directly
3577
- const response = await fetch(`${API}/../.htmlgraph/${collection}/${sessionId}.html`);
3578
- if (!response.ok) throw new Error('Failed to load session file');
3579
-
3580
- const html = await response.text();
3581
- const parser = new DOMParser();
3582
- const doc = parser.parseFromString(html, 'text/html');
3583
-
3584
- // Find the activity log section
3585
- const activitySection = doc.querySelector('section[data-activity-log]');
3586
- if (!activitySection) {
3587
- container.innerHTML = '<div class="loading">No activity log found</div>';
3588
- return;
3589
- }
5476
+ // Use Analytics API to get merged events (including child sessions)
5477
+ const response = await fetch(`${API}/analytics/session?id=${sessionId}&limit=100`);
5478
+ if (!response.ok) throw new Error('Failed to load session events');
3590
5479
 
3591
- // Get the activity items
3592
- const activityItems = activitySection.querySelectorAll('li[data-ts]');
3593
- const eventCount = activitySection.querySelector('h3')?.textContent || 'Activity Log';
5480
+ const data = await response.json();
5481
+ const events = data.events || [];
3594
5482
 
3595
- if (activityItems.length === 0) {
5483
+ if (events.length === 0) {
3596
5484
  container.innerHTML = '<div class="loading">No events recorded</div>';
3597
5485
  return;
3598
5486
  }
3599
5487
 
3600
- // Render activity log (show first 50 events)
3601
- const limit = 50;
3602
- const events = Array.from(activityItems).slice(0, limit);
3603
-
3604
5488
  let html_content = `
3605
5489
  <div class="activity-header">
3606
- <strong>${eventCount}</strong>
3607
- ${activityItems.length > limit ? `<span>(showing first ${limit})</span>` : ''}
5490
+ <strong>${events.length}</strong>
5491
+ <span>(most recent first)</span>
3608
5492
  </div>
3609
5493
  <ol class="activity-list" reversed>
3610
5494
  `;
3611
5495
 
3612
5496
  events.forEach(item => {
3613
- const ts = new Date(item.dataset.ts).toLocaleString();
3614
- const tool = item.dataset.tool || 'Unknown';
3615
- const success = item.dataset.success === 'true';
3616
- const feature = item.dataset.feature || '';
3617
- const drift = item.dataset.drift ? parseFloat(item.dataset.drift).toFixed(2) : '';
3618
- const content = item.textContent.trim();
5497
+ const ts = new Date(item.ts).toLocaleString();
5498
+ const tool = item.tool || 'Unknown';
5499
+ const success = item.success === 1 || item.success === true;
5500
+ const feature = item.feature_id || '';
5501
+ const drift = item.drift_score ? parseFloat(item.drift_score).toFixed(2) : '';
5502
+ const content = item.summary || '';
5503
+
5504
+ // Identify child events (from sub-sessions)
5505
+ const isChild = item.session_id !== sessionId;
5506
+ const childClass = isChild ? 'child-event' : '';
5507
+ const childBadge = isChild ? '<span class="badge" style="background: var(--bg-tertiary); color: var(--text-muted);">sub-task</span>' : '';
3619
5508
 
3620
5509
  const statusIcon = success ? '✅' : '❌';
3621
5510
  const featureBadge = feature ? `<span class="badge">${feature}</span>` : '';
3622
5511
  const driftBadge = drift ? `<span class="badge drift-${drift >= 0.7 ? 'high' : 'low'}">drift: ${drift}</span>` : '';
3623
5512
 
3624
5513
  html_content += `
3625
- <li class="activity-item">
5514
+ <li class="activity-item ${childClass}" style="${isChild ? 'padding-left: 1.5rem; border-left: 2px solid var(--border);' : ''}">
3626
5515
  <div class="activity-meta">
3627
5516
  <span class="activity-time">${ts}</span>
3628
5517
  ${statusIcon}
3629
5518
  <span class="activity-tool">${tool}</span>
5519
+ ${childBadge}
3630
5520
  ${featureBadge}
3631
5521
  ${driftBadge}
3632
5522
  </div>
3633
- <div class="activity-content">${content}</div>
5523
+ <div class="activity-content">${escapeHtml(content)}</div>
3634
5524
  </li>
3635
5525
  `;
3636
5526
  });
@@ -3639,10 +5529,280 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3639
5529
  container.innerHTML = html_content;
3640
5530
 
3641
5531
  } catch (err) {
5532
+ console.error("Activity log error:", err);
3642
5533
  container.innerHTML = `<div class="loading">Error loading activity log: ${err.message}</div>`;
3643
5534
  }
3644
5535
  }
3645
5536
 
5537
+ /**
5538
+ * Fetch complete activity feed from all sources (hooks, subagents, spikes).
5539
+ * This provides unified visibility into ALL activity including delegated work.
5540
+ * See GitHub issue #14859 for Claude Code hook limitations.
5541
+ */
5542
+ async function fetchCompleteActivityFeed(sessionId = null, limit = 100) {
5543
+ const container = document.getElementById('complete-activity-container');
5544
+ if (!container) return;
5545
+
5546
+ try {
5547
+ let url = `${API}/complete-activity-feed?limit=${limit}`;
5548
+ if (sessionId) url += `&session_id=${sessionId}`;
5549
+
5550
+ const response = await fetch(url);
5551
+ if (!response.ok) throw new Error('Failed to load complete activity feed');
5552
+
5553
+ const data = await response.json();
5554
+ const events = data.events || [];
5555
+ const sources = data.sources || {};
5556
+
5557
+ if (events.length === 0) {
5558
+ container.innerHTML = `
5559
+ <div class="loading">
5560
+ No events recorded yet.
5561
+ <div style="font-size: 0.75rem; margin-top: 0.5rem; color: var(--text-muted);">
5562
+ Events are captured via PreToolUse hooks and SubagentStop hooks.
5563
+ </div>
5564
+ </div>
5565
+ `;
5566
+ return;
5567
+ }
5568
+
5569
+ // Build parent-child map
5570
+ const parentMap = new Map();
5571
+ const topLevelEvents = [];
5572
+
5573
+ events.forEach(event => {
5574
+ if (event.parent_event_id) {
5575
+ if (!parentMap.has(event.parent_event_id)) {
5576
+ parentMap.set(event.parent_event_id, []);
5577
+ }
5578
+ parentMap.get(event.parent_event_id).push(event);
5579
+ } else {
5580
+ topLevelEvents.push(event);
5581
+ }
5582
+ });
5583
+
5584
+ // Build source summary
5585
+ const sourceSummary = `
5586
+ <div style="display: flex; gap: 1rem; margin-bottom: 1rem; flex-wrap: wrap;">
5587
+ <span class="badge source source-hook">Hook Events: ${sources.hook_events || 0}</span>
5588
+ <span class="badge source source-subagent">Subagent: ${sources.delegations || 0}</span>
5589
+ <span class="badge source source-spike">SDK Spikes: ${sources.spike_logs || 0}</span>
5590
+ </div>
5591
+ `;
5592
+
5593
+ let html_content = `
5594
+ <div class="activity-header">
5595
+ <strong>${events.length}</strong>
5596
+ <span>events from all sources (${topLevelEvents.length} top-level)</span>
5597
+ </div>
5598
+ ${sourceSummary}
5599
+ <ol class="activity-list" reversed>
5600
+ `;
5601
+
5602
+ // Model name formatter - Extract short name from full model ID
5603
+ function formatModelName(model) {
5604
+ if (!model) return null;
5605
+ const modelStr = String(model).toLowerCase();
5606
+ if (modelStr.includes('haiku')) return 'Haiku';
5607
+ if (modelStr.includes('sonnet')) return 'Sonnet 4.5';
5608
+ if (modelStr.includes('opus')) return 'Opus 4.5';
5609
+ return null; // Unknown model
5610
+ }
5611
+
5612
+ // Determine model badge CSS class
5613
+ function getModelBadgeClass(model) {
5614
+ if (!model) return 'model-default';
5615
+ const modelStr = String(model).toLowerCase();
5616
+ if (modelStr.includes('haiku')) return 'model-haiku';
5617
+ if (modelStr.includes('sonnet')) return 'model-sonnet';
5618
+ if (modelStr.includes('opus')) return 'model-opus';
5619
+ return 'model-default';
5620
+ }
5621
+
5622
+ // Render helper function
5623
+ function renderEvent(item, isChild = false, depth = 0) {
5624
+ const ts = item.timestamp ? new Date(item.timestamp).toLocaleString() : 'Unknown';
5625
+ const tool = item.tool_name || item.event_type || 'Unknown';
5626
+ const agentId = item.agent_id || 'unknown';
5627
+ const status = item.status || 'recorded';
5628
+ const source = item.source || 'hook_event';
5629
+ const content = item.input_summary || item.output_summary || '';
5630
+ const eventId = item.event_id || '';
5631
+ const hasChildren = parentMap.has(eventId);
5632
+
5633
+ // Source-specific styling
5634
+ let sourceClass = 'hook';
5635
+ let sourceBadge = '<span class="badge source source-hook">HOOK</span>';
5636
+ if (source === 'spike_log') {
5637
+ sourceClass = 'spike';
5638
+ sourceBadge = '<span class="badge source source-spike">SDK</span>';
5639
+ } else if (source === 'delegation' || item.event_type === 'delegation') {
5640
+ sourceClass = 'subagent';
5641
+ sourceBadge = '<span class="badge source source-subagent">SUBAGENT</span>';
5642
+ } else if (item.event_type === 'handoff') {
5643
+ sourceClass = 'delegation';
5644
+ sourceBadge = '<span class="badge source source-delegation">HANDOFF</span>';
5645
+ }
5646
+
5647
+ // Status icon
5648
+ const statusIcon = status === 'completed' ? '✅' :
5649
+ status === 'error' ? '❌' :
5650
+ status === 'subagent_completed' ? '🤖' : '📊';
5651
+
5652
+ // Agent badge
5653
+ const agentClass = agentId.includes('claude') ? 'agent-claude' :
5654
+ agentId.includes('gemini') ? 'agent-gemini' :
5655
+ agentId.includes('codex') ? 'agent-codex' :
5656
+ agentId.includes('subagent') ? 'agent-default' : '';
5657
+ const agentBadge = agentClass ? `<span class="badge agent ${agentClass}">${agentId}</span>` :
5658
+ `<span class="badge">${agentId}</span>`;
5659
+
5660
+ // Model badge - display which AI model executed the event
5661
+ const model = item.model || null;
5662
+ const modelName = formatModelName(model);
5663
+ const modelBadgeClass = getModelBadgeClass(model);
5664
+ const modelBadge = modelName ? `<span class="badge model ${modelBadgeClass}">${modelName}</span>` : '';
5665
+
5666
+ // Add expand icon for parents
5667
+ const expandIcon = hasChildren ? '<span class="expand-icon"></span>' : '';
5668
+ const indent = isChild ? 'padding-left: ' + (1.5 + depth * 1.5) + 'rem;' : '';
5669
+
5670
+ return `
5671
+ <li class="activity-item ${hasChildren ? 'parent-event' : ''} ${isChild ? 'child-event' : ''}"
5672
+ data-event-id="${eventId}"
5673
+ style="border-left: 3px solid ${sourceClass === 'hook' ? '#2979FF' : sourceClass === 'spike' ? '#00C853' : sourceClass === 'subagent' ? '#7C4DFF' : '#FF9100'}; ${indent}">
5674
+ <div class="activity-meta">
5675
+ ${expandIcon}
5676
+ <span class="activity-time">${ts}</span>
5677
+ ${statusIcon}
5678
+ ${sourceBadge}
5679
+ <span class="activity-tool">${tool}</span>
5680
+ ${agentBadge}
5681
+ ${modelBadge}
5682
+ </div>
5683
+ <div class="activity-content">${escapeHtml(content.substring(0, 200))}${content.length > 200 ? '...' : ''}</div>
5684
+ </li>
5685
+ `;
5686
+ }
5687
+
5688
+ // Render events recursively
5689
+ function renderEventTree(eventList, depth = 0) {
5690
+ let html = '';
5691
+ eventList.forEach(item => {
5692
+ html += renderEvent(item, depth > 0, depth);
5693
+ const eventId = item.event_id || '';
5694
+ if (parentMap.has(eventId)) {
5695
+ const children = parentMap.get(eventId);
5696
+ html += renderEventTree(children, depth + 1);
5697
+ }
5698
+ });
5699
+ return html;
5700
+ }
5701
+
5702
+ html_content += renderEventTree(topLevelEvents);
5703
+ html_content += `</ol>`;
5704
+
5705
+ // Add limitation notice
5706
+ html_content += `
5707
+ <div style="margin-top: 1rem; padding: 0.75rem; background: var(--bg-tertiary); border-radius: 4px; font-size: 0.75rem; color: var(--text-muted);">
5708
+ <strong>Note:</strong> Subagent tool activity is not captured (Claude Code limitation).
5709
+ <a href="https://github.com/anthropics/claude-code/issues/14859" target="_blank" style="color: var(--status-active);">See GitHub #14859</a>
5710
+ </div>
5711
+ `;
5712
+
5713
+ container.innerHTML = html_content;
5714
+
5715
+ // Add click handlers for expandable parent events
5716
+ const parentEvents = container.querySelectorAll('.activity-item.parent-event');
5717
+ parentEvents.forEach(parentEl => {
5718
+ parentEl.addEventListener('click', (e) => {
5719
+ // Don't toggle if clicking on a link
5720
+ if (e.target.tagName === 'A') return;
5721
+
5722
+ const eventId = parentEl.dataset.eventId;
5723
+ const isExpanded = parentEl.classList.contains('expanded');
5724
+
5725
+ if (isExpanded) {
5726
+ // Collapse - hide children
5727
+ parentEl.classList.remove('expanded');
5728
+ hideChildren(eventId);
5729
+ } else {
5730
+ // Expand - show direct children only
5731
+ parentEl.classList.add('expanded');
5732
+ showDirectChildren(eventId);
5733
+ }
5734
+ });
5735
+ });
5736
+
5737
+ // Helper to show direct children
5738
+ function showDirectChildren(parentId) {
5739
+ const allItems = container.querySelectorAll('.activity-item');
5740
+ let foundParent = false;
5741
+ let parentDepth = -1;
5742
+
5743
+ allItems.forEach(item => {
5744
+ if (item.dataset.eventId === parentId) {
5745
+ foundParent = true;
5746
+ // Calculate depth from indent
5747
+ const style = item.getAttribute('style') || '';
5748
+ const match = style.match(/padding-left:\s*([\d.]+)rem/);
5749
+ parentDepth = match ? parseFloat(match[1]) : 0;
5750
+ } else if (foundParent) {
5751
+ const style = item.getAttribute('style') || '';
5752
+ const match = style.match(/padding-left:\s*([\d.]+)rem/);
5753
+ const itemDepth = match ? parseFloat(match[1]) : 0;
5754
+
5755
+ // Stop when we reach same or lower depth (sibling or parent)
5756
+ if (itemDepth <= parentDepth) {
5757
+ foundParent = false;
5758
+ return;
5759
+ }
5760
+
5761
+ // Show only direct children (next depth level)
5762
+ if (itemDepth === parentDepth + 1.5) {
5763
+ item.style.display = 'block';
5764
+ }
5765
+ }
5766
+ });
5767
+ }
5768
+
5769
+ // Helper to hide all descendants
5770
+ function hideChildren(parentId) {
5771
+ const allItems = container.querySelectorAll('.activity-item');
5772
+ let foundParent = false;
5773
+ let parentDepth = -1;
5774
+
5775
+ allItems.forEach(item => {
5776
+ if (item.dataset.eventId === parentId) {
5777
+ foundParent = true;
5778
+ // Calculate depth from indent
5779
+ const style = item.getAttribute('style') || '';
5780
+ const match = style.match(/padding-left:\s*([\d.]+)rem/);
5781
+ parentDepth = match ? parseFloat(match[1]) : 0;
5782
+ } else if (foundParent) {
5783
+ const style = item.getAttribute('style') || '';
5784
+ const match = style.match(/padding-left:\s*([\d.]+)rem/);
5785
+ const itemDepth = match ? parseFloat(match[1]) : 0;
5786
+
5787
+ // Stop when we reach same or lower depth
5788
+ if (itemDepth <= parentDepth) {
5789
+ foundParent = false;
5790
+ return;
5791
+ }
5792
+
5793
+ // Hide all descendants and collapse them
5794
+ item.style.display = 'none';
5795
+ item.classList.remove('expanded');
5796
+ }
5797
+ });
5798
+ }
5799
+
5800
+ } catch (err) {
5801
+ console.error("Complete activity feed error:", err);
5802
+ container.innerHTML = `<div class="loading">Error loading activity feed: ${err.message}</div>`;
5803
+ }
5804
+ }
5805
+
3646
5806
  async function fetchTranscriptStats(sessionId) {
3647
5807
  const container = document.getElementById('transcript-stats-container');
3648
5808
  if (!container) return;
@@ -3731,6 +5891,24 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3731
5891
 
3732
5892
  let bodyHtml = '';
3733
5893
 
5894
+ // Helper function to get agent badge class
5895
+ function getAgentClass(agentName) {
5896
+ if (!agentName) return 'agent-default';
5897
+ const name = agentName.toLowerCase();
5898
+ // Primary agents
5899
+ if (name.includes('claude')) return 'agent-claude';
5900
+ if (name.includes('codex')) return 'agent-codex';
5901
+ if (name.includes('orchestrator')) return 'agent-orchestrator';
5902
+ if (name.includes('gemini-2') || name.includes('gemini 2')) return 'agent-gemini-2';
5903
+ if (name.includes('gemini')) return 'agent-gemini';
5904
+ // Secondary agents (backward compatibility)
5905
+ if (name.includes('analyst')) return 'agent-analyst';
5906
+ if (name.includes('developer')) return 'agent-developer';
5907
+ if (name.includes('researcher')) return 'agent-researcher';
5908
+ if (name.includes('debugger')) return 'agent-debugger';
5909
+ return 'agent-default';
5910
+ }
5911
+
3734
5912
  // Meta section
3735
5913
  bodyHtml += `
3736
5914
  <div class="panel-section">
@@ -3739,7 +5917,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3739
5917
  <span class="badge priority-${node.priority}">${node.priority}</span>
3740
5918
  <span class="badge type">${node.type}</span>
3741
5919
  <span class="badge">${node.status}</span>
3742
- ${node.agent_assigned ? `<span class="badge">Agent: ${node.agent_assigned}</span>` : ''}
5920
+ ${node.agent_assigned ? `<span class="badge agent ${getAgentClass(node.agent_assigned)}">Agent: ${node.agent_assigned}</span>` : ''}
3743
5921
  </div>
3744
5922
  </div>
3745
5923
  `;
@@ -3768,6 +5946,43 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
3768
5946
  `;
3769
5947
  }
3770
5948
 
5949
+ // Delegation information section (if delegation data exists in properties)
5950
+ if (node.properties && (node.properties.delegated_tasks || node.properties.delegations)) {
5951
+ const delegations = node.properties.delegations || node.properties.delegated_tasks || [];
5952
+ if (delegations && delegations.length > 0) {
5953
+ bodyHtml += `
5954
+ <div class="panel-section">
5955
+ <h3>Delegations (${delegations.length})</h3>
5956
+ <div class="delegations-list">
5957
+ ${delegations.map((d, idx) => {
5958
+ const spawner = d.spawner || d.executor || 'unknown';
5959
+ const executorType = d.executor_type || 'direct';
5960
+ let executorBadge = 'delegation-direct';
5961
+ if (executorType === 'external_cli') executorBadge = 'delegation-external';
5962
+ else if (executorType === 'fallback') executorBadge = 'delegation-fallback';
5963
+
5964
+ const tokens = d.tokens_used ? ` (${d.tokens_used} tokens)` : '';
5965
+ const cost = d.cost ? ` - $${d.cost.toFixed(2)}` : '';
5966
+
5967
+ return `
5968
+ <div class="delegation-item">
5969
+ <div class="delegation-meta">
5970
+ <span class="badge delegation ${executorBadge}">${spawner}</span>
5971
+ <span class="badge delegation">${executorType}</span>
5972
+ ${tokens ? `<span class="mono">${tokens}</span>` : ''}
5973
+ ${cost ? `<span class="mono">${cost}</span>` : ''}
5974
+ </div>
5975
+ ${d.task_id ? `<div class="delegation-task">Task: ${d.task_id}</div>` : ''}
5976
+ ${d.timestamp ? `<div class="delegation-time">${new Date(d.timestamp).toLocaleString()}</div>` : ''}
5977
+ </div>
5978
+ `;
5979
+ }).join('')}
5980
+ </div>
5981
+ </div>
5982
+ `;
5983
+ }
5984
+ }
5985
+
3771
5986
  // Edges section (excluding implemented-in which gets special handling)
3772
5987
  const edgeTypes = Object.keys(node.edges || {}).filter(t => t !== 'implemented-in');
3773
5988
  if (edgeTypes.length > 0) {
@@ -4089,213 +6304,796 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4089
6304
  updateKanbanGrid();
4090
6305
  }
4091
6306
 
4092
- // =====================================================================
4093
- // Graph Visualization
4094
- // =====================================================================
6307
+ // =====================================================================
6308
+ // Graph Visualization
6309
+ // =====================================================================
6310
+
6311
+ let visNetwork = null; // Vis.js network instance
6312
+
6313
+ function getNodeColor(node) {
6314
+ const colors = {
6315
+ 'done': getComputedStyle(document.documentElement).getPropertyValue('--status-done').trim(),
6316
+ 'in-progress': getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(),
6317
+ 'blocked': getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(),
6318
+ 'todo': getComputedStyle(document.documentElement).getPropertyValue('--status-todo').trim()
6319
+ };
6320
+ return colors[node.status] || colors['todo'];
6321
+ }
6322
+
6323
+ function getNodeRadius(node) {
6324
+ const statusSizes = {
6325
+ 'done': 20,
6326
+ 'in-progress': 35,
6327
+ 'blocked': 30,
6328
+ 'todo': 28
6329
+ };
6330
+ return statusSizes[node.status] || 25;
6331
+ }
6332
+
6333
+ function wrapText(text, maxCharsPerLine = 10) {
6334
+ const words = text.split(/\s+/);
6335
+ const lines = [];
6336
+ let currentLine = '';
6337
+
6338
+ for (const word of words) {
6339
+ const testLine = currentLine ? currentLine + ' ' + word : word;
6340
+ if (testLine.length <= maxCharsPerLine) {
6341
+ currentLine = testLine;
6342
+ } else {
6343
+ if (currentLine) lines.push(currentLine);
6344
+ currentLine = word.length > maxCharsPerLine
6345
+ ? word.substring(0, maxCharsPerLine - 1) + '…'
6346
+ : word;
6347
+ }
6348
+ }
6349
+ if (currentLine) lines.push(currentLine);
6350
+
6351
+ if (lines.length > 3) {
6352
+ lines.length = 3;
6353
+ lines[2] = lines[2].substring(0, lines[2].length - 1) + '…';
6354
+ }
6355
+
6356
+ return lines.join('\n');
6357
+ }
6358
+
6359
+ function buildGraphData(nodes) {
6360
+ const graphNodes = nodes.map(n => ({
6361
+ id: n.id,
6362
+ title: n.title,
6363
+ status: n.status,
6364
+ type: n.type,
6365
+ priority: n.priority,
6366
+ edges: n.edges || {},
6367
+ _collection: n._collection
6368
+ }));
6369
+
6370
+ const nodeIds = new Set(nodes.map(n => n.id));
6371
+ const graphEdges = [];
6372
+
6373
+ nodes.forEach(node => {
6374
+ Object.entries(node.edges || {}).forEach(([edgeType, edges]) => {
6375
+ edges.forEach(edge => {
6376
+ if (nodeIds.has(edge.target_id)) {
6377
+ graphEdges.push({
6378
+ from: node.id,
6379
+ to: edge.target_id,
6380
+ type: edgeType
6381
+ });
6382
+ }
6383
+ });
6384
+ });
6385
+ });
6386
+
6387
+ return { nodes: graphNodes, edges: graphEdges };
6388
+ }
6389
+
6390
+ // Graph State Management
6391
+ let graphState = {
6392
+ allNodes: [],
6393
+ allEdges: [],
6394
+ visibleNodeIds: new Set(),
6395
+ searchQuery: '',
6396
+ filters: {
6397
+ todo: true,
6398
+ 'in-progress': true,
6399
+ blocked: true,
6400
+ done: false
6401
+ }
6402
+ };
6403
+
6404
+ function applyGraphFilters() {
6405
+ const filters = {};
6406
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6407
+ filters[cb.dataset.status] = cb.checked;
6408
+ });
6409
+
6410
+ graphState.filters = filters;
6411
+ graphState.searchQuery = (document.getElementById('graph-search')?.value || '').toLowerCase();
6412
+
6413
+ // Determine visible nodes
6414
+ graphState.visibleNodeIds = new Set();
6415
+ graphState.allNodes.forEach(node => {
6416
+ const statusMatch = filters[node.status] || false;
6417
+ const searchMatch = !graphState.searchQuery || node.title.toLowerCase().includes(graphState.searchQuery);
6418
+ if (statusMatch && searchMatch) {
6419
+ graphState.visibleNodeIds.add(node.id);
6420
+ }
6421
+ });
6422
+
6423
+ // Update Vis.js network with visible nodes and edges
6424
+ if (visNetwork) {
6425
+ const visibleNodes = graphState.allNodes.filter(n => graphState.visibleNodeIds.has(n.id));
6426
+ const visibleEdges = graphState.allEdges.filter(e =>
6427
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
6428
+ );
6429
+
6430
+ const nodesDataset = new vis.DataSet(visibleNodes.map(n => ({
6431
+ id: n.id,
6432
+ label: wrapText(n.title),
6433
+ title: n.title + '\nStatus: ' + n.status,
6434
+ color: {
6435
+ background: getNodeColor(n),
6436
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
6437
+ highlight: {
6438
+ background: getNodeColor(n),
6439
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
6440
+ }
6441
+ },
6442
+ size: getNodeRadius(n),
6443
+ font: {
6444
+ size: 12,
6445
+ face: "'JetBrains Mono', monospace",
6446
+ color: 'white',
6447
+ strokeWidth: 0
6448
+ },
6449
+ physics: true,
6450
+ borderWidth: 2,
6451
+ status: n.status,
6452
+ _collection: n._collection,
6453
+ x: undefined, // Let physics handle positioning
6454
+ y: undefined
6455
+ })));
6456
+
6457
+ const edgesDataset = new vis.DataSet(visibleEdges.map(e => ({
6458
+ from: e.from,
6459
+ to: e.to,
6460
+ arrows: 'to',
6461
+ smooth: { type: 'continuous' },
6462
+ color: e.type === 'blocked_by'
6463
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
6464
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
6465
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
6466
+ width: 1.5
6467
+ })));
6468
+
6469
+ visNetwork.setData({ nodes: nodesDataset, edges: edgesDataset });
6470
+ }
6471
+
6472
+ updateGraphStats();
6473
+ localStorage.setItem('graphFilters', JSON.stringify(graphState.filters));
6474
+ }
6475
+
6476
+ function updateGraphStats() {
6477
+ const visibleNodeCount = graphState.visibleNodeIds.size;
6478
+ const visibleEdgeCount = graphState.allEdges.filter(e =>
6479
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
6480
+ ).length;
6481
+ const nodeCountEl = document.getElementById('graph-node-count');
6482
+ const edgeCountEl = document.getElementById('graph-edge-count');
6483
+ if (nodeCountEl) nodeCountEl.textContent = `${visibleNodeCount} nodes`;
6484
+ if (edgeCountEl) edgeCountEl.textContent = `${visibleEdgeCount} edges`;
6485
+ }
6486
+
6487
+ function resetGraphView() {
6488
+ const searchEl = document.getElementById('graph-search');
6489
+ if (searchEl) searchEl.value = '';
6490
+ graphState.searchQuery = '';
6491
+ applyGraphFilters();
6492
+ if (visNetwork) visNetwork.fit();
6493
+ }
6494
+
6495
+ function showAllNodes() {
6496
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6497
+ cb.checked = true;
6498
+ });
6499
+ graphState.filters = { todo: true, 'in-progress': true, blocked: true, done: true };
6500
+ applyGraphFilters();
6501
+ if (visNetwork) visNetwork.fit();
6502
+ }
6503
+
6504
+ function renderGraph(nodes) {
6505
+ if (nodes.length === 0) {
6506
+ if (visNetwork) visNetwork.destroy();
6507
+ visNetwork = null;
6508
+ return;
6509
+ }
6510
+
6511
+ const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
6512
+
6513
+ graphState.allNodes = graphNodes;
6514
+ graphState.allEdges = graphEdges;
6515
+
6516
+ // FILTER FIRST: Apply default filters before rendering
6517
+ // Default: show todo, in-progress, blocked (exclude done items)
6518
+ graphState.visibleNodeIds = new Set();
6519
+ graphState.allNodes.forEach(node => {
6520
+ if (graphState.filters[node.status] !== false) {
6521
+ graphState.visibleNodeIds.add(node.id);
6522
+ }
6523
+ });
6524
+
6525
+ // Only render visible nodes and edges
6526
+ const visibleNodes = graphState.allNodes.filter(n => graphState.visibleNodeIds.has(n.id));
6527
+ const visibleEdges = graphState.allEdges.filter(e =>
6528
+ graphState.visibleNodeIds.has(e.from) && graphState.visibleNodeIds.has(e.to)
6529
+ );
6530
+
6531
+ // Create Vis.js nodes dataset with FILTERED nodes only
6532
+ const nodesData = new vis.DataSet(visibleNodes.map(n => ({
6533
+ id: n.id,
6534
+ label: wrapText(n.title),
6535
+ title: n.title + '\nStatus: ' + n.status,
6536
+ color: {
6537
+ background: getNodeColor(n),
6538
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
6539
+ highlight: {
6540
+ background: getNodeColor(n),
6541
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
6542
+ }
6543
+ },
6544
+ size: getNodeRadius(n),
6545
+ font: {
6546
+ size: 12,
6547
+ face: "'JetBrains Mono', monospace",
6548
+ color: 'white',
6549
+ strokeWidth: 0
6550
+ },
6551
+ physics: true,
6552
+ borderWidth: 2,
6553
+ status: n.status,
6554
+ _collection: n._collection
6555
+ })));
6556
+
6557
+ // Create Vis.js edges dataset with FILTERED edges only
6558
+ const edgesData = new vis.DataSet(visibleEdges.map(e => ({
6559
+ from: e.from,
6560
+ to: e.to,
6561
+ arrows: 'to',
6562
+ smooth: { type: 'continuous' },
6563
+ color: e.type === 'blocked_by'
6564
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
6565
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
6566
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
6567
+ width: 1.5
6568
+ })));
6569
+
6570
+ // Destroy existing network if it exists
6571
+ if (visNetwork) {
6572
+ visNetwork.destroy();
6573
+ }
6574
+
6575
+ // Create new Vis.js network
6576
+ const container = document.getElementById('graph-network');
6577
+ const data = {
6578
+ nodes: nodesData,
6579
+ edges: edgesData
6580
+ };
6581
+
6582
+ // Optimize physics based on node count
6583
+ const nodeCount = visibleNodes.length;
6584
+ const stabilizationIterations = nodeCount > 300 ? 100 : (nodeCount > 150 ? 150 : 200);
6585
+
6586
+ const options = {
6587
+ physics: {
6588
+ enabled: true,
6589
+ stabilization: {
6590
+ iterations: stabilizationIterations,
6591
+ fit: true
6592
+ },
6593
+ barnesHut: {
6594
+ gravitationalConstant: -30000,
6595
+ centralGravity: 0.3,
6596
+ springLength: 200,
6597
+ springConstant: 0.04
6598
+ },
6599
+ maxVelocity: 50
6600
+ },
6601
+ interaction: {
6602
+ navigationButtons: true,
6603
+ keyboard: true,
6604
+ zoomView: true,
6605
+ dragView: true
6606
+ },
6607
+ nodes: {
6608
+ shape: 'circle',
6609
+ scaling: {
6610
+ min: 10,
6611
+ max: 50
6612
+ }
6613
+ }
6614
+ };
6615
+
6616
+ visNetwork = new vis.Network(container, data, options);
6617
+
6618
+ // Handle node clicks
6619
+ visNetwork.on('click', (params) => {
6620
+ if (params.nodes.length > 0) {
6621
+ const nodeId = params.nodes[0];
6622
+ const node = graphState.allNodes.find(n => n.id === nodeId);
6623
+ if (node) {
6624
+ openPanel(node._collection, node.id);
6625
+ }
6626
+ }
6627
+ });
6628
+
6629
+ // Apply filters after network is initialized
6630
+ applyGraphFilters();
6631
+ }
6632
+
6633
+ // =====================================================================
6634
+ // Agent Skills Analysis
6635
+ // =====================================================================
6636
+
6637
+ function analyzeAgentSkills(sessions) {
6638
+ const skillProfiles = {};
6639
+ const agents = [...new Set(sessions.map(s => s.properties?.agent).filter(Boolean))];
6640
+ agents.forEach(agent => {
6641
+ skillProfiles[agent] = {Implementation: 0, Analysis: 0, Testing: 0, Documentation: 0, Coordination: 0};
6642
+ });
6643
+ sessions.forEach(session => {
6644
+ const agent = session.properties?.agent;
6645
+ if (!agent) return;
6646
+ const desc = (session.name || session.id || '').toLowerCase();
6647
+ const cnt = session.properties?.event_count || 0;
6648
+ if (desc.includes('test') || desc.includes('validate')) skillProfiles[agent].Testing += Math.min(cnt / 10, 2);
6649
+ if (desc.includes('implement') || desc.includes('code') || desc.includes('build')) skillProfiles[agent].Implementation += Math.min(cnt / 10, 2);
6650
+ if (desc.includes('analyze') || desc.includes('research')) skillProfiles[agent].Analysis += Math.min(cnt / 10, 2);
6651
+ if (desc.includes('document') || desc.includes('explain')) skillProfiles[agent].Documentation += Math.min(cnt / 10, 2);
6652
+ if (desc.includes('coordinate') || desc.includes('delegate')) skillProfiles[agent].Coordination += Math.min(cnt / 10, 2);
6653
+ if (agent.includes('Claude')) {
6654
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
6655
+ skillProfiles[agent].Documentation = Math.max(skillProfiles[agent].Documentation, 4);
6656
+ }
6657
+ if (agent.includes('Codex')) {
6658
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 5);
6659
+ skillProfiles[agent].Testing = Math.max(skillProfiles[agent].Testing, 4);
6660
+ }
6661
+ if (agent.includes('Orchestrator')) {
6662
+ skillProfiles[agent].Coordination = Math.max(skillProfiles[agent].Coordination, 5);
6663
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4);
6664
+ }
6665
+ if (agent.includes('Gemini')) {
6666
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
6667
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 3);
6668
+ }
6669
+ });
6670
+ agents.forEach(agent => {
6671
+ Object.keys(skillProfiles[agent]).forEach(skill => {
6672
+ skillProfiles[agent][skill] = Math.min(5, Math.max(1, skillProfiles[agent][skill]));
6673
+ });
6674
+ });
6675
+ return { agents, skillProfiles };
6676
+ }
6677
+
6678
+ function getProficiencyColor(level) {
6679
+ return `proficiency-${Math.round(level)}`;
6680
+ }
4095
6681
 
4096
- let simulation = null;
6682
+ function getProficiencyLabel(level) {
6683
+ const labels = ['', 'Novice', 'Beginner', 'Intermediate', 'Advanced', 'Expert'];
6684
+ return labels[Math.round(level)] || 'Expert';
6685
+ }
4097
6686
 
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'];
6687
+ function renderSkillsMatrix(agents, skillProfiles) {
6688
+ const skills = ['Implementation', 'Analysis', 'Testing', 'Documentation', 'Coordination'];
6689
+ let html = '<div class="skills-matrix">';
6690
+ html += '<div class="skills-matrix-cell skills-matrix-header-row">AGENT</div>';
6691
+ skills.forEach(skill => html += `<div class="skills-matrix-cell skills-matrix-header-row">${skill}</div>`);
6692
+ agents.forEach(agent => {
6693
+ html += `<div class="skills-matrix-cell skills-matrix-agent-name">${agent}</div>`;
6694
+ skills.forEach(skill => {
6695
+ const level = skillProfiles[agent][skill];
6696
+ const rnd = Math.round(level);
6697
+ html += `<div class="skills-matrix-cell"><div class="proficiency-dot ${getProficiencyColor(level)}" title="${getProficiencyLabel(level)} (${rnd}/5)">${rnd}</div></div>`;
6698
+ });
6699
+ });
6700
+ html += '</div><div class="skill-category-legend"><div style="font-weight: 600; width: 100%; margin-bottom: 0.5rem;">Proficiency Scale:</div>';
6701
+ for (let i = 1; i <= 5; i++) {
6702
+ html += `<div class="skill-category-item"><span class="proficiency-dot proficiency-${i}">${i}</span> ${getProficiencyLabel(i)}</div>`;
6703
+ }
6704
+ html += '</div>';
6705
+ return html;
4106
6706
  }
4107
6707
 
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);
6708
+ async function loadAndRenderAgents() {
6709
+ const el = document.getElementById('skills-matrix-content');
6710
+ try {
6711
+ let sessions = allSessions;
6712
+ if (!sessions.length) {
6713
+ const r = await fetch(`${API}/sessions`);
6714
+ if (!r.ok) throw new Error('Failed to load');
6715
+ sessions = (await r.json()).nodes || [];
6716
+ }
6717
+ if (!sessions.length) {
6718
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents found</div>';
6719
+ return;
6720
+ }
6721
+ const { agents, skillProfiles } = analyzeAgentSkills(sessions);
6722
+ if (!agents.length) {
6723
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents</div>';
6724
+ return;
6725
+ }
6726
+ el.innerHTML = renderSkillsMatrix(agents, skillProfiles);
6727
+ } catch (e) {
6728
+ el.innerHTML = `<div style="padding: 2rem; color: red;">Error: ${e.message}</div>`;
6729
+ }
4112
6730
  }
4113
6731
 
4114
- function wrapText(text, maxCharsPerLine = 10) {
4115
- const words = text.split(/\s+/);
4116
- const lines = [];
4117
- let currentLine = '';
6732
+ async function loadOrchestrationView() {
6733
+ const el = document.getElementById('orchestration-content');
6734
+ try {
6735
+ const r = await fetch(`${API}/orchestration`);
6736
+ if (!r.ok) throw new Error('Failed to load orchestration data');
6737
+ const data = await r.json();
4118
6738
 
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;
6739
+ if (data.delegation_count === 0) {
6740
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No delegations found</div>';
6741
+ return;
4128
6742
  }
4129
- }
4130
- if (currentLine) lines.push(currentLine);
4131
6743
 
4132
- if (lines.length > 3) {
4133
- lines.length = 3;
4134
- lines[2] = lines[2].substring(0, lines[2].length - 1) + '…';
4135
- }
6744
+ // Render delegation summary
6745
+ let html = '<div style="display: flex; gap: 1rem; margin-bottom: 1.5rem;">';
6746
+ html += `<div class="metric-card"><div class="metric-value">${data.delegation_count}</div><div class="metric-label">Total Delegations</div></div>`;
6747
+ html += `<div class="metric-card"><div class="metric-value">${data.unique_agents}</div><div class="metric-label">Agents Involved</div></div>`;
6748
+ html += '</div>';
6749
+
6750
+ // Render delegation chains
6751
+ html += '<div class="delegations-list">';
6752
+ for (const [fromAgent, delegations] of Object.entries(data.delegation_chains)) {
6753
+ html += `<div style="margin-bottom: 1.5rem;">`;
6754
+ html += `<h4 style="margin-bottom: 0.75rem; color: var(--text-primary);">${fromAgent}</h4>`;
6755
+ delegations.forEach(d => {
6756
+ const statusColor = d.status === 'completed' ? 'var(--status-done)' :
6757
+ d.status === 'failed' ? 'var(--status-blocked)' :
6758
+ 'var(--status-active)';
6759
+ html += `<div class="delegation-item" style="margin-left: 1.5rem; margin-bottom: 0.5rem; padding: 0.75rem; background: var(--bg-tertiary); border-radius: 6px;">`;
6760
+ html += `<div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.25rem;">`;
6761
+ html += `<span style="color: var(--text-secondary);">→</span>`;
6762
+ html += `<strong style="color: var(--text-primary);">${d.to_agent}</strong>`;
6763
+ html += `<span class="badge" style="background: ${statusColor}; color: white; font-size: 0.65rem;">${d.status}</span>`;
6764
+ html += `</div>`;
6765
+ html += `<div style="color: var(--text-secondary); font-size: 0.875rem; margin-left: 1.5rem;">${d.task}</div>`;
6766
+ if (d.timestamp) {
6767
+ html += `<div style="color: var(--text-muted); font-size: 0.75rem; margin-left: 1.5rem; margin-top: 0.25rem;">${d.timestamp.replace('T', ' ').substring(0, 19)}</div>`;
6768
+ }
6769
+ html += `</div>`;
6770
+ });
6771
+ html += `</div>`;
6772
+ }
6773
+ html += '</div>';
4136
6774
 
4137
- return lines;
6775
+ el.innerHTML = html;
6776
+ } catch (e) {
6777
+ el.innerHTML = `<div style="padding: 2rem; color: red;">Error: ${e.message}</div>`;
6778
+ }
4138
6779
  }
4139
6780
 
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
- }));
4152
6781
 
4153
- const nodeIds = new Set(nodes.map(n => n.id));
4154
- const graphEdges = [];
6782
+ // =====================================================================
6783
+ // View Toggle
6784
+ // =====================================================================
4155
6785
 
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
- });
6786
+ function switchView(view) {
6787
+ const kanban = document.getElementById('kanban');
6788
+ const graph = document.getElementById('graph-container');
6789
+ const analytics = document.getElementById('analytics');
6790
+ const agents = document.getElementById('agents');
6791
+ const sessions = document.getElementById('sessions');
6792
+ const buttons = document.querySelectorAll('.view-btn');
6793
+
6794
+ buttons.forEach(btn => {
6795
+ btn.classList.toggle('active', btn.dataset.view === view);
4168
6796
  });
4169
6797
 
4170
- return { nodes: graphNodes, edges: graphEdges };
6798
+ if (view === 'kanban') {
6799
+ kanban.classList.add('active');
6800
+ graph.classList.remove('active');
6801
+ analytics.classList.remove('active');
6802
+ agents.classList.remove('active');
6803
+ sessions.classList.remove('active');
6804
+ renderKanban(allNodes);
6805
+ } else if (view === 'graph') {
6806
+ kanban.classList.remove('active');
6807
+ graph.classList.add('active');
6808
+ analytics.classList.remove('active');
6809
+ agents.classList.remove('active');
6810
+ sessions.classList.remove('active');
6811
+ renderGraph(allNodes);
6812
+ } else if (view === 'analytics') {
6813
+ kanban.classList.remove('active');
6814
+ graph.classList.remove('active');
6815
+ analytics.classList.add('active');
6816
+ agents.classList.remove('active');
6817
+ sessions.classList.remove('active');
6818
+ ensureAnalyticsLoaded(false);
6819
+ } else if (view === 'agents') {
6820
+ kanban.classList.remove('active');
6821
+ graph.classList.remove('active');
6822
+ analytics.classList.remove('active');
6823
+ agents.classList.add('active');
6824
+ sessions.classList.remove('active');
6825
+ loadAndRenderAgents();
6826
+ loadOrchestrationView();
6827
+ } else if (view === 'sessions') {
6828
+ kanban.classList.remove('active');
6829
+ graph.classList.remove('active');
6830
+ analytics.classList.remove('active');
6831
+ agents.classList.remove('active');
6832
+ sessions.classList.add('active');
6833
+ loadAndRenderSessions();
6834
+ }
4171
6835
  }
4172
6836
 
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
6837
 
4178
- edgesGroup.innerHTML = '';
4179
- nodesGroup.innerHTML = '';
6838
+ // =====================================================================
6839
+ // Init
6840
+ // =====================================================================
4180
6841
 
4181
- if (nodes.length === 0) return;
6842
+ document.getElementById('panel-close').addEventListener('click', closePanel);
6843
+ document.getElementById('panel-overlay').addEventListener('click', closePanel);
4182
6844
 
4183
- const rect = svg.getBoundingClientRect();
4184
- const width = rect.width || 800;
4185
- const height = rect.height || 500;
6845
+ document.querySelectorAll('.view-btn').forEach(btn => {
6846
+ btn.addEventListener('click', () => switchView(btn.dataset.view));
6847
+ });
4186
6848
 
4187
- const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
4188
- const nodeById = new Map(graphNodes.map(n => [n.id, n]));
6849
+ document.getElementById('analytics-refresh').addEventListener('click', () => {
6850
+ ensureAnalyticsLoaded(true);
6851
+ });
4189
6852
 
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
- });
6853
+ document.getElementById('analytics-features').addEventListener('click', (e) => {
6854
+ const btn = e.target.closest && e.target.closest('button[data-feature]');
6855
+ if (!btn) return;
6856
+ loadFeatureAnalytics(btn.dataset.feature).catch(err => renderAnalyticsError(err));
6857
+ });
4195
6858
 
4196
- if (simulation) simulation.stop();
6859
+ // Session filter event listeners
6860
+ document.getElementById('filter-status').addEventListener('change', applySessionFilters);
6861
+ document.getElementById('filter-agent').addEventListener('change', applySessionFilters);
6862
+ document.getElementById('filter-search').addEventListener('input', applySessionFilters);
6863
+ document.getElementById('filter-date-from').addEventListener('change', applySessionFilters);
6864
+ document.getElementById('filter-date-to').addEventListener('change', applySessionFilters);
6865
+ document.getElementById('filter-clear').addEventListener('click', clearSessionFilters);
6866
+ document.getElementById('compare-sessions-btn').addEventListener('click', compareSessions);
4197
6867
 
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);
6868
+ // Session comparison modal
6869
+ document.getElementById('comparison-close').addEventListener('click', closeComparison);
6870
+ document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4208
6871
 
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
- });
6872
+ document.addEventListener('keydown', (e) => {
6873
+ if (e.key === 'Escape') closePanel();
6874
+ });
4217
6875
 
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;
6876
+ loadData().then(async ({ status, nodes }) => {
6877
+ await renderKanban(nodes);
6878
+ updateKanbanGrid();
6879
+ }).catch(err => {
6880
+ console.error('Error loading dashboard data:', err);
6881
+ });
4223
6882
 
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));
6883
+ function showAllNodes() {
6884
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
6885
+ cb.checked = true;
6886
+ });
6887
+ graphState.filters = { todo: true, 'in-progress': true, blocked: true, done: true };
6888
+ applyGraphFilters();
6889
+ }
4228
6890
 
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
- });
6891
+ function renderGraph(nodes) {
6892
+ if (nodes.length === 0) {
6893
+ if (visNetwork) visNetwork.destroy();
6894
+ visNetwork = null;
6895
+ return;
6896
+ }
4241
6897
 
4242
- g.appendChild(circle);
4243
- g.appendChild(text);
4244
- nodesGroup.appendChild(g);
6898
+ const { nodes: graphNodes, edges: graphEdges } = buildGraphData(nodes);
4245
6899
 
4246
- g.addEventListener('click', () => {
4247
- openPanel(node._collection, node.id);
4248
- });
6900
+ graphState.allNodes = graphNodes;
6901
+ graphState.allEdges = graphEdges;
4249
6902
 
4250
- let isDragging = false;
4251
- let dragOffset = { x: 0, y: 0 };
6903
+ // Create Vis.js nodes dataset
6904
+ const nodesData = new vis.DataSet(graphNodes.map(n => ({
6905
+ id: n.id,
6906
+ label: wrapText(n.title),
6907
+ title: n.title + '\nStatus: ' + n.status,
6908
+ color: {
6909
+ background: getNodeColor(n),
6910
+ border: getComputedStyle(document.documentElement).getPropertyValue('--border-strong').trim(),
6911
+ highlight: {
6912
+ background: getNodeColor(n),
6913
+ border: getComputedStyle(document.documentElement).getPropertyValue('--accent').trim()
6914
+ }
6915
+ },
6916
+ size: getNodeRadius(n),
6917
+ font: {
6918
+ size: 12,
6919
+ face: "'JetBrains Mono', monospace",
6920
+ color: 'white',
6921
+ strokeWidth: 0
6922
+ },
6923
+ physics: true,
6924
+ borderWidth: 2,
6925
+ status: n.status,
6926
+ _collection: n._collection
6927
+ })));
6928
+
6929
+ // Create Vis.js edges dataset
6930
+ const edgesData = new vis.DataSet(graphEdges.map(e => ({
6931
+ from: e.from,
6932
+ to: e.to,
6933
+ arrows: 'to',
6934
+ smooth: { type: 'continuous' },
6935
+ color: e.type === 'blocked_by'
6936
+ ? { color: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-blocked').trim() }
6937
+ : { color: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim(), highlight: getComputedStyle(document.documentElement).getPropertyValue('--status-active').trim() },
6938
+ dashes: e.type === 'blocked_by' ? [6, 4] : false,
6939
+ width: 1.5
6940
+ })));
6941
+
6942
+ // Destroy existing network if it exists
6943
+ if (visNetwork) {
6944
+ visNetwork.destroy();
6945
+ }
4252
6946
 
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
- });
6947
+ // Create new Vis.js network
6948
+ const container = document.getElementById('graph-network');
6949
+ const data = {
6950
+ nodes: nodesData,
6951
+ edges: edgesData
6952
+ };
4259
6953
 
4260
- document.addEventListener('mousemove', (e) => {
4261
- if (isDragging) {
4262
- node.fx = e.clientX - dragOffset.x;
4263
- node.fy = e.clientY - dragOffset.y;
6954
+ const options = {
6955
+ physics: {
6956
+ enabled: true,
6957
+ stabilization: {
6958
+ iterations: 200,
6959
+ fit: true
6960
+ },
6961
+ barnesHut: {
6962
+ gravitationalConstant: -30000,
6963
+ centralGravity: 0.3,
6964
+ springLength: 200,
6965
+ springConstant: 0.04
6966
+ },
6967
+ maxVelocity: 50
6968
+ },
6969
+ interaction: {
6970
+ navigationButtons: true,
6971
+ keyboard: true,
6972
+ zoomView: true,
6973
+ dragView: true
6974
+ },
6975
+ nodes: {
6976
+ shape: 'circle',
6977
+ scaling: {
6978
+ min: 10,
6979
+ max: 50
4264
6980
  }
4265
- });
6981
+ }
6982
+ };
4266
6983
 
4267
- document.addEventListener('mouseup', () => {
4268
- if (isDragging) {
4269
- isDragging = false;
4270
- node.fx = null;
4271
- node.fy = null;
4272
- simulation.alphaTarget(0);
4273
- }
4274
- });
4275
- });
6984
+ visNetwork = new vis.Network(container, data, options);
4276
6985
 
4277
- function updatePositions() {
4278
- const nodeElements = nodesGroup.querySelectorAll('.graph-node');
4279
- nodeElements.forEach(g => {
4280
- const node = nodeById.get(g.dataset.id);
6986
+ // Handle node clicks
6987
+ visNetwork.on('click', (params) => {
6988
+ if (params.nodes.length > 0) {
6989
+ const nodeId = params.nodes[0];
6990
+ const node = graphState.allNodes.find(n => n.id === nodeId);
4281
6991
  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})`);
6992
+ openPanel(node._collection, node.id);
4285
6993
  }
6994
+ }
6995
+ });
6996
+
6997
+ // Apply filters after network is initialized
6998
+ applyGraphFilters();
6999
+ }
7000
+
7001
+ // =====================================================================
7002
+ // Agent Skills Analysis
7003
+ // =====================================================================
7004
+
7005
+ function analyzeAgentSkills(sessions) {
7006
+ const skillProfiles = {};
7007
+ const agents = [...new Set(sessions.map(s => s.properties?.agent).filter(Boolean))];
7008
+ agents.forEach(agent => {
7009
+ skillProfiles[agent] = {Implementation: 0, Analysis: 0, Testing: 0, Documentation: 0, Coordination: 0};
7010
+ });
7011
+ sessions.forEach(session => {
7012
+ const agent = session.properties?.agent;
7013
+ if (!agent) return;
7014
+ const desc = (session.name || session.id || '').toLowerCase();
7015
+ const cnt = session.properties?.event_count || 0;
7016
+ if (desc.includes('test') || desc.includes('validate')) skillProfiles[agent].Testing += Math.min(cnt / 10, 2);
7017
+ if (desc.includes('implement') || desc.includes('code') || desc.includes('build')) skillProfiles[agent].Implementation += Math.min(cnt / 10, 2);
7018
+ if (desc.includes('analyze') || desc.includes('research')) skillProfiles[agent].Analysis += Math.min(cnt / 10, 2);
7019
+ if (desc.includes('document') || desc.includes('explain')) skillProfiles[agent].Documentation += Math.min(cnt / 10, 2);
7020
+ if (desc.includes('coordinate') || desc.includes('delegate')) skillProfiles[agent].Coordination += Math.min(cnt / 10, 2);
7021
+ if (agent.includes('Claude')) {
7022
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
7023
+ skillProfiles[agent].Documentation = Math.max(skillProfiles[agent].Documentation, 4);
7024
+ }
7025
+ if (agent.includes('Codex')) {
7026
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 5);
7027
+ skillProfiles[agent].Testing = Math.max(skillProfiles[agent].Testing, 4);
7028
+ }
7029
+ if (agent.includes('Orchestrator')) {
7030
+ skillProfiles[agent].Coordination = Math.max(skillProfiles[agent].Coordination, 5);
7031
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4);
7032
+ }
7033
+ if (agent.includes('Gemini')) {
7034
+ skillProfiles[agent].Analysis = Math.max(skillProfiles[agent].Analysis, 4.5);
7035
+ skillProfiles[agent].Implementation = Math.max(skillProfiles[agent].Implementation, 3);
7036
+ }
7037
+ });
7038
+ agents.forEach(agent => {
7039
+ Object.keys(skillProfiles[agent]).forEach(skill => {
7040
+ skillProfiles[agent][skill] = Math.min(5, Math.max(1, skillProfiles[agent][skill]));
4286
7041
  });
7042
+ });
7043
+ return { agents, skillProfiles };
7044
+ }
4287
7045
 
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
- }
7046
+ function getProficiencyColor(level) {
7047
+ return `proficiency-${Math.round(level)}`;
7048
+ }
7049
+
7050
+ function getProficiencyLabel(level) {
7051
+ const labels = ['', 'Novice', 'Beginner', 'Intermediate', 'Advanced', 'Expert'];
7052
+ return labels[Math.round(level)] || 'Expert';
7053
+ }
7054
+
7055
+ function renderSkillsMatrix(agents, skillProfiles) {
7056
+ const skills = ['Implementation', 'Analysis', 'Testing', 'Documentation', 'Coordination'];
7057
+ let html = '<div class="skills-matrix">';
7058
+ html += '<div class="skills-matrix-cell skills-matrix-header-row">AGENT</div>';
7059
+ skills.forEach(skill => html += `<div class="skills-matrix-cell skills-matrix-header-row">${skill}</div>`);
7060
+ agents.forEach(agent => {
7061
+ html += `<div class="skills-matrix-cell skills-matrix-agent-name">${agent}</div>`;
7062
+ skills.forEach(skill => {
7063
+ const level = skillProfiles[agent][skill];
7064
+ const rnd = Math.round(level);
7065
+ html += `<div class="skills-matrix-cell"><div class="proficiency-dot ${getProficiencyColor(level)}" title="${getProficiencyLabel(level)} (${rnd}/5)">${rnd}</div></div>`;
4298
7066
  });
7067
+ });
7068
+ html += '</div><div class="skill-category-legend"><div style="font-weight: 600; width: 100%; margin-bottom: 0.5rem;">Proficiency Scale:</div>';
7069
+ for (let i = 1; i <= 5; i++) {
7070
+ html += `<div class="skill-category-item"><span class="proficiency-dot proficiency-${i}">${i}</span> ${getProficiencyLabel(i)}</div>`;
7071
+ }
7072
+ html += '</div>';
7073
+ return html;
7074
+ }
7075
+
7076
+ async function loadAndRenderAgents() {
7077
+ const el = document.getElementById('skills-matrix-content');
7078
+ try {
7079
+ let sessions = allSessions;
7080
+ if (!sessions.length) {
7081
+ const r = await fetch(`${API}/sessions`);
7082
+ if (!r.ok) throw new Error('Failed to load');
7083
+ sessions = (await r.json()).nodes || [];
7084
+ }
7085
+ if (!sessions.length) {
7086
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents found</div>';
7087
+ return;
7088
+ }
7089
+ const { agents, skillProfiles } = analyzeAgentSkills(sessions);
7090
+ if (!agents.length) {
7091
+ el.innerHTML = '<div style="padding: 2rem; color: var(--text-muted);">No agents</div>';
7092
+ return;
7093
+ }
7094
+ el.innerHTML = renderSkillsMatrix(agents, skillProfiles);
7095
+ } catch (e) {
7096
+ el.innerHTML = `<div style="padding: 2rem; color: red;">Error: ${e.message}</div>`;
4299
7097
  }
4300
7098
  }
4301
7099
 
@@ -4307,6 +7105,7 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4307
7105
  const kanban = document.getElementById('kanban');
4308
7106
  const graph = document.getElementById('graph-container');
4309
7107
  const analytics = document.getElementById('analytics');
7108
+ const agents = document.getElementById('agents');
4310
7109
  const sessions = document.getElementById('sessions');
4311
7110
  const buttons = document.querySelectorAll('.view-btn');
4312
7111
 
@@ -4318,29 +7117,42 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4318
7117
  kanban.classList.add('active');
4319
7118
  graph.classList.remove('active');
4320
7119
  analytics.classList.remove('active');
7120
+ agents.classList.remove('active');
4321
7121
  sessions.classList.remove('active');
4322
7122
  renderKanban(allNodes);
4323
7123
  } else if (view === 'graph') {
4324
7124
  kanban.classList.remove('active');
4325
7125
  graph.classList.add('active');
4326
7126
  analytics.classList.remove('active');
7127
+ agents.classList.remove('active');
4327
7128
  sessions.classList.remove('active');
4328
7129
  renderGraph(allNodes);
4329
7130
  } else if (view === 'analytics') {
4330
7131
  kanban.classList.remove('active');
4331
7132
  graph.classList.remove('active');
4332
7133
  analytics.classList.add('active');
7134
+ agents.classList.remove('active');
4333
7135
  sessions.classList.remove('active');
4334
7136
  ensureAnalyticsLoaded(false);
7137
+ } else if (view === 'agents') {
7138
+ kanban.classList.remove('active');
7139
+ graph.classList.remove('active');
7140
+ analytics.classList.remove('active');
7141
+ agents.classList.add('active');
7142
+ sessions.classList.remove('active');
7143
+ loadAndRenderAgents();
7144
+ loadOrchestrationView();
4335
7145
  } else if (view === 'sessions') {
4336
7146
  kanban.classList.remove('active');
4337
7147
  graph.classList.remove('active');
4338
7148
  analytics.classList.remove('active');
7149
+ agents.classList.remove('active');
4339
7150
  sessions.classList.add('active');
4340
7151
  loadAndRenderSessions();
4341
7152
  }
4342
7153
  }
4343
7154
 
7155
+
4344
7156
  // =====================================================================
4345
7157
  // Init
4346
7158
  // =====================================================================
@@ -4375,6 +7187,11 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4375
7187
  document.getElementById('comparison-close').addEventListener('click', closeComparison);
4376
7188
  document.getElementById('comparison-overlay').addEventListener('click', closeComparison);
4377
7189
 
7190
+ // Graph filter event listeners
7191
+ document.querySelectorAll('.graph-filter-checkbox input').forEach(cb => {
7192
+ cb.addEventListener('change', applyGraphFilters);
7193
+ });
7194
+
4378
7195
  document.addEventListener('keydown', (e) => {
4379
7196
  if (e.key === 'Escape') closePanel();
4380
7197
  });
@@ -4560,6 +7377,53 @@ ${node.ts ? node.ts.slice(0, 19).replace('T', ' ') : ''}`;
4560
7377
  closeSpecPlanModal();
4561
7378
  }
4562
7379
  });
7380
+
7381
+ // Convert UTC timestamps to local timezone
7382
+ function convertTimestampsToLocal() {
7383
+ const timestampElements = document.querySelectorAll('[data-utc-time]');
7384
+ timestampElements.forEach(element => {
7385
+ const utcTime = element.getAttribute('data-utc-time');
7386
+ if (utcTime) {
7387
+ try {
7388
+ // Parse ISO 8601 UTC time - convert naive datetime to UTC format
7389
+ // Input: "2026-01-06 18:01:19" → "2026-01-06T18:01:19Z"
7390
+ const date = new Date(utcTime.replace(' ', 'T') + 'Z');
7391
+ // Convert to local timezone using Intl API for best compatibility
7392
+ const localTime = new Intl.DateTimeFormat('en-US', {
7393
+ year: 'numeric',
7394
+ month: '2-digit',
7395
+ day: '2-digit',
7396
+ hour: '2-digit',
7397
+ minute: '2-digit',
7398
+ second: '2-digit',
7399
+ hour12: false,
7400
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
7401
+ }).format(date);
7402
+ // Replace the displayed timestamp with local time
7403
+ element.textContent = localTime;
7404
+ // Add title attribute to show full ISO format on hover
7405
+ element.setAttribute('title', `UTC: ${utcTime} | Local: ${localTime}`);
7406
+ } catch (err) {
7407
+ console.warn('Failed to convert timestamp:', utcTime, err);
7408
+ }
7409
+ }
7410
+ });
7411
+ }
7412
+
7413
+ // Convert timestamps on page load
7414
+ document.addEventListener('DOMContentLoaded', convertTimestampsToLocal);
7415
+
7416
+ // Also convert timestamps when new content is dynamically loaded (e.g., via HTMX)
7417
+ if (typeof htmx !== 'undefined') {
7418
+ document.addEventListener('htmx:afterSwap', convertTimestampsToLocal);
7419
+ }
7420
+
7421
+ // Convert timestamps via WebSocket updates
7422
+ const originalWebSocketOpen = WebSocket.prototype.open;
7423
+ if (originalWebSocketOpen) {
7424
+ // Re-convert after WebSocket message arrives
7425
+ document.addEventListener('ws:update', convertTimestampsToLocal);
7426
+ }
4563
7427
  </script>
4564
7428
 
4565
7429
  <!-- Spec/Plan Modal -->