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.
@@ -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-DjEycx1f.js"></script>
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-CX6VBcgs.css">
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
- <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>
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
- {/* 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} />
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
- </ScrollArea>
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
- <ScrollArea className="flex-1">
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
- </ScrollArea>
179
+ </div>
194
180
  );
195
181
  }
196
182
 
@@ -205,7 +191,7 @@ function JournalistTab({ journalist }) {
205
191
  }
206
192
 
207
193
  return (
208
- <ScrollArea className="flex-1">
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
- </ScrollArea>
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 overflow-hidden">
301
- <RotationTab tokens={tokens} rotation={rotation} />
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 overflow-hidden">
304
- <AdaptiveTab adaptive={adaptive} />
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 overflow-hidden">
307
- <JournalistTab journalist={journalist} />
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 { ScrollArea } from '../components/ui/scroll-area';
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 { Clock, CheckCircle, XCircle, AlertTriangle, ShieldCheck, ShieldX } from 'lucide-react';
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
- // ── Pending Approvals ──────────────────────────────────────
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={13} className="text-warning" />
27
- <span className="text-xs font-semibold text-warning font-sans">Pending Approval ({pending.length})</span>
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.5 rounded-md bg-warning/5 border border-warning/20">
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 font-mono truncate mt-0.5">{item.action.filePath}</div>}
36
- <div className="text-2xs text-text-4 font-sans mt-0.5">{timeAgo(item.requestedAt)}</div>
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={11} /> Approve
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={11} /> Reject
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
- <ScrollArea className="flex-1">
193
+ <div className="flex-1 overflow-y-auto">
87
194
  <PendingApprovals />
88
- <div className="p-4 space-y-2">
89
- {allHistory.length === 0 && (
90
- <div className="text-center py-12 text-text-4 font-sans">
91
- <CheckCircle size={32} className="mx-auto mb-2" />
92
- <p className="text-sm">No approval history</p>
93
- <p className="text-2xs text-text-4 mt-1">Approvals appear when agents use "Agent Approve" permission mode</p>
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
- <div key={item.id || i} className="flex items-center gap-3 px-3 py-2 rounded-md bg-surface-1 border border-border-subtle">
98
- {(item.status === 'approved' || item.verdict === 'approved') ? (
99
- <CheckCircle size={14} className="text-success flex-shrink-0" />
100
- ) : (
101
- <XCircle size={14} className="text-danger flex-shrink-0" />
102
- )}
103
- <div className="flex-1 min-w-0">
104
- <div className="text-xs text-text-0 font-sans truncate">
105
- {item.agentName}: {item.action?.description || item.action || 'action'}
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
- {item.reason && <div className="text-2xs text-text-3 font-sans truncate">{item.reason}</div>}
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
- <span className="text-2xs text-text-4 font-sans flex-shrink-0">
110
- {timeAgo(item.resolvedAt || item.timestamp)}
111
- </span>
112
- </div>
113
- ))}
227
+ );
228
+ })}
114
229
  </div>
115
- </ScrollArea>
230
+ </div>
116
231
  );
117
232
  }
118
233
 
119
- // ── Schedules Tab ──────────────────────────────────────────
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
- <ScrollArea className="flex-1">
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-sans">
157
- <Clock size={32} className="mx-auto mb-2" />
158
- <p className="text-sm">No schedules configured</p>
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
- <Card key={s.id} className="p-3">
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-sm font-semibold text-text-0 font-sans">{s.name}</span>
167
- <Badge variant={s.enabled ? 'success' : 'default'}>{s.enabled ? 'Active' : 'Paused'}</Badge>
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
- <div className="text-2xs text-text-3 font-mono mt-0.5">{s.cron} · {s.role}</div>
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="ghost"
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 ? 'Pause' : 'Enable'}
307
+ {s.enabled ? <><Pause size={10} /> Pause</> : <><Play size={10} /> Enable</>}
177
308
  </Button>
178
309
  </div>
179
- </Card>
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
- </ScrollArea>
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="approvals" className="flex flex-col h-full">
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-base font-semibold text-text-0 font-sans">Management</h2>
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="approvals">Approvals</TabsTrigger>
196
- <TabsTrigger value="schedules">Schedules</TabsTrigger>
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.2",
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
- const entry = this.registry.find((s) => s.id === skillId);
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