groove-dev 0.18.0 → 0.18.2

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 (40) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +4 -3
  3. package/node_modules/@groove-dev/daemon/package.json +4 -3
  4. package/node_modules/@groove-dev/daemon/src/api.js +109 -9
  5. package/node_modules/@groove-dev/daemon/src/index.js +68 -1
  6. package/node_modules/@groove-dev/daemon/src/process.js +83 -11
  7. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  8. package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
  9. package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  12. package/node_modules/@groove-dev/gui/package.json +5 -4
  13. package/node_modules/@groove-dev/gui/src/App.jsx +122 -72
  14. package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
  15. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +46 -6
  16. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
  17. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +91 -6
  18. package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
  19. package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
  20. package/package.json +1 -1
  21. package/packages/cli/package.json +4 -3
  22. package/packages/daemon/package.json +4 -3
  23. package/packages/daemon/src/api.js +109 -9
  24. package/packages/daemon/src/index.js +68 -1
  25. package/packages/daemon/src/process.js +83 -11
  26. package/packages/daemon/src/registry.js +1 -1
  27. package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
  28. package/packages/gui/dist/index.html +1 -1
  29. package/packages/gui/package.json +5 -4
  30. package/packages/gui/src/App.jsx +122 -72
  31. package/packages/gui/src/components/AgentActions.jsx +130 -1
  32. package/packages/gui/src/components/AgentChat.jsx +46 -6
  33. package/packages/gui/src/components/AgentNode.jsx +13 -83
  34. package/packages/gui/src/components/SpawnPanel.jsx +91 -6
  35. package/packages/gui/src/stores/groove.js +31 -2
  36. package/packages/gui/src/views/AgentTree.jsx +133 -67
  37. package/node_modules/@groove-dev/gui/.groove/daemon.host +0 -1
  38. package/node_modules/@groove-dev/gui/.groove/daemon.pid +0 -1
  39. package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +0 -182
  40. package/packages/gui/dist/assets/index-DXkccbmd.js +0 -182
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-DXkccbmd.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-x5suAiK7.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-BhjOFLBc.css">
9
9
  </head>
10
10
  <body>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.11.0",
4
- "description": "GROOVE GUI visual agent control plane",
3
+ "version": "0.18.2",
4
+ "description": "GROOVE GUI \u2014 visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -34,5 +34,6 @@
34
34
  "devDependencies": {
35
35
  "@vitejs/plugin-react": "^4.3.0",
36
36
  "vite": "^6.0.0"
37
- }
38
- }
37
+ },
38
+ "private": true
39
+ }
@@ -1,7 +1,7 @@
1
1
  // GROOVE GUI — App Root
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useEffect } from 'react';
4
+ import React, { useEffect, useState, useRef } from 'react';
5
5
  import { useGrooveStore } from './stores/groove';
6
6
  import AgentTree from './views/AgentTree';
7
7
  import AgentPanel from './components/AgentPanel';
@@ -16,17 +16,23 @@ import IntegrationsStore from './views/IntegrationsStore';
16
16
  import ScheduleManager from './views/ScheduleManager';
17
17
  import FileEditor from './views/FileEditor';
18
18
 
19
- const TABS = [
19
+ const MAIN_TABS = [
20
20
  { id: 'agents', label: 'Agents' },
21
21
  { id: 'editor', label: 'Editor' },
22
- { id: 'integrations', label: 'Integrations' },
23
- { id: 'skills', label: 'Skills' },
24
22
  { id: 'stats', label: 'Stats' },
25
- { id: 'schedules', label: 'Schedules' },
26
23
  { id: 'teams', label: 'Teams' },
27
24
  { id: 'approvals', label: 'Approvals' },
28
25
  ];
29
26
 
27
+ const DROPDOWN_TABS = [
28
+ { id: 'journalist', label: 'Journalist' },
29
+ { id: 'integrations', label: 'Integrations' },
30
+ { id: 'skills', label: 'Skills' },
31
+ { id: 'schedules', label: 'Schedules' },
32
+ ];
33
+
34
+ const DROPDOWN_IDS = new Set(DROPDOWN_TABS.map((t) => t.id));
35
+
30
36
  class ErrorBoundary extends React.Component {
31
37
  constructor(props) { super(props); this.state = { error: null }; }
32
38
  static getDerivedStateFromError(error) { return { error }; }
@@ -57,72 +63,99 @@ function AppInner() {
57
63
  const openDetail = useGrooveStore((s) => s.openDetail);
58
64
  const closeDetail = useGrooveStore((s) => s.closeDetail);
59
65
 
66
+ const [dropdownOpen, setDropdownOpen] = useState(false);
67
+ const dropdownRef = useRef(null);
68
+ const moreBtnRef = useRef(null);
69
+
60
70
  useEffect(() => { connect(); }, [connect]);
61
71
 
62
- const runningCount = agents.filter((a) => a.status === 'running').length;
72
+ useEffect(() => {
73
+ if (!dropdownOpen) return;
74
+ const handler = (e) => {
75
+ if (dropdownRef.current?.contains(e.target)) return;
76
+ if (moreBtnRef.current?.contains(e.target)) return;
77
+ setDropdownOpen(false);
78
+ };
79
+ document.addEventListener('mousedown', handler);
80
+ return () => document.removeEventListener('mousedown', handler);
81
+ }, [dropdownOpen]);
82
+
63
83
  const hasAgents = agents.length > 0;
84
+ const moreActive = DROPDOWN_IDS.has(activeTab);
64
85
 
65
86
  return (
66
87
  <div style={styles.root}>
67
88
  {/* Header */}
68
89
  <header style={styles.header}>
69
- <div style={styles.headerLeft}>
70
- <img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
71
- {daemonHost && (
72
- <span style={styles.hostBadge}>{daemonHost}</span>
73
- )}
74
- </div>
90
+ <img src="/groove-logo-short.png" alt="GROOVE" style={{ height: 18, marginTop: 3, opacity: 0.85 }} />
91
+ {daemonHost && (
92
+ <span style={styles.hostBadge}>{daemonHost}</span>
93
+ )}
75
94
 
95
+ <div style={{ flex: 1 }} />
76
96
 
77
- <div style={styles.headerCenter}>
78
- {connected && TABS.map((tab) => (
97
+ {connected && MAIN_TABS.map((tab) => (
98
+ <button
99
+ key={tab.id}
100
+ onClick={() => setActiveTab(tab.id)}
101
+ style={{
102
+ ...styles.tabBtn,
103
+ color: activeTab === tab.id ? 'var(--text-bright)' : 'var(--text-dim)',
104
+ }}
105
+ >
106
+ {tab.label}
107
+ </button>
108
+ ))}
109
+
110
+ {connected && (
111
+ <div style={{ position: 'relative' }}>
79
112
  <button
80
- key={tab.id}
81
- onClick={() => setActiveTab(tab.id)}
113
+ ref={moreBtnRef}
114
+ onClick={() => setDropdownOpen((o) => !o)}
82
115
  style={{
83
116
  ...styles.tabBtn,
84
- color: activeTab === tab.id ? 'var(--text-bright)' : 'var(--text-primary)',
85
- borderBottom: activeTab === tab.id ? '2px solid var(--accent)' : '2px solid transparent',
86
- background: activeTab === tab.id ? 'var(--bg-active)' : 'transparent',
117
+ color: moreActive || dropdownOpen ? 'var(--text-bright)' : 'var(--text-dim)',
87
118
  }}
88
119
  >
89
- {tab.label}
120
+ More {'\u25BE'}
90
121
  </button>
91
- ))}
92
- </div>
122
+ {dropdownOpen && (
123
+ <div ref={dropdownRef} style={styles.dropdown}>
124
+ {DROPDOWN_TABS.map((tab) => (
125
+ <button
126
+ key={tab.id}
127
+ onClick={() => {
128
+ if (tab.id === 'journalist') {
129
+ detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' });
130
+ } else {
131
+ setActiveTab(tab.id);
132
+ }
133
+ setDropdownOpen(false);
134
+ }}
135
+ style={{
136
+ ...styles.dropdownItem,
137
+ color: (tab.id === 'journalist' ? detailPanel?.type === 'journalist' : activeTab === tab.id) ? 'var(--text-bright)' : 'var(--text-primary)',
138
+ }}
139
+ >
140
+ {tab.label}
141
+ </button>
142
+ ))}
143
+ </div>
144
+ )}
145
+ </div>
146
+ )}
93
147
 
94
- <div style={styles.headerRight}>
95
- {statusMessage && (
96
- <span style={styles.statusText}>{statusMessage}</span>
97
- )}
98
- <span style={styles.agentCount}>
99
- {runningCount > 0
100
- ? `${runningCount} running`
101
- : agents.length > 0
102
- ? `${agents.length} agent${agents.length !== 1 ? 's' : ''}`
103
- : ''}
104
- </span>
105
- {connected && (
106
- <>
107
- <button
108
- onClick={() => detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' })}
109
- style={{
110
- ...styles.tabBtn,
111
- color: detailPanel?.type === 'journalist' ? 'var(--text-bright)' : 'var(--text-primary)',
112
- borderBottom: detailPanel?.type === 'journalist' ? '2px solid var(--purple)' : '2px solid transparent',
113
- }}
114
- >
115
- Journalist
116
- </button>
117
- <button
118
- onClick={() => openDetail({ type: 'spawn' })}
119
- style={styles.spawnBtn}
120
- >
121
- + Spawn
122
- </button>
123
- </>
124
- )}
125
- </div>
148
+ {statusMessage && (
149
+ <span style={styles.statusText}>{statusMessage}</span>
150
+ )}
151
+ {connected && (
152
+ <button
153
+ onClick={() => openDetail({ type: 'spawn' })}
154
+ style={styles.spawnBtn}
155
+ >
156
+ + Spawn
157
+ </button>
158
+ )}
126
159
  </header>
127
160
 
128
161
  {/* Status pill — bottom left */}
@@ -205,51 +238,68 @@ const styles = {
205
238
  height: 40,
206
239
  padding: '0 16px',
207
240
  borderBottom: '1px solid var(--border)',
208
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
241
+ display: 'flex', alignItems: 'center', gap: 2,
209
242
  background: 'var(--bg-chrome)',
210
243
  flexShrink: 0,
211
244
  position: 'relative',
212
245
  },
213
- headerLeft: {
214
- display: 'flex', alignItems: 'center', gap: 8,
215
- },
216
246
  hostBadge: {
217
247
  fontSize: 9, fontWeight: 600, letterSpacing: 0.5,
218
248
  color: 'var(--text-dim)', background: 'var(--bg-active)',
219
249
  padding: '2px 6px', borderRadius: 3,
220
250
  border: '1px solid var(--border)',
221
251
  fontFamily: 'var(--font)',
222
- },
223
- logo: {
224
- fontSize: 13, fontWeight: 600, letterSpacing: 1.5,
225
- color: 'var(--text-bright)',
226
- },
227
- headerCenter: {
228
- display: 'flex', alignItems: 'center', gap: 0,
229
- },
230
- headerRight: {
231
- display: 'flex', alignItems: 'center', gap: 10,
252
+ marginRight: 4,
232
253
  },
233
254
  tabBtn: {
234
- padding: '10px 14px',
255
+ padding: '0 10px',
235
256
  background: 'transparent',
236
257
  border: 'none',
237
258
  borderBottom: '2px solid transparent',
238
- fontSize: 12, fontWeight: 500,
259
+ fontSize: 11, fontWeight: 500,
239
260
  fontFamily: 'var(--font)',
240
261
  cursor: 'pointer',
241
262
  transition: 'color 0.1s',
263
+ alignSelf: 'stretch',
264
+ display: 'flex',
265
+ alignItems: 'center',
266
+ marginTop: 2,
267
+ },
268
+ dropdown: {
269
+ position: 'absolute',
270
+ top: '100%',
271
+ left: 0,
272
+ background: '#1e2228',
273
+ border: '1px solid var(--border)',
274
+ borderRadius: 6,
275
+ padding: '4px 0',
276
+ zIndex: 100,
277
+ minWidth: 160,
278
+ boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
279
+ },
280
+ dropdownItem: {
281
+ display: 'block',
282
+ width: '100%',
283
+ padding: '8px 16px',
284
+ background: 'transparent',
285
+ border: 'none',
286
+ fontSize: 11,
287
+ fontWeight: 500,
288
+ fontFamily: 'var(--font)',
289
+ color: 'var(--text-primary)',
290
+ cursor: 'pointer',
291
+ textAlign: 'left',
242
292
  },
243
293
  spawnBtn: {
244
294
  padding: '4px 12px',
245
295
  background: 'transparent',
246
296
  border: '1px solid var(--accent)',
247
297
  borderRadius: 2,
248
- color: 'var(--accent)', fontSize: 12, fontWeight: 600,
298
+ color: 'var(--accent)', fontSize: 11, fontWeight: 600,
249
299
  fontFamily: 'var(--font)',
250
300
  cursor: 'pointer',
301
+ marginLeft: 12,
251
302
  },
252
- agentCount: { fontSize: 11, color: 'var(--text-dim)' },
253
303
  statusText: { fontSize: 11, color: 'var(--text-dim)', fontStyle: 'italic' },
254
304
  mainRow: {
255
305
  flex: 1, display: 'flex', overflow: 'hidden',
@@ -3,6 +3,49 @@
3
3
 
4
4
  import React, { useState, useEffect } from 'react';
5
5
  import { useGrooveStore } from '../stores/groove';
6
+ // System directory browser — browses absolute paths, not limited to project dir
7
+ function SystemDirPicker({ initial, onSelect, onClose }) {
8
+ const [currentPath, setCurrentPath] = useState(initial || '');
9
+ const [dirs, setDirs] = useState([]);
10
+ const [parentPath, setParentPath] = useState(null);
11
+
12
+ useEffect(() => {
13
+ fetch(`/api/browse-system?path=${encodeURIComponent(currentPath || '')}`)
14
+ .then((r) => r.json())
15
+ .then((data) => {
16
+ setDirs(data.dirs || []);
17
+ setParentPath(data.parent);
18
+ if (data.current) setCurrentPath(data.current);
19
+ })
20
+ .catch(() => {});
21
+ }, [currentPath]);
22
+
23
+ return (
24
+ <div style={{ border: '1px solid var(--border)', borderRadius: 4, background: 'var(--bg-base)', marginTop: 6, maxHeight: 200, display: 'flex', flexDirection: 'column' }}>
25
+ <div style={{ padding: '4px 8px', borderBottom: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexShrink: 0 }}>
26
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>{currentPath}</span>
27
+ <button onClick={onClose} style={{ background: 'none', border: 'none', color: 'var(--text-dim)', cursor: 'pointer', fontSize: 12, fontFamily: 'var(--font)' }}>&times;</button>
28
+ </div>
29
+ <div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
30
+ {parentPath !== null && (
31
+ <button onClick={() => setCurrentPath(parentPath)} style={{ width: '100%', padding: '4px 8px', background: 'none', border: 'none', borderBottom: '1px solid var(--border)', textAlign: 'left', cursor: 'pointer', fontFamily: 'var(--font)', fontSize: 11, color: 'var(--text-muted)' }}>
32
+ ..
33
+ </button>
34
+ )}
35
+ {dirs.map((d) => (
36
+ <button key={d.path} onClick={() => setCurrentPath(d.path)} style={{ width: '100%', padding: '4px 8px', background: 'none', border: 'none', borderBottom: '1px solid var(--border)', textAlign: 'left', cursor: 'pointer', fontFamily: 'var(--font)', fontSize: 11, color: 'var(--text-primary)' }}>
37
+ {d.name}{d.hasChildren ? '/' : ''}
38
+ </button>
39
+ ))}
40
+ </div>
41
+ <div style={{ padding: '4px 8px', borderTop: '1px solid var(--border)', flexShrink: 0 }}>
42
+ <button onClick={() => { onSelect(currentPath); onClose(); }} style={{ width: '100%', padding: '4px 8px', background: 'var(--accent)', color: 'var(--bg-base)', border: 'none', borderRadius: 3, fontSize: 11, fontWeight: 600, cursor: 'pointer', fontFamily: 'var(--font)' }}>
43
+ Select This Directory
44
+ </button>
45
+ </div>
46
+ </div>
47
+ );
48
+ }
6
49
 
7
50
  export default function AgentActions({ agent }) {
8
51
  const killAgent = useGrooveStore((s) => s.killAgent);
@@ -17,6 +60,9 @@ export default function AgentActions({ agent }) {
17
60
  const [editPrompt, setEditPrompt] = useState('');
18
61
  const [editingPrompt, setEditingPrompt] = useState(false);
19
62
  const [selectedModel, setSelectedModel] = useState(agent.model || '');
63
+ const [editingDir, setEditingDir] = useState(false);
64
+ const [dirInput, setDirInput] = useState(agent.workingDir || '');
65
+ const [showDirPicker, setShowDirPicker] = useState(false);
20
66
  const [providerList, setProviderList] = useState([]);
21
67
  const [installedSkills, setInstalledSkills] = useState([]);
22
68
  const [showSkillPicker, setShowSkillPicker] = useState(false);
@@ -149,10 +195,40 @@ export default function AgentActions({ agent }) {
149
195
  }
150
196
  }
151
197
 
198
+ const [editingName, setEditingName] = useState(false);
199
+ const [nameInput, setNameInput] = useState(agent.name || '');
200
+
152
201
  return (
153
202
  <div style={styles.container}>
203
+ {/* Agent Name */}
204
+ <div style={styles.sectionLabel}>NAME</div>
205
+ {!editingName ? (
206
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
207
+ <span style={{ flex: 1, fontSize: 13, fontWeight: 600, color: 'var(--text-bright)' }}>{agent.name}</span>
208
+ <button onClick={() => { setEditingName(true); setNameInput(agent.name || ''); }} style={styles.editBtn}>Rename</button>
209
+ </div>
210
+ ) : (
211
+ <div>
212
+ <input style={styles.textarea} value={nameInput} onChange={(e) => setNameInput(e.target.value)}
213
+ placeholder="Agent name..." autoFocus
214
+ onKeyDown={(e) => {
215
+ if (e.key === 'Enter' && nameInput.trim()) {
216
+ fetch(`/api/agents/${agent.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: nameInput.trim() }) });
217
+ showStatus('renamed'); setEditingName(false);
218
+ }
219
+ }} />
220
+ <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
221
+ <button onClick={() => {
222
+ fetch(`/api/agents/${agent.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: nameInput.trim() }) });
223
+ showStatus('renamed'); setEditingName(false);
224
+ }} style={styles.saveBtn} disabled={!nameInput.trim()}>Save</button>
225
+ <button onClick={() => setEditingName(false)} style={styles.cancelBtn}>Cancel</button>
226
+ </div>
227
+ </div>
228
+ )}
229
+
154
230
  {/* Lifecycle controls */}
155
- <div style={styles.sectionLabel}>LIFECYCLE</div>
231
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>LIFECYCLE</div>
156
232
 
157
233
  <div style={styles.btnGrid}>
158
234
  {isAlive && (
@@ -216,6 +292,59 @@ export default function AgentActions({ agent }) {
216
292
  </select>
217
293
  <div style={styles.fieldHint}>Changes take effect on next rotation</div>
218
294
 
295
+ {/* Working directory */}
296
+ <div style={{ ...styles.sectionLabel, marginTop: 20 }}>DIRECTORY</div>
297
+ {!editingDir ? (
298
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
299
+ <div style={{ flex: 1, fontSize: 11, color: agent.workingDir ? 'var(--text-primary)' : 'var(--text-muted)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
300
+ {agent.workingDir || 'project root'}
301
+ </div>
302
+ <button onClick={() => { setEditingDir(true); setDirInput(agent.workingDir || ''); }} style={styles.editBtn}>
303
+ Change
304
+ </button>
305
+ </div>
306
+ ) : (
307
+ <div>
308
+ <div style={{ display: 'flex', gap: 6 }}>
309
+ <input
310
+ style={{ ...styles.textarea, flex: 1 }}
311
+ value={dirInput}
312
+ onChange={(e) => setDirInput(e.target.value)}
313
+ placeholder="/absolute/path/to/project"
314
+ autoFocus
315
+ />
316
+ <button onClick={() => setShowDirPicker(true)} style={{ ...styles.editBtn, flexShrink: 0, marginTop: 0 }}>
317
+ Browse
318
+ </button>
319
+ </div>
320
+ {showDirPicker && (
321
+ <SystemDirPicker
322
+ initial={dirInput}
323
+ onSelect={(path) => { setDirInput(path); setShowDirPicker(false); }}
324
+ onClose={() => setShowDirPicker(false)}
325
+ />
326
+ )}
327
+ <div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
328
+ <button onClick={async () => {
329
+ try {
330
+ await fetch(`/api/agents/${agent.id}`, {
331
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' },
332
+ body: JSON.stringify({ workingDir: dirInput.trim() || null }),
333
+ });
334
+ showStatus(`directory set — takes effect on next rotation/restart`);
335
+ setEditingDir(false);
336
+ } catch (err) { showStatus(`failed: ${err.message}`); }
337
+ }} style={styles.saveBtn}>
338
+ Save
339
+ </button>
340
+ <button onClick={() => { setEditingDir(false); setShowDirPicker(false); }} style={styles.cancelBtn}>
341
+ Cancel
342
+ </button>
343
+ </div>
344
+ <div style={styles.fieldHint}>Takes effect on next rotation or restart</div>
345
+ </div>
346
+ )}
347
+
219
348
  {/* Prompt modification */}
220
349
  <div style={{ ...styles.sectionLabel, marginTop: 20 }}>PROMPT</div>
221
350
  {agent.prompt && !editingPrompt && (
@@ -111,7 +111,7 @@ export default function AgentChat({ agent }) {
111
111
  </div>
112
112
 
113
113
  {/* Launch Team button — shown when planner completes */}
114
- {agent.role === 'planner' && agent.status === 'completed' && (
114
+ {agent.role === 'planner' && (agent.status === 'completed' || agent.status === 'crashed' || agent.status === 'stopped') && (
115
115
  <LaunchTeamButton showStatus={showStatus} />
116
116
  )}
117
117
 
@@ -137,6 +137,25 @@ export default function AgentChat({ agent }) {
137
137
  >
138
138
  Send
139
139
  </button>
140
+ <button
141
+ type="button"
142
+ onClick={() => {
143
+ const { chatHistory, activityLog } = useGrooveStore.getState();
144
+ const newChat = { ...chatHistory }; delete newChat[agent.id];
145
+ const newLog = { ...activityLog }; delete newLog[agent.id];
146
+ useGrooveStore.setState({ chatHistory: newChat, activityLog: newLog });
147
+ try { localStorage.setItem('groove:chatHistory', JSON.stringify(newChat)); } catch {}
148
+ try { localStorage.setItem('groove:activityLog', JSON.stringify(newLog)); } catch {}
149
+ }}
150
+ title="Clear chat history"
151
+ style={{
152
+ background: 'none', border: 'none', color: 'var(--text-muted)',
153
+ fontSize: 11, cursor: 'pointer', fontFamily: 'var(--font)',
154
+ padding: '4px 6px', flexShrink: 0,
155
+ }}
156
+ >
157
+ Clear
158
+ </button>
140
159
  </div>
141
160
  </div>
142
161
  );
@@ -183,10 +202,14 @@ function LaunchTeamButton({ showStatus }) {
183
202
  const [launched, setLaunched] = useState(false);
184
203
 
185
204
  useEffect(() => {
186
- fetch('/api/recommended-team')
205
+ const load = () => fetch('/api/recommended-team')
187
206
  .then((r) => r.json())
188
- .then((d) => { if (d.exists && d.agents.length > 0) setTeam(d.agents); })
207
+ .then((d) => { setTeam(d.exists && d.agents.length > 0 ? d.agents : null); })
189
208
  .catch(() => {});
209
+ load();
210
+ // Re-check every 5s in case planner just finished writing a new team
211
+ const interval = setInterval(load, 5000);
212
+ return () => clearInterval(interval);
190
213
  }, []);
191
214
 
192
215
  async function handleLaunch() {
@@ -208,16 +231,27 @@ function LaunchTeamButton({ showStatus }) {
208
231
 
209
232
  if (!team || launched) return null;
210
233
 
234
+ const phase1 = team.filter((a) => !a.phase || a.phase === 1);
235
+ const phase2 = team.filter((a) => a.phase === 2);
236
+
211
237
  return (
212
238
  <div style={styles.launchBox}>
213
- <div style={styles.launchHeader}>Recommended Team ({team.length} agents)</div>
239
+ <div style={styles.launchHeader}>Recommended Team</div>
214
240
  <div style={styles.launchList}>
215
- {team.map((a, i) => (
216
- <div key={i} style={styles.launchAgent}>
241
+ {phase1.map((a, i) => (
242
+ <div key={`p1-${i}`} style={styles.launchAgent}>
243
+ <span style={styles.launchPhase}>1</span>
217
244
  <span style={styles.launchRole}>{a.role}</span>
218
245
  <span style={styles.launchPrompt}>{(a.prompt || '').slice(0, 80)}{(a.prompt || '').length > 80 ? '...' : ''}</span>
219
246
  </div>
220
247
  ))}
248
+ {phase2.map((a, i) => (
249
+ <div key={`p2-${i}`} style={{ ...styles.launchAgent, opacity: 0.7 }}>
250
+ <span style={{ ...styles.launchPhase, background: 'var(--bg-active)', color: 'var(--text-dim)' }}>2</span>
251
+ <span style={styles.launchRole}>{a.role} <span style={{ fontWeight: 400, color: 'var(--text-dim)', fontSize: 9 }}>auto after phase 1</span></span>
252
+ <span style={styles.launchPrompt}>{(a.prompt || '').slice(0, 80)}{(a.prompt || '').length > 80 ? '...' : ''}</span>
253
+ </div>
254
+ ))}
221
255
  </div>
222
256
  <button
223
257
  type="button"
@@ -464,6 +498,12 @@ const styles = {
464
498
  display: 'flex', alignItems: 'baseline', gap: 6,
465
499
  fontSize: 10, padding: '2px 0',
466
500
  },
501
+ launchPhase: {
502
+ width: 16, height: 16, borderRadius: 3, flexShrink: 0,
503
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
504
+ fontSize: 9, fontWeight: 700,
505
+ background: 'rgba(51, 175, 188, 0.15)', color: 'var(--accent)',
506
+ },
467
507
  launchRole: {
468
508
  fontWeight: 600, color: 'var(--accent)', minWidth: 60,
469
509
  },