groove-dev 0.27.14 → 0.27.17

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 (169) hide show
  1. package/README.md +37 -1
  2. package/developerID_application.cer +0 -0
  3. package/node_modules/@groove-dev/daemon/src/api.js +587 -68
  4. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  5. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  6. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  7. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  10. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  11. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  12. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  13. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  14. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  15. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  16. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  17. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  19. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  21. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  22. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  23. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  24. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  25. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  26. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  27. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  28. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  30. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  31. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  32. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  33. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  34. package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
  35. package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  37. package/node_modules/@groove-dev/gui/index.html +1 -0
  38. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  39. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  43. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  44. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  45. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  46. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  48. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  49. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  50. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
  51. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  52. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  53. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  54. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  55. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  56. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  57. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  58. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  59. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  60. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  61. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  62. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  66. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  67. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  68. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  71. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  72. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  74. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  75. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  76. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  77. package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
  78. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  79. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  80. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  81. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  82. package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
  83. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  84. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  85. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  86. package/package.json +1 -1
  87. package/packages/daemon/src/api.js +587 -68
  88. package/packages/daemon/src/classifier.js +24 -0
  89. package/packages/daemon/src/credentials.js +12 -2
  90. package/packages/daemon/src/federation/ambassador.js +204 -0
  91. package/packages/daemon/src/federation/connection.js +359 -0
  92. package/packages/daemon/src/federation/contracts.js +112 -0
  93. package/packages/daemon/src/federation/whitelist.js +190 -0
  94. package/packages/daemon/src/federation.js +166 -7
  95. package/packages/daemon/src/index.js +172 -19
  96. package/packages/daemon/src/introducer.js +52 -7
  97. package/packages/daemon/src/journalist.js +46 -1
  98. package/packages/daemon/src/memory.js +36 -16
  99. package/packages/daemon/src/process.js +140 -23
  100. package/packages/daemon/src/providers/base.js +1 -0
  101. package/packages/daemon/src/providers/claude-code.js +1 -0
  102. package/packages/daemon/src/providers/codex.js +124 -28
  103. package/packages/daemon/src/providers/gemini.js +104 -17
  104. package/packages/daemon/src/providers/index.js +17 -0
  105. package/packages/daemon/src/registry.js +10 -1
  106. package/packages/daemon/src/rotator.js +93 -30
  107. package/packages/daemon/src/skills.js +33 -3
  108. package/packages/daemon/src/terminal-pty.js +9 -1
  109. package/packages/daemon/src/tool-executor.js +11 -5
  110. package/packages/daemon/src/toys.js +69 -0
  111. package/packages/daemon/src/tunnel-manager.js +24 -5
  112. package/packages/daemon/templates/toys-catalog.json +242 -0
  113. package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
  114. package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
  115. package/packages/gui/dist/index.html +3 -2
  116. package/packages/gui/index.html +1 -0
  117. package/packages/gui/src/app.css +7 -0
  118. package/packages/gui/src/app.jsx +37 -10
  119. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  120. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  121. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  122. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  123. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  124. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  125. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  126. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  127. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  128. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  129. package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
  130. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  131. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  132. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  133. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  134. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  135. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  136. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  137. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  138. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  139. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  140. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  141. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  142. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  143. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  144. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  145. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  146. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  147. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  148. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  149. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  150. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  151. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  152. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  153. package/packages/gui/src/components/ui/toast.jsx +2 -2
  154. package/packages/gui/src/lib/electron.js +15 -0
  155. package/packages/gui/src/lib/format.js +1 -0
  156. package/packages/gui/src/stores/groove.js +373 -58
  157. package/packages/gui/src/views/agents.jsx +148 -42
  158. package/packages/gui/src/views/editor.jsx +92 -2
  159. package/packages/gui/src/views/federation.jsx +37 -0
  160. package/packages/gui/src/views/marketplace.jsx +2 -42
  161. package/packages/gui/src/views/settings.jsx +32 -132
  162. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  163. package/packages/gui/src/views/teams.jsx +3 -3
  164. package/packages/gui/src/views/toys.jsx +162 -0
  165. package/plans/chat-persistence-refactor.md +154 -0
  166. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  167. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  168. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  169. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
10
10
  import { cn } from '../lib/cn';
11
11
  import { Button } from '../components/ui/button';
12
12
  import { Badge } from '../components/ui/badge';
13
- import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil } from 'lucide-react';
13
+ import { Plus, Users, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, ChevronLeft, ChevronRight, FolderOpen } from 'lucide-react';
14
14
 
15
15
  const NODE_TYPES = { agentNode: AgentNode, rootNode: RootNode };
16
16
  const NODE_W = 220;
@@ -20,12 +20,28 @@ const NODE_Y_GAP = 130;
20
20
  const MAX_PER_ROW = 4;
21
21
  const ROOT_ID = '__groove_root__';
22
22
 
23
- function loadPositions() {
24
- try { return JSON.parse(localStorage.getItem('groove:nodePositions') || '{}'); } catch { return {}; }
23
+ function loadPositions(teamId) {
24
+ if (!teamId) return {};
25
+ try { return JSON.parse(localStorage.getItem(`groove:nodePositions:${teamId}`) || '{}'); } catch { return {}; }
25
26
  }
26
27
 
27
- function savePositions(positions) {
28
- try { localStorage.setItem('groove:nodePositions', JSON.stringify(positions)); } catch {}
28
+ // Drop high-volume caches (chatHistory, activityLog) to free quota.
29
+ // Used as a fallback when setItem fails on savePositions.
30
+ function freeLocalStorage() {
31
+ let freed = false;
32
+ for (const key of ['groove:chatHistory', 'groove:activityLog']) {
33
+ if (localStorage.getItem(key) !== null) { localStorage.removeItem(key); freed = true; }
34
+ }
35
+ return freed;
36
+ }
37
+
38
+ function savePositions(teamId, positions) {
39
+ if (!teamId) return;
40
+ const key = `groove:nodePositions:${teamId}`;
41
+ const s = JSON.stringify(positions);
42
+ try { localStorage.setItem(key, s); return; } catch { /* quota */ }
43
+ if (!freeLocalStorage()) return;
44
+ try { localStorage.setItem(key, s); } catch { /* still over — give up silently */ }
29
45
  }
30
46
 
31
47
  function loadTeamViewports() {
@@ -72,6 +88,29 @@ export function TeamTabBar() {
72
88
  const submitting = useRef(false);
73
89
  const [dragId, setDragId] = useState(null);
74
90
  const [dragOverId, setDragOverId] = useState(null);
91
+ const scrollRef = useRef(null);
92
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
93
+ const [canScrollRight, setCanScrollRight] = useState(false);
94
+
95
+ const checkScroll = useCallback(() => {
96
+ const el = scrollRef.current;
97
+ if (!el) return;
98
+ setCanScrollLeft(el.scrollLeft > 0);
99
+ setCanScrollRight(el.scrollLeft + el.clientWidth < el.scrollWidth - 1);
100
+ }, []);
101
+
102
+ useEffect(() => {
103
+ const el = scrollRef.current;
104
+ if (!el) return;
105
+ checkScroll();
106
+ el.addEventListener('scroll', checkScroll);
107
+ const ro = new ResizeObserver(checkScroll);
108
+ ro.observe(el);
109
+ return () => {
110
+ el.removeEventListener('scroll', checkScroll);
111
+ ro.disconnect();
112
+ };
113
+ }, [checkScroll, teams.length]);
75
114
 
76
115
  function handleCreate() {
77
116
  const name = newName.trim();
@@ -79,7 +118,10 @@ export function TeamTabBar() {
79
118
  submitting.current = true;
80
119
  setNewName('');
81
120
  setCreating(false);
82
- createTeam(name).finally(() => { submitting.current = false; });
121
+ createTeam(name).finally(() => {
122
+ submitting.current = false;
123
+ setTimeout(() => { if (scrollRef.current) scrollRef.current.scrollTo({ left: scrollRef.current.scrollWidth, behavior: 'smooth' }); }, 100);
124
+ });
83
125
  }
84
126
 
85
127
  function startRename(team) {
@@ -95,7 +137,20 @@ export function TeamTabBar() {
95
137
  }
96
138
 
97
139
  return (
98
- <div className="flex items-end px-0 pt-0 pb-0 bg-surface-1 border-b border-border gap-0 flex-shrink-0">
140
+ <div className="flex items-end px-0 pt-0 pb-0 bg-surface-1 border-b border-border gap-0 flex-shrink-0 overflow-hidden">
141
+ {canScrollLeft && (
142
+ <button
143
+ onClick={() => scrollRef.current?.scrollBy({ left: -300, behavior: 'smooth' })}
144
+ className="w-6 h-9 flex items-center justify-center bg-accent/15 text-accent hover:bg-accent/25 transition-colors flex-shrink-0 cursor-pointer"
145
+ >
146
+ <ChevronLeft size={14} />
147
+ </button>
148
+ )}
149
+ <div
150
+ ref={scrollRef}
151
+ className="flex items-end flex-1 min-w-0 overflow-x-auto gap-0"
152
+ style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', WebkitOverflowScrolling: 'touch' }}
153
+ >
99
154
  {teams.map((team) => {
100
155
  const count = agents.filter((a) => a.teamId === team.id).length;
101
156
  const isActive = team.id === activeTeamId;
@@ -122,7 +177,7 @@ export function TeamTabBar() {
122
177
  onClick={() => !isRenaming && switchTeam(team.id)}
123
178
  onDoubleClick={() => startRename(team)}
124
179
  className={cn(
125
- 'group relative flex items-center gap-2 px-4 h-9 text-xs font-sans cursor-pointer select-none transition-colors',
180
+ 'group relative flex items-center gap-2 px-4 h-9 text-xs font-sans cursor-pointer select-none transition-colors flex-shrink-0',
126
181
  isActive
127
182
  ? 'text-text-0 font-semibold border-x border-x-border bg-[#242830]'
128
183
  : 'text-text-3 hover:text-text-1 hover:bg-surface-3/50',
@@ -202,9 +257,8 @@ export function TeamTabBar() {
202
257
  );
203
258
  })}
204
259
 
205
- {/* Create new team */}
206
260
  {creating ? (
207
- <div className="flex items-center gap-1.5 px-3 h-9 bg-surface-3/50">
261
+ <div className="flex items-center gap-1.5 px-3 h-9 flex-shrink-0">
208
262
  <input
209
263
  value={newName}
210
264
  onChange={(e) => setNewName(e.target.value)}
@@ -227,10 +281,20 @@ export function TeamTabBar() {
227
281
  ) : (
228
282
  <button
229
283
  onClick={() => setCreating(true)}
230
- className="flex items-center justify-center w-9 h-9 text-text-4 hover:text-text-1 hover:bg-surface-3/50 cursor-pointer transition-colors"
284
+ className="flex items-center justify-center w-6 h-6 my-auto mx-2 rounded-full bg-accent/15 text-accent hover:bg-accent/25 cursor-pointer transition-colors flex-shrink-0"
231
285
  title="New team"
232
286
  >
233
- <Plus size={14} />
287
+ <Plus size={12} />
288
+ </button>
289
+ )}
290
+
291
+ </div>
292
+ {canScrollRight && (
293
+ <button
294
+ onClick={() => scrollRef.current?.scrollBy({ left: 300, behavior: 'smooth' })}
295
+ className="w-6 h-9 flex items-center justify-center bg-accent/15 text-accent hover:bg-accent/25 transition-colors flex-shrink-0 cursor-pointer"
296
+ >
297
+ <ChevronRight size={14} />
234
298
  </button>
235
299
  )}
236
300
  </div>
@@ -246,10 +310,23 @@ function AgentTreeInner() {
246
310
  const selectAgent = useGrooveStore((s) => s.selectAgent);
247
311
  const closeDetail = useGrooveStore((s) => s.closeDetail);
248
312
 
249
- const agents = useMemo(
250
- () => allAgents.filter((a) => a.teamId === activeTeamId),
251
- [allAgents, activeTeamId],
252
- );
313
+ const prevAgentsRef = useRef([]);
314
+ const agents = useMemo(() => {
315
+ const next = allAgents
316
+ .filter((a) => a.teamId === activeTeamId)
317
+ .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id));
318
+ const prev = prevAgentsRef.current;
319
+ if (prev.length === next.length && prev.every((p, i) => p.id === next[i].id && p.status === next[i].status && p.name === next[i].name && p.model === next[i].model && p.tokensUsed === next[i].tokensUsed && p.contextUsage === next[i].contextUsage)) return prev;
320
+ prevAgentsRef.current = next;
321
+ return next;
322
+ }, [allAgents, activeTeamId]);
323
+
324
+ const positionsRef = useRef(loadPositions(activeTeamId));
325
+ const positionsTeamRef = useRef(activeTeamId);
326
+ if (positionsTeamRef.current !== activeTeamId) {
327
+ positionsTeamRef.current = activeTeamId;
328
+ positionsRef.current = loadPositions(activeTeamId);
329
+ }
253
330
 
254
331
  const { fitView, setViewport } = useReactFlow();
255
332
  const [prevCount, setPrevCount] = useState(0);
@@ -257,7 +334,7 @@ function AgentTreeInner() {
257
334
 
258
335
  // Build nodes — positions are stable, data updates flow to node components
259
336
  const targetNodes = useMemo(() => {
260
- const saved = loadPositions();
337
+ const saved = positionsRef.current;
261
338
  const runningCount = agents.filter((a) => a.status === 'running').length;
262
339
 
263
340
  const nodes = [
@@ -271,17 +348,14 @@ function AgentTreeInner() {
271
348
  },
272
349
  ];
273
350
 
274
- // Track occupied positions so new nodes don't overlap existing ones
275
351
  const occupied = new Set();
276
352
  const posKey = (x, y) => `${Math.round(x / 100)},${Math.round(y / 100)}`;
277
353
 
278
- // Mark root node position as occupied
279
354
  const rootPos = saved[ROOT_ID] || { x: 0, y: 0 };
280
355
  occupied.add(posKey(rootPos.x, rootPos.y));
281
356
 
282
- // First pass: place agents with saved positions
283
357
  const pending = [];
284
- agents.forEach((agent, i) => {
358
+ agents.forEach((agent) => {
285
359
  const key = agent.name || agent.id;
286
360
  if (saved[key]) {
287
361
  const pos = saved[key];
@@ -292,37 +366,51 @@ function AgentTreeInner() {
292
366
  draggable: true, selectable: true,
293
367
  });
294
368
  } else {
295
- pending.push({ agent, index: i });
369
+ pending.push(agent);
296
370
  }
297
371
  });
298
372
 
299
- // Second pass: place new agents in non-overlapping positions
300
- for (const { agent, index } of pending) {
301
- const row = Math.floor(index / MAX_PER_ROW);
302
- const col = index % MAX_PER_ROW;
303
- const totalInRow = Math.min(agents.length - row * MAX_PER_ROW, MAX_PER_ROW);
373
+ pending.forEach((agent, idx) => {
374
+ const row = Math.floor(idx / MAX_PER_ROW);
375
+ const col = idx % MAX_PER_ROW;
376
+ const totalInRow = Math.min(pending.length - row * MAX_PER_ROW, MAX_PER_ROW);
304
377
  const offsetX = -((totalInRow - 1) * NODE_X_GAP) / 2;
305
378
  let pos = { x: offsetX + col * NODE_X_GAP, y: NODE_Y_GAP + row * NODE_Y_GAP };
306
379
 
307
- // If position is occupied, shift down until we find empty space
308
380
  while (occupied.has(posKey(pos.x, pos.y))) {
309
381
  pos = { x: pos.x, y: pos.y + NODE_Y_GAP };
310
382
  }
311
383
  occupied.add(posKey(pos.x, pos.y));
312
384
 
385
+ const key = agent.name || agent.id;
313
386
  nodes.push({
314
387
  id: agent.id, type: 'agentNode', position: pos,
315
388
  data: { agent, timeline: tokenTimeline[agent.id] || [] },
316
389
  draggable: true, selectable: true,
317
390
  });
318
- }
391
+ });
319
392
 
320
393
  return nodes;
321
- }, [agents, tokenTimeline]);
394
+ }, [agents, tokenTimeline, activeTeamId]);
395
+
396
+ // Auto-save positions for newly placed nodes to positionsRef + localStorage
397
+ useEffect(() => {
398
+ const newPositions = {};
399
+ targetNodes.forEach((n) => {
400
+ const key = n.id === ROOT_ID ? ROOT_ID : (n.data?.agent?.name || n.id);
401
+ if (!positionsRef.current[key]) {
402
+ newPositions[key] = n.position;
403
+ }
404
+ });
405
+ if (Object.keys(newPositions).length > 0) {
406
+ Object.assign(positionsRef.current, newPositions);
407
+ savePositions(activeTeamId, positionsRef.current);
408
+ }
409
+ }, [targetNodes, activeTeamId]);
322
410
 
323
411
  // Build edges — compute closest handle based on saved node positions
324
412
  const targetEdges = useMemo(() => {
325
- const saved = loadPositions();
413
+ const saved = loadPositions(activeTeamId);
326
414
  const rootPos = saved[ROOT_ID] || { x: 0, y: 0 };
327
415
 
328
416
  return agents.map((agent, i) => {
@@ -354,7 +442,7 @@ function AgentTreeInner() {
354
442
  animated: agent.status === 'running',
355
443
  };
356
444
  });
357
- }, [agents]);
445
+ }, [agents, activeTeamId]);
358
446
 
359
447
  const [nodes, setNodes, onNodesChange] = useNodesState(targetNodes);
360
448
  const [edges, setEdges, onEdgesChange] = useEdgesState(targetEdges);
@@ -364,15 +452,12 @@ function AgentTreeInner() {
364
452
  useEffect(() => {
365
453
  setNodes((current) => {
366
454
  const currentMap = new Map(current.map((n) => [n.id, n]));
367
- const newIds = new Set(targetNodes.map((n) => n.id));
368
455
 
369
456
  return targetNodes.map((tn) => {
370
457
  const existing = currentMap.get(tn.id);
371
458
  if (existing) {
372
- // Preserve existing position, update data only
373
459
  return { ...existing, data: tn.data };
374
460
  }
375
- // New node — use calculated position
376
461
  return tn;
377
462
  });
378
463
  });
@@ -474,13 +559,12 @@ function AgentTreeInner() {
474
559
  }, [nodes, setEdges]);
475
560
 
476
561
  const onNodeDragStop = useCallback((_e, node) => {
477
- const agent = agents.find((a) => a.id === node.id);
478
- // Save by agent name (stable across resumes/rotations) instead of ID (changes each session)
479
- const key = node.id === ROOT_ID ? ROOT_ID : (agent?.name || node.id);
480
- const saved = loadPositions();
562
+ const key = node.id === ROOT_ID ? ROOT_ID : (node.data?.agent?.name || node.id);
563
+ positionsRef.current[key] = node.position;
564
+ const saved = loadPositions(activeTeamId);
481
565
  saved[key] = node.position;
482
- savePositions(saved);
483
- }, [agents]);
566
+ savePositions(activeTeamId, saved);
567
+ }, [activeTeamId]);
484
568
 
485
569
  return (
486
570
  <ReactFlow
@@ -556,6 +640,19 @@ function EmptyState({ onPlanner, onSpawn }) {
556
640
  </button>
557
641
  </div>
558
642
 
643
+ {window.groove?.openFolder && (
644
+ <div className="max-w-sm mx-auto">
645
+ <p className="text-xs text-text-3 mb-2">Or open a different project</p>
646
+ <button
647
+ onClick={() => window.groove.openFolder()}
648
+ className="w-full h-10 rounded-lg border border-border-subtle bg-surface-2 hover:bg-surface-3 text-sm text-text-1 font-medium flex items-center justify-center gap-2 cursor-pointer transition-colors"
649
+ >
650
+ <FolderOpen size={16} className="text-accent" />
651
+ Open Folder
652
+ </button>
653
+ </div>
654
+ )}
655
+
559
656
  <p className="text-xs text-text-4 font-sans">
560
657
  <kbd className="font-mono bg-surface-4 px-1.5 py-0.5 rounded text-text-3">Cmd+K</kbd>
561
658
  <span className="mx-1.5">command palette</span>
@@ -745,12 +842,21 @@ export default function AgentsView() {
745
842
  ) : teamAgents.length === 0 ? (
746
843
  <EmptyState onPlanner={launchPlanner} onSpawn={() => openDetail({ type: 'spawn' })} />
747
844
  ) : (
748
- <ReactFlowProvider>
845
+ <ReactFlowProvider key={activeTeamId}>
749
846
  <AgentTreeInner />
750
847
  </ReactFlowProvider>
751
848
  )}
752
849
  </div>
753
850
  <RecommendedTeamCard />
851
+ {!isLoading && teamAgents.length > 0 && (
852
+ <button
853
+ onClick={() => openDetail({ type: 'spawn' })}
854
+ className="absolute bottom-4 left-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
855
+ >
856
+ <Plus size={14} />
857
+ Spawn
858
+ </button>
859
+ )}
754
860
  </div>
755
861
  );
756
862
  }
@@ -1,10 +1,13 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useRef, useCallback } from 'react';
3
3
  import { useGrooveStore } from '../stores/groove';
4
4
  import { FileTree } from '../components/editor/file-tree';
5
5
  import { EditorTabs } from '../components/editor/editor-tabs';
6
6
  import { CodeEditor } from '../components/editor/code-editor';
7
7
  import { MediaViewer, isMediaFile } from '../components/editor/media-viewer';
8
+ import { EditorStatusBar } from '../components/editor/editor-status-bar';
9
+ import { GotoLine } from '../components/editor/goto-line';
10
+ import { Breadcrumbs } from '../components/editor/breadcrumbs';
8
11
  import { Code2, AlertTriangle, RefreshCw, X, Eye, FileCode } from 'lucide-react';
9
12
  import { Button } from '../components/ui/button';
10
13
  import { api } from '../lib/api';
@@ -15,6 +18,10 @@ function isHtmlFile(path) {
15
18
  return ext === 'html' || ext === 'htm';
16
19
  }
17
20
 
21
+ const SIDEBAR_DEFAULT = 240;
22
+ const SIDEBAR_MIN = 160;
23
+ const SIDEBAR_MAX = 400;
24
+
18
25
  export default function EditorView() {
19
26
  const activeFile = useGrooveStore((s) => s.editorActiveFile);
20
27
  const files = useGrooveStore((s) => s.editorFiles);
@@ -23,10 +30,19 @@ export default function EditorView() {
23
30
  const saveFile = useGrooveStore((s) => s.saveFile);
24
31
  const reloadFile = useGrooveStore((s) => s.reloadFile);
25
32
  const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
33
+ const sidebarWidth = useGrooveStore((s) => s.editorSidebarWidth);
34
+ const setSidebarWidth = useGrooveStore((s) => s.setEditorSidebarWidth);
26
35
 
27
36
  const [rootDir, setRootDir] = useState('');
28
37
  const [previewMode, setPreviewMode] = useState(false);
29
38
  const [previewKey, setPreviewKey] = useState(0);
39
+ const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
40
+ const [showGotoLine, setShowGotoLine] = useState(false);
41
+
42
+ const editorViewRef = useRef(null);
43
+ const dragging = useRef(false);
44
+ const startX = useRef(0);
45
+ const startW = useRef(0);
30
46
 
31
47
  // Fetch root dir
32
48
  useEffect(() => {
@@ -36,6 +52,53 @@ export default function EditorView() {
36
52
  // Reset preview mode when switching files
37
53
  useEffect(() => { setPreviewMode(false); }, [activeFile]);
38
54
 
55
+ // Keyboard shortcuts
56
+ useEffect(() => {
57
+ function handleKeyDown(e) {
58
+ if ((e.metaKey || e.ctrlKey) && e.key === 'g') {
59
+ e.preventDefault();
60
+ setShowGotoLine(true);
61
+ }
62
+ }
63
+ document.addEventListener('keydown', handleKeyDown);
64
+ return () => document.removeEventListener('keydown', handleKeyDown);
65
+ }, []);
66
+
67
+ // Sidebar resize handlers
68
+ const onSidebarMouseDown = useCallback((e) => {
69
+ e.preventDefault();
70
+ dragging.current = true;
71
+ startX.current = e.clientX;
72
+ startW.current = sidebarWidth;
73
+
74
+ function onMouseMove(e) {
75
+ if (!dragging.current) return;
76
+ const delta = e.clientX - startX.current;
77
+ const newW = Math.min(Math.max(startW.current + delta, SIDEBAR_MIN), SIDEBAR_MAX);
78
+ setSidebarWidth(newW);
79
+ }
80
+
81
+ function onMouseUp() {
82
+ dragging.current = false;
83
+ document.removeEventListener('mousemove', onMouseMove);
84
+ document.removeEventListener('mouseup', onMouseUp);
85
+ }
86
+
87
+ document.addEventListener('mousemove', onMouseMove);
88
+ document.addEventListener('mouseup', onMouseUp);
89
+ }, [sidebarWidth, setSidebarWidth]);
90
+
91
+ function handleGoto(line) {
92
+ const view = editorViewRef.current;
93
+ if (!view) return;
94
+ const docLine = view.state.doc.line(Math.min(line, view.state.doc.lines));
95
+ view.dispatch({
96
+ selection: { anchor: docLine.from },
97
+ scrollIntoView: true,
98
+ });
99
+ view.focus();
100
+ }
101
+
39
102
  const file = activeFile ? files[activeFile] : null;
40
103
  const isMedia = activeFile && isMediaFile(activeFile);
41
104
  const isHtml = activeFile && isHtmlFile(activeFile);
@@ -44,8 +107,14 @@ export default function EditorView() {
44
107
  return (
45
108
  <div className="flex h-full">
46
109
  {/* File tree sidebar */}
47
- <div className="w-60 flex-shrink-0 border-r border-border">
110
+ <div className="flex-shrink-0 border-r border-border relative" style={{ width: sidebarWidth }}>
48
111
  <FileTree rootDir={rootDir} />
112
+ {/* Drag handle */}
113
+ <div
114
+ className="absolute top-0 right-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
115
+ onMouseDown={onSidebarMouseDown}
116
+ onDoubleClick={() => setSidebarWidth(SIDEBAR_DEFAULT)}
117
+ />
49
118
  </div>
50
119
 
51
120
  {/* Editor area */}
@@ -53,8 +122,20 @@ export default function EditorView() {
53
122
  {/* Tab bar */}
54
123
  <EditorTabs />
55
124
 
125
+ {/* Breadcrumbs */}
126
+ {activeFile && !isMedia && <Breadcrumbs path={activeFile} />}
127
+
56
128
  {/* Content */}
57
129
  <div className="flex-1 relative min-h-0">
130
+ {/* Go to line dialog */}
131
+ {showGotoLine && (
132
+ <GotoLine
133
+ currentLine={cursorPos.line}
134
+ onGoto={handleGoto}
135
+ onClose={() => setShowGotoLine(false)}
136
+ />
137
+ )}
138
+
58
139
  {/* External change banner */}
59
140
  {hasExternalChange && (
60
141
  <div className="absolute top-0 left-0 right-0 z-10 flex items-center gap-2 px-4 py-2 bg-warning/10 border-b border-warning/20">
@@ -120,6 +201,8 @@ export default function EditorView() {
120
201
  language={file.language}
121
202
  onChange={(content) => updateFileContent(activeFile, content)}
122
203
  onSave={() => saveFile(activeFile)}
204
+ onCursorChange={setCursorPos}
205
+ viewRef={editorViewRef}
123
206
  />
124
207
  )
125
208
  )}
@@ -132,9 +215,16 @@ export default function EditorView() {
132
215
  language={file.language}
133
216
  onChange={(content) => updateFileContent(activeFile, content)}
134
217
  onSave={() => saveFile(activeFile)}
218
+ onCursorChange={setCursorPos}
219
+ viewRef={editorViewRef}
135
220
  />
136
221
  )}
137
222
  </div>
223
+
224
+ {/* Status bar */}
225
+ {activeFile && !isMedia && (
226
+ <EditorStatusBar cursorPos={cursorPos} language={file?.language} />
227
+ )}
138
228
  </div>
139
229
  </div>
140
230
  );
@@ -0,0 +1,37 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useEffect } from 'react';
3
+ import { useGrooveStore } from '../stores/groove';
4
+ import { FederationPanel } from '../components/settings/federation-panel';
5
+ import { ProGate } from '../components/pro/pro-gate';
6
+ import { ScrollArea } from '../components/ui/scroll-area';
7
+ import { Globe } from 'lucide-react';
8
+
9
+ export default function FederationView() {
10
+ const fetchFederationStatus = useGrooveStore((s) => s.fetchFederationStatus);
11
+
12
+ useEffect(() => {
13
+ fetchFederationStatus();
14
+ }, []);
15
+
16
+ return (
17
+ <div className="flex flex-col h-full bg-surface-0">
18
+ <div className="flex items-center gap-3 px-6 py-4 border-b border-border">
19
+ <div className="w-8 h-8 rounded-md bg-accent/10 flex items-center justify-center">
20
+ <Globe size={16} className="text-accent" />
21
+ </div>
22
+ <div>
23
+ <h1 className="text-sm font-semibold text-text-0 font-sans">Federation</h1>
24
+ <p className="text-2xs text-text-3 font-sans">Connect to remote Groove daemons</p>
25
+ </div>
26
+ </div>
27
+
28
+ <ScrollArea className="flex-1">
29
+ <div className="max-w-2xl mx-auto px-6 py-5">
30
+ <ProGate feature="Federation" featureKey="federation" description="Daemon-to-daemon federation over Tailscale mesh for multi-machine agent coordination">
31
+ <FederationPanel />
32
+ </ProGate>
33
+ </div>
34
+ </ScrollArea>
35
+ </div>
36
+ );
37
+ }
@@ -22,8 +22,8 @@ import { RepoImport } from '../components/marketplace/repo-import';
22
22
  import { RepoCard } from '../components/marketplace/repo-card';
23
23
  import { RepoNukeDialog } from '../components/marketplace/repo-nuke-dialog';
24
24
  import {
25
- ChevronLeft, ChevronDown, Sparkles, Plug, LogIn, LogOut,
26
- User, Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
25
+ ChevronLeft, ChevronDown, Sparkles, Plug, LogIn,
26
+ Upload, Package, Download, ShoppingBag, RefreshCw, Trash2,
27
27
  GitBranch,
28
28
  } from 'lucide-react';
29
29
 
@@ -588,45 +588,6 @@ function MyLibrary() {
588
588
  );
589
589
  }
590
590
 
591
- // ── Auth Area (header right) ─────────────────────────────
592
- function AuthArea() {
593
- const authenticated = useGrooveStore((s) => s.marketplaceAuthenticated);
594
- const user = useGrooveStore((s) => s.marketplaceUser);
595
- const login = useGrooveStore((s) => s.marketplaceLogin);
596
- const logout = useGrooveStore((s) => s.marketplaceLogout);
597
-
598
- if (authenticated) {
599
- return (
600
- <div className="flex items-center gap-1">
601
- <div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded bg-surface-3 border border-border-subtle">
602
- <div className="w-4 h-4 rounded-full bg-accent/20 flex items-center justify-center">
603
- <User size={9} className="text-accent" />
604
- </div>
605
- <span className="text-xs text-text-0 font-sans font-medium max-w-[120px] truncate">
606
- {user?.displayName || user?.id || 'Account'}
607
- </span>
608
- </div>
609
- <button
610
- onClick={logout}
611
- className="flex items-center gap-1 px-2 py-1.5 rounded text-xs text-text-3 hover:text-text-0 hover:bg-surface-3 font-sans cursor-pointer transition-colors"
612
- >
613
- <LogOut size={11} />
614
- </button>
615
- </div>
616
- );
617
- }
618
-
619
- return (
620
- <button
621
- onClick={login}
622
- className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold font-sans text-text-0 bg-accent/15 border border-accent/25 rounded hover:bg-accent/25 cursor-pointer transition-colors"
623
- >
624
- <LogIn size={12} />
625
- Sign in
626
- </button>
627
- );
628
- }
629
-
630
591
  // ── GitHub Browse ───────────────────────────────────────
631
592
  function GitHubBrowse() {
632
593
  const importedRepos = useGrooveStore((s) => s.importedRepos);
@@ -735,7 +696,6 @@ export default function MarketplaceView() {
735
696
  })}
736
697
  </div>
737
698
  <div className="flex-1" />
738
- <AuthArea />
739
699
  </div>
740
700
  </div>
741
701