groove-dev 0.26.29 → 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.
- package/node_modules/@groove-dev/daemon/src/api.js +3 -4
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BnNZzcsd.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index--XNm9lTq.js → index-vxioP1y2.js} +99 -99
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +78 -35
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +211 -91
- package/node_modules/@groove-dev/gui/src/stores/groove.js +16 -2
- package/package.json +2 -2
- package/packages/daemon/src/api.js +3 -4
- package/packages/daemon/src/gateways/manager.js +1 -1
- package/packages/daemon/src/process.js +2 -1
- package/packages/gui/dist/assets/index-BnNZzcsd.css +1 -0
- package/packages/gui/dist/assets/{index--XNm9lTq.js → index-vxioP1y2.js} +99 -99
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +78 -35
- package/packages/gui/src/components/dashboard/intel-panel.jsx +211 -91
- package/packages/gui/src/stores/groove.js +16 -2
- package/node_modules/@groove-dev/gui/dist/assets/index-CGFWAGJ6.css +0 -1
- package/packages/gui/dist/assets/index-CGFWAGJ6.css +0 -1
|
@@ -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
|
|
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-
|
|
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-
|
|
20
|
-
{/* Top row
|
|
21
|
-
<div className="flex items-center gap-2">
|
|
22
|
-
{/* Status
|
|
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="
|
|
35
|
-
|
|
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
|
|
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
|
-
{/*
|
|
93
|
+
{/* Context bar */}
|
|
75
94
|
<div className="flex items-center gap-2">
|
|
76
|
-
<div
|
|
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-[
|
|
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
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
<
|
|
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-
|
|
35
|
-
<div className="flex items-center justify-between
|
|
36
|
-
<span className="text-text-2">{label}</span>
|
|
37
|
-
<
|
|
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-[
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<div className="text-
|
|
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-
|
|
65
|
-
<div className="text-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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-
|
|
79
|
-
<div className="space-y-
|
|
80
|
-
{
|
|
81
|
-
<div key={i} className="flex items-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
|
|
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="
|
|
99
|
-
|
|
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
|
-
|
|
113
|
-
{
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.30",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"dev:gui": "npm run dev -w packages/gui",
|
|
54
54
|
"build": "npm run build -w packages/gui",
|
|
55
55
|
"build:gui": "npm run build -w packages/gui",
|
|
56
|
-
"test": "
|
|
56
|
+
"test": "node --test packages/daemon/test/*.test.js",
|
|
57
57
|
"prepublishOnly": "npm run build:gui"
|
|
58
58
|
},
|
|
59
59
|
"bundledDependencies": [
|
|
@@ -1799,7 +1799,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1799
1799
|
phase2 = [{
|
|
1800
1800
|
name: 'qc-agent',
|
|
1801
1801
|
role: 'fullstack', phase: 2, scope: [],
|
|
1802
|
-
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests,
|
|
1802
|
+
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
|
|
1803
1803
|
}];
|
|
1804
1804
|
}
|
|
1805
1805
|
|
|
@@ -1814,12 +1814,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1814
1814
|
const prompt = config.prompt || '';
|
|
1815
1815
|
|
|
1816
1816
|
// Reuse an existing agent with matching role in this team — never spawn
|
|
1817
|
-
// duplicates. The team's agents persist across tasks.
|
|
1817
|
+
// duplicates. The team's agents persist across tasks regardless of status.
|
|
1818
1818
|
const existing = teamAgents.find((a) =>
|
|
1819
1819
|
a.role === config.role &&
|
|
1820
1820
|
a.role !== 'planner' &&
|
|
1821
|
-
(
|
|
1822
|
-
!reused.some((r) => r.id === a.id) // don't reuse the same agent twice
|
|
1821
|
+
!reused.some((r) => r.id === a.id)
|
|
1823
1822
|
);
|
|
1824
1823
|
|
|
1825
1824
|
if (existing && prompt) {
|
|
@@ -785,7 +785,7 @@ export class GatewayManager {
|
|
|
785
785
|
if (phase2.length === 0 && phase1.length >= 2) {
|
|
786
786
|
phase2 = [{
|
|
787
787
|
role: 'fullstack', phase: 2, scope: [],
|
|
788
|
-
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests,
|
|
788
|
+
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers. Commit all changes.',
|
|
789
789
|
}];
|
|
790
790
|
}
|
|
791
791
|
|