groove-dev 0.27.174 → 0.27.179
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/AUTONOMOUS_AGENT_FRAMEWORK.md +654 -0
- package/innerchat/Screenshot_2026-06-09_at_12.09.42_PM.png +0 -0
- package/innerchat/Screenshot_2026-06-09_at_2.19.42_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +3 -1
- package/node_modules/@groove-dev/daemon/src/autostate.js +198 -0
- package/node_modules/@groove-dev/daemon/src/index.js +6 -0
- package/node_modules/@groove-dev/daemon/src/orchestrator.js +585 -0
- package/node_modules/@groove-dev/daemon/src/process.js +27 -2
- package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
- package/node_modules/@groove-dev/daemon/src/routes/auto-agents.js +264 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CTer01Vg.js +1065 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +53 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +95 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +50 -1
- package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
- package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
- package/node_modules/@groove-dev/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +16 -3
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +14 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +2 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/auto-agents-slice.js +149 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +14 -11
- package/node_modules/@groove-dev/gui/src/views/auto-agents.jsx +70 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -6
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +3 -1
- package/packages/daemon/src/autostate.js +198 -0
- package/packages/daemon/src/index.js +6 -0
- package/packages/daemon/src/orchestrator.js +585 -0
- package/packages/daemon/src/process.js +27 -2
- package/packages/daemon/src/registry.js +2 -1
- package/packages/daemon/src/routes/auto-agents.js +264 -0
- package/packages/daemon/src/validate.js +6 -0
- package/packages/gui/dist/assets/index-CTer01Vg.js +1065 -0
- package/packages/gui/dist/assets/index-DTFtRtkx.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +2 -0
- package/packages/gui/src/components/agents/agent-config.jsx +53 -0
- package/packages/gui/src/components/agents/agent-feed.jsx +4 -4
- package/packages/gui/src/components/agents/agent-panel.jsx +95 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +50 -1
- package/packages/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
- package/packages/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
- package/packages/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
- package/packages/gui/src/components/fleet/fleet-pane.jsx +16 -3
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/groove.js +14 -0
- package/packages/gui/src/stores/slices/agents-slice.js +2 -0
- package/packages/gui/src/stores/slices/auto-agents-slice.js +149 -0
- package/packages/gui/src/stores/slices/ui-slice.js +14 -11
- package/packages/gui/src/views/auto-agents.jsx +70 -0
- package/packages/gui/src/views/settings.jsx +13 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-BvJwMNAX.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DZY-EWPs.js +0 -1030
- package/packages/gui/dist/assets/index-BvJwMNAX.css +0 -1
- package/packages/gui/dist/assets/index-DZY-EWPs.js +0 -1030
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Check, Users, Plus,
|
|
16
16
|
} from 'lucide-react';
|
|
17
17
|
import { api } from '../../lib/api';
|
|
18
|
+
import { useToast } from '../../lib/hooks/use-toast';
|
|
18
19
|
import { Dialog, DialogContent } from '../ui/dialog';
|
|
19
20
|
import { INTEGRATION_LOGOS } from '../../lib/integration-logos';
|
|
20
21
|
|
|
@@ -52,6 +53,7 @@ export function SpawnWizard() {
|
|
|
52
53
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
53
54
|
const closeDetail = useGrooveStore((s) => s.closeDetail);
|
|
54
55
|
const spawnAgent = useGrooveStore((s) => s.spawnAgent);
|
|
56
|
+
const toast = useToast();
|
|
55
57
|
const fetchProviders = useGrooveStore((s) => s.fetchProviders);
|
|
56
58
|
const teams = useGrooveStore((s) => s.teams);
|
|
57
59
|
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
@@ -84,6 +86,8 @@ export function SpawnWizard() {
|
|
|
84
86
|
const [ollamaInstalled, setOllamaInstalled] = useState([]);
|
|
85
87
|
const [ollamaServerRunning, setOllamaServerRunning] = useState(false);
|
|
86
88
|
const [fast, setFast] = useState(false);
|
|
89
|
+
const [authMode, setAuthMode] = useState('subscription');
|
|
90
|
+
const [claudeHasKey, setClaudeHasKey] = useState(false);
|
|
87
91
|
const [teamMode, setTeamMode] = useState('new');
|
|
88
92
|
const [newTeamName, setNewTeamName] = useState('');
|
|
89
93
|
const [selectedTeamId, setSelectedTeamId] = useState('');
|
|
@@ -142,7 +146,7 @@ export function SpawnWizard() {
|
|
|
142
146
|
setPersonalities(Array.isArray(data) ? data : data.personalities || []);
|
|
143
147
|
}).catch(() => {});
|
|
144
148
|
setRole(''); setCustomRole(''); setName('');
|
|
145
|
-
setProvider(_presetProvider); setModel(_presetModel); setFast(false);
|
|
149
|
+
setProvider(_presetProvider); setModel(_presetModel); setFast(false); setAuthMode('subscription');
|
|
146
150
|
setTeamMode('new'); setSelectedTeamId('');
|
|
147
151
|
setNewTeamName(_presetModel
|
|
148
152
|
? _presetModel.split(':').pop().replace(/[-_]/g, ' ')
|
|
@@ -173,6 +177,13 @@ export function SpawnWizard() {
|
|
|
173
177
|
}).catch(() => setRecommendations([]));
|
|
174
178
|
}, [selectedRole, open]);
|
|
175
179
|
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
if (!open) return;
|
|
182
|
+
api.get('/credentials')
|
|
183
|
+
.then((list) => setClaudeHasKey(Array.isArray(list) && list.some((c) => c.provider === 'claude-code')))
|
|
184
|
+
.catch(() => setClaudeHasKey(false));
|
|
185
|
+
}, [open]);
|
|
186
|
+
|
|
176
187
|
useEffect(() => {
|
|
177
188
|
if (!open || provider !== 'ollama') { setOllamaInstalled([]); return; }
|
|
178
189
|
api.get('/providers/ollama/models').then((data) => {
|
|
@@ -208,6 +219,7 @@ export function SpawnWizard() {
|
|
|
208
219
|
...(selectedIntegrations.length > 0 && { integrationApproval }),
|
|
209
220
|
...(selectedRepos.length > 0 && { repos: selectedRepos }),
|
|
210
221
|
...(fast && { fast: true }),
|
|
222
|
+
...(provider === 'claude-code' && { authMode }),
|
|
211
223
|
...(selectedPersonality && { personality: selectedPersonality }),
|
|
212
224
|
...(selectedRole === 'ambassador' && selectedPeerId && { peerId: selectedPeerId }),
|
|
213
225
|
...(teamId && { teamId }),
|
|
@@ -220,6 +232,10 @@ export function SpawnWizard() {
|
|
|
220
232
|
|
|
221
233
|
async function handleSpawn() {
|
|
222
234
|
if (!selectedRole) return;
|
|
235
|
+
if (provider === 'claude-code' && authMode === 'usage' && !claudeHasKey) {
|
|
236
|
+
toast.error('Add an Anthropic API key in Settings → Providers to spawn with usage credits.');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
223
239
|
try {
|
|
224
240
|
const preflight = await api.post('/agents/preflight', {
|
|
225
241
|
role: selectedRole,
|
|
@@ -543,6 +559,39 @@ export function SpawnWizard() {
|
|
|
543
559
|
</label>
|
|
544
560
|
)}
|
|
545
561
|
|
|
562
|
+
{/* Billing mode — subscription (OAuth) vs usage credits (API key) */}
|
|
563
|
+
{provider === 'claude-code' && (
|
|
564
|
+
<div className="space-y-1.5">
|
|
565
|
+
<label className="text-xs font-medium text-text-2 font-sans">Billing</label>
|
|
566
|
+
<div className="flex gap-1 p-0.5 rounded-md bg-surface-3 w-fit">
|
|
567
|
+
{[
|
|
568
|
+
{ id: 'subscription', label: 'Subscription' },
|
|
569
|
+
{ id: 'usage', label: 'Usage credits' },
|
|
570
|
+
].map((opt) => (
|
|
571
|
+
<button
|
|
572
|
+
key={opt.id}
|
|
573
|
+
type="button"
|
|
574
|
+
onClick={() => setAuthMode(opt.id)}
|
|
575
|
+
className={cn(
|
|
576
|
+
'px-2.5 py-1 rounded text-2xs font-semibold font-sans transition-colors',
|
|
577
|
+
authMode === opt.id ? 'bg-accent text-white' : 'text-text-2 hover:text-text-0',
|
|
578
|
+
)}
|
|
579
|
+
>
|
|
580
|
+
{opt.label}
|
|
581
|
+
</button>
|
|
582
|
+
))}
|
|
583
|
+
</div>
|
|
584
|
+
{authMode === 'usage' && !claudeHasKey && (
|
|
585
|
+
<p className="text-2xs text-warning font-sans">
|
|
586
|
+
No Anthropic API key stored — add one in Settings → Providers before spawning.
|
|
587
|
+
</p>
|
|
588
|
+
)}
|
|
589
|
+
{authMode === 'subscription' && (
|
|
590
|
+
<p className="text-2xs text-text-3 font-sans">Uses your Claude Code login (Pro/Max).</p>
|
|
591
|
+
)}
|
|
592
|
+
</div>
|
|
593
|
+
)}
|
|
594
|
+
|
|
546
595
|
{/* Ollama model status */}
|
|
547
596
|
{provider === 'ollama' && model && (
|
|
548
597
|
<div className="flex items-center gap-2 flex-wrap text-2xs font-sans">
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useRef, useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { Badge } from '../ui/badge';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { timeAgo, fmtDollar } from '../../lib/format';
|
|
7
|
+
import { cn } from '../../lib/cn';
|
|
8
|
+
import {
|
|
9
|
+
Play, Pause, RotateCcw, MoreHorizontal, Trash2, Pencil, Zap,
|
|
10
|
+
Clock, Activity, DollarSign, Hash,
|
|
11
|
+
} from 'lucide-react';
|
|
12
|
+
|
|
13
|
+
function statusBadge(agent) {
|
|
14
|
+
if (agent.paused) return { variant: 'warning', label: 'Paused', dot: false };
|
|
15
|
+
if (agent.lastRunStatus === 'running') return { variant: 'info', label: 'Running', dot: true };
|
|
16
|
+
if (agent.activeAgent) return { variant: 'info', label: 'Running', dot: true };
|
|
17
|
+
if (!agent.enabled) return { variant: 'default', label: 'Disabled', dot: false };
|
|
18
|
+
if (agent.consecutiveFailures >= 5) return { variant: 'danger', label: 'Auto-Paused', dot: false };
|
|
19
|
+
return { variant: 'success', label: 'Active', dot: false };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AutoAgentCard({ agent }) {
|
|
23
|
+
const selectAutoAgent = useGrooveStore((s) => s.selectAutoAgent);
|
|
24
|
+
const pauseAutoAgent = useGrooveStore((s) => s.pauseAutoAgent);
|
|
25
|
+
const resumeAutoAgent = useGrooveStore((s) => s.resumeAutoAgent);
|
|
26
|
+
const triggerAutoAgent = useGrooveStore((s) => s.triggerAutoAgent);
|
|
27
|
+
const deleteAutoAgent = useGrooveStore((s) => s.deleteAutoAgent);
|
|
28
|
+
const selectedId = useGrooveStore((s) => s.selectedAutoAgentId);
|
|
29
|
+
|
|
30
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
31
|
+
const menuRef = useRef(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!menuOpen) return;
|
|
35
|
+
function handleClick(e) {
|
|
36
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
37
|
+
}
|
|
38
|
+
document.addEventListener('mousedown', handleClick);
|
|
39
|
+
return () => document.removeEventListener('mousedown', handleClick);
|
|
40
|
+
}, [menuOpen]);
|
|
41
|
+
|
|
42
|
+
const a = agent;
|
|
43
|
+
const badge = statusBadge(a);
|
|
44
|
+
const isSelected = selectedId === a.id;
|
|
45
|
+
const phase = a.state?.phase || 'idle';
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
onClick={() => selectAutoAgent(a.id)}
|
|
50
|
+
className={cn(
|
|
51
|
+
'rounded-md border bg-surface-1 overflow-hidden transition-colors cursor-pointer',
|
|
52
|
+
isSelected ? 'border-accent ring-1 ring-accent/30' : 'border-border-subtle hover:border-border',
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{/* Header */}
|
|
56
|
+
<div className="px-4 py-3 flex items-start gap-3">
|
|
57
|
+
<div className="flex-1 min-w-0">
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<span className="text-sm font-semibold text-text-0 font-sans truncate">{a.name}</span>
|
|
60
|
+
<Badge variant={badge.variant} dot={badge.dot ? 'pulse' : undefined} className="text-2xs flex-shrink-0">
|
|
61
|
+
{badge.label}
|
|
62
|
+
</Badge>
|
|
63
|
+
</div>
|
|
64
|
+
{a.description && (
|
|
65
|
+
<p className="text-2xs text-text-3 font-sans mt-0.5 line-clamp-2">{a.description}</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Actions */}
|
|
70
|
+
<div className="flex items-center gap-1 flex-shrink-0">
|
|
71
|
+
{a.paused ? (
|
|
72
|
+
<Button variant="ghost" size="icon-sm" onClick={(e) => { e.stopPropagation(); resumeAutoAgent(a.id); }}>
|
|
73
|
+
<Play size={12} />
|
|
74
|
+
</Button>
|
|
75
|
+
) : (
|
|
76
|
+
<Button variant="ghost" size="icon-sm" onClick={(e) => { e.stopPropagation(); pauseAutoAgent(a.id); }}>
|
|
77
|
+
<Pause size={12} />
|
|
78
|
+
</Button>
|
|
79
|
+
)}
|
|
80
|
+
<Button variant="ghost" size="icon-sm" onClick={(e) => { e.stopPropagation(); triggerAutoAgent(a.id); }}>
|
|
81
|
+
<Zap size={12} />
|
|
82
|
+
</Button>
|
|
83
|
+
<div className="relative" ref={menuRef}>
|
|
84
|
+
<Button variant="ghost" size="icon-sm" onClick={(e) => { e.stopPropagation(); setMenuOpen(!menuOpen); }}>
|
|
85
|
+
<MoreHorizontal size={12} />
|
|
86
|
+
</Button>
|
|
87
|
+
{menuOpen && (
|
|
88
|
+
<div className="absolute right-0 top-full mt-1 z-50 bg-surface-2 border border-border rounded-md shadow-lg py-1 min-w-[140px]">
|
|
89
|
+
<button
|
|
90
|
+
onClick={(e) => { e.stopPropagation(); setMenuOpen(false); deleteAutoAgent(a.id); }}
|
|
91
|
+
className="w-full px-3 py-1.5 text-xs font-sans text-danger hover:bg-surface-3 text-left flex items-center gap-2"
|
|
92
|
+
>
|
|
93
|
+
<Trash2 size={11} /> Delete
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Stats row */}
|
|
102
|
+
<div className="px-4 pb-3 flex items-center gap-4 text-2xs text-text-3 font-mono">
|
|
103
|
+
<span className="flex items-center gap-1">
|
|
104
|
+
<Activity size={10} className="text-text-4" />
|
|
105
|
+
{phase}
|
|
106
|
+
</span>
|
|
107
|
+
<span className="flex items-center gap-1">
|
|
108
|
+
<Hash size={10} className="text-text-4" />
|
|
109
|
+
{a.totalCycles || 0} cycles
|
|
110
|
+
</span>
|
|
111
|
+
<span className="flex items-center gap-1">
|
|
112
|
+
<DollarSign size={10} className="text-text-4" />
|
|
113
|
+
{fmtDollar(a.totalCost || 0)}
|
|
114
|
+
</span>
|
|
115
|
+
<span className="flex items-center gap-1">
|
|
116
|
+
<Clock size={10} className="text-text-4" />
|
|
117
|
+
{a.cadenceDescription || a.cadence}
|
|
118
|
+
</span>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Active agent indicator */}
|
|
122
|
+
{a.activeAgent && (
|
|
123
|
+
<div className="px-4 pb-2">
|
|
124
|
+
<div className="flex items-center gap-2 text-2xs font-sans text-accent">
|
|
125
|
+
<span className="inline-block w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
|
126
|
+
Agent {a.activeAgent.id} running
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
{/* Last run */}
|
|
132
|
+
{a.lastRunAt && !a.activeAgent && (
|
|
133
|
+
<div className="px-4 pb-2 text-2xs text-text-4 font-sans">
|
|
134
|
+
Last run {timeAgo(a.lastRunAt)} · {a.lastRunStatus || 'unknown'}
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { Badge } from '../ui/badge';
|
|
5
|
+
import { Button } from '../ui/button';
|
|
6
|
+
import { Tabs } from '../ui/tabs';
|
|
7
|
+
import { timeAgo, fmtDollar } from '../../lib/format';
|
|
8
|
+
import { cn } from '../../lib/cn';
|
|
9
|
+
import {
|
|
10
|
+
Play, Pause, Zap, ArrowLeft, Save,
|
|
11
|
+
BookOpen, FileText, Clock, Activity, ListOrdered,
|
|
12
|
+
} from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
function JournalTab({ agentId }) {
|
|
15
|
+
const fetchJournal = useGrooveStore((s) => s.fetchAutoAgentJournal);
|
|
16
|
+
const [entries, setEntries] = useState([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setLoading(true);
|
|
21
|
+
fetchJournal(agentId).then((data) => { setEntries(data || []); setLoading(false); });
|
|
22
|
+
}, [agentId]);
|
|
23
|
+
|
|
24
|
+
if (loading) return <div className="p-4 text-xs text-text-4 font-sans">Loading journal...</div>;
|
|
25
|
+
if (entries.length === 0) return <div className="p-4 text-xs text-text-4 font-sans">No journal entries yet.</div>;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="p-3 space-y-2 overflow-y-auto">
|
|
29
|
+
{[...entries].reverse().map((e, i) => (
|
|
30
|
+
<div key={i} className="rounded border border-border-subtle bg-surface-0 px-3 py-2">
|
|
31
|
+
<div className="flex items-center gap-2 text-2xs text-text-3 font-mono">
|
|
32
|
+
<span>{timeAgo(e.timestamp)}</span>
|
|
33
|
+
<Badge variant={e.event?.includes('fail') || e.event?.includes('error') ? 'danger' : 'default'} className="text-2xs">
|
|
34
|
+
{e.event}
|
|
35
|
+
</Badge>
|
|
36
|
+
{e.cycle && <span className="text-text-4">cycle {e.cycle}</span>}
|
|
37
|
+
</div>
|
|
38
|
+
{e.details && <p className="text-xs text-text-1 font-sans mt-1">{e.details}</p>}
|
|
39
|
+
{e.lesson && <p className="text-xs text-accent font-sans mt-1 italic">Lesson: {e.lesson}</p>}
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function RunsTab({ agentId }) {
|
|
47
|
+
const fetchRuns = useGrooveStore((s) => s.fetchAutoAgentRuns);
|
|
48
|
+
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
49
|
+
const [runs, setRuns] = useState([]);
|
|
50
|
+
const [loading, setLoading] = useState(true);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
fetchRuns(agentId).then((data) => { setRuns(data || []); setLoading(false); });
|
|
55
|
+
}, [agentId]);
|
|
56
|
+
|
|
57
|
+
if (loading) return <div className="p-4 text-xs text-text-4 font-sans">Loading runs...</div>;
|
|
58
|
+
if (runs.length === 0) return <div className="p-4 text-xs text-text-4 font-sans">No runs yet.</div>;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="p-3 space-y-2 overflow-y-auto">
|
|
62
|
+
{runs.map((r) => (
|
|
63
|
+
<div
|
|
64
|
+
key={r.runId}
|
|
65
|
+
className="rounded border border-border-subtle bg-surface-0 px-3 py-2 cursor-pointer hover:border-border"
|
|
66
|
+
onClick={() => r.agentId && openDetail({ type: 'agent', agentId: r.agentId })}
|
|
67
|
+
>
|
|
68
|
+
<div className="flex items-center justify-between">
|
|
69
|
+
<div className="flex items-center gap-2 text-2xs font-mono text-text-3">
|
|
70
|
+
<Badge
|
|
71
|
+
variant={r.status === 'completed' ? 'success' : r.status === 'running' ? 'info' : 'danger'}
|
|
72
|
+
className="text-2xs"
|
|
73
|
+
>
|
|
74
|
+
{r.status}
|
|
75
|
+
</Badge>
|
|
76
|
+
<span>{r.agentId}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<span className="text-2xs text-text-4 font-mono">{timeAgo(r.startedAt || r.completedAt)}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex items-center gap-3 mt-1 text-2xs text-text-4 font-mono">
|
|
81
|
+
<span>{r.tokensUsed?.toLocaleString() || 0} tokens</span>
|
|
82
|
+
<span>{fmtDollar(r.costUsd || 0)}</span>
|
|
83
|
+
<span>{r.turns || 0} turns</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function EditorTab({ agentId, type }) {
|
|
92
|
+
const fetchPrompt = useGrooveStore((s) => s.fetchAutoAgentPrompt);
|
|
93
|
+
const fetchRoadmap = useGrooveStore((s) => s.fetchAutoAgentRoadmap);
|
|
94
|
+
const updatePrompt = useGrooveStore((s) => s.updateAutoAgentPrompt);
|
|
95
|
+
const updateRoadmap = useGrooveStore((s) => s.updateAutoAgentRoadmap);
|
|
96
|
+
|
|
97
|
+
const [content, setContent] = useState('');
|
|
98
|
+
const [loading, setLoading] = useState(true);
|
|
99
|
+
const [dirty, setDirty] = useState(false);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setLoading(true);
|
|
103
|
+
setDirty(false);
|
|
104
|
+
const fetcher = type === 'prompt' ? fetchPrompt : fetchRoadmap;
|
|
105
|
+
fetcher(agentId).then((data) => { setContent(data || ''); setLoading(false); });
|
|
106
|
+
}, [agentId, type]);
|
|
107
|
+
|
|
108
|
+
const save = useCallback(() => {
|
|
109
|
+
const updater = type === 'prompt' ? updatePrompt : updateRoadmap;
|
|
110
|
+
updater(agentId, content);
|
|
111
|
+
setDirty(false);
|
|
112
|
+
}, [agentId, type, content]);
|
|
113
|
+
|
|
114
|
+
if (loading) return <div className="p-4 text-xs text-text-4 font-sans">Loading...</div>;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className="flex flex-col h-full">
|
|
118
|
+
<textarea
|
|
119
|
+
value={content}
|
|
120
|
+
onChange={(e) => { setContent(e.target.value); setDirty(true); }}
|
|
121
|
+
className="flex-1 w-full p-3 bg-surface-0 text-text-1 text-xs font-mono resize-none border-0 focus:outline-none"
|
|
122
|
+
placeholder={type === 'prompt' ? 'System prompt for this auto agent...' : 'Roadmap with staged goals...'}
|
|
123
|
+
/>
|
|
124
|
+
{dirty && (
|
|
125
|
+
<div className="px-3 py-2 border-t border-border-subtle flex justify-end">
|
|
126
|
+
<Button variant="primary" size="sm" onClick={save} className="gap-1.5">
|
|
127
|
+
<Save size={12} /> Save {type}
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function StateTab({ agentId }) {
|
|
136
|
+
const fetchDetail = useGrooveStore((s) => s.fetchAutoAgentDetail);
|
|
137
|
+
const [detail, setDetail] = useState(null);
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
fetchDetail(agentId).then(setDetail);
|
|
141
|
+
}, [agentId]);
|
|
142
|
+
|
|
143
|
+
if (!detail) return <div className="p-4 text-xs text-text-4 font-sans">Loading state...</div>;
|
|
144
|
+
|
|
145
|
+
const { state, definition } = detail;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="p-3 space-y-3 overflow-y-auto">
|
|
149
|
+
<div className="space-y-2">
|
|
150
|
+
<h4 className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wide">Current State</h4>
|
|
151
|
+
<div className="grid grid-cols-2 gap-2 text-xs font-sans">
|
|
152
|
+
<div className="bg-surface-0 rounded px-3 py-2 border border-border-subtle">
|
|
153
|
+
<span className="text-text-4 text-2xs">Phase</span>
|
|
154
|
+
<p className="text-text-0 font-mono">{state?.phase || 'idle'}</p>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="bg-surface-0 rounded px-3 py-2 border border-border-subtle">
|
|
157
|
+
<span className="text-text-4 text-2xs">Cycle</span>
|
|
158
|
+
<p className="text-text-0 font-mono">{state?.cycle || 0}</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
{state?.phase_note && (
|
|
162
|
+
<div className="bg-surface-0 rounded px-3 py-2 border border-border-subtle">
|
|
163
|
+
<span className="text-text-4 text-2xs">Phase Note</span>
|
|
164
|
+
<p className="text-text-1 text-xs font-sans">{state.phase_note}</p>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
{state?.champion && (
|
|
168
|
+
<div className="bg-surface-0 rounded px-3 py-2 border border-border-subtle">
|
|
169
|
+
<span className="text-text-4 text-2xs">Champion</span>
|
|
170
|
+
<pre className="text-text-1 text-2xs font-mono mt-1 whitespace-pre-wrap">{JSON.stringify(state.champion, null, 2)}</pre>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
{state?.error && (
|
|
174
|
+
<div className="bg-danger/10 rounded px-3 py-2 border border-danger/30">
|
|
175
|
+
<span className="text-danger text-2xs font-semibold">Error</span>
|
|
176
|
+
<p className="text-danger text-xs font-sans">{state.error}</p>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Config summary */}
|
|
182
|
+
<div className="space-y-2">
|
|
183
|
+
<h4 className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wide">Configuration</h4>
|
|
184
|
+
<div className="bg-surface-0 rounded px-3 py-2 border border-border-subtle text-2xs font-mono text-text-3 space-y-1">
|
|
185
|
+
<div>Cadence: {definition?.cadenceDescription || definition?.cadence}</div>
|
|
186
|
+
<div>Provider: {definition?.agentConfig?.provider} · Model: {definition?.agentConfig?.model}</div>
|
|
187
|
+
<div>Timeout: {definition?.timeout}s · Stale threshold: {definition?.staleThresholdMinutes}min</div>
|
|
188
|
+
{definition?.maxIterations && <div>Max iterations: {definition.maxIterations}</div>}
|
|
189
|
+
{definition?.guardrails?.length > 0 && <div>Guardrails: {definition.guardrails.length}</div>}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* History */}
|
|
194
|
+
{state?.history?.length > 0 && (
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<h4 className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wide">
|
|
197
|
+
History ({state.history.length})
|
|
198
|
+
</h4>
|
|
199
|
+
<div className="space-y-1">
|
|
200
|
+
{state.history.slice(-10).reverse().map((h, i) => (
|
|
201
|
+
<div key={i} className="text-2xs font-mono text-text-3 bg-surface-0 rounded px-2 py-1 border border-border-subtle">
|
|
202
|
+
[{timeAgo(h.timestamp)}] {h.tag || h.event}: {h.note || h.details || ''}
|
|
203
|
+
</div>
|
|
204
|
+
))}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const TABS = [
|
|
213
|
+
{ id: 'state', label: 'State', icon: Activity },
|
|
214
|
+
{ id: 'journal', label: 'Journal', icon: BookOpen },
|
|
215
|
+
{ id: 'runs', label: 'Runs', icon: ListOrdered },
|
|
216
|
+
{ id: 'prompt', label: 'Prompt', icon: FileText },
|
|
217
|
+
{ id: 'roadmap', label: 'Roadmap', icon: Clock },
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
export function AutoAgentDetail({ agentId, onBack }) {
|
|
221
|
+
const [activeTab, setActiveTab] = useState('state');
|
|
222
|
+
const autoAgents = useGrooveStore((s) => s.autoAgents);
|
|
223
|
+
const pauseAutoAgent = useGrooveStore((s) => s.pauseAutoAgent);
|
|
224
|
+
const resumeAutoAgent = useGrooveStore((s) => s.resumeAutoAgent);
|
|
225
|
+
const triggerAutoAgent = useGrooveStore((s) => s.triggerAutoAgent);
|
|
226
|
+
|
|
227
|
+
const agent = autoAgents.find((a) => a.id === agentId);
|
|
228
|
+
if (!agent) return <div className="p-4 text-xs text-text-4">Agent not found</div>;
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<div className="flex flex-col h-full">
|
|
232
|
+
{/* Header */}
|
|
233
|
+
<div className="px-4 py-3 border-b border-border-subtle bg-surface-1 flex items-center gap-3">
|
|
234
|
+
<Button variant="ghost" size="icon-sm" onClick={onBack}>
|
|
235
|
+
<ArrowLeft size={14} />
|
|
236
|
+
</Button>
|
|
237
|
+
<div className="flex-1 min-w-0">
|
|
238
|
+
<h3 className="text-sm font-semibold text-text-0 font-sans truncate">{agent.name}</h3>
|
|
239
|
+
<p className="text-2xs text-text-3 font-sans truncate">{agent.description}</p>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="flex items-center gap-1">
|
|
242
|
+
{agent.paused ? (
|
|
243
|
+
<Button variant="ghost" size="sm" onClick={() => resumeAutoAgent(agentId)} className="gap-1 text-xs">
|
|
244
|
+
<Play size={12} /> Resume
|
|
245
|
+
</Button>
|
|
246
|
+
) : (
|
|
247
|
+
<Button variant="ghost" size="sm" onClick={() => pauseAutoAgent(agentId)} className="gap-1 text-xs">
|
|
248
|
+
<Pause size={12} /> Pause
|
|
249
|
+
</Button>
|
|
250
|
+
)}
|
|
251
|
+
<Button variant="primary" size="sm" onClick={() => triggerAutoAgent(agentId)} className="gap-1 text-xs">
|
|
252
|
+
<Zap size={12} /> Trigger
|
|
253
|
+
</Button>
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Tab bar */}
|
|
258
|
+
<div className="flex items-center border-b border-border-subtle bg-surface-1 px-2">
|
|
259
|
+
{TABS.map((tab) => (
|
|
260
|
+
<button
|
|
261
|
+
key={tab.id}
|
|
262
|
+
onClick={() => setActiveTab(tab.id)}
|
|
263
|
+
className={cn(
|
|
264
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs font-sans border-b-2 transition-colors cursor-pointer',
|
|
265
|
+
activeTab === tab.id
|
|
266
|
+
? 'border-accent text-accent'
|
|
267
|
+
: 'border-transparent text-text-3 hover:text-text-1',
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
<tab.icon size={12} />
|
|
271
|
+
{tab.label}
|
|
272
|
+
</button>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Tab content */}
|
|
277
|
+
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
278
|
+
{activeTab === 'state' && <StateTab agentId={agentId} />}
|
|
279
|
+
{activeTab === 'journal' && <JournalTab agentId={agentId} />}
|
|
280
|
+
{activeTab === 'runs' && <RunsTab agentId={agentId} />}
|
|
281
|
+
{activeTab === 'prompt' && <EditorTab agentId={agentId} type="prompt" />}
|
|
282
|
+
{activeTab === 'roadmap' && <EditorTab agentId={agentId} type="roadmap" />}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
}
|