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.
- package/CLAUDE.md +7 -0
- package/node_modules/@groove-dev/cli/package.json +4 -3
- package/node_modules/@groove-dev/daemon/package.json +4 -3
- package/node_modules/@groove-dev/daemon/src/api.js +109 -9
- package/node_modules/@groove-dev/daemon/src/index.js +68 -1
- package/node_modules/@groove-dev/daemon/src/process.js +83 -11
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
- package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +5 -4
- package/node_modules/@groove-dev/gui/src/App.jsx +122 -72
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +46 -6
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +91 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
- package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
- package/package.json +1 -1
- package/packages/cli/package.json +4 -3
- package/packages/daemon/package.json +4 -3
- package/packages/daemon/src/api.js +109 -9
- package/packages/daemon/src/index.js +68 -1
- package/packages/daemon/src/process.js +83 -11
- package/packages/daemon/src/registry.js +1 -1
- package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +5 -4
- package/packages/gui/src/App.jsx +122 -72
- package/packages/gui/src/components/AgentActions.jsx +130 -1
- package/packages/gui/src/components/AgentChat.jsx +46 -6
- package/packages/gui/src/components/AgentNode.jsx +13 -83
- package/packages/gui/src/components/SpawnPanel.jsx +91 -6
- package/packages/gui/src/stores/groove.js +31 -2
- package/packages/gui/src/views/AgentTree.jsx +133 -67
- package/node_modules/@groove-dev/gui/.groove/daemon.host +0 -1
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +0 -182
- 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-
|
|
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.
|
|
4
|
-
"description": "GROOVE GUI
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
70
|
-
|
|
71
|
-
{daemonHost
|
|
72
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
onClick={() =>
|
|
113
|
+
ref={moreBtnRef}
|
|
114
|
+
onClick={() => setDropdownOpen((o) => !o)}
|
|
82
115
|
style={{
|
|
83
116
|
...styles.tabBtn,
|
|
84
|
-
color:
|
|
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
|
-
{
|
|
120
|
+
More {'\u25BE'}
|
|
90
121
|
</button>
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
{statusMessage
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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',
|
|
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
|
|
255
|
+
padding: '0 10px',
|
|
235
256
|
background: 'transparent',
|
|
236
257
|
border: 'none',
|
|
237
258
|
borderBottom: '2px solid transparent',
|
|
238
|
-
fontSize:
|
|
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:
|
|
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)' }}>×</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) => {
|
|
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
|
|
239
|
+
<div style={styles.launchHeader}>Recommended Team</div>
|
|
214
240
|
<div style={styles.launchList}>
|
|
215
|
-
{
|
|
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
|
},
|