groove-dev 0.22.31 → 0.24.0

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 (52) hide show
  1. package/node_modules/@groove-dev/cli/src/setup.js +7 -9
  2. package/node_modules/@groove-dev/daemon/src/api.js +87 -4
  3. package/node_modules/@groove-dev/daemon/src/process.js +1 -0
  4. package/node_modules/@groove-dev/daemon/src/teams.js +77 -5
  5. package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
  6. package/node_modules/@groove-dev/daemon/test/teams.test.js +5 -5
  7. package/node_modules/@groove-dev/gui/dist/assets/index-CqdQP7yG.js +587 -0
  8. package/node_modules/@groove-dev/gui/dist/assets/index-DvcNOnKP.css +1 -0
  9. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  10. package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +139 -0
  11. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +4 -1
  12. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +16 -14
  13. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +105 -0
  14. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -38
  15. package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +28 -9
  16. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
  17. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
  18. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +121 -0
  19. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +152 -34
  20. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
  21. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +4 -2
  23. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -52
  24. package/package.json +1 -1
  25. package/packages/cli/src/setup.js +7 -9
  26. package/packages/daemon/src/api.js +87 -4
  27. package/packages/daemon/src/process.js +1 -0
  28. package/packages/daemon/src/teams.js +77 -5
  29. package/packages/daemon/src/validate.js +1 -0
  30. package/packages/gui/dist/assets/index-CqdQP7yG.js +587 -0
  31. package/packages/gui/dist/assets/index-DvcNOnKP.css +1 -0
  32. package/packages/gui/dist/index.html +2 -2
  33. package/packages/gui/src/components/agents/agent-mdfiles.jsx +139 -0
  34. package/packages/gui/src/components/agents/agent-panel.jsx +4 -1
  35. package/packages/gui/src/components/dashboard/activity-feed.jsx +16 -14
  36. package/packages/gui/src/components/dashboard/cache-ring.jsx +105 -0
  37. package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -38
  38. package/packages/gui/src/components/dashboard/header-bar.jsx +28 -9
  39. package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
  40. package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
  41. package/packages/gui/src/components/dashboard/routing-chart.jsx +121 -0
  42. package/packages/gui/src/components/dashboard/token-chart.jsx +152 -34
  43. package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
  44. package/packages/gui/src/lib/theme-hex.js +7 -0
  45. package/packages/gui/src/stores/groove.js +4 -2
  46. package/packages/gui/src/views/dashboard.jsx +97 -52
  47. package/node_modules/@groove-dev/gui/dist/assets/index-CL4GvVoL.css +0 -1
  48. package/node_modules/@groove-dev/gui/dist/assets/index-D_tSBDCx.js +0 -577
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
  50. package/packages/gui/dist/assets/index-CL4GvVoL.css +0 -1
  51. package/packages/gui/dist/assets/index-D_tSBDCx.js +0 -577
  52. package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
@@ -1,64 +1,132 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Avatar } from '../ui/avatar';
3
- import { Badge } from '../ui/badge';
4
- import { StatusDot } from '../ui/status-dot';
5
- import { fmtNum, fmtPct } from '../../lib/format';
2
+ import { memo } from 'react';
3
+ import { fmtNum, fmtDollar } from '../../lib/format';
6
4
  import { cn } from '../../lib/cn';
5
+ import { statusColor } from '../../lib/status';
7
6
  import { ScrollArea } from '../ui/scroll-area';
8
7
 
9
- const STATUS_VARIANT = {
10
- running: 'success', starting: 'warning', stopped: 'default',
11
- crashed: 'danger', completed: 'accent', killed: 'default',
12
- };
8
+ const COST_SOURCE_LABEL = { actual: 'ACT', estimated: 'EST', local: 'LOC' };
13
9
 
14
- export function FleetPanel({ agents }) {
15
- if (!agents?.length) {
10
+ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
11
+ const isAlive = agent.status === 'running' || agent.status === 'starting';
12
+ const contextPct = Math.round((agent.contextUsage || 0) * 100);
13
+ const sColor = isRotating ? '#c678dd' : statusColor(agent.status);
14
+ const quality = agent.quality;
15
+ const successRate = quality?.toolSuccessRate != null ? Math.round(quality.toolSuccessRate * 100) : null;
16
+
17
+ return (
18
+ <div className="flex items-center gap-2 px-3 py-1.5 hover:bg-[#24282f] transition-colors">
19
+ {/* Status square */}
20
+ <span className="relative flex-shrink-0 w-[6px] h-[6px]">
21
+ <span className="absolute inset-0 rounded-sm" style={{ background: sColor }} />
22
+ {isAlive && (
23
+ <span
24
+ className="absolute inset-[-2px] rounded-sm"
25
+ style={{ background: sColor, opacity: 0.15, animation: 'node-pulse-bar 2s ease-in-out infinite' }}
26
+ />
27
+ )}
28
+ </span>
29
+
30
+ {/* Name + role/model */}
31
+ <div className="flex-1 min-w-0">
32
+ <div className="text-[11px] font-semibold text-[#e6e6e6] font-sans truncate leading-none">{agent.name}</div>
33
+ <div className="flex items-center gap-1 mt-0.5">
34
+ <span className="text-[8px] font-mono text-[#505862] uppercase tracking-wider">{agent.role}</span>
35
+ <span className="text-[8px] text-[#2a2e36]">/</span>
36
+ <span className="text-[8px] font-mono text-[#3a3f4b]">{shortModel(agent.model)}</span>
37
+ </div>
38
+ </div>
39
+
40
+ {/* Tokens + cost */}
41
+ <div className="text-right flex-shrink-0">
42
+ <div className="text-[11px] font-mono text-[#bcc2cd] tabular-nums leading-none">{fmtNum(agent.tokens || 0)}</div>
43
+ {(agent.costUsd || 0) > 0 && (
44
+ <div className="text-[8px] font-mono text-[#3a3f4b] mt-0.5">{fmtDollar(agent.costUsd)}</div>
45
+ )}
46
+ </div>
47
+
48
+ {/* Quality / tool success */}
49
+ {successRate != null && (
50
+ <span
51
+ className="text-[7px] font-mono font-bold uppercase px-1 py-px rounded-sm flex-shrink-0"
52
+ style={{
53
+ color: successRate >= 90 ? '#4ae168' : successRate >= 70 ? '#e5c07b' : '#e06c75',
54
+ background: successRate >= 90 ? 'rgba(74,225,104,0.1)' : successRate >= 70 ? 'rgba(229,192,123,0.1)' : 'rgba(224,108,117,0.1)',
55
+ }}
56
+ >
57
+ {successRate}%
58
+ </span>
59
+ )}
60
+
61
+ {/* Cost source badge */}
62
+ {agent.costSource && agent.costSource !== 'actual' && (
63
+ <span className="text-[7px] font-mono text-[#3a3f4b] uppercase tracking-wider flex-shrink-0">
64
+ {COST_SOURCE_LABEL[agent.costSource] || ''}
65
+ </span>
66
+ )}
67
+
68
+ {/* Context bar */}
69
+ <div className="w-12 flex-shrink-0">
70
+ <div className="flex items-center justify-end gap-1 mb-0.5">
71
+ <span className="text-[8px] font-mono text-[#505862] tabular-nums">{contextPct}%</span>
72
+ </div>
73
+ <div className="h-[2px] bg-[#1a1e25] rounded-full overflow-hidden">
74
+ <div
75
+ className="h-full rounded-full transition-all duration-700"
76
+ style={{
77
+ width: `${Math.max(contextPct, 1)}%`,
78
+ background: contextPct > 80 ? '#e06c75' : contextPct > 60 ? '#e5c07b' : isAlive ? '#61afef' : '#333842',
79
+ }}
80
+ />
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ });
86
+
87
+ function shortModel(id) {
88
+ if (!id || id === 'auto' || id === 'default') return 'default';
89
+ const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/);
90
+ if (claude) return `${claude[1][0].toUpperCase()}${claude[1].slice(1)} ${claude[2]}.${claude[3]}`;
91
+ if (id.startsWith('gemini-')) return id.replace('gemini-', 'Gem ').replace('-preview', '');
92
+ return id.length > 12 ? id.slice(0, 12) + '...' : id;
93
+ }
94
+
95
+ const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
96
+ if (!agentBreakdown?.length) {
16
97
  return (
17
- <div className="flex-1 flex items-center justify-center text-xs text-text-4 font-sans p-4">
18
- No agents running
98
+ <div className="flex-1 flex items-center justify-center text-[9px] text-[#3a3f4b] font-mono p-4">
99
+ No agents
19
100
  </div>
20
101
  );
21
102
  }
22
103
 
23
104
  // Group by team
24
105
  const teams = {};
25
- for (const a of agents) {
26
- const team = a.teamId || 'Ungrouped';
106
+ for (const a of agentBreakdown) {
107
+ const team = a.teamId || 'ungrouped';
27
108
  if (!teams[team]) teams[team] = [];
28
109
  teams[team].push(a);
29
110
  }
30
111
 
112
+ const rotatingSet = new Set(rotating);
113
+
31
114
  return (
32
115
  <ScrollArea className="flex-1">
33
- <div className="p-3 space-y-4">
116
+ <div className="py-1">
34
117
  {Object.entries(teams).map(([team, members]) => (
35
118
  <div key={team}>
36
- <div className="text-2xs font-semibold text-text-3 font-sans uppercase tracking-wider mb-2">{team}</div>
37
- <div className="space-y-1">
38
- {members.map((a) => (
39
- <div key={a.id} className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-surface-5 transition-colors">
40
- <StatusDot status={a.status} size="sm" />
41
- <Avatar name={a.name} role={a.role} size="sm" />
42
- <div className="flex-1 min-w-0">
43
- <div className="text-xs text-text-0 font-sans truncate">{a.name}</div>
44
- </div>
45
- <span className="text-2xs font-mono text-text-3">{fmtNum(a.tokensUsed || 0)}</span>
46
- {/* Mini context bar */}
47
- <div className="w-10 h-1 bg-surface-0 rounded-full overflow-hidden">
48
- <div
49
- className="h-full rounded-full"
50
- style={{
51
- width: `${Math.min(a.contextUsage || 0, 100)}%`,
52
- background: (a.contextUsage || 0) >= 80 ? 'var(--color-danger)' : (a.contextUsage || 0) >= 60 ? 'var(--color-warning)' : 'var(--color-success)',
53
- }}
54
- />
55
- </div>
56
- </div>
57
- ))}
119
+ <div className="px-3 pt-2 pb-1">
120
+ <span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">{team}</span>
58
121
  </div>
122
+ {members.map((a) => (
123
+ <AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
124
+ ))}
59
125
  </div>
60
126
  ))}
61
127
  </div>
62
128
  </ScrollArea>
63
129
  );
64
- }
130
+ });
131
+
132
+ export { FleetPanel };
@@ -1,26 +1,45 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
2
3
  import { fmtUptime, timeAgo } from '../../lib/format';
3
4
  import { StatusDot } from '../ui/status-dot';
4
5
  import { RefreshCw } from 'lucide-react';
5
6
 
6
- export function DashboardHeader({ connected, runningCount, totalCount, uptime, lastFetch }) {
7
+ const DashboardHeader = memo(function DashboardHeader({ connected, runningCount, totalCount, uptime, lastFetch, activeTeam }) {
7
8
  return (
8
- <div className="flex items-center gap-4 px-4 py-2.5 bg-surface-1 border-b border-border">
9
- <h2 className="text-sm font-semibold text-text-0 font-sans">Command Center</h2>
9
+ <div className="flex items-center gap-4 px-4 py-2 bg-surface-1 border-b border-[#262a32]">
10
+ <h2 className="text-[12px] font-semibold text-[#e6e6e6] font-sans tracking-wide uppercase">Command Center</h2>
11
+
12
+ {activeTeam && (
13
+ <>
14
+ <span className="text-[#2a2e36]">/</span>
15
+ <span className="text-[9px] font-mono text-[#505862]">{activeTeam.name}</span>
16
+ </>
17
+ )}
18
+
10
19
  <div className="flex-1" />
11
20
 
12
21
  {connected && (
13
- <div className="flex items-center gap-4 text-2xs text-text-3 font-sans">
14
- <span>{runningCount}/{totalCount} agents</span>
15
- {uptime > 0 && <span>Up {fmtUptime(uptime)}</span>}
22
+ <div className="flex items-center gap-3.5 text-[9px] font-mono text-[#505862]">
23
+ <span>
24
+ <span className="text-[#8b929e]">{runningCount}</span>
25
+ <span className="text-[#3a3f4b]">/{totalCount}</span>
26
+ <span className="ml-1">agents</span>
27
+ </span>
28
+ {uptime > 0 && (
29
+ <span>Up {fmtUptime(uptime)}</span>
30
+ )}
16
31
  {lastFetch > 0 && (
17
- <span className="flex items-center gap-1 text-text-4">
18
- <RefreshCw size={10} /> {timeAgo(lastFetch)}
32
+ <span className="flex items-center gap-1 text-[#3a3f4b]">
33
+ <RefreshCw size={8} />
34
+ <span>{timeAgo(lastFetch)}</span>
19
35
  </span>
20
36
  )}
21
37
  </div>
22
38
  )}
39
+
23
40
  <StatusDot status={connected ? 'running' : 'crashed'} size="sm" />
24
41
  </div>
25
42
  );
26
- }
43
+ });
44
+
45
+ export { DashboardHeader };
@@ -0,0 +1,269 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
3
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
4
+ import { ScrollArea } from '../ui/scroll-area';
5
+ import { fmtNum, fmtPct, timeAgo } from '../../lib/format';
6
+ import { cn } from '../../lib/cn';
7
+ import { HEX } from '../../lib/theme-hex';
8
+ import { RotateCw, Brain, Radio } from 'lucide-react';
9
+
10
+ /* ── Tiny SVG sparkline for inline use ──────────────────────── */
11
+ function TinySparkline({ data, color = HEX.accent, width = 60, height = 16 }) {
12
+ if (!data || data.length < 2) return <div style={{ width, height }} />;
13
+ const vals = Array.isArray(data[0]) ? data : data.map((d) => (typeof d === 'number' ? d : d.v));
14
+ const min = Math.min(...vals);
15
+ const max = Math.max(...vals);
16
+ const range = max - min || 1;
17
+
18
+ const points = vals.map((v, i) => {
19
+ const x = (i / (vals.length - 1)) * width;
20
+ const y = height - ((v - min) / range) * (height - 2) - 1;
21
+ return `${x},${y}`;
22
+ }).join(' ');
23
+
24
+ return (
25
+ <svg width={width} height={height} className="flex-shrink-0">
26
+ <polyline points={points} fill="none" stroke={color} strokeWidth="1" strokeLinejoin="round" strokeOpacity="0.6" />
27
+ </svg>
28
+ );
29
+ }
30
+
31
+ /* ── Savings bar ────────────────────────────────────────────── */
32
+ function SavingsBar({ label, value, total, color }) {
33
+ const pct = total > 0 ? (value / total) * 100 : 0;
34
+ return (
35
+ <div className="space-y-0.5">
36
+ <div className="flex items-center justify-between text-[9px] font-mono">
37
+ <span className="text-[#6e7681]">{label}</span>
38
+ <span className="text-[#8b929e] tabular-nums">{fmtNum(value)}</span>
39
+ </div>
40
+ <div className="h-[2px] bg-[#1a1e25] rounded-full overflow-hidden">
41
+ <div
42
+ className="h-full rounded-full transition-all duration-500"
43
+ style={{ width: `${Math.min(pct, 100)}%`, background: color }}
44
+ />
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ /* ── Rotation Tab ───────────────────────────────────────────── */
51
+ function RotationTab({ tokens, rotation }) {
52
+ const savings = tokens?.savings || {};
53
+ const totalSaved = savings.total || 0;
54
+ const totalUsed = tokens?.totalTokens || 0;
55
+ const hypothetical = totalUsed + totalSaved;
56
+
57
+ return (
58
+ <ScrollArea className="flex-1">
59
+ <div className="p-3 space-y-4">
60
+ {/* Big numbers */}
61
+ <div className="flex gap-4">
62
+ <div>
63
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Rotations</div>
64
+ <div className="text-[18px] font-mono font-semibold text-[#bcc2cd] tabular-nums leading-none">
65
+ {rotation?.totalRotations || 0}
66
+ </div>
67
+ </div>
68
+ <div>
69
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Saved</div>
70
+ <div className="text-[18px] font-mono font-semibold text-[#4ae168] tabular-nums leading-none">
71
+ {fmtNum(totalSaved)}
72
+ </div>
73
+ {hypothetical > 0 && (
74
+ <div className="text-[8px] font-mono text-[#505862] mt-0.5">
75
+ {fmtPct((totalSaved / hypothetical) * 100)} of total
76
+ </div>
77
+ )}
78
+ </div>
79
+ </div>
80
+
81
+ {/* Savings breakdown */}
82
+ <div className="space-y-2">
83
+ <SavingsBar label="Rotation" value={savings.fromRotation || 0} total={hypothetical} color={HEX.accent} />
84
+ <SavingsBar label="Conflict prevention" value={savings.fromConflictPrevention || 0} total={hypothetical} color={HEX.purple} />
85
+ <SavingsBar label="Cold-start skip" value={savings.fromColdStartSkip || 0} total={hypothetical} color={HEX.info} />
86
+ </div>
87
+
88
+ {/* Rotation history */}
89
+ {rotation?.history?.length > 0 && (
90
+ <div>
91
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-1.5">Recent</div>
92
+ <div className="space-y-1">
93
+ {rotation.history.slice(-8).reverse().map((r, i) => (
94
+ <div key={i} className="flex items-center gap-2 text-[9px] font-mono px-2 py-1 bg-[#1a1e25] rounded">
95
+ <span className="text-[#8b929e] font-medium capitalize truncate flex-1">{r.agentName || r.role}</span>
96
+ <span className="text-[#505862] tabular-nums">{fmtPct((r.contextUsage || 0) * 100)}</span>
97
+ <span className="text-[#3a3f4b]">{timeAgo(r.timestamp)}</span>
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ )}
103
+ </div>
104
+ </ScrollArea>
105
+ );
106
+ }
107
+
108
+ /* ── Adaptive Tab ───────────────────────────────────────────── */
109
+ function AdaptiveTab({ adaptive }) {
110
+ if (!adaptive?.length) {
111
+ return (
112
+ <div className="flex-1 flex items-center justify-center text-[9px] text-[#3a3f4b] font-mono p-4">
113
+ No adaptive profiles
114
+ </div>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <ScrollArea className="flex-1">
120
+ <div className="p-3 space-y-3">
121
+ {adaptive.map((p) => (
122
+ <div key={p.key} className="bg-[#1a1e25] rounded px-2.5 py-2 space-y-1.5">
123
+ {/* Profile header */}
124
+ <div className="flex items-center gap-2">
125
+ <span className="text-[9px] font-mono text-[#8b929e] flex-1 truncate">{p.key}</span>
126
+ <span className="text-[10px] font-mono font-semibold text-[#bcc2cd] tabular-nums">
127
+ {fmtPct(p.threshold * 100)}
128
+ </span>
129
+ <span
130
+ className={cn(
131
+ 'text-[7px] font-mono font-bold uppercase px-1 py-px rounded-sm',
132
+ p.converged
133
+ ? 'text-[#4ae168] bg-[rgba(74,225,104,0.1)]'
134
+ : 'text-[#505862] bg-[rgba(80,88,98,0.1)]',
135
+ )}
136
+ >
137
+ {p.converged ? 'CONV' : `${p.adjustments} adj`}
138
+ </span>
139
+ </div>
140
+
141
+ {/* Threshold drift sparkline */}
142
+ {p.thresholdHistory?.length > 1 && (
143
+ <div className="flex items-center gap-2">
144
+ <span className="text-[7px] font-mono text-[#3a3f4b] uppercase tracking-wider">Drift</span>
145
+ <TinySparkline
146
+ data={p.thresholdHistory.map((h) => h.v)}
147
+ color={p.converged ? HEX.success : HEX.accent}
148
+ width={80}
149
+ height={14}
150
+ />
151
+ </div>
152
+ )}
153
+
154
+ {/* Quality scores sparkline */}
155
+ {p.recentScores?.length > 1 && (
156
+ <div className="flex items-center gap-2">
157
+ <span className="text-[7px] font-mono text-[#3a3f4b] uppercase tracking-wider">Quality</span>
158
+ <TinySparkline
159
+ data={p.recentScores}
160
+ color={HEX.warning}
161
+ width={80}
162
+ height={14}
163
+ />
164
+ </div>
165
+ )}
166
+ </div>
167
+ ))}
168
+ </div>
169
+ </ScrollArea>
170
+ );
171
+ }
172
+
173
+ /* ── Journalist Tab ─────────────────────────────────────────── */
174
+ function JournalistTab({ journalist }) {
175
+ if (!journalist) {
176
+ return (
177
+ <div className="flex-1 flex items-center justify-center text-[9px] text-[#3a3f4b] font-mono p-4">
178
+ Journalist inactive
179
+ </div>
180
+ );
181
+ }
182
+
183
+ return (
184
+ <ScrollArea className="flex-1">
185
+ <div className="p-3 space-y-3">
186
+ {/* Status row */}
187
+ <div className="flex items-center gap-3">
188
+ <div>
189
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Cycles</div>
190
+ <div className="text-[16px] font-mono font-semibold text-[#bcc2cd] tabular-nums leading-none">
191
+ {journalist.cycleCount || 0}
192
+ </div>
193
+ </div>
194
+ {journalist.lastCycleAt && (
195
+ <div>
196
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5">Last</div>
197
+ <div className="text-[10px] font-mono text-[#6e7681]">{timeAgo(journalist.lastCycleAt)}</div>
198
+ </div>
199
+ )}
200
+ {journalist.synthesizing && (
201
+ <span className="text-[7px] font-mono font-bold text-[#33afbc] uppercase tracking-wider animate-pulse">
202
+ Synthesizing
203
+ </span>
204
+ )}
205
+ </div>
206
+
207
+ {/* Last summary */}
208
+ {journalist.lastSummary && (
209
+ <div>
210
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-1">Summary</div>
211
+ <div className="text-[9px] font-sans text-[#6e7681] leading-relaxed bg-[#1a1e25] rounded px-2.5 py-2 max-h-32 overflow-y-auto">
212
+ {journalist.lastSummary}
213
+ </div>
214
+ </div>
215
+ )}
216
+
217
+ {/* Recent history */}
218
+ {journalist.recentHistory?.length > 0 && (
219
+ <div>
220
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-1.5">History</div>
221
+ <div className="space-y-1">
222
+ {journalist.recentHistory.slice().reverse().map((h, i) => (
223
+ <div key={i} className="flex items-center gap-2 text-[9px] font-mono px-2 py-1 bg-[#1a1e25] rounded">
224
+ <span className="text-[#505862]">#{h.cycle}</span>
225
+ <span className="text-[#6e7681] flex-1 truncate">{h.agentCount} agents</span>
226
+ <span className="text-[#3a3f4b]">{timeAgo(h.timestamp)}</span>
227
+ </div>
228
+ ))}
229
+ </div>
230
+ </div>
231
+ )}
232
+ </div>
233
+ </ScrollArea>
234
+ );
235
+ }
236
+
237
+ /* ── Intel Panel (main export) ──────────────────────────────── */
238
+ const IntelPanel = memo(function IntelPanel({ tokens, rotation, adaptive, journalist }) {
239
+ return (
240
+ <Tabs defaultValue="rotation" className="flex flex-col h-full">
241
+ <TabsList className="flex-shrink-0 px-1 border-b border-[#262a32]">
242
+ <TabsTrigger value="rotation" className="text-[9px] px-2.5 py-1.5 gap-1">
243
+ <RotateCw size={10} />
244
+ Rotation
245
+ </TabsTrigger>
246
+ <TabsTrigger value="adaptive" className="text-[9px] px-2.5 py-1.5 gap-1">
247
+ <Brain size={10} />
248
+ Adaptive
249
+ </TabsTrigger>
250
+ <TabsTrigger value="journalist" className="text-[9px] px-2.5 py-1.5 gap-1">
251
+ <Radio size={10} />
252
+ Journalist
253
+ </TabsTrigger>
254
+ </TabsList>
255
+
256
+ <TabsContent value="rotation" className="flex-1 min-h-0 overflow-hidden">
257
+ <RotationTab tokens={tokens} rotation={rotation} />
258
+ </TabsContent>
259
+ <TabsContent value="adaptive" className="flex-1 min-h-0 overflow-hidden">
260
+ <AdaptiveTab adaptive={adaptive} />
261
+ </TabsContent>
262
+ <TabsContent value="journalist" className="flex-1 min-h-0 overflow-hidden">
263
+ <JournalistTab journalist={journalist} />
264
+ </TabsContent>
265
+ </Tabs>
266
+ );
267
+ });
268
+
269
+ export { IntelPanel };
@@ -1,8 +1,9 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
2
3
  import { cn } from '../../lib/cn';
3
4
  import { HEX } from '../../lib/theme-hex';
4
5
 
5
- function MiniSparkline({ data, color = HEX.accent, width = 80, height = 24 }) {
6
+ function MiniSparkline({ data, color = HEX.accent, width = 72, height = 22 }) {
6
7
  if (!data || data.length < 2) return <div style={{ width, height }} />;
7
8
  const vals = data.map((d) => d.v);
8
9
  const min = Math.min(...vals);
@@ -15,31 +16,56 @@ function MiniSparkline({ data, color = HEX.accent, width = 80, height = 24 }) {
15
16
  return `${x},${y}`;
16
17
  }).join(' ');
17
18
 
19
+ const gradId = `kpi-${color.replace('#', '')}`;
20
+
18
21
  return (
19
22
  <svg width={width} height={height} className="flex-shrink-0">
20
23
  <defs>
21
- <linearGradient id={`kpi-${color}`} x1="0" y1="0" x2="0" y2="1">
22
- <stop offset="0%" stopColor={color} stopOpacity="0.3" />
24
+ <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
25
+ <stop offset="0%" stopColor={color} stopOpacity="0.15" />
23
26
  <stop offset="100%" stopColor={color} stopOpacity="0" />
24
27
  </linearGradient>
25
28
  </defs>
26
- <polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#kpi-${color})`} />
27
- <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
29
+ <polygon points={`0,${height} ${points} ${width},${height}`} fill={`url(#${gradId})`} />
30
+ <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeOpacity="0.7" />
28
31
  </svg>
29
32
  );
30
33
  }
31
34
 
32
- export function KpiCard({ label, value, sparkData, color = HEX.accent, className }) {
35
+ const KpiCard = memo(function KpiCard({ label, value, sparkData, color = HEX.accent, className }) {
33
36
  return (
34
37
  <div className={cn(
35
- 'flex items-center gap-3 px-4 py-3 bg-surface-1 border-b border-border-subtle min-w-0',
38
+ 'flex items-center gap-2.5 px-3 py-2.5 min-w-0',
39
+ 'bg-surface-1',
36
40
  className,
37
41
  )}>
38
42
  <div className="flex-1 min-w-0">
39
- <div className="text-2xs text-text-3 font-sans uppercase tracking-wider mb-0.5 truncate">{label}</div>
40
- <div className="text-lg font-semibold font-mono text-text-0 tabular-nums">{value}</div>
43
+ <div className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest mb-0.5 truncate">{label}</div>
44
+ <div className="text-[15px] font-semibold font-mono text-[#bcc2cd] tabular-nums leading-none">{value}</div>
41
45
  </div>
42
46
  <MiniSparkline data={sparkData} color={color} />
43
47
  </div>
44
48
  );
49
+ });
50
+
51
+ export function KpiStrip({ kpis }) {
52
+ return (
53
+ <div className="flex flex-wrap" style={{ background: '#1a1e25' }}>
54
+ {kpis.map((kpi, i) => (
55
+ <KpiCard
56
+ key={kpi.label}
57
+ label={kpi.label}
58
+ value={kpi.value}
59
+ sparkData={kpi.sparkData}
60
+ color={kpi.color}
61
+ className={cn(
62
+ 'flex-1 basis-[12.5%] min-w-[140px]',
63
+ 'border-b border-r border-[#262a32]',
64
+ )}
65
+ />
66
+ ))}
67
+ </div>
68
+ );
45
69
  }
70
+
71
+ export { KpiCard };