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
|
@@ -17,10 +17,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
17
17
|
// UI state — unified panel model
|
|
18
18
|
activeTab: 'agents', // 'agents' | 'stats' | 'teams' | 'approvals' | 'editor'
|
|
19
19
|
detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
|
|
20
|
-
activityLog: {},
|
|
20
|
+
activityLog: (() => { try { return JSON.parse(localStorage.getItem('groove:activityLog') || '{}'); } catch { return {}; } })(),
|
|
21
21
|
statusMessage: null, // inline status text (replaces toast notifications)
|
|
22
22
|
commandHistory: [], // last 50 commands for command bar
|
|
23
|
-
chatHistory: {
|
|
23
|
+
chatHistory: (() => { try { return JSON.parse(localStorage.getItem('groove:chatHistory') || '{}'); } catch { return {}; } })(),
|
|
24
24
|
tokenTimeline: {}, // { [agentId]: [{ t: timestamp, v: tokensUsed }] }
|
|
25
25
|
dashTelemetry: {}, // { [agentId]: [{ t, v, name }] } — persists across tab switches
|
|
26
26
|
|
|
@@ -88,6 +88,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
88
88
|
type: data.type,
|
|
89
89
|
}];
|
|
90
90
|
set({ activityLog: log });
|
|
91
|
+
try { localStorage.setItem('groove:activityLog', JSON.stringify(log)); } catch { /* full */ }
|
|
91
92
|
break;
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -98,6 +99,22 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
98
99
|
: msg.status === 'killed' ? `${name} killed`
|
|
99
100
|
: `${name} crashed (exit ${msg.code})`;
|
|
100
101
|
get().showStatus(text);
|
|
102
|
+
|
|
103
|
+
// Check if all agents are done and no fullstack/QC exists — nudge user
|
|
104
|
+
setTimeout(() => {
|
|
105
|
+
const agents = get().agents;
|
|
106
|
+
const alive = agents.filter((a) => a.status === 'running' || a.status === 'starting');
|
|
107
|
+
const done = agents.filter((a) => a.status === 'completed' || a.status === 'crashed');
|
|
108
|
+
const hasQC = agents.some((a) => a.role === 'fullstack' && (a.status === 'running' || a.status === 'starting'));
|
|
109
|
+
if (alive.length === 0 && done.length >= 2 && !hasQC) {
|
|
110
|
+
get().showStatus('All agents finished — no QC agent. Consider spawning a fullstack to audit and integrate.');
|
|
111
|
+
}
|
|
112
|
+
}, 2000);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'phase2:spawned': {
|
|
117
|
+
get().showStatus(`QC agent ${msg.name} auto-spawned — auditing phase 1 work`);
|
|
101
118
|
break;
|
|
102
119
|
}
|
|
103
120
|
|
|
@@ -212,6 +229,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
212
229
|
history[agentId] = [...history[agentId].slice(-100), {
|
|
213
230
|
from, text, timestamp: Date.now(), isQuery,
|
|
214
231
|
}];
|
|
232
|
+
try { localStorage.setItem('groove:chatHistory', JSON.stringify(history)); } catch { /* full */ }
|
|
215
233
|
return { chatHistory: history };
|
|
216
234
|
});
|
|
217
235
|
},
|
|
@@ -239,9 +257,20 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
239
257
|
set((s) => {
|
|
240
258
|
const history = { ...s.chatHistory };
|
|
241
259
|
history[newAgent.id] = [...oldChat];
|
|
260
|
+
try { localStorage.setItem('groove:chatHistory', JSON.stringify(history)); } catch {}
|
|
242
261
|
return { chatHistory: history };
|
|
243
262
|
});
|
|
244
263
|
}
|
|
264
|
+
// Carry activity log (agent responses)
|
|
265
|
+
const oldLog = get().activityLog[id] || [];
|
|
266
|
+
if (oldLog.length > 0) {
|
|
267
|
+
set((s) => {
|
|
268
|
+
const log = { ...s.activityLog };
|
|
269
|
+
log[newAgent.id] = [...oldLog];
|
|
270
|
+
try { localStorage.setItem('groove:activityLog', JSON.stringify(log)); } catch {}
|
|
271
|
+
return { activityLog: log };
|
|
272
|
+
});
|
|
273
|
+
}
|
|
245
274
|
// Also carry token timeline for continuity in stats
|
|
246
275
|
const oldTimeline = get().tokenTimeline[id] || [];
|
|
247
276
|
if (oldTimeline.length > 0) {
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
// GROOVE GUI — Agent Tree View (React Flow)
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import React, { useMemo, useCallback, useEffect, useRef } from 'react';
|
|
5
|
-
import { ReactFlow, Background, useReactFlow, ReactFlowProvider } from '@xyflow/react';
|
|
4
|
+
import React, { useMemo, useCallback, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import { ReactFlow, Background, useReactFlow, ReactFlowProvider, applyNodeChanges } from '@xyflow/react';
|
|
6
6
|
import '@xyflow/react/dist/style.css';
|
|
7
7
|
import { useGrooveStore } from '../stores/groove';
|
|
8
8
|
import AgentNode from '../components/AgentNode';
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
import { Handle, Position } from '@xyflow/react';
|
|
11
|
+
|
|
12
|
+
function GrooveRootNode({ data }) {
|
|
13
|
+
return (
|
|
14
|
+
<div style={data.style}>
|
|
15
|
+
{data.label}
|
|
16
|
+
<Handle id="s-top" type="source" position={Position.Top} style={{ opacity: 0, width: 1, height: 1 }} />
|
|
17
|
+
<Handle id="s-bottom" type="source" position={Position.Bottom} style={{ opacity: 0, width: 1, height: 1 }} />
|
|
18
|
+
<Handle id="s-left" type="source" position={Position.Left} style={{ opacity: 0, width: 1, height: 1 }} />
|
|
19
|
+
<Handle id="s-right" type="source" position={Position.Right} style={{ opacity: 0, width: 1, height: 1 }} />
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const nodeTypes = { agent: AgentNode, grooveRoot: GrooveRootNode };
|
|
11
25
|
|
|
12
26
|
const MAX_PER_ROW = 4;
|
|
13
27
|
const NODE_X_SPACING = 250;
|
|
@@ -22,92 +36,143 @@ function AgentTreeInner() {
|
|
|
22
36
|
|
|
23
37
|
const selectedAgentId = detailPanel?.type === 'agent' ? detailPanel.agentId : null;
|
|
24
38
|
const prevCountRef = useRef(0);
|
|
39
|
+
// Position map keyed by agent NAME — persisted to localStorage
|
|
40
|
+
const positionMapRef = useRef(() => {
|
|
41
|
+
try { return JSON.parse(localStorage.getItem('groove:nodePositions') || '{}'); } catch { return {}; }
|
|
42
|
+
});
|
|
43
|
+
// Lazy init the ref
|
|
44
|
+
if (typeof positionMapRef.current === 'function') positionMapRef.current = positionMapRef.current();
|
|
45
|
+
const nextSlotRef = useRef(Object.keys(positionMapRef.current).length);
|
|
46
|
+
const [flowNodes, setFlowNodes] = useState([]);
|
|
47
|
+
|
|
48
|
+
// Compute target nodes + edges from agent state
|
|
49
|
+
const { targetNodes } = useMemo(() => {
|
|
50
|
+
const allAgentNodes = agents.map((agent) => {
|
|
51
|
+
const posKey = agent.name || agent.id;
|
|
52
|
+
if (!positionMapRef.current[posKey]) {
|
|
53
|
+
const slot = nextSlotRef.current;
|
|
54
|
+
positionMapRef.current[posKey] = {
|
|
55
|
+
x: (slot % MAX_PER_ROW) * NODE_X_SPACING,
|
|
56
|
+
y: 160 + Math.floor(slot / MAX_PER_ROW) * NODE_Y_SPACING,
|
|
57
|
+
};
|
|
58
|
+
nextSlotRef.current += 1;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
id: agent.id,
|
|
62
|
+
type: 'agent',
|
|
63
|
+
position: positionMapRef.current[posKey],
|
|
64
|
+
data: { ...agent, selected: agent.id === selectedAgentId },
|
|
65
|
+
draggable: true,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
25
68
|
|
|
26
|
-
|
|
27
|
-
const running = agents.filter((a) => a.status === 'running' || a.status === 'starting');
|
|
28
|
-
const done = agents.filter((a) => a.status !== 'running' && a.status !== 'starting');
|
|
29
|
-
|
|
30
|
-
const runningNodes = running.map((agent, i) => ({
|
|
31
|
-
id: agent.id,
|
|
32
|
-
type: 'agent',
|
|
33
|
-
position: {
|
|
34
|
-
x: (i % MAX_PER_ROW) * NODE_X_SPACING,
|
|
35
|
-
y: 80 + Math.floor(i / MAX_PER_ROW) * NODE_Y_SPACING,
|
|
36
|
-
},
|
|
37
|
-
data: { ...agent, selected: agent.id === selectedAgentId },
|
|
38
|
-
draggable: true,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
const runningRows = Math.ceil(running.length / MAX_PER_ROW) || 1;
|
|
42
|
-
const doneStartY = 80 + runningRows * NODE_Y_SPACING + 50;
|
|
43
|
-
|
|
44
|
-
const doneNodes = done.map((agent, i) => ({
|
|
45
|
-
id: agent.id,
|
|
46
|
-
type: 'agent',
|
|
47
|
-
position: {
|
|
48
|
-
x: (i % MAX_PER_ROW) * NODE_X_SPACING,
|
|
49
|
-
y: doneStartY + Math.floor(i / MAX_PER_ROW) * NODE_Y_SPACING,
|
|
50
|
-
},
|
|
51
|
-
data: { ...agent, selected: agent.id === selectedAgentId },
|
|
52
|
-
draggable: true,
|
|
53
|
-
}));
|
|
54
|
-
|
|
55
|
-
const allAgentNodes = [...runningNodes, ...doneNodes];
|
|
56
|
-
|
|
57
|
-
const maxPerRow = Math.min(Math.max(running.length, done.length, 1), MAX_PER_ROW);
|
|
69
|
+
const maxPerRow = Math.min(Math.max(agents.length, 1), MAX_PER_ROW);
|
|
58
70
|
const totalWidth = maxPerRow * NODE_X_SPACING;
|
|
59
71
|
|
|
60
|
-
// GROOVE root node — clean, rounded, matching
|
|
61
72
|
const grooveNode = {
|
|
62
73
|
id: 'groove-root',
|
|
63
|
-
type: '
|
|
74
|
+
type: 'grooveRoot',
|
|
64
75
|
position: { x: (totalWidth - NODE_X_SPACING) / 2 + 25, y: 0 },
|
|
65
|
-
data: {
|
|
76
|
+
data: {
|
|
77
|
+
label: 'GROOVE',
|
|
78
|
+
style: {
|
|
79
|
+
background: '#282c34',
|
|
80
|
+
color: '#e6e6e6',
|
|
81
|
+
border: '1px solid #3e4451',
|
|
82
|
+
borderRadius: 24,
|
|
83
|
+
fontWeight: 600,
|
|
84
|
+
fontSize: 11,
|
|
85
|
+
letterSpacing: 6,
|
|
86
|
+
padding: '10px 36px 9px',
|
|
87
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', Consolas, monospace",
|
|
88
|
+
position: 'relative',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
66
91
|
selectable: false,
|
|
67
92
|
draggable: false,
|
|
68
|
-
style: {
|
|
69
|
-
background: '#282c34',
|
|
70
|
-
color: '#e6e6e6',
|
|
71
|
-
border: '1px solid #3e4451',
|
|
72
|
-
borderRadius: 24,
|
|
73
|
-
fontWeight: 600,
|
|
74
|
-
fontSize: 11,
|
|
75
|
-
letterSpacing: 6,
|
|
76
|
-
padding: '10px 36px 9px',
|
|
77
|
-
fontFamily: "'JetBrains Mono', 'SF Mono', Consolas, monospace",
|
|
78
|
-
},
|
|
79
93
|
};
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
return { targetNodes: [grooveNode, ...allAgentNodes] };
|
|
96
|
+
}, [agents, selectedAgentId]);
|
|
97
|
+
|
|
98
|
+
// Compute edges from current flowNodes so they update on drag
|
|
99
|
+
const edges = useMemo(() => {
|
|
100
|
+
const root = flowNodes.find((n) => n.id === 'groove-root');
|
|
101
|
+
if (!root) return [];
|
|
102
|
+
const rootW = 140, rootH = 36;
|
|
103
|
+
const rootCx = root.position.x + rootW / 2;
|
|
104
|
+
const rootCy = root.position.y + rootH / 2;
|
|
105
|
+
|
|
106
|
+
return flowNodes.filter((n) => n.id !== 'groove-root').map((node) => {
|
|
107
|
+
const nw = 210, nh = 120;
|
|
108
|
+
const ncx = node.position.x + nw / 2;
|
|
109
|
+
const ncy = node.position.y + nh / 2;
|
|
110
|
+
const dx = ncx - rootCx;
|
|
111
|
+
const dy = ncy - rootCy;
|
|
112
|
+
|
|
113
|
+
let sourceHandle, targetHandle;
|
|
114
|
+
if (Math.abs(dy) > Math.abs(dx)) {
|
|
115
|
+
if (dy > 0) { sourceHandle = 's-bottom'; targetHandle = 'top'; }
|
|
116
|
+
else { sourceHandle = 's-top'; targetHandle = 'bottom'; }
|
|
117
|
+
} else {
|
|
118
|
+
if (dx > 0) { sourceHandle = 's-right'; targetHandle = 'left'; }
|
|
119
|
+
else { sourceHandle = 's-left'; targetHandle = 'right'; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isRunning = node.data?.status === 'running';
|
|
85
123
|
return {
|
|
86
124
|
id: `groove-${node.id}`,
|
|
87
|
-
source: 'groove-root',
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
style: {
|
|
91
|
-
stroke: isRunning ? '#5c6370' : '#2c313a',
|
|
92
|
-
strokeWidth: 1,
|
|
93
|
-
},
|
|
125
|
+
source: 'groove-root', target: node.id,
|
|
126
|
+
sourceHandle, targetHandle, type: 'default',
|
|
127
|
+
style: { stroke: isRunning ? '#5c6370' : '#2c313a', strokeWidth: 1 },
|
|
94
128
|
animated: isRunning,
|
|
95
129
|
};
|
|
96
130
|
});
|
|
131
|
+
}, [flowNodes]);
|
|
97
132
|
|
|
98
|
-
|
|
99
|
-
|
|
133
|
+
// Sync target nodes into flow state — preserve user-dragged positions
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setFlowNodes((prev) => {
|
|
136
|
+
const prevMap = new Map(prev.map((n) => [n.id, n]));
|
|
137
|
+
return targetNodes.map((target) => {
|
|
138
|
+
const existing = prevMap.get(target.id);
|
|
139
|
+
if (existing) {
|
|
140
|
+
// Keep dragged position, only update data
|
|
141
|
+
return { ...existing, data: target.data };
|
|
142
|
+
}
|
|
143
|
+
return target;
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}, [targetNodes]);
|
|
147
|
+
|
|
148
|
+
// Handle ALL node changes including drag — this is what makes drag work
|
|
149
|
+
const onNodesChange = useCallback((changes) => {
|
|
150
|
+
setFlowNodes((nds) => {
|
|
151
|
+
const updated = applyNodeChanges(changes, nds);
|
|
152
|
+
// Save final drag positions to the ref (keyed by name for stability)
|
|
153
|
+
for (const change of changes) {
|
|
154
|
+
if (change.type === 'position' && change.position && !change.dragging) {
|
|
155
|
+
const node = updated.find((n) => n.id === change.id);
|
|
156
|
+
const name = node?.data?.name || change.id;
|
|
157
|
+
positionMapRef.current[name] = change.position;
|
|
158
|
+
try { localStorage.setItem('groove:nodePositions', JSON.stringify(positionMapRef.current)); } catch {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return updated;
|
|
162
|
+
});
|
|
163
|
+
}, []);
|
|
100
164
|
|
|
101
165
|
useEffect(() => {
|
|
102
166
|
const currentCount = agents.length;
|
|
103
|
-
|
|
104
|
-
|
|
167
|
+
const prevCount = prevCountRef.current;
|
|
168
|
+
if (prevCount === 0 && currentCount === 1) {
|
|
169
|
+
setTimeout(() => fitView({ padding: 0.3, maxZoom: 0.85, duration: 200 }), 50);
|
|
105
170
|
}
|
|
106
171
|
prevCountRef.current = currentCount;
|
|
107
172
|
}, [agents.length, fitView]);
|
|
108
173
|
|
|
109
174
|
useEffect(() => {
|
|
110
|
-
setTimeout(() => fitView({ padding: 0.3, maxZoom:
|
|
175
|
+
setTimeout(() => fitView({ padding: 0.3, maxZoom: 0.85, duration: 0 }), 100);
|
|
111
176
|
}, []);
|
|
112
177
|
|
|
113
178
|
const onNodeClick = useCallback((event, node) => {
|
|
@@ -121,10 +186,11 @@ function AgentTreeInner() {
|
|
|
121
186
|
|
|
122
187
|
return (
|
|
123
188
|
<ReactFlow
|
|
124
|
-
nodes={
|
|
189
|
+
nodes={flowNodes}
|
|
125
190
|
edges={edges}
|
|
126
191
|
nodeTypes={nodeTypes}
|
|
127
192
|
onNodeClick={onNodeClick}
|
|
193
|
+
onNodesChange={onNodesChange}
|
|
128
194
|
onPaneClick={onPaneClick}
|
|
129
195
|
proOptions={{ hideAttribution: true }}
|
|
130
196
|
nodesConnectable={false}
|
|
@@ -132,7 +198,7 @@ function AgentTreeInner() {
|
|
|
132
198
|
minZoom={0.3}
|
|
133
199
|
maxZoom={2}
|
|
134
200
|
fitView
|
|
135
|
-
fitViewOptions={{ padding: 0.3, maxZoom:
|
|
201
|
+
fitViewOptions={{ padding: 0.3, maxZoom: 0.85 }}
|
|
136
202
|
>
|
|
137
203
|
<Background color="#3e4451" gap={20} size={1} />
|
|
138
204
|
</ReactFlow>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GROOVE GUI — File Editor View
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
-
import React, { useState, useCallback } from 'react';
|
|
4
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
5
5
|
import { useGrooveStore } from '../stores/groove';
|
|
6
6
|
import FileTree from '../components/FileTree';
|
|
7
7
|
import EditorTabs from '../components/EditorTabs';
|
|
@@ -18,9 +18,39 @@ export default function FileEditor() {
|
|
|
18
18
|
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
19
19
|
const reloadFile = useGrooveStore((s) => s.reloadFile);
|
|
20
20
|
const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
|
|
21
|
+
const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
|
|
21
22
|
|
|
22
23
|
const [terminalOpen, setTerminalOpen] = useState(false);
|
|
23
24
|
const [terminalHeight, setTerminalHeight] = useState(220);
|
|
25
|
+
const [rootDir, setRootDir] = useState('');
|
|
26
|
+
const [rootInput, setRootInput] = useState('');
|
|
27
|
+
const [editingRoot, setEditingRoot] = useState(false);
|
|
28
|
+
|
|
29
|
+
// Fetch current editor root on mount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
fetch('/api/files/root').then((r) => r.json()).then((d) => {
|
|
32
|
+
setRootDir(d.root || '');
|
|
33
|
+
setRootInput(d.root || '');
|
|
34
|
+
}).catch(() => {});
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
async function handleRootChange() {
|
|
38
|
+
if (!rootInput.trim()) return;
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch('/api/files/root', {
|
|
41
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
body: JSON.stringify({ root: rootInput.trim() }),
|
|
43
|
+
});
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
if (data.ok) {
|
|
46
|
+
setRootDir(data.root);
|
|
47
|
+
setEditingRoot(false);
|
|
48
|
+
// Clear tree cache and reload
|
|
49
|
+
useGrooveStore.setState({ editorTreeCache: {}, editorOpenTabs: [], editorActiveFile: null, editorFiles: {} });
|
|
50
|
+
fetchTreeDir('');
|
|
51
|
+
}
|
|
52
|
+
} catch { /* ignore */ }
|
|
53
|
+
}
|
|
24
54
|
|
|
25
55
|
const file = activeFile ? files[activeFile] : null;
|
|
26
56
|
const isChanged = activeFile && changedFiles[activeFile];
|
|
@@ -54,6 +84,29 @@ export default function FileEditor() {
|
|
|
54
84
|
|
|
55
85
|
return (
|
|
56
86
|
<div style={styles.container}>
|
|
87
|
+
{/* Directory bar */}
|
|
88
|
+
<div style={styles.dirBar}>
|
|
89
|
+
<span style={styles.dirLabel}>DIR</span>
|
|
90
|
+
{editingRoot ? (
|
|
91
|
+
<div style={{ display: 'flex', flex: 1, gap: 4 }}>
|
|
92
|
+
<input
|
|
93
|
+
style={styles.dirInput}
|
|
94
|
+
value={rootInput}
|
|
95
|
+
onChange={(e) => setRootInput(e.target.value)}
|
|
96
|
+
onKeyDown={(e) => { if (e.key === 'Enter') handleRootChange(); if (e.key === 'Escape') { setEditingRoot(false); setRootInput(rootDir); } }}
|
|
97
|
+
placeholder="/absolute/path/to/project"
|
|
98
|
+
autoFocus
|
|
99
|
+
/>
|
|
100
|
+
<button onClick={handleRootChange} style={styles.dirOkBtn}>Open</button>
|
|
101
|
+
<button onClick={() => { setEditingRoot(false); setRootInput(rootDir); }} style={styles.dirCancelBtn}>×</button>
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<button onClick={() => setEditingRoot(true)} style={styles.dirPath} title="Click to change directory">
|
|
105
|
+
{rootDir || '...'}
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
57
110
|
{/* Tab bar */}
|
|
58
111
|
<EditorTabs />
|
|
59
112
|
|
|
@@ -130,6 +183,37 @@ const styles = {
|
|
|
130
183
|
display: 'flex', flexDirection: 'column',
|
|
131
184
|
height: '100%', width: '100%',
|
|
132
185
|
},
|
|
186
|
+
dirBar: {
|
|
187
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
188
|
+
padding: '4px 12px', flexShrink: 0,
|
|
189
|
+
borderBottom: '1px solid var(--border)',
|
|
190
|
+
background: 'var(--bg-chrome)',
|
|
191
|
+
},
|
|
192
|
+
dirLabel: {
|
|
193
|
+
fontSize: 9, fontWeight: 700, color: 'var(--text-dim)',
|
|
194
|
+
letterSpacing: 1, flexShrink: 0,
|
|
195
|
+
},
|
|
196
|
+
dirPath: {
|
|
197
|
+
background: 'none', border: 'none', color: 'var(--text-primary)',
|
|
198
|
+
fontSize: 11, fontFamily: 'var(--font)', cursor: 'pointer',
|
|
199
|
+
padding: '2px 6px', borderRadius: 3, textAlign: 'left',
|
|
200
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
201
|
+
flex: 1,
|
|
202
|
+
},
|
|
203
|
+
dirInput: {
|
|
204
|
+
flex: 1, background: 'var(--bg-surface)', border: '1px solid var(--accent)',
|
|
205
|
+
borderRadius: 3, padding: '2px 8px', color: 'var(--text-primary)',
|
|
206
|
+
fontSize: 11, outline: 'none', fontFamily: 'var(--font)',
|
|
207
|
+
},
|
|
208
|
+
dirOkBtn: {
|
|
209
|
+
background: 'var(--accent)', color: 'var(--bg-base)', border: 'none',
|
|
210
|
+
borderRadius: 3, padding: '2px 10px', fontSize: 10, fontWeight: 600,
|
|
211
|
+
cursor: 'pointer', fontFamily: 'var(--font)',
|
|
212
|
+
},
|
|
213
|
+
dirCancelBtn: {
|
|
214
|
+
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
215
|
+
fontSize: 14, cursor: 'pointer', padding: '0 4px',
|
|
216
|
+
},
|
|
133
217
|
contentRow: {
|
|
134
218
|
flex: 1, display: 'flex', overflow: 'hidden',
|
|
135
219
|
},
|