groove-dev 0.23.0 → 0.24.1

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 (35) hide show
  1. package/node_modules/@groove-dev/gui/dist/assets/index-C0naHS9e.css +1 -0
  2. package/node_modules/@groove-dev/gui/dist/assets/index-DJbDjzF2.js +587 -0
  3. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  4. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +11 -9
  5. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +102 -0
  6. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +106 -39
  7. package/node_modules/@groove-dev/gui/src/components/dashboard/header-bar.jsx +27 -8
  8. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +269 -0
  9. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +35 -9
  10. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +117 -0
  11. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +154 -36
  12. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +28 -8
  13. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +7 -0
  14. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +97 -54
  15. package/package.json +1 -1
  16. package/packages/gui/dist/assets/index-C0naHS9e.css +1 -0
  17. package/packages/gui/dist/assets/index-DJbDjzF2.js +587 -0
  18. package/packages/gui/dist/index.html +2 -2
  19. package/packages/gui/src/components/dashboard/activity-feed.jsx +11 -9
  20. package/packages/gui/src/components/dashboard/cache-ring.jsx +102 -0
  21. package/packages/gui/src/components/dashboard/fleet-panel.jsx +106 -39
  22. package/packages/gui/src/components/dashboard/header-bar.jsx +27 -8
  23. package/packages/gui/src/components/dashboard/intel-panel.jsx +269 -0
  24. package/packages/gui/src/components/dashboard/kpi-card.jsx +35 -9
  25. package/packages/gui/src/components/dashboard/routing-chart.jsx +117 -0
  26. package/packages/gui/src/components/dashboard/token-chart.jsx +154 -36
  27. package/packages/gui/src/lib/hooks/use-dashboard.js +28 -8
  28. package/packages/gui/src/lib/theme-hex.js +7 -0
  29. package/packages/gui/src/views/dashboard.jsx +97 -54
  30. package/node_modules/@groove-dev/gui/dist/assets/index-Cg9SzKgD.css +0 -1
  31. package/node_modules/@groove-dev/gui/dist/assets/index-QmFja2dw.js +0 -582
  32. package/node_modules/@groove-dev/gui/src/components/dashboard/savings-panel.jsx +0 -122
  33. package/packages/gui/dist/assets/index-Cg9SzKgD.css +0 -1
  34. package/packages/gui/dist/assets/index-QmFja2dw.js +0 -582
  35. package/packages/gui/src/components/dashboard/savings-panel.jsx +0 -122
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-QmFja2dw.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DJbDjzF2.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-Cg9SzKgD.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-C0naHS9e.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -1,7 +1,7 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
2
3
  import { RotateCw, Zap, AlertTriangle, CheckCircle, UserPlus } from 'lucide-react';
3
4
  import { timeAgo } from '../../lib/format';
4
- import { cn } from '../../lib/cn';
5
5
 
6
6
  const ICONS = {
7
7
  spawn: UserPlus,
@@ -19,10 +19,10 @@ const ICON_COLORS = {
19
19
  default: 'text-text-3',
20
20
  };
21
21
 
22
- export function ActivityFeed({ events = [] }) {
22
+ const ActivityFeed = memo(function ActivityFeed({ events = [] }) {
23
23
  if (!events.length) {
24
24
  return (
25
- <div className="text-xs text-text-4 font-sans py-3 text-center">
25
+ <div className="text-xs text-text-3 font-mono py-2.5 text-center">
26
26
  No recent activity
27
27
  </div>
28
28
  );
@@ -30,17 +30,19 @@ export function ActivityFeed({ events = [] }) {
30
30
 
31
31
  return (
32
32
  <div className="flex items-center gap-3 overflow-x-auto py-2 px-3">
33
- {events.slice(-10).reverse().map((event, i) => {
33
+ {events.slice(-15).reverse().map((event, i) => {
34
34
  const Icon = ICONS[event.type] || ICONS.default;
35
35
  const color = ICON_COLORS[event.type] || ICON_COLORS.default;
36
36
  return (
37
- <div key={i} className="flex items-center gap-1.5 flex-shrink-0 text-2xs font-sans">
38
- <Icon size={12} className={color} />
39
- <span className="text-text-2 whitespace-nowrap">{event.text}</span>
40
- <span className="text-text-4">{timeAgo(event.timestamp)}</span>
37
+ <div key={i} className="flex items-center gap-1.5 flex-shrink-0">
38
+ <Icon size={11} className={color} />
39
+ <span className="text-xs font-sans text-text-2 whitespace-nowrap">{event.text}</span>
40
+ <span className="text-2xs font-mono text-text-4">{timeAgo(event.timestamp || event.t)}</span>
41
41
  </div>
42
42
  );
43
43
  })}
44
44
  </div>
45
45
  );
46
- }
46
+ });
47
+
48
+ export { ActivityFeed };
@@ -0,0 +1,102 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useRef, useEffect, memo } from 'react';
3
+ import { HEX } from '../../lib/theme-hex';
4
+ import { fmtNum } from '../../lib/format';
5
+
6
+ const CacheRing = memo(function CacheRing({ cacheRead = 0, cacheCreation = 0, totalInput = 0, size = 140 }) {
7
+ const canvasRef = useRef(null);
8
+ const total = cacheRead + cacheCreation + totalInput;
9
+ const hitRate = total > 0 ? (cacheRead / total) * 100 : 0;
10
+
11
+ useEffect(() => {
12
+ const canvas = canvasRef.current;
13
+ if (!canvas) return;
14
+ const dpr = window.devicePixelRatio || 1;
15
+ canvas.width = size * dpr;
16
+ canvas.height = size * dpr;
17
+ const ctx = canvas.getContext('2d');
18
+ ctx.scale(dpr, dpr);
19
+ ctx.clearRect(0, 0, size, size);
20
+
21
+ const cx = size / 2;
22
+ const cy = size / 2;
23
+ const radius = (size - 12) / 2;
24
+ const strokeWidth = 5;
25
+
26
+ const startAngle = (135 * Math.PI) / 180;
27
+ const endAngle = (405 * Math.PI) / 180;
28
+ const sweep = endAngle - startAngle;
29
+
30
+ // Background track
31
+ ctx.beginPath();
32
+ ctx.arc(cx, cy, radius, startAngle, endAngle);
33
+ ctx.strokeStyle = HEX.surface4;
34
+ ctx.lineWidth = strokeWidth;
35
+ ctx.lineCap = 'round';
36
+ ctx.stroke();
37
+
38
+ if (total > 0) {
39
+ const readPct = cacheRead / total;
40
+ const createPct = cacheCreation / total;
41
+
42
+ if (readPct > 0) {
43
+ const segEnd = startAngle + sweep * readPct;
44
+ ctx.beginPath();
45
+ ctx.arc(cx, cy, radius, startAngle, segEnd);
46
+ ctx.strokeStyle = HEX.accent;
47
+ ctx.lineWidth = strokeWidth;
48
+ ctx.lineCap = 'round';
49
+ ctx.stroke();
50
+ }
51
+
52
+ if (createPct > 0) {
53
+ const segStart = startAngle + sweep * readPct;
54
+ const segEnd = segStart + sweep * createPct;
55
+ ctx.beginPath();
56
+ ctx.arc(cx, cy, radius, segStart, segEnd);
57
+ ctx.strokeStyle = HEX.purple;
58
+ ctx.lineWidth = strokeWidth;
59
+ ctx.lineCap = 'butt';
60
+ ctx.stroke();
61
+ }
62
+ }
63
+
64
+ // Center text
65
+ ctx.textAlign = 'center';
66
+ ctx.textBaseline = 'middle';
67
+ ctx.font = `600 ${size * 0.2}px 'JetBrains Mono Variable', monospace`;
68
+ ctx.fillStyle = HEX.text0;
69
+ ctx.fillText(`${Math.round(hitRate)}%`, cx, cy - 3);
70
+
71
+ ctx.font = `500 ${size * 0.08}px 'JetBrains Mono Variable', monospace`;
72
+ ctx.fillStyle = HEX.text3;
73
+ ctx.fillText('CACHE', cx, cy + size * 0.13);
74
+ }, [cacheRead, cacheCreation, totalInput, size, total, hitRate]);
75
+
76
+ return (
77
+ <div className="flex flex-col items-center justify-center h-full px-3 py-3">
78
+ <canvas
79
+ ref={canvasRef}
80
+ className="flex-shrink-0"
81
+ style={{ width: size, height: size }}
82
+ />
83
+ <div className="w-full mt-3 space-y-1.5 max-w-[160px]">
84
+ <StatRow color={HEX.accent} label="Read" value={fmtNum(cacheRead)} />
85
+ <StatRow color={HEX.purple} label="Create" value={fmtNum(cacheCreation)} />
86
+ <StatRow color={HEX.surface5} label="Miss" value={fmtNum(Math.max(totalInput - cacheRead - cacheCreation, 0))} />
87
+ </div>
88
+ </div>
89
+ );
90
+ });
91
+
92
+ function StatRow({ color, label, value }) {
93
+ return (
94
+ <div className="flex items-center gap-2 text-xs font-mono">
95
+ <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: color }} />
96
+ <span className="text-text-3 uppercase tracking-wider flex-1">{label}</span>
97
+ <span className="text-text-1 tabular-nums">{value}</span>
98
+ </div>
99
+ );
100
+ }
101
+
102
+ export { CacheRing };
@@ -1,64 +1,131 @@
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-surface-3 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-xs font-semibold text-text-0 font-sans truncate leading-none">{agent.name}</div>
33
+ <div className="flex items-center gap-1 mt-0.5">
34
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">{agent.role}</span>
35
+ <span className="text-2xs text-text-4">/</span>
36
+ <span className="text-2xs font-mono text-text-3">{shortModel(agent.model)}</span>
37
+ </div>
38
+ </div>
39
+
40
+ {/* Tokens + cost */}
41
+ <div className="text-right flex-shrink-0">
42
+ <div className="text-xs font-mono text-text-1 tabular-nums leading-none">{fmtNum(agent.tokens || 0)}</div>
43
+ {(agent.costUsd || 0) > 0 && (
44
+ <div className="text-2xs font-mono text-text-3 mt-0.5">{fmtDollar(agent.costUsd)}</div>
45
+ )}
46
+ </div>
47
+
48
+ {/* Quality / tool success */}
49
+ {successRate != null && (
50
+ <span
51
+ className="text-2xs 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-2xs font-mono text-text-4 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-2xs font-mono text-text-2 tabular-nums">{contextPct}%</span>
72
+ </div>
73
+ <div className="h-[2px] bg-surface-0 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-xs text-text-3 font-mono p-4">
99
+ No agents
19
100
  </div>
20
101
  );
21
102
  }
22
103
 
23
- // Group by team
24
104
  const teams = {};
25
- for (const a of agents) {
26
- const team = a.teamId || 'Ungrouped';
105
+ for (const a of agentBreakdown) {
106
+ const team = a.teamId || 'ungrouped';
27
107
  if (!teams[team]) teams[team] = [];
28
108
  teams[team].push(a);
29
109
  }
30
110
 
111
+ const rotatingSet = new Set(rotating);
112
+
31
113
  return (
32
114
  <ScrollArea className="flex-1">
33
- <div className="p-3 space-y-4">
115
+ <div className="py-1">
34
116
  {Object.entries(teams).map(([team, members]) => (
35
117
  <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
- ))}
118
+ <div className="px-3 pt-2 pb-1">
119
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">{team}</span>
58
120
  </div>
121
+ {members.map((a) => (
122
+ <AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
123
+ ))}
59
124
  </div>
60
125
  ))}
61
126
  </div>
62
127
  </ScrollArea>
63
128
  );
64
- }
129
+ });
130
+
131
+ 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-border">
10
+ <h2 className="text-xs font-semibold text-text-0 font-sans tracking-wide uppercase">Command Center</h2>
11
+
12
+ {activeTeam && (
13
+ <>
14
+ <span className="text-text-4">/</span>
15
+ <span className="text-xs font-mono text-text-2">{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-xs font-mono text-text-2">
23
+ <span>
24
+ <span className="text-text-1">{runningCount}</span>
25
+ <span className="text-text-3">/{totalCount}</span>
26
+ <span className="ml-1 text-text-3">agents</span>
27
+ </span>
28
+ {uptime > 0 && (
29
+ <span className="text-text-3">Up {fmtUptime(uptime)}</span>
30
+ )}
16
31
  {lastFetch > 0 && (
17
32
  <span className="flex items-center gap-1 text-text-4">
18
- <RefreshCw size={10} /> {timeAgo(lastFetch)}
33
+ <RefreshCw size={9} />
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.7" />
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-xs font-mono">
37
+ <span className="text-text-2">{label}</span>
38
+ <span className="text-text-1 tabular-nums">{fmtNum(value)}</span>
39
+ </div>
40
+ <div className="h-[2px] bg-surface-0 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-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Rotations</div>
64
+ <div className="text-xl font-mono font-semibold text-text-0 tabular-nums leading-none">
65
+ {rotation?.totalRotations || 0}
66
+ </div>
67
+ </div>
68
+ <div>
69
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Saved</div>
70
+ <div className="text-xl font-mono font-semibold text-success tabular-nums leading-none">
71
+ {fmtNum(totalSaved)}
72
+ </div>
73
+ {hypothetical > 0 && (
74
+ <div className="text-2xs font-mono text-text-3 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-2xs font-mono text-text-3 uppercase tracking-wider 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-xs font-mono px-2 py-1 bg-surface-0 rounded">
95
+ <span className="text-text-1 font-medium capitalize truncate flex-1">{r.agentName || r.role}</span>
96
+ <span className="text-text-3 tabular-nums">{fmtPct((r.contextUsage || 0) * 100)}</span>
97
+ <span className="text-text-4">{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-xs text-text-3 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-surface-0 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-xs font-mono text-text-1 flex-1 truncate">{p.key}</span>
126
+ <span className="text-xs font-mono font-semibold text-text-0 tabular-nums">
127
+ {fmtPct(p.threshold * 100)}
128
+ </span>
129
+ <span
130
+ className={cn(
131
+ 'text-2xs font-mono font-bold uppercase px-1 py-px rounded-sm',
132
+ p.converged
133
+ ? 'text-success bg-success/10'
134
+ : 'text-text-3 bg-surface-3',
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-2xs font-mono text-text-3 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-2xs font-mono text-text-3 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-xs text-text-3 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-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Cycles</div>
190
+ <div className="text-lg font-mono font-semibold text-text-0 tabular-nums leading-none">
191
+ {journalist.cycleCount || 0}
192
+ </div>
193
+ </div>
194
+ {journalist.lastCycleAt && (
195
+ <div>
196
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Last</div>
197
+ <div className="text-xs font-mono text-text-2">{timeAgo(journalist.lastCycleAt)}</div>
198
+ </div>
199
+ )}
200
+ {journalist.synthesizing && (
201
+ <span className="text-2xs font-mono font-bold text-accent 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-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Summary</div>
211
+ <div className="text-xs font-sans text-text-2 leading-relaxed bg-surface-0 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-2xs font-mono text-text-3 uppercase tracking-wider 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-xs font-mono px-2 py-1 bg-surface-0 rounded">
224
+ <span className="text-text-3">#{h.cycle}</span>
225
+ <span className="text-text-2 flex-1 truncate">{h.agentCount} agents</span>
226
+ <span className="text-text-4">{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">
242
+ <TabsTrigger value="rotation" className="text-xs px-2.5 py-1.5 gap-1">
243
+ <RotateCw size={11} />
244
+ Rotation
245
+ </TabsTrigger>
246
+ <TabsTrigger value="adaptive" className="text-xs px-2.5 py-1.5 gap-1">
247
+ <Brain size={11} />
248
+ Adaptive
249
+ </TabsTrigger>
250
+ <TabsTrigger value="journalist" className="text-xs px-2.5 py-1.5 gap-1">
251
+ <Radio size={11} />
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 };