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.
Files changed (131) hide show
  1. package/node_modules/@groove-dev/cli/package.json +4 -3
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
  4. package/node_modules/@groove-dev/daemon/package.json +4 -3
  5. package/node_modules/@groove-dev/daemon/src/api.js +212 -21
  6. package/node_modules/@groove-dev/daemon/src/index.js +68 -1
  7. package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
  8. package/node_modules/@groove-dev/daemon/src/process.js +83 -11
  9. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  10. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  11. package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  13. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  14. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  15. package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  17. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  18. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  19. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  20. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  21. package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
  22. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  23. package/node_modules/@groove-dev/gui/package.json +5 -4
  24. package/node_modules/@groove-dev/gui/src/App.jsx +149 -76
  25. package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
  26. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
  27. package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
  28. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
  30. package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
  31. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  32. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
  33. package/package.json +1 -2
  34. package/packages/cli/package.json +4 -3
  35. package/packages/daemon/integrations-registry.json +0 -40
  36. package/packages/daemon/package.json +4 -3
  37. package/packages/daemon/src/api.js +212 -21
  38. package/packages/daemon/src/index.js +68 -1
  39. package/packages/daemon/src/integrations.js +59 -20
  40. package/packages/daemon/src/process.js +83 -11
  41. package/packages/daemon/src/providers/claude-code.js +4 -0
  42. package/packages/daemon/src/registry.js +1 -1
  43. package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
  44. package/packages/gui/dist/index.html +1 -1
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  58. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  60. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  62. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  64. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  66. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  67. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  68. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  69. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  70. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  71. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  72. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  73. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  74. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  75. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  76. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  77. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  78. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  79. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  91. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  92. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  93. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  94. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  95. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  96. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  97. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  98. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  99. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  100. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  101. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  102. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  103. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  104. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  105. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  106. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  107. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  108. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  109. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  110. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  111. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  112. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  113. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  114. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  115. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  116. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  117. package/packages/gui/package.json +5 -4
  118. package/packages/gui/src/App.jsx +149 -76
  119. package/packages/gui/src/components/AgentActions.jsx +130 -1
  120. package/packages/gui/src/components/AgentChat.jsx +47 -7
  121. package/packages/gui/src/components/AgentNode.jsx +13 -83
  122. package/packages/gui/src/components/SpawnPanel.jsx +918 -580
  123. package/packages/gui/src/stores/groove.js +31 -2
  124. package/packages/gui/src/views/AgentTree.jsx +133 -67
  125. package/packages/gui/src/views/FileEditor.jsx +85 -1
  126. package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
  127. package/docs/FILE-EDITOR-PLAN.md +0 -253
  128. package/docs/GUI_DESIGN_SPEC.md +0 -402
  129. package/docs/SKILLS-API-SPEC.md +0 -277
  130. package/node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js +0 -156
  131. 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: {}, // { [agentId]: [{ from, text, timestamp, isQuery }] }
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
- const nodeTypes = { agent: AgentNode };
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
- const { nodes, edges } = useMemo(() => {
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: 'default',
74
+ type: 'grooveRoot',
64
75
  position: { x: (totalWidth - NODE_X_SPACING) / 2 + 25, y: 0 },
65
- data: { label: 'GROOVE' },
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
- // Bezier spline edges the brand
82
- const edges = allAgentNodes.map((node) => {
83
- const agent = agents.find((a) => a.id === node.id);
84
- const isRunning = agent?.status === 'running';
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
- target: node.id,
89
- type: 'default', // Bezier curve (spline)
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
- return { nodes: [grooveNode, ...allAgentNodes], edges };
99
- }, [agents, selectedAgentId]);
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
- if (currentCount !== prevCountRef.current) {
104
- setTimeout(() => fitView({ padding: 0.3, maxZoom: 1.4, duration: 200 }), 50);
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: 1.4, duration: 0 }), 100);
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={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: 1.4 }}
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}>&times;</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
  },