groove-dev 0.17.8 → 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/node_modules/@groove-dev/cli/package.json +4 -3
- package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
- package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
- package/node_modules/@groove-dev/daemon/package.json +4 -3
- package/node_modules/@groove-dev/daemon/src/api.js +212 -21
- package/node_modules/@groove-dev/daemon/src/index.js +68 -1
- package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
- package/node_modules/@groove-dev/daemon/src/process.js +83 -11
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
- 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/codebase-index.json +64 -0
- package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
- package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
- package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
- package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
- package/node_modules/@groove-dev/gui/.groove/state.json +3 -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 +149 -76
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
- 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/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
- package/package.json +1 -2
- package/packages/cli/package.json +4 -3
- package/packages/daemon/integrations-registry.json +0 -40
- package/packages/daemon/package.json +4 -3
- package/packages/daemon/src/api.js +212 -21
- package/packages/daemon/src/index.js +68 -1
- package/packages/daemon/src/integrations.js +59 -20
- package/packages/daemon/src/process.js +83 -11
- package/packages/daemon/src/providers/claude-code.js +4 -0
- 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/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/package.json +3 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react.js +5 -0
- package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
- package/packages/gui/package.json +5 -4
- package/packages/gui/src/App.jsx +149 -76
- package/packages/gui/src/components/AgentActions.jsx +130 -1
- package/packages/gui/src/components/AgentChat.jsx +47 -7
- package/packages/gui/src/components/AgentNode.jsx +13 -83
- package/packages/gui/src/components/SpawnPanel.jsx +918 -580
- package/packages/gui/src/stores/groove.js +31 -2
- package/packages/gui/src/views/AgentTree.jsx +133 -67
- package/packages/gui/src/views/FileEditor.jsx +85 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
- package/docs/FILE-EDITOR-PLAN.md +0 -253
- package/docs/GUI_DESIGN_SPEC.md +0 -402
- package/docs/SKILLS-API-SPEC.md +0 -277
- package/node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js +0 -156
- package/packages/gui/dist/assets/index-D5dtDQf0.js +0 -156
package/packages/gui/src/App.jsx
CHANGED
|
@@ -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,18 +16,41 @@ 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
|
|
|
30
|
-
|
|
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
|
+
|
|
36
|
+
class ErrorBoundary extends React.Component {
|
|
37
|
+
constructor(props) { super(props); this.state = { error: null }; }
|
|
38
|
+
static getDerivedStateFromError(error) { return { error }; }
|
|
39
|
+
render() {
|
|
40
|
+
if (this.state.error) {
|
|
41
|
+
return React.createElement('div', {
|
|
42
|
+
style: { padding: 40, color: '#e06c75', fontFamily: 'monospace', fontSize: 13, background: '#1e2127', height: '100vh' }
|
|
43
|
+
},
|
|
44
|
+
React.createElement('h2', { style: { color: '#e6e6e6' } }, 'GROOVE — Render Error'),
|
|
45
|
+
React.createElement('pre', { style: { whiteSpace: 'pre-wrap', marginTop: 16, color: '#e06c75' } }, this.state.error.message),
|
|
46
|
+
React.createElement('pre', { style: { whiteSpace: 'pre-wrap', marginTop: 8, color: '#7a8394', fontSize: 11 } }, this.state.error.stack),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return this.props.children;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function AppInner() {
|
|
31
54
|
const agents = useGrooveStore((s) => s.agents);
|
|
32
55
|
const connected = useGrooveStore((s) => s.connected);
|
|
33
56
|
const activeTab = useGrooveStore((s) => s.activeTab);
|
|
@@ -40,72 +63,99 @@ export default function App() {
|
|
|
40
63
|
const openDetail = useGrooveStore((s) => s.openDetail);
|
|
41
64
|
const closeDetail = useGrooveStore((s) => s.closeDetail);
|
|
42
65
|
|
|
66
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
67
|
+
const dropdownRef = useRef(null);
|
|
68
|
+
const moreBtnRef = useRef(null);
|
|
69
|
+
|
|
43
70
|
useEffect(() => { connect(); }, [connect]);
|
|
44
71
|
|
|
45
|
-
|
|
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
|
+
|
|
46
83
|
const hasAgents = agents.length > 0;
|
|
84
|
+
const moreActive = DROPDOWN_IDS.has(activeTab);
|
|
47
85
|
|
|
48
86
|
return (
|
|
49
87
|
<div style={styles.root}>
|
|
50
88
|
{/* Header */}
|
|
51
89
|
<header style={styles.header}>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
{daemonHost
|
|
55
|
-
|
|
56
|
-
)}
|
|
57
|
-
</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
|
+
)}
|
|
58
94
|
|
|
95
|
+
<div style={{ flex: 1 }} />
|
|
59
96
|
|
|
60
|
-
|
|
61
|
-
|
|
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' }}>
|
|
62
112
|
<button
|
|
63
|
-
|
|
64
|
-
onClick={() =>
|
|
113
|
+
ref={moreBtnRef}
|
|
114
|
+
onClick={() => setDropdownOpen((o) => !o)}
|
|
65
115
|
style={{
|
|
66
116
|
...styles.tabBtn,
|
|
67
|
-
color:
|
|
68
|
-
borderBottom: activeTab === tab.id ? '2px solid var(--accent)' : '2px solid transparent',
|
|
69
|
-
background: activeTab === tab.id ? 'var(--bg-active)' : 'transparent',
|
|
117
|
+
color: moreActive || dropdownOpen ? 'var(--text-bright)' : 'var(--text-dim)',
|
|
70
118
|
}}
|
|
71
119
|
>
|
|
72
|
-
{
|
|
120
|
+
More {'\u25BE'}
|
|
73
121
|
</button>
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
)}
|
|
76
147
|
|
|
77
|
-
|
|
78
|
-
{statusMessage
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{connected && (
|
|
89
|
-
<>
|
|
90
|
-
<button
|
|
91
|
-
onClick={() => detailPanel?.type === 'journalist' ? closeDetail() : openDetail({ type: 'journalist' })}
|
|
92
|
-
style={{
|
|
93
|
-
...styles.tabBtn,
|
|
94
|
-
color: detailPanel?.type === 'journalist' ? 'var(--text-bright)' : 'var(--text-primary)',
|
|
95
|
-
borderBottom: detailPanel?.type === 'journalist' ? '2px solid var(--purple)' : '2px solid transparent',
|
|
96
|
-
}}
|
|
97
|
-
>
|
|
98
|
-
Journalist
|
|
99
|
-
</button>
|
|
100
|
-
<button
|
|
101
|
-
onClick={() => openDetail({ type: 'spawn' })}
|
|
102
|
-
style={styles.spawnBtn}
|
|
103
|
-
>
|
|
104
|
-
+ Spawn
|
|
105
|
-
</button>
|
|
106
|
-
</>
|
|
107
|
-
)}
|
|
108
|
-
</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
|
+
)}
|
|
109
159
|
</header>
|
|
110
160
|
|
|
111
161
|
{/* Status pill — bottom left */}
|
|
@@ -155,23 +205,29 @@ export default function App() {
|
|
|
155
205
|
{activeTab === 'approvals' && <ApprovalQueue />}
|
|
156
206
|
</main>
|
|
157
207
|
|
|
158
|
-
{/* Detail panel —
|
|
159
|
-
{detailPanel && (
|
|
208
|
+
{/* Detail panel — sidebar for agent/journalist */}
|
|
209
|
+
{detailPanel && detailPanel.type !== 'spawn' && (
|
|
160
210
|
<aside style={{
|
|
161
211
|
...styles.detailPanel,
|
|
162
212
|
width: detailPanel.type === 'agent' ? '45%' : 320,
|
|
163
213
|
}}>
|
|
164
214
|
<button onClick={closeDetail} style={styles.closeBtn}>x</button>
|
|
165
215
|
{detailPanel.type === 'agent' && <AgentPanel />}
|
|
166
|
-
{detailPanel.type === 'spawn' && <SpawnPanel />}
|
|
167
216
|
{detailPanel.type === 'journalist' && <JournalistFeed />}
|
|
168
217
|
</aside>
|
|
169
218
|
)}
|
|
219
|
+
|
|
220
|
+
{/* Spawn panel — full-screen overlay */}
|
|
221
|
+
{detailPanel?.type === 'spawn' && <SpawnPanel />}
|
|
170
222
|
</div>
|
|
171
223
|
</div>
|
|
172
224
|
);
|
|
173
225
|
}
|
|
174
226
|
|
|
227
|
+
export default function App() {
|
|
228
|
+
return React.createElement(ErrorBoundary, null, React.createElement(AppInner));
|
|
229
|
+
}
|
|
230
|
+
|
|
175
231
|
const styles = {
|
|
176
232
|
root: {
|
|
177
233
|
width: '100%', height: '100%',
|
|
@@ -182,51 +238,68 @@ const styles = {
|
|
|
182
238
|
height: 40,
|
|
183
239
|
padding: '0 16px',
|
|
184
240
|
borderBottom: '1px solid var(--border)',
|
|
185
|
-
display: 'flex', alignItems: 'center',
|
|
241
|
+
display: 'flex', alignItems: 'center', gap: 2,
|
|
186
242
|
background: 'var(--bg-chrome)',
|
|
187
243
|
flexShrink: 0,
|
|
188
244
|
position: 'relative',
|
|
189
245
|
},
|
|
190
|
-
headerLeft: {
|
|
191
|
-
display: 'flex', alignItems: 'center', gap: 8,
|
|
192
|
-
},
|
|
193
246
|
hostBadge: {
|
|
194
247
|
fontSize: 9, fontWeight: 600, letterSpacing: 0.5,
|
|
195
248
|
color: 'var(--text-dim)', background: 'var(--bg-active)',
|
|
196
249
|
padding: '2px 6px', borderRadius: 3,
|
|
197
250
|
border: '1px solid var(--border)',
|
|
198
251
|
fontFamily: 'var(--font)',
|
|
199
|
-
|
|
200
|
-
logo: {
|
|
201
|
-
fontSize: 13, fontWeight: 600, letterSpacing: 1.5,
|
|
202
|
-
color: 'var(--text-bright)',
|
|
203
|
-
},
|
|
204
|
-
headerCenter: {
|
|
205
|
-
display: 'flex', alignItems: 'center', gap: 0,
|
|
206
|
-
},
|
|
207
|
-
headerRight: {
|
|
208
|
-
display: 'flex', alignItems: 'center', gap: 10,
|
|
252
|
+
marginRight: 4,
|
|
209
253
|
},
|
|
210
254
|
tabBtn: {
|
|
211
|
-
padding: '10px
|
|
255
|
+
padding: '0 10px',
|
|
212
256
|
background: 'transparent',
|
|
213
257
|
border: 'none',
|
|
214
258
|
borderBottom: '2px solid transparent',
|
|
215
|
-
fontSize:
|
|
259
|
+
fontSize: 11, fontWeight: 500,
|
|
216
260
|
fontFamily: 'var(--font)',
|
|
217
261
|
cursor: 'pointer',
|
|
218
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',
|
|
219
292
|
},
|
|
220
293
|
spawnBtn: {
|
|
221
294
|
padding: '4px 12px',
|
|
222
295
|
background: 'transparent',
|
|
223
296
|
border: '1px solid var(--accent)',
|
|
224
297
|
borderRadius: 2,
|
|
225
|
-
color: 'var(--accent)', fontSize:
|
|
298
|
+
color: 'var(--accent)', fontSize: 11, fontWeight: 600,
|
|
226
299
|
fontFamily: 'var(--font)',
|
|
227
300
|
cursor: 'pointer',
|
|
301
|
+
marginLeft: 12,
|
|
228
302
|
},
|
|
229
|
-
agentCount: { fontSize: 11, color: 'var(--text-dim)' },
|
|
230
303
|
statusText: { fontSize: 11, color: 'var(--text-dim)', fontStyle: 'italic' },
|
|
231
304
|
mainRow: {
|
|
232
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"
|
|
@@ -233,7 +267,7 @@ function LaunchTeamButton({ showStatus }) {
|
|
|
233
267
|
|
|
234
268
|
// ── FORMATTED TEXT — renders markdown-like agent output cleanly ──
|
|
235
269
|
|
|
236
|
-
function FormattedText({ text }) {
|
|
270
|
+
export function FormattedText({ text }) {
|
|
237
271
|
if (!text) return null;
|
|
238
272
|
const lines = text.split('\n');
|
|
239
273
|
|
|
@@ -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
|
},
|