groove-dev 0.25.2 → 0.25.4
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/skills.js +6 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B8gbMMHj.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/{index-DjEycx1f.js → index-DtW5ej1k.js} +150 -130
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +47 -55
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +217 -67
- package/package.json +1 -1
- package/packages/daemon/src/skills.js +6 -1
- package/packages/gui/dist/assets/index-B8gbMMHj.css +1 -0
- package/packages/gui/dist/assets/{index-DjEycx1f.js → index-DtW5ej1k.js} +150 -130
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/src/components/dashboard/intel-panel.jsx +47 -55
- package/packages/gui/src/views/teams.jsx +217 -67
- package/node_modules/@groove-dev/gui/dist/assets/index-CX6VBcgs.css +0 -1
- package/packages/gui/dist/assets/index-CX6VBcgs.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-DtW5ej1k.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-B8gbMMHj.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="root"></div>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
|
|
4
|
-
import { ScrollArea } from '../ui/scroll-area';
|
|
5
4
|
import { fmtNum, fmtPct, timeAgo } from '../../lib/format';
|
|
6
5
|
import { cn } from '../../lib/cn';
|
|
7
6
|
import { HEX } from '../../lib/theme-hex';
|
|
@@ -55,53 +54,40 @@ function RotationTab({ tokens, rotation }) {
|
|
|
55
54
|
const hypothetical = totalUsed + totalSaved;
|
|
56
55
|
|
|
57
56
|
return (
|
|
58
|
-
<
|
|
59
|
-
<div className="
|
|
60
|
-
|
|
61
|
-
|
|
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>
|
|
57
|
+
<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>
|
|
79
62
|
</div>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
)}
|
|
86
69
|
</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
70
|
</div>
|
|
104
|
-
|
|
71
|
+
<div className="space-y-2">
|
|
72
|
+
<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} />
|
|
74
|
+
<SavingsBar label="Cold-start skip" value={savings.fromColdStartSkip || 0} total={hypothetical} color={HEX.info} />
|
|
75
|
+
</div>
|
|
76
|
+
{rotation?.history?.length > 0 && (
|
|
77
|
+
<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>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
105
91
|
);
|
|
106
92
|
}
|
|
107
93
|
|
|
@@ -122,7 +108,7 @@ function AdaptiveTab({ adaptive }) {
|
|
|
122
108
|
}
|
|
123
109
|
|
|
124
110
|
return (
|
|
125
|
-
<
|
|
111
|
+
<div>
|
|
126
112
|
<div className="p-3 space-y-1">
|
|
127
113
|
{/* Header */}
|
|
128
114
|
<div className="flex items-center gap-2 px-2 pb-1 text-2xs font-mono text-text-4 uppercase tracking-wider">
|
|
@@ -190,7 +176,7 @@ function AdaptiveTab({ adaptive }) {
|
|
|
190
176
|
);
|
|
191
177
|
})}
|
|
192
178
|
</div>
|
|
193
|
-
</
|
|
179
|
+
</div>
|
|
194
180
|
);
|
|
195
181
|
}
|
|
196
182
|
|
|
@@ -205,7 +191,7 @@ function JournalistTab({ journalist }) {
|
|
|
205
191
|
}
|
|
206
192
|
|
|
207
193
|
return (
|
|
208
|
-
<
|
|
194
|
+
<div>
|
|
209
195
|
<div className="p-3 space-y-3">
|
|
210
196
|
{/* Status row */}
|
|
211
197
|
<div className="flex items-center gap-3">
|
|
@@ -274,7 +260,7 @@ function JournalistTab({ journalist }) {
|
|
|
274
260
|
</div>
|
|
275
261
|
)}
|
|
276
262
|
</div>
|
|
277
|
-
</
|
|
263
|
+
</div>
|
|
278
264
|
);
|
|
279
265
|
}
|
|
280
266
|
|
|
@@ -297,14 +283,20 @@ const IntelPanel = memo(function IntelPanel({ tokens, rotation, adaptive, journa
|
|
|
297
283
|
</TabsTrigger>
|
|
298
284
|
</TabsList>
|
|
299
285
|
|
|
300
|
-
<TabsContent value="rotation" className="flex-1 min-h-0
|
|
301
|
-
<
|
|
286
|
+
<TabsContent value="rotation" className="flex-1 min-h-0 relative">
|
|
287
|
+
<div className="absolute inset-0 overflow-y-auto">
|
|
288
|
+
<RotationTab tokens={tokens} rotation={rotation} />
|
|
289
|
+
</div>
|
|
302
290
|
</TabsContent>
|
|
303
|
-
<TabsContent value="adaptive" className="flex-1 min-h-0
|
|
304
|
-
<
|
|
291
|
+
<TabsContent value="adaptive" className="flex-1 min-h-0 relative">
|
|
292
|
+
<div className="absolute inset-0 overflow-y-auto">
|
|
293
|
+
<AdaptiveTab adaptive={adaptive} />
|
|
294
|
+
</div>
|
|
305
295
|
</TabsContent>
|
|
306
|
-
<TabsContent value="journalist" className="flex-1 min-h-0
|
|
307
|
-
<
|
|
296
|
+
<TabsContent value="journalist" className="flex-1 min-h-0 relative">
|
|
297
|
+
<div className="absolute inset-0 overflow-y-auto">
|
|
298
|
+
<JournalistTab journalist={journalist} />
|
|
299
|
+
</div>
|
|
308
300
|
</TabsContent>
|
|
309
301
|
</Tabs>
|
|
310
302
|
);
|
|
@@ -3,16 +3,129 @@ import { useState, useEffect } from 'react';
|
|
|
3
3
|
import { useGrooveStore } from '../stores/groove';
|
|
4
4
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../components/ui/tabs';
|
|
5
5
|
import { Button } from '../components/ui/button';
|
|
6
|
-
import { Card } from '../components/ui/card';
|
|
7
6
|
import { Badge } from '../components/ui/badge';
|
|
8
|
-
import {
|
|
9
|
-
import { Skeleton } from '../components/ui/skeleton';
|
|
7
|
+
import { StatusDot } from '../components/ui/status-dot';
|
|
10
8
|
import { api } from '../lib/api';
|
|
11
9
|
import { useToast } from '../lib/hooks/use-toast';
|
|
12
|
-
import { timeAgo } from '../lib/format';
|
|
13
|
-
import {
|
|
10
|
+
import { fmtNum, fmtDollar, timeAgo, fmtUptime } from '../lib/format';
|
|
11
|
+
import { cn } from '../lib/cn';
|
|
12
|
+
import {
|
|
13
|
+
Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX,
|
|
14
|
+
Users, Folder, Cpu, Trash2, Play, Pause, LayoutDashboard, ListChecks, Calendar,
|
|
15
|
+
} from 'lucide-react';
|
|
14
16
|
|
|
15
|
-
// ──
|
|
17
|
+
// ── Team Dashboard ────────────────────────────────────────────
|
|
18
|
+
function TeamsDashboard() {
|
|
19
|
+
const teams = useGrooveStore((s) => s.teams);
|
|
20
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
21
|
+
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
22
|
+
const deleteTeam = useGrooveStore((s) => s.deleteTeam);
|
|
23
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
24
|
+
|
|
25
|
+
if (teams.length === 0) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="flex-1 flex items-center justify-center">
|
|
28
|
+
<div className="text-center space-y-2">
|
|
29
|
+
<Users size={28} className="mx-auto text-text-4" />
|
|
30
|
+
<p className="text-xs font-sans text-text-3">No teams yet</p>
|
|
31
|
+
<p className="text-2xs font-sans text-text-4">Teams are created when you spawn agents or launch a planner</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex-1 overflow-y-auto">
|
|
39
|
+
<div className="p-4 space-y-3">
|
|
40
|
+
{teams.map((team) => {
|
|
41
|
+
const teamAgents = agents.filter((a) => a.teamId === team.id);
|
|
42
|
+
const running = teamAgents.filter((a) => a.status === 'running' || a.status === 'starting');
|
|
43
|
+
const completed = teamAgents.filter((a) => a.status === 'completed');
|
|
44
|
+
const crashed = teamAgents.filter((a) => a.status === 'crashed');
|
|
45
|
+
const totalTokens = teamAgents.reduce((s, a) => s + (a.tokensUsed || 0), 0);
|
|
46
|
+
const totalCost = teamAgents.reduce((s, a) => s + (a.costUsd || 0), 0);
|
|
47
|
+
const isActive = team.id === activeTeamId;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
key={team.id}
|
|
52
|
+
className={cn(
|
|
53
|
+
'rounded-md border bg-surface-1 overflow-hidden transition-colors',
|
|
54
|
+
isActive ? 'border-accent/30' : 'border-border-subtle',
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{/* Header */}
|
|
58
|
+
<div className="px-4 py-3 flex items-center gap-3">
|
|
59
|
+
<div className="flex-1 min-w-0">
|
|
60
|
+
<div className="flex items-center gap-2">
|
|
61
|
+
<span className="text-sm font-semibold text-text-0 font-sans">{team.name}</span>
|
|
62
|
+
{isActive && <Badge variant="accent" className="text-2xs">Active</Badge>}
|
|
63
|
+
</div>
|
|
64
|
+
{team.workingDir && (
|
|
65
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
66
|
+
<Folder size={10} className="text-text-4" />
|
|
67
|
+
<span className="text-2xs font-mono text-text-3 truncate">{team.workingDir}</span>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => {
|
|
73
|
+
if (teamAgents.some((a) => a.status === 'running')) {
|
|
74
|
+
addToast('error', 'Stop running agents first');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
deleteTeam(team.id);
|
|
78
|
+
}}
|
|
79
|
+
className="p-1.5 text-text-4 hover:text-danger rounded transition-colors cursor-pointer"
|
|
80
|
+
title="Delete team"
|
|
81
|
+
>
|
|
82
|
+
<Trash2 size={13} />
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Stats row */}
|
|
87
|
+
<div className="px-4 py-2.5 border-t border-border-subtle bg-surface-0 flex items-center gap-4">
|
|
88
|
+
<Stat label="Agents" value={teamAgents.length} />
|
|
89
|
+
<Stat label="Running" value={running.length} color={running.length > 0 ? 'text-success' : undefined} />
|
|
90
|
+
<Stat label="Done" value={completed.length} />
|
|
91
|
+
<Stat label="Crashed" value={crashed.length} color={crashed.length > 0 ? 'text-danger' : undefined} />
|
|
92
|
+
<div className="flex-1" />
|
|
93
|
+
<Stat label="Tokens" value={fmtNum(totalTokens)} />
|
|
94
|
+
{totalCost > 0 && <Stat label="Cost" value={fmtDollar(totalCost)} />}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Agent list */}
|
|
98
|
+
{teamAgents.length > 0 && (
|
|
99
|
+
<div className="border-t border-border-subtle">
|
|
100
|
+
{teamAgents.map((a) => (
|
|
101
|
+
<div key={a.id} className="flex items-center gap-2 px-4 py-1.5 border-b border-border-subtle last:border-b-0">
|
|
102
|
+
<StatusDot status={a.status} size="sm" />
|
|
103
|
+
<span className="text-xs font-semibold text-text-0 font-sans truncate">{a.name}</span>
|
|
104
|
+
<span className="text-2xs font-mono text-text-3 uppercase">{a.role}</span>
|
|
105
|
+
<div className="flex-1" />
|
|
106
|
+
<span className="text-2xs font-mono text-text-2 tabular-nums">{fmtNum(a.tokensUsed || 0)}</span>
|
|
107
|
+
</div>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function Stat({ label, value, color }) {
|
|
120
|
+
return (
|
|
121
|
+
<div className="text-center">
|
|
122
|
+
<div className={cn('text-xs font-mono tabular-nums', color || 'text-text-1')}>{value}</div>
|
|
123
|
+
<div className="text-2xs font-mono text-text-4 uppercase tracking-wider">{label}</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Approvals ─────────────────────────────────────────────────
|
|
16
129
|
function PendingApprovals() {
|
|
17
130
|
const pending = useGrooveStore((s) => s.pendingApprovals);
|
|
18
131
|
const approveRequest = useGrooveStore((s) => s.approveRequest);
|
|
@@ -23,24 +136,24 @@ function PendingApprovals() {
|
|
|
23
136
|
return (
|
|
24
137
|
<div className="px-4 pt-4 space-y-2">
|
|
25
138
|
<div className="flex items-center gap-2 mb-1">
|
|
26
|
-
<AlertTriangle size={
|
|
27
|
-
<span className="text-
|
|
139
|
+
<AlertTriangle size={12} className="text-warning" />
|
|
140
|
+
<span className="text-2xs font-mono text-warning uppercase tracking-wider">Pending ({pending.length})</span>
|
|
28
141
|
</div>
|
|
29
142
|
{pending.map((item) => (
|
|
30
|
-
<div key={item.id} className="flex items-center gap-3 px-3 py-2
|
|
143
|
+
<div key={item.id} className="flex items-center gap-3 px-3 py-2 rounded-md bg-warning/5 border border-warning/20">
|
|
31
144
|
<div className="flex-1 min-w-0">
|
|
32
145
|
<div className="text-xs text-text-0 font-sans font-medium truncate">
|
|
33
146
|
{item.agentName}: {item.action?.description || item.action?.type || 'action'}
|
|
34
147
|
</div>
|
|
35
|
-
{item.action?.filePath && <div className="text-2xs text-text-3
|
|
36
|
-
<div className="text-2xs text-text-4 font-
|
|
148
|
+
{item.action?.filePath && <div className="text-2xs font-mono text-text-3 truncate mt-0.5">{item.action.filePath}</div>}
|
|
149
|
+
<div className="text-2xs text-text-4 font-mono mt-0.5">{timeAgo(item.requestedAt)}</div>
|
|
37
150
|
</div>
|
|
38
151
|
<div className="flex gap-1.5 flex-shrink-0">
|
|
39
152
|
<Button variant="primary" size="sm" onClick={() => approveRequest(item.id)} className="h-7 px-2.5 gap-1 text-2xs">
|
|
40
|
-
<ShieldCheck size={
|
|
153
|
+
<ShieldCheck size={10} /> Approve
|
|
41
154
|
</Button>
|
|
42
155
|
<Button variant="danger" size="sm" onClick={() => rejectRequest(item.id)} className="h-7 px-2.5 gap-1 text-2xs">
|
|
43
|
-
<ShieldX size={
|
|
156
|
+
<ShieldX size={10} /> Reject
|
|
44
157
|
</Button>
|
|
45
158
|
</div>
|
|
46
159
|
</div>
|
|
@@ -49,7 +162,6 @@ function PendingApprovals() {
|
|
|
49
162
|
);
|
|
50
163
|
}
|
|
51
164
|
|
|
52
|
-
// ── Resolved Approvals / History ───────────────────────────
|
|
53
165
|
function ApprovalsTab() {
|
|
54
166
|
const resolved = useGrooveStore((s) => s.resolvedApprovals);
|
|
55
167
|
const [pmHistory, setPmHistory] = useState([]);
|
|
@@ -69,7 +181,6 @@ function ApprovalsTab() {
|
|
|
69
181
|
setLoading(false);
|
|
70
182
|
}
|
|
71
183
|
|
|
72
|
-
// Merge PM history with real-time resolved approvals, dedup by id
|
|
73
184
|
const seen = new Set();
|
|
74
185
|
const allHistory = [...resolved, ...pmHistory].filter((item) => {
|
|
75
186
|
const key = item.id || `${item.agentName}-${item.timestamp}`;
|
|
@@ -78,45 +189,49 @@ function ApprovalsTab() {
|
|
|
78
189
|
return true;
|
|
79
190
|
});
|
|
80
191
|
|
|
81
|
-
if (loading && allHistory.length === 0) {
|
|
82
|
-
return <div className="p-4 space-y-2">{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-12 rounded-md" />)}</div>;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
192
|
return (
|
|
86
|
-
<
|
|
193
|
+
<div className="flex-1 overflow-y-auto">
|
|
87
194
|
<PendingApprovals />
|
|
88
|
-
<div className="p-4 space-y-
|
|
89
|
-
{allHistory.length === 0 && (
|
|
90
|
-
<div className="text-center py-12 text-text-4 font-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
195
|
+
<div className="p-4 space-y-1.5">
|
|
196
|
+
{loading && allHistory.length === 0 && (
|
|
197
|
+
<div className="text-center py-12 text-text-4 font-mono text-xs">Loading...</div>
|
|
198
|
+
)}
|
|
199
|
+
{!loading && allHistory.length === 0 && (
|
|
200
|
+
<div className="text-center py-12">
|
|
201
|
+
<CheckCircle size={24} className="mx-auto mb-2 text-text-4" />
|
|
202
|
+
<p className="text-xs font-sans text-text-3">No approval history</p>
|
|
203
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Approvals appear when agents use Auto permission mode</p>
|
|
94
204
|
</div>
|
|
95
205
|
)}
|
|
96
|
-
{allHistory.map((item, i) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
206
|
+
{allHistory.map((item, i) => {
|
|
207
|
+
const approved = item.status === 'approved' || item.verdict === 'approved';
|
|
208
|
+
return (
|
|
209
|
+
<div key={item.id || i} className="flex items-center gap-2.5 px-3 py-2 rounded-md bg-surface-0 border border-border-subtle">
|
|
210
|
+
{approved ? (
|
|
211
|
+
<CheckCircle size={12} className="text-success flex-shrink-0" />
|
|
212
|
+
) : (
|
|
213
|
+
<XCircle size={12} className="text-danger flex-shrink-0" />
|
|
214
|
+
)}
|
|
215
|
+
<div className="flex-1 min-w-0">
|
|
216
|
+
<div className="text-xs text-text-1 font-sans truncate">
|
|
217
|
+
<span className="font-medium text-text-0">{item.agentName}</span>
|
|
218
|
+
<span className="text-text-3 mx-1">·</span>
|
|
219
|
+
<span>{item.action?.description || item.action || 'action'}</span>
|
|
220
|
+
</div>
|
|
221
|
+
{item.reason && <div className="text-2xs text-text-3 font-sans truncate mt-0.5">{item.reason}</div>}
|
|
106
222
|
</div>
|
|
107
|
-
|
|
223
|
+
<span className="text-2xs font-mono text-text-4 flex-shrink-0">
|
|
224
|
+
{timeAgo(item.resolvedAt || item.timestamp)}
|
|
225
|
+
</span>
|
|
108
226
|
</div>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
</span>
|
|
112
|
-
</div>
|
|
113
|
-
))}
|
|
227
|
+
);
|
|
228
|
+
})}
|
|
114
229
|
</div>
|
|
115
|
-
</
|
|
230
|
+
</div>
|
|
116
231
|
);
|
|
117
232
|
}
|
|
118
233
|
|
|
119
|
-
// ── Schedules
|
|
234
|
+
// ── Schedules ─────────────────────────────────────────────────
|
|
120
235
|
function SchedulesTab() {
|
|
121
236
|
const [schedules, setSchedules] = useState([]);
|
|
122
237
|
const [loading, setLoading] = useState(true);
|
|
@@ -145,58 +260,93 @@ function SchedulesTab() {
|
|
|
145
260
|
}
|
|
146
261
|
}
|
|
147
262
|
|
|
148
|
-
if (loading) {
|
|
149
|
-
return <div className="p-4 space-y-2">{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-16 rounded-md" />)}</div>;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
263
|
return (
|
|
153
|
-
<
|
|
264
|
+
<div className="flex-1 overflow-y-auto">
|
|
154
265
|
<div className="p-4 space-y-2">
|
|
155
|
-
{schedules.length === 0 && (
|
|
156
|
-
<div className="text-center py-12 text-text-4 font-
|
|
157
|
-
|
|
158
|
-
|
|
266
|
+
{loading && schedules.length === 0 && (
|
|
267
|
+
<div className="text-center py-12 text-text-4 font-mono text-xs">Loading...</div>
|
|
268
|
+
)}
|
|
269
|
+
{!loading && schedules.length === 0 && (
|
|
270
|
+
<div className="text-center py-12">
|
|
271
|
+
<Calendar size={24} className="mx-auto mb-2 text-text-4" />
|
|
272
|
+
<p className="text-xs font-sans text-text-3">No schedules configured</p>
|
|
273
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Use the CLI to create agent schedules</p>
|
|
159
274
|
</div>
|
|
160
275
|
)}
|
|
161
276
|
{schedules.map((s) => (
|
|
162
|
-
<
|
|
163
|
-
<div className="flex items-center gap-3">
|
|
277
|
+
<div key={s.id} className="rounded-md border border-border-subtle bg-surface-0 overflow-hidden">
|
|
278
|
+
<div className="flex items-center gap-3 px-4 py-3">
|
|
164
279
|
<div className="flex-1 min-w-0">
|
|
165
280
|
<div className="flex items-center gap-2">
|
|
166
|
-
<span className="text-
|
|
167
|
-
<Badge variant={s.enabled ? 'success' : 'default'}
|
|
281
|
+
<span className="text-xs font-semibold text-text-0 font-sans">{s.name}</span>
|
|
282
|
+
<Badge variant={s.enabled ? 'success' : 'default'} className="text-2xs">
|
|
283
|
+
{s.enabled ? 'Active' : 'Paused'}
|
|
284
|
+
</Badge>
|
|
285
|
+
</div>
|
|
286
|
+
<div className="flex items-center gap-2 mt-1">
|
|
287
|
+
<span className="text-2xs font-mono text-text-2">{s.cron}</span>
|
|
288
|
+
<span className="text-2xs text-text-4">·</span>
|
|
289
|
+
<span className="text-2xs font-mono text-text-3 uppercase">{s.role}</span>
|
|
290
|
+
{s.teamId && (
|
|
291
|
+
<>
|
|
292
|
+
<span className="text-2xs text-text-4">·</span>
|
|
293
|
+
<span className="text-2xs font-sans text-text-3">{s.teamName || s.teamId}</span>
|
|
294
|
+
</>
|
|
295
|
+
)}
|
|
168
296
|
</div>
|
|
169
|
-
|
|
297
|
+
{s.prompt && (
|
|
298
|
+
<div className="text-2xs font-sans text-text-4 mt-1 truncate">{s.prompt}</div>
|
|
299
|
+
)}
|
|
170
300
|
</div>
|
|
171
301
|
<Button
|
|
172
|
-
variant="
|
|
302
|
+
variant="secondary"
|
|
173
303
|
size="sm"
|
|
174
304
|
onClick={() => toggleSchedule(s.id, s.enabled)}
|
|
305
|
+
className="h-7 px-2.5 gap-1 text-2xs"
|
|
175
306
|
>
|
|
176
|
-
{s.enabled ?
|
|
307
|
+
{s.enabled ? <><Pause size={10} /> Pause</> : <><Play size={10} /> Enable</>}
|
|
177
308
|
</Button>
|
|
178
309
|
</div>
|
|
179
|
-
|
|
310
|
+
{s.lastRunAt && (
|
|
311
|
+
<div className="px-4 py-1.5 border-t border-border-subtle bg-surface-1 text-2xs font-mono text-text-4">
|
|
312
|
+
Last run: {timeAgo(s.lastRunAt)}
|
|
313
|
+
{s.nextRunAt && <span className="ml-3">Next: {timeAgo(s.nextRunAt)}</span>}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
180
317
|
))}
|
|
181
318
|
</div>
|
|
182
|
-
</
|
|
319
|
+
</div>
|
|
183
320
|
);
|
|
184
321
|
}
|
|
185
322
|
|
|
186
|
-
// ── Main View
|
|
323
|
+
// ── Main View ─────────────────────────────────────────────────
|
|
187
324
|
export default function TeamsView() {
|
|
188
325
|
return (
|
|
189
|
-
<Tabs defaultValue="
|
|
326
|
+
<Tabs defaultValue="dashboard" className="flex flex-col h-full">
|
|
190
327
|
<div className="px-4 pt-3 bg-surface-1 border-b border-border">
|
|
191
328
|
<div className="flex items-center gap-4 mb-0">
|
|
192
|
-
<h2 className="text-
|
|
329
|
+
<h2 className="text-xs font-semibold text-text-0 font-sans tracking-wide uppercase">Management</h2>
|
|
193
330
|
</div>
|
|
194
331
|
<TabsList className="border-b-0">
|
|
195
|
-
<TabsTrigger value="
|
|
196
|
-
|
|
332
|
+
<TabsTrigger value="dashboard" className="inline-flex items-center gap-1.5">
|
|
333
|
+
<LayoutDashboard size={12} />
|
|
334
|
+
Teams
|
|
335
|
+
</TabsTrigger>
|
|
336
|
+
<TabsTrigger value="approvals" className="inline-flex items-center gap-1.5">
|
|
337
|
+
<ListChecks size={12} />
|
|
338
|
+
Approvals
|
|
339
|
+
</TabsTrigger>
|
|
340
|
+
<TabsTrigger value="schedules" className="inline-flex items-center gap-1.5">
|
|
341
|
+
<Calendar size={12} />
|
|
342
|
+
Schedules
|
|
343
|
+
</TabsTrigger>
|
|
197
344
|
</TabsList>
|
|
198
345
|
</div>
|
|
199
346
|
|
|
347
|
+
<TabsContent value="dashboard" className="flex-1 min-h-0">
|
|
348
|
+
<TeamsDashboard />
|
|
349
|
+
</TabsContent>
|
|
200
350
|
<TabsContent value="approvals" className="flex-1 min-h-0">
|
|
201
351
|
<ApprovalsTab />
|
|
202
352
|
</TabsContent>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.4",
|
|
4
4
|
"description": "Open-source agent orchestration layer — the AI company OS. 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.",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
|
|
@@ -264,7 +264,12 @@ export class SkillStore {
|
|
|
264
264
|
* Downloads content from live API, falls back to contentUrl, then local plugins.
|
|
265
265
|
*/
|
|
266
266
|
async install(skillId) {
|
|
267
|
-
|
|
267
|
+
let entry = this.registry.find((s) => s.id === skillId);
|
|
268
|
+
// Registry may be stale — refresh and retry lookup
|
|
269
|
+
if (!entry) {
|
|
270
|
+
await this._refreshRegistry();
|
|
271
|
+
entry = this.registry.find((s) => s.id === skillId);
|
|
272
|
+
}
|
|
268
273
|
if (!entry) throw new Error(`Skill not found: ${skillId}`);
|
|
269
274
|
if (this._isInstalled(skillId)) throw new Error(`Skill already installed: ${skillId}`);
|
|
270
275
|
|