groove-dev 0.27.142 → 0.27.144

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 (187) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
@@ -1,379 +1,147 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { memo } from 'react';
3
- import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
4
3
  import { fmtNum, fmtPct, fmtDollar, timeAgo } from '../../lib/format';
5
- import { cn } from '../../lib/cn';
6
- import { HEX } from '../../lib/theme-hex';
4
+ import { HEX, hexAlpha } from '../../lib/theme-hex';
7
5
  import { roleColor } from '../../lib/status';
8
- import { Activity, Brain, Radio, AlertTriangle, CheckCircle, RotateCw, HelpCircle, BookOpen } from 'lucide-react';
6
+ import { HelpCircle } from 'lucide-react';
9
7
  import { Tooltip } from '../ui/tooltip';
10
8
 
11
- /* ── Tiny SVG sparkline for inline use ──────────────────────── */
12
- function TinySparkline({ data, color = HEX.accent, width = 60, height = 16 }) {
13
- if (!data || data.length < 2) return <div style={{ width, height }} />;
14
- const vals = Array.isArray(data[0]) ? data : data.map((d) => (typeof d === 'number' ? d : d.v));
15
- const min = Math.min(...vals);
16
- const max = Math.max(...vals);
17
- const range = max - min || 1;
18
-
19
- const points = vals.map((v, i) => {
20
- const x = (i / (vals.length - 1)) * width;
21
- const y = height - ((v - min) / range) * (height - 2) - 1;
22
- return `${x},${y}`;
23
- }).join(' ');
24
-
25
- return (
26
- <svg width={width} height={height} className="flex-shrink-0">
27
- <polyline points={points} fill="none" stroke={color} strokeWidth="1" strokeLinejoin="round" strokeOpacity="0.7" />
28
- </svg>
29
- );
30
- }
31
-
32
- /* ── Info tip (? icon with tooltip) ────────────────────────── */
33
- function InfoTip({ text, side = 'bottom' }) {
9
+ function Tip({ text }) {
34
10
  return (
35
- <Tooltip content={<span className="max-w-[220px] block leading-relaxed">{text}</span>} side={side}>
36
- <HelpCircle size={10} className="text-text-4 hover:text-text-2 cursor-help flex-shrink-0 transition-colors inline-block ml-1" />
11
+ <Tooltip content={<span className="max-w-[220px] block leading-relaxed">{text}</span>} side="bottom">
12
+ <HelpCircle size={9} className="text-text-4 hover:text-text-2 cursor-help flex-shrink-0 transition-colors ml-0.5" />
37
13
  </Tooltip>
38
14
  );
39
15
  }
40
16
 
41
- /* ── Quality score color ───────────────────────────────────── */
42
- function qualityColor(score) {
43
- if (score == null) return HEX.text3;
44
- if (score >= 70) return '#4ae168';
45
- if (score >= 40) return '#e5c07b';
46
- return '#e06c75';
47
- }
48
-
49
- /* ── Signal pill ───────────────────────────────────────────── */
50
- function SignalPill({ label, value, danger }) {
51
- if (value == null || value === 0) return null;
17
+ function Label({ children, tip }) {
52
18
  return (
53
- <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
54
- {label}: <span style={{ color: danger ? '#e06c75' : HEX.text1 }}>{value}</span>
55
- </span>
19
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider flex items-center gap-0.5">
20
+ {children}{tip && <Tip text={tip} />}
21
+ </div>
56
22
  );
57
23
  }
58
24
 
59
- /* ── Quality bar ───────────────────────────────────────────── */
60
- function QualityBar({ score }) {
61
- const pct = Math.max(0, Math.min(100, score || 0));
62
- const color = qualityColor(score);
25
+ function Stat({ label, value, tip }) {
63
26
  return (
64
- <div className="h-0.5 rounded-sm overflow-hidden flex-1" style={{ background: 'rgba(51,175,188,0.08)' }}>
65
- <div className="h-full rounded-sm transition-all duration-700" style={{ width: `${pct}%`, background: color }} />
27
+ <div>
28
+ <Label tip={tip}>{label}</Label>
29
+ <div className="text-sm font-mono font-semibold text-text-1 tabular-nums leading-snug">{value}</div>
66
30
  </div>
67
31
  );
68
32
  }
69
33
 
70
- /* ── Progress bar ──────────────────────────────────────────── */
71
- function ProgressBar({ label, value, total, color }) {
72
- const pct = total > 0 ? (value / total) * 100 : 0;
34
+ function Section({ title, children, tip }) {
73
35
  return (
74
- <div className="space-y-1">
75
- <div className="flex items-center justify-between">
76
- <span className="text-xs font-mono text-text-2">{label}</span>
77
- <div className="flex items-center gap-2">
78
- <span className="text-xs font-mono font-semibold tabular-nums" style={{ color }}>
79
- {pct >= 1 ? Math.round(pct) : pct > 0 ? pct.toFixed(1) : 0}%
80
- </span>
81
- <span className="text-2xs font-mono text-text-3 tabular-nums w-10 text-right">{fmtNum(value)}</span>
82
- </div>
83
- </div>
84
- <div className="h-0.5 rounded-sm overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
85
- <div
86
- className="h-full rounded-sm transition-all duration-500"
87
- style={{ width: `${Math.min(pct, 100)}%`, background: color }}
88
- />
36
+ <div className="px-3 py-2.5">
37
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-2 flex items-center gap-0.5">
38
+ {title}{tip && <Tip text={tip} />}
89
39
  </div>
40
+ {children}
90
41
  </div>
91
42
  );
92
43
  }
93
44
 
94
- /* ── Health Tab ─────────────────────────────────────────────── */
95
- function HealthTab({ tokens, rotation, agentBreakdown }) {
96
- const recentHistory = (rotation?.history || []).slice(-10).reverse();
97
- const liveScores = rotation?.liveScores || {};
45
+ function Divider() {
46
+ return <div className="h-px bg-border mx-3" />;
47
+ }
98
48
 
99
- const runningAgents = (agentBreakdown || []).filter((a) => a.status === 'running');
49
+ /* ── Metrics row ───────────────────────────────────────────── */
50
+ function MetricsRow({ tokens, rotation, agentBreakdown }) {
100
51
  const allAgents = agentBreakdown || [];
101
- const agentsWithQuality = allAgents.filter((a) => a.quality?.score != null);
102
- const avgQuality = agentsWithQuality.length > 0
103
- ? Math.round(agentsWithQuality.reduce((s, a) => s + a.quality.score, 0) / agentsWithQuality.length)
52
+ const withQuality = allAgents.filter((a) => a.quality?.score != null);
53
+ const avgQ = withQuality.length > 0
54
+ ? Math.round(withQuality.reduce((s, a) => s + a.quality.score, 0) / withQuality.length)
104
55
  : null;
105
56
 
106
- const completions = allAgents.filter((a) => a.status === 'completed' || a.status === 'stopped').length;
107
- const crashes = allAgents.filter((a) => a.status === 'crashed').length;
108
- const completionRate = (completions + crashes) > 0
109
- ? Math.round((completions / (completions + crashes)) * 100)
110
- : 100;
111
-
112
57
  return (
113
- <div className="p-3 space-y-4">
114
- {/* Hero stats */}
115
- <div className="grid grid-cols-4 gap-2">
116
- <div className="bg-surface-0 rounded p-2.5">
117
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
118
- Quality
119
- <InfoTip text="Average session quality score (0-100). Based on error rate, tool failures, repetitions, and file churn. Below 40 triggers auto-rotation to prevent wasted tokens." />
120
- </div>
121
- <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
122
- {avgQuality ?? '—'}
123
- </div>
124
- </div>
125
- <div className="bg-surface-0 rounded p-2.5">
126
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
127
- Rotations
128
- <InfoTip text="Context rotations: quality-based (q), context threshold (c), and natural compactions (n) from provider-managed context resets. Each rotation preserves progress via a journalist handoff brief." />
129
- </div>
130
- <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
131
- {rotation?.totalRotations || 0}
132
- </div>
133
- {(rotation?.qualityRotations > 0 || rotation?.contextRotations > 0 || rotation?.naturalCompactions > 0) && (
134
- <div className="text-2xs font-mono text-text-4 mt-0.5">
135
- {rotation.qualityRotations || 0}q / {rotation.contextRotations || 0}c / {rotation.naturalCompactions || 0}n
136
- </div>
137
- )}
138
- </div>
139
- <div className="bg-surface-0 rounded p-2.5">
140
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
141
- Cache
142
- <InfoTip text="Prompt cache hit rate. Cache reads are ~90% cheaper than regular input tokens. Managed by your AI provider — GROOVE tracks it, doesn't control it." />
143
- </div>
144
- <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
145
- {fmtPct((tokens?.cacheHitRate || 0) * 100)}
146
- </div>
147
- </div>
148
- <div className="bg-surface-0 rounded p-2.5">
149
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
150
- Success
151
- <InfoTip text="Agent completion rate. Completed agents vs. crashed agents. High success rate means agents are finishing tasks without errors." />
152
- </div>
153
- <div className="text-base font-mono font-bold text-text-1 tabular-nums leading-none">
154
- {completionRate}%
155
- </div>
156
- </div>
157
- </div>
158
-
159
- {/* Running agents with quality signals */}
160
- {runningAgents.length > 0 && (
161
- <div>
162
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2">Live Agent Quality</div>
163
- <div className="space-y-2">
164
- {runningAgents.map((agent) => {
165
- const q = agent.quality || {};
166
- const live = liveScores[agent.id];
167
- const score = live?.score ?? q.score;
168
- const rc = roleColor(agent.role);
169
- return (
170
- <div key={agent.id} className="bg-surface-0 rounded p-2.5">
171
- <div className="flex items-center gap-2 mb-1.5">
172
- <span className="text-2xs font-mono font-semibold capitalize px-1.5 py-px rounded-sm" style={{ background: rc.bg, color: rc.text }}>
173
- {agent.role}
174
- </span>
175
- <span className="text-xs font-mono text-text-1 truncate flex-1">{agent.name}</span>
176
- <span className="text-sm font-mono font-bold tabular-nums" style={{ color: qualityColor(score) }}>
177
- {score ?? '—'}
178
- </span>
179
- </div>
180
- <QualityBar score={score} />
181
- <div className="flex flex-wrap gap-1.5 mt-1.5">
182
- <SignalPill label="Errors" value={q.errorCount} danger />
183
- <SignalPill label="Reps" value={q.repetitions} danger />
184
- <SignalPill label="Churn" value={q.fileChurn} danger />
185
- <SignalPill label="Tools" value={q.toolCalls} />
186
- <SignalPill label="Files" value={q.filesWritten} />
187
- {q.toolCalls > 0 && (
188
- <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
189
- Success: <span style={{ color: q.toolSuccessRate >= 0.8 ? '#4ae168' : '#e5c07b' }}>
190
- {Math.round(q.toolSuccessRate * 100)}%
191
- </span>
192
- </span>
193
- )}
194
- {q.eventCount > 0 && (
195
- <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
196
- Events: <span className="text-text-2">{q.eventCount}</span>
197
- </span>
198
- )}
199
- </div>
200
- </div>
201
- );
202
- })}
203
- </div>
204
- </div>
58
+ <div className="px-3 py-3 flex items-start gap-5">
59
+ <Stat label="Quality" value={avgQ ?? '—'} tip="Average session quality (0-100). Below 40 triggers auto-rotation." />
60
+ <Stat label="Cache" value={fmtPct((tokens?.cacheHitRate || 0) * 100)} tip="Prompt cache hit rate. Higher = faster + cheaper." />
61
+ <Stat label="Rotations" value={rotation?.totalRotations || 0} tip="Total context rotations this session." />
62
+ {(tokens?.totalCostUsd || 0) > 0 && (
63
+ <Stat label="Cost" value={fmtDollar(tokens.totalCostUsd)} tip="Total cost reported by providers." />
205
64
  )}
65
+ </div>
66
+ );
67
+ }
206
68
 
207
- {/* Internal Overhead (GROOVE's own cost to coordinate) */}
208
- {tokens?.internalOverhead?.tokens > 0 && (
209
- <div>
210
- <div className="flex items-center justify-between mb-2">
211
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider flex items-center">
212
- GROOVE Overhead
213
- <InfoTip text="Tokens consumed by GROOVE's own coordination: the Journalist (synthesis), PM (approval gates), Planner, Task Negotiator, Gateway, and user Q&A. Previously invisible — now tracked for honest ROI." />
214
- </span>
215
- <div className="flex items-center gap-2">
216
- <span className="text-2xs font-mono text-text-2 tabular-nums">
217
- {fmtNum(tokens.internalOverhead.tokens)} tokens
218
- </span>
219
- <span className="text-2xs font-mono text-text-3 tabular-nums">
220
- {fmtDollar(tokens.internalOverhead.costUsd || 0)}
221
- </span>
222
- {tokens.totalTokens > 0 && (
223
- <span className="text-2xs font-mono font-semibold tabular-nums" style={{ color: HEX.purple }}>
224
- {Math.round((tokens.internalOverhead.tokens / tokens.totalTokens) * 100)}%
69
+ /* ── Live agents ───────────────────────────────────────────── */
70
+ function LiveAgents({ agentBreakdown, rotation }) {
71
+ const liveScores = rotation?.liveScores || {};
72
+ const running = (agentBreakdown || []).filter((a) => a.status === 'running');
73
+ if (running.length === 0) return null;
74
+
75
+ return (
76
+ <>
77
+ <Divider />
78
+ <Section title="Live agents">
79
+ <div className="space-y-0">
80
+ {running.map((agent) => {
81
+ const q = agent.quality || {};
82
+ const score = liveScores[agent.id]?.score ?? q.score;
83
+ const rc = roleColor(agent.role);
84
+ const issues = [
85
+ q.errorCount > 0 && `${q.errorCount} err`,
86
+ q.repetitions > 0 && `${q.repetitions} rep`,
87
+ q.fileChurn > 0 && `${q.fileChurn} churn`,
88
+ ].filter(Boolean);
89
+
90
+ return (
91
+ <div key={agent.id} className="flex items-center gap-2 py-1 text-xs font-mono">
92
+ <span className="text-2xs font-semibold capitalize px-1 py-px rounded-sm flex-shrink-0" style={{ background: rc.bg, color: rc.text }}>
93
+ {agent.role}
225
94
  </span>
226
- )}
227
- </div>
228
- </div>
229
- <div className="grid grid-cols-3 gap-1.5">
230
- {Object.entries(tokens.internalOverhead.components || {})
231
- .sort((a, b) => (b[1].tokens || 0) - (a[1].tokens || 0))
232
- .slice(0, 6)
233
- .map(([id, comp]) => {
234
- const label = id.replace(/^__|__$/g, '').replace(/_/g, ' ');
235
- return (
236
- <div key={id} className="bg-surface-0 rounded px-2 py-1.5">
237
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider truncate">{label}</div>
238
- <div className="text-xs font-mono text-text-1 tabular-nums font-semibold">
239
- {fmtNum(comp.tokens || 0)}
240
- </div>
241
- </div>
242
- );
243
- })}
244
- </div>
95
+ <span className="text-text-2 truncate flex-1">{agent.name}</span>
96
+ {issues.length > 0 && (
97
+ <span className="text-2xs text-text-4 flex-shrink-0">{issues.join(' · ')}</span>
98
+ )}
99
+ <span className="text-text-1 font-semibold tabular-nums flex-shrink-0 w-6 text-right">
100
+ {score ?? '—'}
101
+ </span>
102
+ </div>
103
+ );
104
+ })}
245
105
  </div>
246
- )}
106
+ </Section>
107
+ </>
108
+ );
109
+ }
247
110
 
248
- {/* Coordination savings */}
249
- {(tokens?.savings?.total || 0) > 0 && (
250
- <div>
251
- <div className="flex items-center justify-between mb-2">
252
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider flex items-center">
253
- Coordination Savings
254
- <InfoTip text="Tokens saved vs. uncoordinated agents. Rotation savings are estimated from context degradation (pre/post velocity measurement underway). Conflict prevention and cold-start skip use fixed-overhead models. Compare against GROOVE Overhead above for honest ROI." />
255
- </span>
256
- <span className="text-2xs font-mono text-text-2 tabular-nums">{fmtNum(tokens.savings.total)} tokens</span>
257
- </div>
258
- <div className="space-y-2">
259
- <div className="space-y-1">
260
- <ProgressBar label="Cold-start skip" value={tokens.savings.fromColdStartSkip || 0} total={tokens.savings.total || 1} color={HEX.info} />
261
- <div className="text-2xs font-mono text-text-4 pl-2">estimated · {(tokens?.savings?.fromColdStartSkip || 0) > 0 ? 'fixed overhead per skip' : ''}</div>
262
- </div>
263
- <div className="space-y-1">
264
- <ProgressBar label="Rotation" value={tokens.savings.fromRotation || 0} total={tokens.savings.total || 1} color={HEX.accent} />
265
- <div className="text-2xs font-mono text-text-4 pl-2">estimated · velocity measurement accumulating</div>
266
- </div>
267
- <div className="space-y-1">
268
- <ProgressBar label="Conflict prevention" value={tokens.savings.fromConflictPrevention || 0} total={tokens.savings.total || 1} color="#4ec9d4" />
269
- <div className="text-2xs font-mono text-text-4 pl-2">estimated · fixed overhead per conflict</div>
270
- </div>
271
- </div>
272
- </div>
273
- )}
111
+ /* ── Recent rotations ──────────────────────────────────────── */
112
+ function RecentRotations({ rotation }) {
113
+ const history = (rotation?.history || []).slice(-5).reverse();
114
+ if (history.length === 0) return null;
115
+
116
+ function reason(r) {
117
+ if (r.reason === 'quality_degradation') return `Q:${r.qualityScore}`;
118
+ if (r.reason === 'token_limit_exceeded') return 'tokens';
119
+ if (r.reason === 'runaway_velocity') return 'velocity';
120
+ if (r.reason === 'natural_compaction') return 'compacted';
121
+ return fmtPct((r.contextUsage || 0) * 100);
122
+ }
274
123
 
275
- {/* Rotation timeline */}
276
- {recentHistory.length > 0 ? (
277
- <div>
278
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2.5">Recent Rotations</div>
279
- <div className="space-y-0">
280
- {recentHistory.map((r, i) => {
281
- const isQuality = r.reason === 'quality_degradation';
282
- const isNatural = r.reason === 'natural_compaction';
283
- const isTokenLimit = r.reason === 'token_limit_exceeded';
284
- const isVelocity = r.reason === 'runaway_velocity';
285
- const dotColor = isTokenLimit ? '#e06c75'
286
- : isVelocity ? '#ff8c42'
287
- : isQuality ? '#e5c07b'
288
- : isNatural ? '#c678dd'
289
- : '#33afbc';
290
- return (
291
- <div key={i} className="flex items-start gap-2.5">
292
- <div className="flex flex-col items-center flex-shrink-0">
293
- <div className="h-1.5" />
294
- <div
295
- className="w-2 h-2 rounded-full flex-shrink-0"
296
- style={{
297
- background: i === 0 ? dotColor : `${dotColor}25`,
298
- border: `1px solid ${dotColor}80`,
299
- boxShadow: i === 0 ? `0 0 6px ${dotColor}60` : 'none',
300
- }}
301
- />
302
- {i < recentHistory.length - 1 && (
303
- <div className="w-px flex-1 mt-1" style={{ background: 'rgba(51,175,188,0.15)', minHeight: '12px' }} />
304
- )}
305
- </div>
306
- <div className={cn('flex-1 bg-surface-0 rounded px-2 py-1.5', i < recentHistory.length - 1 && 'mb-2')}>
307
- <div className="flex items-center gap-2">
308
- <span className="text-xs font-mono text-text-1 font-medium capitalize truncate flex-1">
309
- {r.agentName || r.role}
310
- </span>
311
- {isTokenLimit ? (
312
- <span className="text-2xs font-mono font-semibold tabular-nums flex-shrink-0" style={{ color: '#e06c75' }}
313
- title={`Auto-rotated: agent burned ${r.instanceTokens?.toLocaleString()} tokens in one session`}>
314
- T:{fmtPct(((r.instanceTokens || 0) / 1_000_000) * 100).replace('%', 'M')}
315
- </span>
316
- ) : isVelocity ? (
317
- <span className="flex items-center gap-1 flex-shrink-0">
318
- <span className="text-2xs font-mono font-semibold tabular-nums" style={{ color: '#ff8c42' }}
319
- title={`Auto-rotated: runaway velocity (${r.velocity?.toLocaleString()} tokens in recent window)`}>
320
- V:{fmtPct(((r.velocity || 0) / 1_000_000) * 100).replace('%', 'M')}
321
- </span>
322
- {r.velocityDelta != null && r.velocityDelta > 0 && (
323
- <span className="text-2xs font-mono tabular-nums" style={{ color: '#4ae168' }}
324
- title={`Post-rotation velocity dropped by ${r.velocityDelta.toLocaleString()} tokens — rotation worked`}>
325
- ↓{fmtPct((r.velocityDelta / 1_000_000) * 100).replace('%', 'M')}
326
- </span>
327
- )}
328
- </span>
329
- ) : isQuality ? (
330
- <span className="text-2xs font-mono font-semibold tabular-nums flex-shrink-0" style={{ color: '#e5c07b' }}>
331
- Q:{r.qualityScore}
332
- </span>
333
- ) : isNatural ? (
334
- <span className="text-2xs font-mono font-semibold tabular-nums flex-shrink-0" style={{ color: '#c678dd' }}>
335
- {fmtPct((r.contextUsage || 0) * 100)} → {fmtPct((r.contextAfter || 0) * 100)}
336
- </span>
337
- ) : (
338
- <span className="text-2xs font-mono font-semibold tabular-nums flex-shrink-0"
339
- style={{ color: (r.contextUsage || 0) > 0.8 ? '#e06c75' : '#33afbc' }}>
340
- {fmtPct((r.contextUsage || 0) * 100)}
341
- </span>
342
- )}
343
- <span className="text-2xs font-mono text-text-4 flex-shrink-0">{timeAgo(r.timestamp)}</span>
344
- </div>
345
- </div>
346
- </div>
347
- );
348
- })}
349
- </div>
350
- </div>
351
- ) : (
352
- <div className="bg-surface-0 rounded p-3 text-center space-y-1.5">
353
- <div className="text-xs font-mono text-text-2 font-semibold">Monitoring for degradation</div>
354
- <div className="text-2xs font-mono text-text-3 leading-relaxed">
355
- Auto-rotation triggers when session quality drops below 40 (errors, repetitions, file churn) or context exceeds the adaptive threshold.
356
- </div>
124
+ return (
125
+ <>
126
+ <Divider />
127
+ <Section title="Recent rotations">
128
+ <div className="space-y-0">
129
+ {history.map((r, i) => (
130
+ <div key={i} className="flex items-center gap-2 py-0.5 text-xs font-mono">
131
+ <span className="text-text-2 truncate flex-1">{r.agentName || r.role}</span>
132
+ <span className="text-text-3 flex-shrink-0">{reason(r)}</span>
133
+ <span className="text-text-4 flex-shrink-0 w-10 text-right">{timeAgo(r.timestamp)}</span>
134
+ </div>
135
+ ))}
357
136
  </div>
358
- )}
359
- </div>
137
+ </Section>
138
+ </>
360
139
  );
361
140
  }
362
141
 
363
- /* ── Adaptive Tab ───────────────────────────────────────────── */
364
- function AdaptiveTab({ adaptive }) {
365
- if (!adaptive?.length) {
366
- return (
367
- <div className="p-3">
368
- <div className="bg-surface-0 rounded p-4 text-center space-y-2">
369
- <div className="text-xs font-mono text-text-2 font-semibold">No adaptive profiles yet</div>
370
- <div className="text-2xs font-mono text-text-3 leading-relaxed">
371
- Adaptive thresholds learn when each agent role benefits from rotation. GROOVE tracks quality scores and adjusts rotation triggers automatically — converging to the optimal threshold per role and provider.
372
- </div>
373
- </div>
374
- </div>
375
- );
376
- }
142
+ /* ── Adaptive profiles ─────────────────────────────────────── */
143
+ function AdaptiveProfiles({ adaptive }) {
144
+ if (!adaptive?.length) return null;
377
145
 
378
146
  function parseKey(key) {
379
147
  const parts = key.split(':');
@@ -381,365 +149,179 @@ function AdaptiveTab({ adaptive }) {
381
149
  }
382
150
 
383
151
  return (
384
- <div className="p-3 space-y-3">
385
- {adaptive.map((p) => {
386
- const { provider, role } = parseKey(p.key);
387
- const displayRole = role || provider;
388
- const hasHistory = p.thresholdHistory?.length > 1;
389
- const hasScores = p.recentScores?.length > 1;
390
- const rc = roleColor(displayRole);
391
- const signals = p.lastSignals;
392
-
393
- return (
394
- <div
395
- key={p.key}
396
- className="rounded overflow-hidden"
397
- style={{
398
- background: 'rgba(51,175,188,0.04)',
399
- borderLeft: p.converged ? '2px solid #33afbc' : '2px solid rgba(229,192,123,0.35)',
400
- }}
401
- >
402
- <div className="flex items-center gap-2 px-3 pt-2.5 pb-1.5">
403
- <span className="text-xs font-mono font-semibold capitalize px-1.5 py-px rounded-sm" style={{ background: rc.bg, color: rc.text }}>
404
- {displayRole}
405
- </span>
406
- {role && (
407
- <span className="text-2xs font-mono text-text-4 bg-surface-4 px-1.5 py-px rounded-sm">{provider}</span>
408
- )}
409
- <div className="flex-1" />
410
- <span
411
- className="flex items-center gap-1 text-2xs font-mono font-bold px-2 py-px rounded-full"
412
- style={{
413
- background: p.converged ? 'rgba(74,225,104,0.12)' : 'rgba(229,192,123,0.12)',
414
- color: p.converged ? '#4ae168' : '#e5c07b',
415
- }}
416
- >
417
- {!p.converged && (
418
- <span className="w-1.5 h-1.5 rounded-full flex-shrink-0"
419
- style={{ background: '#e5c07b', animation: 'node-pulse-bar 1.5s ease-in-out infinite' }} />
420
- )}
421
- {p.converged ? 'Converged' : 'Learning'}
422
- </span>
423
- </div>
424
-
425
- <div className="flex items-end gap-5 px-3 pb-2">
426
- <div>
427
- <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-0.5">Threshold</div>
428
- <div className="text-3xl font-mono font-bold tabular-nums leading-none"
429
- style={{ color: p.converged ? '#33afbc' : '#e5c07b' }}>
152
+ <>
153
+ <Divider />
154
+ <Section title="Adaptive thresholds" tip="Per-role rotation thresholds. GROOVE learns when each role benefits from rotation and adjusts automatically.">
155
+ <div className="space-y-0">
156
+ {adaptive.map((p) => {
157
+ const { provider, role } = parseKey(p.key);
158
+ const displayRole = role || provider;
159
+ const rc = roleColor(displayRole);
160
+
161
+ return (
162
+ <div key={p.key} className="flex items-center gap-2 py-1 text-xs font-mono">
163
+ <span className="text-2xs font-semibold capitalize px-1 py-px rounded-sm flex-shrink-0" style={{ background: rc.bg, color: rc.text }}>
164
+ {displayRole}
165
+ </span>
166
+ {role && <span className="text-2xs text-text-4 flex-shrink-0">{provider}</span>}
167
+ <div className="flex-1" />
168
+ <span className="text-text-1 font-semibold tabular-nums flex-shrink-0">
430
169
  {fmtPct(p.threshold * 100)}
431
- </div>
432
- </div>
433
- <div className="pb-0.5">
434
- <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-0.5">Adj.</div>
435
- <div className="text-lg font-mono font-semibold text-text-1 tabular-nums">{p.adjustments}</div>
436
- </div>
437
- </div>
438
-
439
- {(hasHistory || hasScores) && (
440
- <div className="px-3 pb-2 space-y-2 overflow-hidden">
441
- {hasHistory && (
442
- <div>
443
- <div className="text-2xs font-mono text-text-4 mb-0.5">Threshold history</div>
444
- <TinySparkline data={p.thresholdHistory.map((h) => h.v)} color={p.converged ? HEX.accent : HEX.warning} width={240} height={32} />
445
- </div>
446
- )}
447
- {hasScores && (
448
- <div>
449
- <div className="text-2xs font-mono text-text-4 mb-0.5">Quality score</div>
450
- <TinySparkline data={p.recentScores} color={HEX.warning} width={240} height={24} />
451
- </div>
452
- )}
453
- </div>
454
- )}
455
-
456
- {signals && (
457
- <div className="flex flex-wrap gap-1.5 px-3 pb-2.5">
458
- <SignalPill label="Errors" value={signals.errorCount} danger />
459
- <SignalPill label="Reps" value={signals.repetitions} danger />
460
- <SignalPill label="Churn" value={signals.fileChurn} danger />
461
- {signals.toolSuccessRate != null && (
462
- <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
463
- Tools: <span className="text-text-1">{Math.round(signals.toolSuccessRate * 100)}%</span>
170
+ </span>
171
+ {p.converged ? (
172
+ <span className="text-2xs font-semibold flex-shrink-0" style={{ color: HEX.accent }}>Converged</span>
173
+ ) : (
174
+ <span className="text-2xs text-text-3 flex-shrink-0 flex items-center gap-1">
175
+ <span className="w-1 h-1 rounded-full [animation:node-pulse-bar_1.5s_ease-in-out_infinite]" style={{ background: HEX.text3 }} />
176
+ Learning
464
177
  </span>
465
178
  )}
466
179
  </div>
467
- )}
468
- </div>
469
- );
470
- })}
471
- </div>
180
+ );
181
+ })}
182
+ </div>
183
+ </Section>
184
+ </>
472
185
  );
473
186
  }
474
187
 
475
- /* ── Journalist Tab ─────────────────────────────────────────── */
476
- function JournalistTab({ journalist }) {
477
- if (!journalist) {
478
- return (
479
- <div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
480
- Journalist inactive
481
- </div>
482
- );
483
- }
188
+ /* ── Journalist ────────────────────────────────────────────── */
189
+ function JournalistSection({ journalist }) {
190
+ if (!journalist) return null;
191
+ const hasCycles = (journalist.cycleCount || 0) > 0;
192
+ if (!hasCycles && !journalist.synthesizing) return null;
484
193
 
485
194
  return (
486
- <div className="p-3 space-y-3">
487
- <div className="flex items-center gap-3">
488
- <div>
489
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Cycles</div>
490
- <div className="text-lg font-mono font-semibold text-text-0 tabular-nums leading-none">{journalist.cycleCount || 0}</div>
195
+ <>
196
+ <Divider />
197
+ <Section title="Journalist">
198
+ <div className="flex items-center gap-3 text-xs font-mono">
199
+ <span className="text-text-2">{journalist.cycleCount || 0} cycles</span>
200
+ {journalist.lastCycleAt && (
201
+ <span className="text-text-4">{timeAgo(journalist.lastCycleAt)}</span>
202
+ )}
203
+ {journalist.synthesizing && (
204
+ <span className="font-semibold text-accent animate-pulse">Synthesizing</span>
205
+ )}
491
206
  </div>
492
- {journalist.lastCycleAt && (
493
- <div>
494
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Last</div>
495
- <div className="text-xs font-mono text-text-2">{timeAgo(journalist.lastCycleAt)}</div>
207
+ {journalist.lastSummary && (
208
+ <div className="text-xs text-text-3 leading-relaxed mt-1.5 line-clamp-3">
209
+ {journalist.lastSummary}
496
210
  </div>
497
211
  )}
498
- {journalist.synthesizing && (
499
- <span className="text-2xs font-mono font-bold text-accent uppercase tracking-wider animate-pulse">Synthesizing</span>
500
- )}
501
- </div>
502
-
503
- {journalist.lastSummary && (
504
- <div>
505
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Summary</div>
506
- <div className="text-xs font-sans text-text-2 leading-relaxed">{journalist.lastSummary}</div>
507
- </div>
508
- )}
509
-
510
- {journalist.projectMap && (
511
- <div>
512
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Project Map</div>
513
- <div className="text-xs font-mono text-text-2 leading-relaxed whitespace-pre-wrap">{journalist.projectMap}</div>
514
- </div>
515
- )}
516
-
517
- {journalist.decisions && (
518
- <div>
519
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Decisions</div>
520
- <div className="text-xs font-mono text-text-2 leading-relaxed whitespace-pre-wrap">{journalist.decisions}</div>
521
- </div>
522
- )}
523
-
524
- {journalist.recentHistory?.length > 0 && (
525
- <div>
526
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1.5">History</div>
527
- <div className="space-y-1">
528
- {journalist.recentHistory.slice().reverse().map((h, i) => (
529
- <div key={i} className="flex items-center gap-2 text-xs font-mono px-2 py-1 bg-surface-0 rounded">
530
- <span className="text-text-3">#{h.cycle}</span>
531
- <span className="text-text-2 flex-1 truncate">{h.agentCount} agents</span>
532
- <span className="text-text-4">{timeAgo(h.timestamp)}</span>
533
- </div>
534
- ))}
535
- </div>
536
- </div>
537
- )}
538
- </div>
212
+ </Section>
213
+ </>
539
214
  );
540
215
  }
541
216
 
542
- /* ── Memory Tab (Layer 7) ───────────────────────────────────── */
543
- function MemoryTab({ memory }) {
217
+ /* ── Memory ────────────────────────────────────────────────── */
218
+ function MemorySection({ memory }) {
544
219
  const constraints = memory?.constraints || [];
545
220
  const discoveries = memory?.discoveries || [];
546
221
  const roles = memory?.roles || [];
547
- const perAgent = memory?.specializations?.perAgent || {};
548
222
  const perRole = memory?.specializations?.perProjectRole || {};
549
- const agentCount = Object.keys(perAgent).length;
550
- const roleCount = Object.keys(perRole).length;
223
+ const agentCount = Object.keys(memory?.specializations?.perAgent || {}).length;
551
224
 
552
- const totalItems = constraints.length + discoveries.length + roles.length;
553
- if (totalItems === 0 && agentCount === 0) {
554
- return (
555
- <div className="p-6 text-center text-xs font-mono text-text-3">
556
- <BookOpen size={24} className="mx-auto mb-2 text-text-4" />
557
- <div>No memory accumulated yet</div>
558
- <div className="text-2xs text-text-4 mt-1">Constraints, handoff chains, and discoveries populate as agents work</div>
559
- </div>
560
- );
561
- }
225
+ const total = constraints.length + discoveries.length + roles.length + agentCount;
226
+ if (total === 0) return null;
562
227
 
563
- return (
564
- <div className="p-3 space-y-4">
565
- {/* Hero stats */}
566
- <div className="grid grid-cols-4 gap-2">
567
- <div className="bg-surface-0 rounded p-2.5">
568
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
569
- Constraints
570
- <InfoTip text="Project rules discovered by agents or set by the user. Every new agent reads these on spawn to avoid rediscovering them." />
571
- </div>
572
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: HEX.accent }}>
573
- {constraints.length}
574
- </div>
575
- </div>
576
- <div className="bg-surface-0 rounded p-2.5">
577
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
578
- Discoveries
579
- <InfoTip text="Error→fix pairs successful agents have recorded. Injected into future agent context so they don't rediscover known solutions." />
580
- </div>
581
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: HEX.success }}>
582
- {discoveries.length}
583
- </div>
584
- </div>
585
- <div className="bg-surface-0 rounded p-2.5">
586
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
587
- Handoff Chains
588
- <InfoTip text="Cumulative rotation briefs per role. Agent #50 knows what agent #1 struggled with. Each role keeps its last 10 rotation briefs." />
589
- </div>
590
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: HEX.purple }}>
591
- {roles.length}
592
- </div>
593
- </div>
594
- <div className="bg-surface-0 rounded p-2.5">
595
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1 flex items-center">
596
- Specializations
597
- <InfoTip text="Per-agent quality profiles: session counts, average quality, file touches, preferred thresholds." />
598
- </div>
599
- <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: HEX.info }}>
600
- {agentCount}
601
- </div>
602
- {roleCount > 0 && (
603
- <div className="text-2xs font-mono text-text-4 mt-0.5">across {roleCount} roles</div>
604
- )}
605
- </div>
606
- </div>
228
+ const counts = [
229
+ constraints.length > 0 && `${constraints.length} constraints`,
230
+ discoveries.length > 0 && `${discoveries.length} discoveries`,
231
+ roles.length > 0 && `${roles.length} role chains`,
232
+ agentCount > 0 && `${agentCount} specializations`,
233
+ ].filter(Boolean);
607
234
 
608
- {/* Constraints list */}
609
- {constraints.length > 0 && (
610
- <div>
611
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2">Project Constraints</div>
612
- <div className="space-y-1">
613
- {constraints.slice(0, 10).map((c) => (
614
- <div key={c.hash} className="flex items-start gap-2 bg-surface-0 rounded px-2 py-1.5">
615
- <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3 uppercase tracking-wider flex-shrink-0 mt-0.5">
616
- {c.category}
617
- </span>
618
- <span className="text-xs font-mono text-text-2 leading-relaxed flex-1">{c.text}</span>
235
+ return (
236
+ <>
237
+ <Divider />
238
+ <Section title="Memory" tip="Persistent knowledge across agent rotations. Constraints, error→fix discoveries, and handoff chains.">
239
+ <div className="text-xs font-mono text-text-3 mb-1.5">{counts.join(' · ')}</div>
240
+
241
+ {constraints.length > 0 && (
242
+ <div className="mt-2 space-y-0">
243
+ <div className="text-2xs font-mono text-text-4 mb-1">Constraints</div>
244
+ {constraints.slice(0, 3).map((c) => (
245
+ <div key={c.hash} className="text-xs font-mono text-text-2 py-0.5 truncate">
246
+ {c.text}
619
247
  </div>
620
248
  ))}
621
- {constraints.length > 10 && (
622
- <div className="text-2xs font-mono text-text-4 px-2">+{constraints.length - 10} more</div>
249
+ {constraints.length > 3 && (
250
+ <div className="text-2xs font-mono text-text-4">+{constraints.length - 3} more</div>
623
251
  )}
624
252
  </div>
625
- </div>
626
- )}
253
+ )}
627
254
 
628
- {/* Recent discoveries */}
629
- {discoveries.length > 0 && (
630
- <div>
631
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2">Recent Discoveries</div>
632
- <div className="space-y-1.5">
633
- {discoveries.slice(0, 8).map((d, i) => {
634
- const rc = roleColor(d.role);
635
- return (
636
- <div key={i} className="bg-surface-0 rounded px-2 py-1.5 space-y-1">
637
- <div className="flex items-center gap-2">
638
- <span className="text-2xs font-mono font-semibold capitalize px-1.5 py-px rounded-sm" style={{ background: rc.bg, color: rc.text }}>
639
- {d.role}
640
- </span>
641
- <span className="text-2xs font-mono text-text-4 flex-1">{timeAgo(d.ts)}</span>
642
- </div>
643
- <div className="text-xs font-mono text-text-2 leading-relaxed">
644
- <span className="text-text-4">When:</span> <span className="text-text-1">{d.trigger}</span>
645
- </div>
646
- <div className="text-xs font-mono text-text-2 leading-relaxed">
647
- <span className="text-text-4">Fix:</span> <span style={{ color: HEX.success }}>{d.fix}</span>
648
- </div>
649
- </div>
650
- );
651
- })}
255
+ {discoveries.length > 0 && (
256
+ <div className="mt-2 space-y-0">
257
+ <div className="text-2xs font-mono text-text-4 mb-1">Discoveries</div>
258
+ {discoveries.slice(0, 3).map((d, i) => (
259
+ <div key={i} className="text-xs font-mono text-text-2 py-0.5 truncate">
260
+ <span className="text-text-4">{d.trigger}</span> {d.fix}
261
+ </div>
262
+ ))}
263
+ {discoveries.length > 3 && (
264
+ <div className="text-2xs font-mono text-text-4">+{discoveries.length - 3} more</div>
265
+ )}
652
266
  </div>
653
- </div>
654
- )}
267
+ )}
655
268
 
656
- {/* Handoff chain roles */}
657
- {roles.length > 0 && (
658
- <div>
659
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2">Active Role Chains</div>
660
- <div className="flex flex-wrap gap-1.5">
661
- {roles.map((role) => {
662
- const rc = roleColor(role);
663
- return (
664
- <span key={role} className="text-2xs font-mono font-semibold capitalize px-2 py-1 rounded" style={{ background: rc.bg, color: rc.text }}>
665
- {role}
269
+ {Object.keys(perRole).length > 0 && (
270
+ <div className="mt-2">
271
+ <div className="text-2xs font-mono text-text-4 mb-1">Role quality</div>
272
+ <div className="flex flex-wrap gap-x-3 gap-y-0.5">
273
+ {Object.entries(perRole).map(([role, data]) => (
274
+ <span key={role} className="text-xs font-mono text-text-3">
275
+ <span className="text-text-2 capitalize">{role}</span> Q:{data.avgQualityScore} <span className="text-text-4">({data.sessionCount}s)</span>
666
276
  </span>
667
- );
668
- })}
277
+ ))}
278
+ </div>
669
279
  </div>
670
- </div>
671
- )}
280
+ )}
281
+ </Section>
282
+ </>
283
+ );
284
+ }
672
285
 
673
- {/* Per-role specialization summary */}
674
- {roleCount > 0 && (
675
- <div>
676
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2">Role Quality Profiles</div>
677
- <div className="space-y-1">
678
- {Object.entries(perRole).map(([role, data]) => {
679
- const rc = roleColor(role);
680
- return (
681
- <div key={role} className="flex items-center gap-2 bg-surface-0 rounded px-2 py-1.5">
682
- <span className="text-2xs font-mono font-semibold capitalize px-1.5 py-px rounded-sm" style={{ background: rc.bg, color: rc.text }}>
683
- {role}
684
- </span>
685
- <span className="text-xs font-mono text-text-3 flex-1">{data.sessionCount} sessions</span>
686
- <span className="text-xs font-mono font-semibold tabular-nums" style={{ color: qualityColor(data.avgQualityScore) }}>
687
- Q:{data.avgQualityScore}
688
- </span>
689
- </div>
690
- );
691
- })}
692
- </div>
693
- </div>
694
- )}
695
- </div>
286
+ /* ── Overhead (collapsed) ──────────────────────────────────── */
287
+ function OverheadSection({ tokens }) {
288
+ if (!tokens?.internalOverhead?.tokens || !tokens?.savings?.total) return null;
289
+ const oh = tokens.internalOverhead;
290
+ const sv = tokens.savings;
291
+ const pct = tokens.totalTokens > 0 ? Math.round((oh.tokens / tokens.totalTokens) * 100) : 0;
292
+
293
+ return (
294
+ <>
295
+ <Divider />
296
+ <div className="px-3 py-2.5 flex items-center gap-3 text-xs font-mono">
297
+ <span className="text-text-4">Overhead</span>
298
+ <span className="text-text-3 tabular-nums">{fmtNum(oh.tokens)} ({pct}%)</span>
299
+ <div className="flex-1" />
300
+ <span className="text-text-4">Saved</span>
301
+ <span className="text-text-3 tabular-nums">{fmtNum(sv.total)}</span>
302
+ </div>
303
+ </>
696
304
  );
697
305
  }
698
306
 
699
307
  /* ── Intel Panel (main export) ──────────────────────────────── */
700
308
  const IntelPanel = memo(function IntelPanel({ tokens, rotation, adaptive, journalist, agentBreakdown, memory }) {
701
309
  return (
702
- <Tabs defaultValue="health" className="flex flex-col h-full">
703
- <TabsList className="flex-shrink-0 px-1">
704
- <TabsTrigger value="health" className="text-xs px-2.5 py-1.5 inline-flex items-center gap-1.5">
705
- <Activity size={11} />
706
- Health
707
- </TabsTrigger>
708
- <TabsTrigger value="memory" className="text-xs px-2.5 py-1.5 inline-flex items-center gap-1.5">
709
- <BookOpen size={11} />
710
- Memory
711
- </TabsTrigger>
712
- <TabsTrigger value="adaptive" className="text-xs px-2.5 py-1.5 inline-flex items-center gap-1.5">
713
- <Brain size={11} />
714
- Adaptive
715
- </TabsTrigger>
716
- <TabsTrigger value="journalist" className="text-xs px-2.5 py-1.5 inline-flex items-center gap-1.5">
717
- <Radio size={11} />
718
- Journalist
719
- </TabsTrigger>
720
- </TabsList>
721
-
722
- <TabsContent value="health" className="flex-1 min-h-0 relative">
723
- <div className="absolute inset-0 overflow-y-auto">
724
- <HealthTab tokens={tokens} rotation={rotation} agentBreakdown={agentBreakdown} />
725
- </div>
726
- </TabsContent>
727
- <TabsContent value="memory" className="flex-1 min-h-0 relative">
728
- <div className="absolute inset-0 overflow-y-auto">
729
- <MemoryTab memory={memory} />
730
- </div>
731
- </TabsContent>
732
- <TabsContent value="adaptive" className="flex-1 min-h-0 relative">
733
- <div className="absolute inset-0 overflow-y-auto">
734
- <AdaptiveTab adaptive={adaptive} />
735
- </div>
736
- </TabsContent>
737
- <TabsContent value="journalist" className="flex-1 min-h-0 relative">
738
- <div className="absolute inset-0 overflow-y-auto">
739
- <JournalistTab journalist={journalist} />
740
- </div>
741
- </TabsContent>
742
- </Tabs>
310
+ <div className="flex flex-col h-full">
311
+ <div className="flex-shrink-0 px-3 pt-2.5 pb-1.5">
312
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Intel</span>
313
+ </div>
314
+ <div className="flex-1 min-h-0 overflow-y-auto">
315
+ <MetricsRow tokens={tokens} rotation={rotation} agentBreakdown={agentBreakdown} />
316
+ <LiveAgents agentBreakdown={agentBreakdown} rotation={rotation} />
317
+ <RecentRotations rotation={rotation} />
318
+ <AdaptiveProfiles adaptive={adaptive} />
319
+ <JournalistSection journalist={journalist} />
320
+ <MemorySection memory={memory} />
321
+ <OverheadSection tokens={tokens} />
322
+ <div className="h-3" />
323
+ </div>
324
+ </div>
743
325
  );
744
326
  });
745
327