groove-dev 0.26.28 → 0.26.30

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.
@@ -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--XNm9lTq.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-vxioP1y2.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-CGFWAGJ6.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-BnNZzcsd.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -1,12 +1,21 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { memo } from 'react';
2
+ import { memo, useState } from 'react';
3
+ import { ChevronDown, ChevronRight } from 'lucide-react';
3
4
  import { fmtNum, fmtDollar } from '../../lib/format';
4
5
  import { cn } from '../../lib/cn';
5
- import { statusColor } from '../../lib/status';
6
+ import { statusColor, roleColor } from '../../lib/status';
6
7
  import { ScrollArea } from '../ui/scroll-area';
7
8
 
8
9
  const COST_SOURCE_LABEL = { actual: 'ACT', estimated: 'EST', local: 'LOC' };
9
10
 
11
+ function shortModel(id) {
12
+ if (!id || id === 'auto' || id === 'default') return 'default';
13
+ const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/);
14
+ if (claude) return `${claude[1][0].toUpperCase()}${claude[1].slice(1)} ${claude[2]}.${claude[3]}`;
15
+ if (id.startsWith('gemini-')) return id.replace('gemini-', 'Gem ').replace('-preview', '');
16
+ return id.length > 12 ? id.slice(0, 12) + '...' : id;
17
+ }
18
+
10
19
  const AgentRow = memo(function AgentRow({ agent, isRotating }) {
11
20
  const isAlive = agent.status === 'running' || agent.status === 'starting';
12
21
  const contextPct = Math.round((agent.contextUsage || 0) * 100);
@@ -14,12 +23,14 @@ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
14
23
  const quality = agent.quality;
15
24
  const successRate = quality?.toolSuccessRate != null ? Math.round(quality.toolSuccessRate * 100) : null;
16
25
  const thresholdPct = agent.rotationThreshold ? Math.round(agent.rotationThreshold * 100) : null;
26
+ const rc = roleColor(agent.role);
27
+ const barColor = contextPct > 80 ? '#e06c75' : contextPct > 60 ? '#e5c07b' : isAlive ? '#33afbc' : '#333842';
17
28
 
18
29
  return (
19
- <div className="px-3 py-2 hover:bg-surface-3 transition-colors space-y-1.5">
20
- {/* Top row: status + name + values */}
21
- <div className="flex items-center gap-2">
22
- {/* Status square */}
30
+ <div className="px-3 pl-6 py-2 hover:bg-[rgba(51,175,188,0.06)] transition-colors space-y-1.5">
31
+ {/* Top row */}
32
+ <div className="flex items-center gap-2 min-w-0">
33
+ {/* Status dot */}
23
34
  <span className="relative flex-shrink-0 w-[6px] h-[6px]">
24
35
  <span className="absolute inset-0 rounded-sm" style={{ background: sColor }} />
25
36
  {isAlive && (
@@ -31,21 +42,29 @@ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
31
42
  </span>
32
43
 
33
44
  {/* Name */}
34
- <div className="min-w-0">
35
- <div className="text-xs font-semibold text-text-0 font-sans truncate leading-none">{agent.name}</div>
36
- <div className="flex items-center gap-1 mt-0.5">
37
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">{agent.role}</span>
38
- <span className="text-2xs text-text-4">/</span>
39
- <span className="text-2xs font-mono text-text-3">{shortModel(agent.model)}</span>
40
- </div>
45
+ <div className="text-xs font-semibold text-text-0 font-sans truncate leading-none flex-shrink-0 max-w-[80px]">
46
+ {agent.name}
41
47
  </div>
42
48
 
49
+ {/* Role pill */}
50
+ <span
51
+ className="text-2xs font-mono font-semibold px-1.5 py-px rounded-sm flex-shrink-0 capitalize"
52
+ style={{ background: rc.bg, color: rc.text }}
53
+ >
54
+ {(agent.role || '').toLowerCase()}
55
+ </span>
56
+
57
+ {/* Model badge */}
58
+ <span className="text-2xs font-mono text-text-4 bg-surface-4 px-1 py-px rounded-sm flex-shrink-0 truncate max-w-[72px]">
59
+ {shortModel(agent.model)}
60
+ </span>
61
+
43
62
  <div className="flex-1" />
44
63
 
45
64
  {/* Quality badge */}
46
65
  {successRate != null && (
47
66
  <span
48
- className="text-2xs font-mono font-bold uppercase px-1 py-px rounded-sm flex-shrink-0"
67
+ className="text-2xs font-mono font-bold px-1 py-px rounded-sm flex-shrink-0"
49
68
  style={{
50
69
  color: successRate >= 90 ? '#4ae168' : successRate >= 70 ? '#e5c07b' : '#e06c75',
51
70
  background: successRate >= 90 ? 'rgba(74,225,104,0.1)' : successRate >= 70 ? 'rgba(229,192,123,0.1)' : 'rgba(224,108,117,0.1)',
@@ -71,19 +90,19 @@ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
71
90
  </div>
72
91
  </div>
73
92
 
74
- {/* Full-width context bar with threshold marker */}
93
+ {/* Context bar */}
75
94
  <div className="flex items-center gap-2">
76
- <div className="relative flex-1 h-[3px] bg-surface-4 rounded-full overflow-visible">
95
+ <div
96
+ className="relative flex-1 h-[4px] rounded-full overflow-visible"
97
+ style={{ background: 'rgba(51,175,188,0.12)' }}
98
+ >
77
99
  <div
78
100
  className="absolute inset-y-0 left-0 rounded-full transition-all duration-700"
79
- style={{
80
- width: `${Math.max(contextPct, 1)}%`,
81
- background: contextPct > 80 ? '#e06c75' : contextPct > 60 ? '#e5c07b' : isAlive ? '#61afef' : '#333842',
82
- }}
101
+ style={{ width: `${Math.max(contextPct, 1)}%`, background: barColor }}
83
102
  />
84
103
  {thresholdPct && (
85
104
  <div
86
- className="absolute top-[-2px] w-px h-[7px]"
105
+ className="absolute top-[-2px] w-px h-[8px]"
87
106
  style={{ left: `${thresholdPct}%`, background: '#c678dd' }}
88
107
  title={`Rotation at ${thresholdPct}%`}
89
108
  />
@@ -95,12 +114,43 @@ const AgentRow = memo(function AgentRow({ agent, isRotating }) {
95
114
  );
96
115
  });
97
116
 
98
- function shortModel(id) {
99
- if (!id || id === 'auto' || id === 'default') return 'default';
100
- const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)/);
101
- if (claude) return `${claude[1][0].toUpperCase()}${claude[1].slice(1)} ${claude[2]}.${claude[3]}`;
102
- if (id.startsWith('gemini-')) return id.replace('gemini-', 'Gem ').replace('-preview', '');
103
- return id.length > 12 ? id.slice(0, 12) + '...' : id;
117
+ function TeamSection({ team, members, rotatingSet }) {
118
+ const [expanded, setExpanded] = useState(true);
119
+ const runningCount = members.filter((a) => a.status === 'running' || a.status === 'starting').length;
120
+ const isActive = runningCount > 0;
121
+ const totalTokens = members.reduce((sum, a) => sum + (a.tokens || 0), 0);
122
+ const totalCost = members.reduce((sum, a) => sum + (a.costUsd || 0), 0);
123
+
124
+ return (
125
+ <div>
126
+ <button
127
+ onClick={() => setExpanded((e) => !e)}
128
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-[rgba(51,175,188,0.08)] bg-[rgba(51,175,188,0.05)]"
129
+ style={{ borderLeft: isActive ? '2px solid #33afbc' : '2px solid transparent' }}
130
+ >
131
+ {expanded
132
+ ? <ChevronDown size={10} className="text-text-4 flex-shrink-0" />
133
+ : <ChevronRight size={10} className="text-text-4 flex-shrink-0" />
134
+ }
135
+ <span className="text-2xs font-mono font-semibold text-text-2 uppercase tracking-widest flex-1 truncate">
136
+ {team === 'ungrouped' ? 'Ungrouped' : team}
137
+ </span>
138
+ <span className="text-2xs font-mono text-text-3 tabular-nums">{fmtNum(totalTokens)}</span>
139
+ {totalCost > 0 && (
140
+ <span className="text-2xs font-mono text-text-4 tabular-nums ml-1">{fmtDollar(totalCost)}</span>
141
+ )}
142
+ <span
143
+ className="text-2xs font-mono tabular-nums flex-shrink-0 ml-1.5"
144
+ style={{ color: isActive ? '#33afbc' : undefined }}
145
+ >
146
+ {runningCount}/{members.length}
147
+ </span>
148
+ </button>
149
+ {expanded && members.map((a) => (
150
+ <AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
151
+ ))}
152
+ </div>
153
+ );
104
154
  }
105
155
 
106
156
  const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
@@ -125,14 +175,7 @@ const FleetPanel = memo(function FleetPanel({ agentBreakdown, rotating = [] }) {
125
175
  <ScrollArea className="flex-1">
126
176
  <div className="py-1">
127
177
  {Object.entries(teams).map(([team, members]) => (
128
- <div key={team}>
129
- <div className="px-3 pt-2 pb-1">
130
- <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">{team}</span>
131
- </div>
132
- {members.map((a) => (
133
- <AgentRow key={a.id} agent={a} isRotating={rotatingSet.has(a.id)} />
134
- ))}
135
- </div>
178
+ <TeamSection key={team} team={team} members={members} rotatingSet={rotatingSet} />
136
179
  ))}
137
180
  </div>
138
181
  </ScrollArea>
@@ -4,6 +4,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
4
4
  import { fmtNum, fmtPct, timeAgo } from '../../lib/format';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { HEX } from '../../lib/theme-hex';
7
+ import { roleColor } from '../../lib/status';
7
8
  import { RotateCw, Brain, Radio } from 'lucide-react';
8
9
 
9
10
  /* ── Tiny SVG sparkline for inline use ──────────────────────── */
@@ -31,12 +32,17 @@ function TinySparkline({ data, color = HEX.accent, width = 60, height = 16 }) {
31
32
  function SavingsBar({ label, value, total, color }) {
32
33
  const pct = total > 0 ? (value / total) * 100 : 0;
33
34
  return (
34
- <div className="space-y-0.5">
35
- <div className="flex items-center justify-between text-xs font-mono">
36
- <span className="text-text-2">{label}</span>
37
- <span className="text-text-1 tabular-nums">{fmtNum(value)}</span>
35
+ <div className="space-y-1">
36
+ <div className="flex items-center justify-between">
37
+ <span className="text-xs font-mono text-text-2">{label}</span>
38
+ <div className="flex items-center gap-2">
39
+ <span className="text-xs font-mono font-semibold tabular-nums" style={{ color }}>
40
+ {Math.round(pct)}%
41
+ </span>
42
+ <span className="text-2xs font-mono text-text-3 tabular-nums w-10 text-right">{fmtNum(value)}</span>
43
+ </div>
38
44
  </div>
39
- <div className="h-[2px] bg-surface-4 rounded-full overflow-hidden">
45
+ <div className="h-[7px] rounded-full overflow-hidden" style={{ background: 'rgba(51,175,188,0.08)' }}>
40
46
  <div
41
47
  className="h-full rounded-full transition-all duration-500"
42
48
  style={{ width: `${Math.min(pct, 100)}%`, background: color }}
@@ -52,40 +58,95 @@ function RotationTab({ tokens, rotation }) {
52
58
  const totalSaved = savings.total || 0;
53
59
  const totalUsed = tokens?.totalTokens || 0;
54
60
  const hypothetical = totalUsed + totalSaved;
61
+ const efficiencyPct = hypothetical > 0 ? Math.round((totalSaved / hypothetical) * 100) : 0;
62
+
63
+ const recentHistory = (rotation?.history || []).slice(-10).reverse();
55
64
 
56
65
  return (
57
66
  <div className="p-3 space-y-4">
58
- <div className="flex gap-4">
59
- <div>
60
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Rotations</div>
61
- <div className="text-xl font-mono font-semibold text-text-0 tabular-nums leading-none">{rotation?.totalRotations || 0}</div>
67
+ {/* Hero stats */}
68
+ <div className="grid grid-cols-3 gap-2">
69
+ <div className="bg-surface-0 rounded p-2.5">
70
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Rotations</div>
71
+ <div className="text-2xl font-mono font-bold text-text-0 tabular-nums leading-none">
72
+ {rotation?.totalRotations || 0}
73
+ </div>
62
74
  </div>
63
- <div>
64
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-0.5">Saved</div>
65
- <div className="text-xl font-mono font-semibold text-success tabular-nums leading-none">{fmtNum(totalSaved)}</div>
66
- {hypothetical > 0 && (
67
- <div className="text-2xs font-mono text-text-3 mt-0.5">{fmtPct((totalSaved / hypothetical) * 100)} of total</div>
68
- )}
75
+ <div className="bg-surface-0 rounded p-2.5">
76
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Saved</div>
77
+ <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: '#4ae168' }}>
78
+ {fmtNum(totalSaved)}
79
+ </div>
80
+ </div>
81
+ <div className="bg-surface-0 rounded p-2.5">
82
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1">Efficiency</div>
83
+ <div className="text-2xl font-mono font-bold tabular-nums leading-none" style={{ color: '#33afbc' }}>
84
+ {efficiencyPct}%
85
+ </div>
69
86
  </div>
70
87
  </div>
71
- <div className="space-y-2">
88
+
89
+ {/* Savings breakdown */}
90
+ <div className="space-y-2.5">
72
91
  <SavingsBar label="Rotation" value={savings.fromRotation || 0} total={hypothetical} color={HEX.accent} />
73
- <SavingsBar label="Conflict prevention" value={savings.fromConflictPrevention || 0} total={hypothetical} color={HEX.purple} />
92
+ <SavingsBar label="Conflict prevention" value={savings.fromConflictPrevention || 0} total={hypothetical} color="#4ec9d4" />
74
93
  <SavingsBar label="Cold-start skip" value={savings.fromColdStartSkip || 0} total={hypothetical} color={HEX.info} />
75
94
  </div>
76
- {rotation?.history?.length > 0 && (
95
+
96
+ {/* Rotation timeline */}
97
+ {recentHistory.length > 0 ? (
77
98
  <div>
78
- <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-1.5">Recent</div>
79
- <div className="space-y-1">
80
- {rotation.history.slice(-8).reverse().map((r, i) => (
81
- <div key={i} className="flex items-center gap-2 text-xs font-mono px-2 py-1 bg-surface-0 rounded">
82
- <span className="text-text-1 font-medium capitalize truncate flex-1">{r.agentName || r.role}</span>
83
- <span className="text-text-3 tabular-nums">{fmtPct((r.contextUsage || 0) * 100)}</span>
84
- <span className="text-text-4">{timeAgo(r.timestamp)}</span>
99
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-wider mb-2.5">Recent Rotations</div>
100
+ <div className="space-y-0">
101
+ {recentHistory.map((r, i) => (
102
+ <div key={i} className="flex items-start gap-2.5">
103
+ {/* Dot + line column */}
104
+ <div className="flex flex-col items-center flex-shrink-0">
105
+ <div className="h-1.5" />
106
+ <div
107
+ className="w-2 h-2 rounded-full flex-shrink-0"
108
+ style={{
109
+ background: i === 0 ? '#33afbc' : 'rgba(51,175,188,0.15)',
110
+ border: '1px solid rgba(51,175,188,0.5)',
111
+ boxShadow: i === 0 ? '0 0 6px rgba(51,175,188,0.35)' : 'none',
112
+ }}
113
+ />
114
+ {i < recentHistory.length - 1 && (
115
+ <div
116
+ className="w-px flex-1 mt-1"
117
+ style={{ background: 'rgba(51,175,188,0.15)', minHeight: '12px' }}
118
+ />
119
+ )}
120
+ </div>
121
+
122
+ {/* Content card */}
123
+ <div className={cn('flex-1 bg-surface-0 rounded px-2 py-1.5', i < recentHistory.length - 1 && 'mb-2')}>
124
+ <div className="flex items-center gap-2">
125
+ <span className="text-xs font-mono text-text-1 font-medium capitalize truncate flex-1">
126
+ {r.agentName || r.role}
127
+ </span>
128
+ <span
129
+ className="text-2xs font-mono font-semibold tabular-nums flex-shrink-0"
130
+ style={{
131
+ color: (r.contextUsage || 0) > 0.8 ? '#e06c75' : (r.contextUsage || 0) > 0.6 ? '#e5c07b' : '#33afbc',
132
+ }}
133
+ >
134
+ {fmtPct((r.contextUsage || 0) * 100)}
135
+ </span>
136
+ <span className="text-2xs font-mono text-text-4 flex-shrink-0">{timeAgo(r.timestamp)}</span>
137
+ </div>
138
+ </div>
85
139
  </div>
86
140
  ))}
87
141
  </div>
88
142
  </div>
143
+ ) : (
144
+ <div className="bg-surface-0 rounded p-3 text-center space-y-1.5">
145
+ <div className="text-xs font-mono text-text-2 font-semibold">No rotations yet</div>
146
+ <div className="text-2xs font-mono text-text-3 leading-relaxed">
147
+ Rotations trigger when an agent's context exceeds its threshold, clearing memory while preserving progress via a handoff brief.
148
+ </div>
149
+ </div>
89
150
  )}
90
151
  </div>
91
152
  );
@@ -95,87 +156,146 @@ function RotationTab({ tokens, rotation }) {
95
156
  function AdaptiveTab({ adaptive }) {
96
157
  if (!adaptive?.length) {
97
158
  return (
98
- <div className="flex-1 flex items-center justify-center text-xs text-text-3 font-mono p-4">
99
- No adaptive profiles
159
+ <div className="p-3">
160
+ <div className="bg-surface-0 rounded p-4 text-center space-y-2">
161
+ <div className="text-xs font-mono text-text-2 font-semibold">No adaptive profiles yet</div>
162
+ <div className="text-2xs font-mono text-text-3 leading-relaxed">
163
+ 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.
164
+ </div>
165
+ </div>
100
166
  </div>
101
167
  );
102
168
  }
103
169
 
104
- // Split provider:role into readable parts
105
170
  function parseKey(key) {
106
171
  const parts = key.split(':');
107
172
  return { provider: parts[0] || key, role: parts[1] || '' };
108
173
  }
109
174
 
110
175
  return (
111
- <div>
112
- <div className="p-3 space-y-1">
113
- {/* Header */}
114
- <div className="flex items-center gap-2 px-2 pb-1 text-2xs font-mono text-text-4 uppercase tracking-wider">
115
- <span className="flex-1">Role</span>
116
- <span className="w-12 text-right">Threshold</span>
117
- <span className="w-12 text-right">Adj.</span>
118
- <span className="w-14 text-right">Status</span>
119
- </div>
120
-
121
- {adaptive.map((p) => {
122
- const { provider, role } = parseKey(p.key);
123
- const hasHistory = p.thresholdHistory?.length > 1;
124
- const hasScores = p.recentScores?.length > 1;
176
+ <div className="p-3 space-y-3">
177
+ {adaptive.map((p) => {
178
+ const { provider, role } = parseKey(p.key);
179
+ const displayRole = role || provider;
180
+ const hasHistory = p.thresholdHistory?.length > 1;
181
+ const hasScores = p.recentScores?.length > 1;
182
+ const rc = roleColor(displayRole);
183
+ const signals = p.lastSignals;
125
184
 
126
- return (
127
- <div key={p.key} className="bg-surface-0 rounded px-2 py-1.5">
128
- {/* Main row */}
129
- <div className="flex items-center gap-2">
130
- <div className="flex-1 min-w-0">
131
- <span className="text-xs font-mono text-text-1 capitalize">{role || provider}</span>
132
- {role && <span className="text-2xs font-mono text-text-4 ml-1.5">{provider}</span>}
133
- </div>
134
- <span className="w-12 text-right text-xs font-mono font-semibold text-text-0 tabular-nums">
135
- {fmtPct(p.threshold * 100)}
136
- </span>
137
- <span className="w-12 text-right text-2xs font-mono text-text-3 tabular-nums">
138
- {p.adjustments}
139
- </span>
140
- <span className={cn(
141
- 'w-14 text-right text-2xs font-mono font-bold uppercase',
142
- p.converged ? 'text-success' : 'text-text-4',
143
- )}>
144
- {p.converged ? 'Converged' : 'Learning'}
185
+ return (
186
+ <div
187
+ key={p.key}
188
+ className="rounded overflow-hidden"
189
+ style={{
190
+ background: 'rgba(51,175,188,0.04)',
191
+ borderLeft: p.converged ? '2px solid #33afbc' : '2px solid rgba(229,192,123,0.35)',
192
+ }}
193
+ >
194
+ {/* Card header */}
195
+ <div className="flex items-center gap-2 px-3 pt-2.5 pb-1.5">
196
+ <span
197
+ className="text-xs font-mono font-semibold capitalize px-1.5 py-px rounded-sm"
198
+ style={{ background: rc.bg, color: rc.text }}
199
+ >
200
+ {displayRole}
201
+ </span>
202
+ {role && (
203
+ <span className="text-2xs font-mono text-text-4 bg-surface-4 px-1.5 py-px rounded-sm">
204
+ {provider}
145
205
  </span>
146
- </div>
206
+ )}
207
+ <div className="flex-1" />
208
+ {/* Convergence pill */}
209
+ <span
210
+ className="flex items-center gap-1 text-2xs font-mono font-bold px-2 py-px rounded-full"
211
+ style={{
212
+ background: p.converged ? 'rgba(74,225,104,0.12)' : 'rgba(229,192,123,0.12)',
213
+ color: p.converged ? '#4ae168' : '#e5c07b',
214
+ }}
215
+ >
216
+ {!p.converged && (
217
+ <span
218
+ className="w-1.5 h-1.5 rounded-full flex-shrink-0"
219
+ style={{ background: '#e5c07b', animation: 'node-pulse-bar 1.5s ease-in-out infinite' }}
220
+ />
221
+ )}
222
+ {p.converged ? 'Converged' : 'Learning'}
223
+ </span>
224
+ </div>
147
225
 
148
- {/* Sparklines row (if data exists) */}
149
- {(hasHistory || hasScores) && (
150
- <div className="flex items-center gap-3 mt-1.5 pl-1">
151
- {hasHistory && (
152
- <div className="flex items-center gap-1.5">
153
- <span className="text-2xs font-mono text-text-4">Threshold</span>
154
- <TinySparkline
155
- data={p.thresholdHistory.map((h) => h.v)}
156
- color={p.converged ? HEX.success : HEX.accent}
157
- width={70}
158
- height={12}
159
- />
160
- </div>
161
- )}
162
- {hasScores && (
163
- <div className="flex items-center gap-1.5">
164
- <span className="text-2xs font-mono text-text-4">Quality</span>
165
- <TinySparkline
166
- data={p.recentScores}
167
- color={HEX.warning}
168
- width={70}
169
- height={12}
170
- />
171
- </div>
172
- )}
226
+ {/* Threshold hero + adjustments */}
227
+ <div className="flex items-end gap-5 px-3 pb-2">
228
+ <div>
229
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-0.5">Threshold</div>
230
+ <div
231
+ className="text-3xl font-mono font-bold tabular-nums leading-none"
232
+ style={{ color: p.converged ? '#33afbc' : '#e5c07b' }}
233
+ >
234
+ {fmtPct(p.threshold * 100)}
173
235
  </div>
174
- )}
236
+ </div>
237
+ <div className="pb-0.5">
238
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider mb-0.5">Adj.</div>
239
+ <div className="text-lg font-mono font-semibold text-text-1 tabular-nums">{p.adjustments}</div>
240
+ </div>
175
241
  </div>
176
- );
177
- })}
178
- </div>
242
+
243
+ {/* Sparklines */}
244
+ {(hasHistory || hasScores) && (
245
+ <div className="px-3 pb-2 space-y-2 overflow-hidden">
246
+ {hasHistory && (
247
+ <div>
248
+ <div className="text-2xs font-mono text-text-4 mb-0.5">Threshold history</div>
249
+ <TinySparkline
250
+ data={p.thresholdHistory.map((h) => h.v)}
251
+ color={p.converged ? HEX.accent : HEX.warning}
252
+ width={240}
253
+ height={32}
254
+ />
255
+ </div>
256
+ )}
257
+ {hasScores && (
258
+ <div>
259
+ <div className="text-2xs font-mono text-text-4 mb-0.5">Quality score</div>
260
+ <TinySparkline
261
+ data={p.recentScores}
262
+ color={HEX.warning}
263
+ width={240}
264
+ height={24}
265
+ />
266
+ </div>
267
+ )}
268
+ </div>
269
+ )}
270
+
271
+ {/* Signal pills */}
272
+ {signals && (
273
+ <div className="flex flex-wrap gap-1.5 px-3 pb-2.5">
274
+ {signals.errorCount != null && (
275
+ <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
276
+ Errors: <span className="text-danger">{signals.errorCount}</span>
277
+ </span>
278
+ )}
279
+ {signals.toolSuccessRate != null && (
280
+ <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
281
+ Tools: <span className="text-text-1">{Math.round(signals.toolSuccessRate * 100)}%</span>
282
+ </span>
283
+ )}
284
+ {signals.fileChurn != null && (
285
+ <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
286
+ Churn: <span className="text-text-1">{signals.fileChurn}</span>
287
+ </span>
288
+ )}
289
+ {signals.repetitions != null && (
290
+ <span className="text-2xs font-mono px-1.5 py-px rounded-sm bg-surface-4 text-text-3">
291
+ Reps: <span className="text-warning">{signals.repetitions}</span>
292
+ </span>
293
+ )}
294
+ </div>
295
+ )}
296
+ </div>
297
+ );
298
+ })}
179
299
  </div>
180
300
  );
181
301
  }
@@ -536,8 +536,15 @@ export const useGrooveStore = create((set, get) => ({
536
536
  // Auto-delegate — all agents already exist in the team
537
537
  set({ recommendedTeam: null });
538
538
  const result = await api.post('/recommended-team/launch');
539
- const names = result.agents?.map((a) => a.name).join(', ') || '';
539
+ const agents = result.agents || [];
540
+ const names = agents.map((a) => a.name).join(', ') || '';
540
541
  get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
542
+ // Set thinking indicator for all delegated agents so the UI shows activity
543
+ if (agents.length > 0) {
544
+ set((s) => ({
545
+ thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
546
+ }));
547
+ }
541
548
  api.post('/cleanup').catch(() => {});
542
549
  return;
543
550
  }
@@ -560,7 +567,14 @@ export const useGrooveStore = create((set, get) => ({
560
567
  result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
561
568
  result.projectDir ? `→ ${result.projectDir}/` : '',
562
569
  ].filter(Boolean).join(' · ');
563
- get().addToast('success', `Launched ${result.launched} agents`, sub || undefined);
570
+ get().addToast('success', `Launched ${(result.launched || 0) + (result.reused || 0)} agents`, sub || undefined);
571
+ // Set thinking indicator for all launched/reused agents
572
+ const launchedAgents = result.agents || [];
573
+ if (launchedAgents.length > 0) {
574
+ set((s) => ({
575
+ thinkingAgents: new Set([...s.thinkingAgents, ...launchedAgents.map((a) => a.id)]),
576
+ }));
577
+ }
564
578
  // Clean up stale files
565
579
  api.post('/cleanup').catch(() => {});
566
580
  return result;