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.
Files changed (68) hide show
  1. package/AUTONOMOUS_AGENT_FRAMEWORK.md +654 -0
  2. package/innerchat/Screenshot_2026-06-09_at_12.09.42_PM.png +0 -0
  3. package/innerchat/Screenshot_2026-06-09_at_2.19.42_PM.png +0 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +3 -1
  7. package/node_modules/@groove-dev/daemon/src/autostate.js +198 -0
  8. package/node_modules/@groove-dev/daemon/src/index.js +6 -0
  9. package/node_modules/@groove-dev/daemon/src/orchestrator.js +585 -0
  10. package/node_modules/@groove-dev/daemon/src/process.js +27 -2
  11. package/node_modules/@groove-dev/daemon/src/registry.js +2 -1
  12. package/node_modules/@groove-dev/daemon/src/routes/auto-agents.js +264 -0
  13. package/node_modules/@groove-dev/daemon/src/validate.js +6 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-CTer01Vg.js +1065 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/index-DTFtRtkx.css +1 -0
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +53 -0
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +4 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +95 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +50 -1
  23. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  24. package/node_modules/@groove-dev/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  25. package/node_modules/@groove-dev/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  26. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +16 -3
  27. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  28. package/node_modules/@groove-dev/gui/src/stores/groove.js +14 -0
  29. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +2 -0
  30. package/node_modules/@groove-dev/gui/src/stores/slices/auto-agents-slice.js +149 -0
  31. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +14 -11
  32. package/node_modules/@groove-dev/gui/src/views/auto-agents.jsx +70 -0
  33. package/node_modules/@groove-dev/gui/src/views/settings.jsx +13 -6
  34. package/package.json +1 -1
  35. package/packages/cli/package.json +1 -1
  36. package/packages/daemon/package.json +1 -1
  37. package/packages/daemon/src/api.js +3 -1
  38. package/packages/daemon/src/autostate.js +198 -0
  39. package/packages/daemon/src/index.js +6 -0
  40. package/packages/daemon/src/orchestrator.js +585 -0
  41. package/packages/daemon/src/process.js +27 -2
  42. package/packages/daemon/src/registry.js +2 -1
  43. package/packages/daemon/src/routes/auto-agents.js +264 -0
  44. package/packages/daemon/src/validate.js +6 -0
  45. package/packages/gui/dist/assets/index-CTer01Vg.js +1065 -0
  46. package/packages/gui/dist/assets/index-DTFtRtkx.css +1 -0
  47. package/packages/gui/dist/index.html +2 -2
  48. package/packages/gui/package.json +1 -1
  49. package/packages/gui/src/App.jsx +2 -0
  50. package/packages/gui/src/components/agents/agent-config.jsx +53 -0
  51. package/packages/gui/src/components/agents/agent-feed.jsx +4 -4
  52. package/packages/gui/src/components/agents/agent-panel.jsx +95 -2
  53. package/packages/gui/src/components/agents/spawn-wizard.jsx +50 -1
  54. package/packages/gui/src/components/auto-agents/auto-agent-card.jsx +139 -0
  55. package/packages/gui/src/components/auto-agents/auto-agent-detail.jsx +286 -0
  56. package/packages/gui/src/components/auto-agents/setup-wizard.jsx +284 -0
  57. package/packages/gui/src/components/fleet/fleet-pane.jsx +16 -3
  58. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  59. package/packages/gui/src/stores/groove.js +14 -0
  60. package/packages/gui/src/stores/slices/agents-slice.js +2 -0
  61. package/packages/gui/src/stores/slices/auto-agents-slice.js +149 -0
  62. package/packages/gui/src/stores/slices/ui-slice.js +14 -11
  63. package/packages/gui/src/views/auto-agents.jsx +70 -0
  64. package/packages/gui/src/views/settings.jsx +13 -6
  65. package/node_modules/@groove-dev/gui/dist/assets/index-BvJwMNAX.css +0 -1
  66. package/node_modules/@groove-dev/gui/dist/assets/index-DZY-EWPs.js +0 -1030
  67. package/packages/gui/dist/assets/index-BvJwMNAX.css +0 -1
  68. 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
+ }