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