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
@@ -19,7 +19,7 @@ function persistJSON(key, value) {
19
19
  }
20
20
 
21
21
  // Clear stale persisted data on version change
22
- const STORE_VERSION = '0.22.27';
22
+ const STORE_VERSION = '0.22.28';
23
23
  if (loadJSON('groove:storeVersion') !== STORE_VERSION) {
24
24
  localStorage.removeItem('groove:chatHistory');
25
25
  localStorage.removeItem('groove:activityLog');
@@ -42,12 +42,23 @@ export const useGrooveStore = create((set, get) => ({
42
42
  // ── Gateways ──────────────────────────────────────────────
43
43
  gateways: [],
44
44
 
45
+ // ── Federation ────────────────────────────────────────────
46
+ federation: {
47
+ peers: [],
48
+ whitelist: [],
49
+ connections: [],
50
+ pouchLog: [],
51
+ ambassadors: [],
52
+ selectedPeerId: null,
53
+ },
54
+
45
55
  // ── Navigation ────────────────────────────────────────────
46
56
  activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
47
57
  detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
48
58
  teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
49
59
  commandPaletteOpen: false,
50
60
  quickConnectOpen: false,
61
+ upgradeModalOpen: false,
51
62
 
52
63
  // ── Node expansion (click-to-open persistent panels) ───────
53
64
  expandedNodes: loadJSON('groove:expandedNodes'),
@@ -70,6 +81,7 @@ export const useGrooveStore = create((set, get) => ({
70
81
 
71
82
  // ── Recommended Team ──────────────────────────────────────
72
83
  recommendedTeam: null, // { name, agents: [...] } from planner
84
+ _delegatingTeamIds: new Set(),
73
85
 
74
86
  // ── Journalist ────────────────────────────────────────────
75
87
  journalistStatus: null, // { cycleCount, lastCycleTime, history, lastSynthesis }
@@ -77,6 +89,16 @@ export const useGrooveStore = create((set, get) => ({
77
89
  // ── Marketplace Auth ───────────────────────────────────────
78
90
  marketplaceUser: null, // { id, displayName, avatar, ... } or null
79
91
  marketplaceAuthenticated: false,
92
+ edition: 'community', // 'community' | 'pro' — runtime edition from /edition
93
+ subscription: {
94
+ plan: 'community',
95
+ status: 'none',
96
+ active: false,
97
+ features: [],
98
+ seats: 1,
99
+ periodEnd: null,
100
+ cancelAtPeriodEnd: false,
101
+ },
80
102
 
81
103
  // ── Toasts ────────────────────────────────────────────────
82
104
  toasts: [],
@@ -96,6 +118,10 @@ export const useGrooveStore = create((set, get) => ({
96
118
  editorTreeCache: {},
97
119
  editorChangedFiles: {},
98
120
  editorRecentSaves: {},
121
+ editorSidebarWidth: Number(localStorage.getItem('groove:editorSidebarWidth')) || 240,
122
+
123
+ // ── Onboarding ────────────────────────────────────────────
124
+ onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
99
125
 
100
126
  // ── Connection ────────────────────────────────────────────
101
127
 
@@ -116,14 +142,23 @@ export const useGrooveStore = create((set, get) => ({
116
142
  get().fetchTeams();
117
143
  get().fetchApprovals();
118
144
  get().checkMarketplaceAuth();
145
+ get().fetchTunnels();
146
+ if (!get().onboardingComplete) get().fetchOnboardingStatus();
147
+ if (window.groove?.auth?.onSubscriptionStatus) {
148
+ window.groove.auth.onSubscriptionStatus((data) => {
149
+ if (data) set({ subscription: { ...get().subscription, ...data } });
150
+ });
151
+ }
119
152
  };
120
153
 
121
154
  ws.onmessage = (event) => {
122
155
  const msg = JSON.parse(event.data);
156
+ if (!msg || typeof msg !== 'object' || Object.hasOwn(msg, '__proto__') || Object.hasOwn(msg, 'constructor')) return;
123
157
  switch (msg.type) {
124
158
  case 'state': {
125
159
  const timeline = { ...get().tokenTimeline };
126
160
  const now = Date.now();
161
+ const liveIds = new Set(msg.data.map((a) => a.id));
127
162
  for (const agent of msg.data) {
128
163
  if (!timeline[agent.id]) timeline[agent.id] = [];
129
164
  const arr = timeline[agent.id];
@@ -133,15 +168,30 @@ export const useGrooveStore = create((set, get) => ({
133
168
  if (arr.length > 200) timeline[agent.id] = arr.slice(-200);
134
169
  }
135
170
  }
171
+ // Prune stale agent data from timeline, chatHistory, activityLog.
172
+ // Without this, localStorage fills with dead agents' data until quota is
173
+ // exceeded and nothing else (e.g. node positions) can save.
174
+ let prunedChat = null, prunedLog = null;
175
+ const st = get();
176
+ for (const id of Object.keys(timeline)) if (!liveIds.has(id)) delete timeline[id];
177
+ for (const id of Object.keys(st.chatHistory)) {
178
+ if (!liveIds.has(id)) { if (!prunedChat) prunedChat = { ...st.chatHistory }; delete prunedChat[id]; }
179
+ }
180
+ for (const id of Object.keys(st.activityLog)) {
181
+ if (!liveIds.has(id)) { if (!prunedLog) prunedLog = { ...st.activityLog }; delete prunedLog[id]; }
182
+ }
136
183
  // Only replace agents array if something meaningful changed
137
184
  // (prevents React Flow tree flicker on every lastActivity update)
138
- const prev = get().agents;
185
+ const prev = st.agents;
139
186
  const changed = msg.data.length !== prev.length || msg.data.some((a, i) => {
140
187
  const p = prev[i];
141
188
  return !p || p.id !== a.id || p.status !== a.status || p.tokensUsed !== a.tokensUsed
142
189
  || p.contextUsage !== a.contextUsage || p.name !== a.name || p.model !== a.model;
143
190
  });
144
- set({ agents: changed ? msg.data : prev, tokenTimeline: timeline, hydrated: true });
191
+ const nextState = { agents: changed ? msg.data : prev, tokenTimeline: timeline, hydrated: true };
192
+ if (prunedChat) { nextState.chatHistory = prunedChat; persistJSON('groove:chatHistory', prunedChat); }
193
+ if (prunedLog) { nextState.activityLog = prunedLog; persistJSON('groove:activityLog', prunedLog); }
194
+ set(nextState);
145
195
 
146
196
  // Poll for recommended-team.json while a planner is running
147
197
  const hasRunningPlanner = msg.data.some((a) => a.role === 'planner' && a.status === 'running');
@@ -258,6 +308,15 @@ export const useGrooveStore = create((set, get) => ({
258
308
  const type = msg.status === 'completed' ? 'success' : isKill ? 'info' : 'warning';
259
309
  get().addToast(type, text, msg.error ? msg.error.slice(0, 200) : undefined);
260
310
 
311
+ // Clear thinking indicator — agent is no longer active
312
+ if (get().thinkingAgents.has(msg.agentId)) {
313
+ set((s) => {
314
+ const next = new Set(s.thinkingAgents);
315
+ next.delete(msg.agentId);
316
+ return { thinkingAgents: next };
317
+ });
318
+ }
319
+
261
320
  // Log crash error to agent chat so user can see what happened
262
321
  if (msg.error && msg.agentId) {
263
322
  get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
@@ -377,6 +436,30 @@ export const useGrooveStore = create((set, get) => ({
377
436
  set({ gateways: msg.data || [] });
378
437
  break;
379
438
 
439
+ case 'federation:whitelist':
440
+ set((s) => ({ federation: { ...s.federation, whitelist: msg.data || [] } }));
441
+ break;
442
+
443
+ case 'federation:connection':
444
+ set((s) => {
445
+ const conns = [...s.federation.connections];
446
+ const idx = conns.findIndex((c) => c.ip === msg.data?.ip);
447
+ if (idx >= 0) conns[idx] = { ...conns[idx], ...msg.data };
448
+ else conns.push(msg.data);
449
+ return { federation: { ...s.federation, connections: conns } };
450
+ });
451
+ break;
452
+
453
+ case 'federation:pouch':
454
+ case 'federation:pouch-log':
455
+ set((s) => ({
456
+ federation: {
457
+ ...s.federation,
458
+ pouchLog: [...s.federation.pouchLog, msg.data].slice(-200),
459
+ },
460
+ }));
461
+ break;
462
+
380
463
  case 'tunnel.connected':
381
464
  set({ activeTunnelId: msg.data?.id || null });
382
465
  get().fetchTunnels();
@@ -394,6 +477,34 @@ export const useGrooveStore = create((set, get) => ({
394
477
  set({ savedTunnels: tunnels });
395
478
  break;
396
479
  }
480
+
481
+ case 'subscription:updated': {
482
+ const subUpdate = { subscription: msg.data };
483
+ if (msg.data?.active === true && !get().marketplaceAuthenticated) {
484
+ subUpdate.marketplaceAuthenticated = true;
485
+ }
486
+ set(subUpdate);
487
+ api.get('/edition').then((ed) => {
488
+ set({
489
+ edition: ed.edition || 'community',
490
+ subscription: {
491
+ plan: ed.plan || 'community',
492
+ status: ed.status || (ed.subscriptionActive ? 'active' : 'none'),
493
+ active: ed.subscriptionActive === true,
494
+ features: ed.features || [],
495
+ seats: ed.seats || 1,
496
+ periodEnd: ed.periodEnd || null,
497
+ cancelAtPeriodEnd: ed.cancelAtPeriodEnd || false,
498
+ },
499
+ });
500
+ }).catch(() => {});
501
+ break;
502
+ }
503
+
504
+ case 'auth:expired':
505
+ set({ marketplaceAuthenticated: false, marketplaceUser: null });
506
+ get().addToast('warning', 'Session expired', 'Please sign in again');
507
+ break;
397
508
  }
398
509
  };
399
510
 
@@ -465,7 +576,7 @@ export const useGrooveStore = create((set, get) => ({
465
576
  const team = get().teams.find((t) => t.id === id);
466
577
  if (team?.isDefault) { get().addToast('warning', 'Cannot delete the default team'); return; }
467
578
  try {
468
- await api.delete(`/teams/${id}`);
579
+ await api.delete(`/teams/${encodeURIComponent(id)}`);
469
580
  // WS team:deleted handler removes from array and switches activeTeamId
470
581
  get().addToast('info', `Team "${team?.name}" deleted`);
471
582
  } catch (err) {
@@ -483,7 +594,7 @@ export const useGrooveStore = create((set, get) => ({
483
594
 
484
595
  async renameTeam(id, name) {
485
596
  try {
486
- const team = await api.patch(`/teams/${id}`, { name });
597
+ const team = await api.patch(`/teams/${encodeURIComponent(id)}`, { name });
487
598
  set((s) => ({ teams: s.teams.map((t) => (t.id === id ? team : t)) }));
488
599
  return team;
489
600
  } catch (err) {
@@ -510,6 +621,7 @@ export const useGrooveStore = create((set, get) => ({
510
621
  },
511
622
  toggleCommandPalette() { set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })); },
512
623
  toggleQuickConnect() { set((s) => ({ quickConnectOpen: !s.quickConnectOpen })); },
624
+ setUpgradeModalOpen: (open) => set({ upgradeModalOpen: open }),
513
625
 
514
626
  setDetailPanelWidth(w) {
515
627
  set({ detailPanelWidth: w });
@@ -552,6 +664,21 @@ export const useGrooveStore = create((set, get) => ({
552
664
  marketplaceAuthenticated: data.authenticated || false,
553
665
  marketplaceUser: data.user || null,
554
666
  });
667
+ try {
668
+ const edition = await api.get('/edition');
669
+ set({
670
+ edition: edition.edition || 'community',
671
+ subscription: {
672
+ plan: edition.plan || 'community',
673
+ status: edition.status || (edition.subscriptionActive ? 'active' : 'none'),
674
+ active: edition.subscriptionActive === true,
675
+ features: edition.features || [],
676
+ seats: edition.seats || 1,
677
+ periodEnd: edition.periodEnd || null,
678
+ cancelAtPeriodEnd: edition.cancelAtPeriodEnd || false,
679
+ },
680
+ });
681
+ } catch { /* edition endpoint may not exist */ }
555
682
  } catch {
556
683
  set({ marketplaceAuthenticated: false, marketplaceUser: null });
557
684
  }
@@ -569,6 +696,38 @@ export const useGrooveStore = create((set, get) => ({
569
696
  clearInterval(poll);
570
697
  set({ marketplaceAuthenticated: true, marketplaceUser: status.user });
571
698
  get().addToast('success', `Signed in as ${status.user?.displayName || status.user?.id || 'user'}`);
699
+ try {
700
+ const edition = await api.get('/edition');
701
+ set({
702
+ edition: edition.edition || 'community',
703
+ subscription: {
704
+ plan: edition.plan || 'community',
705
+ status: edition.status || (edition.subscriptionActive ? 'active' : 'none'),
706
+ active: edition.subscriptionActive === true,
707
+ features: edition.features || [],
708
+ seats: edition.seats || 1,
709
+ periodEnd: edition.periodEnd || null,
710
+ cancelAtPeriodEnd: edition.cancelAtPeriodEnd || false,
711
+ },
712
+ });
713
+ } catch { /* edition endpoint may not exist */ }
714
+ setTimeout(async () => {
715
+ try {
716
+ const e = await api.get('/edition');
717
+ set({
718
+ edition: e.edition || 'community',
719
+ subscription: {
720
+ plan: e.plan || 'community',
721
+ status: e.status || (e.subscriptionActive ? 'active' : 'none'),
722
+ active: e.subscriptionActive === true,
723
+ features: e.features || [],
724
+ seats: e.seats || 1,
725
+ periodEnd: e.periodEnd || null,
726
+ cancelAtPeriodEnd: e.cancelAtPeriodEnd || false,
727
+ },
728
+ });
729
+ } catch { /* delayed re-fetch may fail */ }
730
+ }, 2000);
572
731
  }
573
732
  } catch { /* keep polling */ }
574
733
  }, 2000);
@@ -600,6 +759,65 @@ export const useGrooveStore = create((set, get) => ({
600
759
  }
601
760
  },
602
761
 
762
+ // ── Subscription ────────────────────────────────────────────
763
+
764
+ async fetchSubscriptionPlans() {
765
+ return api.get('/subscription/plans');
766
+ },
767
+
768
+ async startCheckout(priceId) {
769
+ try {
770
+ const data = await api.post('/subscription/checkout', { priceId });
771
+ if (data.url) {
772
+ if (window.groove?.openExternal) {
773
+ window.groove.openExternal(data.url);
774
+ } else {
775
+ window.open(data.url, '_blank');
776
+ }
777
+ }
778
+ return data;
779
+ } catch (err) {
780
+ if (err.status === 401 || err.message?.includes('Not authenticated')) {
781
+ get().addToast('info', 'Please sign in to subscribe');
782
+ get().marketplaceLogin();
783
+ } else if (err.status === 409) {
784
+ get().addToast('info', 'Already subscribed', 'Use Manage Subscription to switch plans');
785
+ } else {
786
+ get().addToast('error', 'Checkout failed', err.message);
787
+ }
788
+ throw err;
789
+ }
790
+ },
791
+
792
+ async openPortal() {
793
+ try {
794
+ const data = await api.post('/subscription/portal');
795
+ if (data.url) {
796
+ if (window.groove?.openExternal) {
797
+ window.groove.openExternal(data.url);
798
+ } else {
799
+ window.open(data.url, '_blank');
800
+ }
801
+ }
802
+ return data;
803
+ } catch (err) {
804
+ get().addToast('error', 'Portal failed', err.message);
805
+ throw err;
806
+ }
807
+ },
808
+
809
+ async updateSeats(seats) {
810
+ try {
811
+ const data = await api.patch('/subscription', { seats });
812
+ set({ subscription: { ...get().subscription, ...data } });
813
+ get().addToast('success', `Updated to ${seats} seat${seats !== 1 ? 's' : ''}`);
814
+ return data;
815
+ } catch (err) {
816
+ get().addToast('error', 'Seat update failed', err.message);
817
+ throw err;
818
+ }
819
+ },
820
+
603
821
  // ── Approvals ──────────────────────────────────────────────
604
822
 
605
823
  async fetchApprovals() {
@@ -614,7 +832,7 @@ export const useGrooveStore = create((set, get) => ({
614
832
 
615
833
  async approveRequest(id) {
616
834
  try {
617
- await api.post(`/approvals/${id}/approve`);
835
+ await api.post(`/approvals/${encodeURIComponent(id)}/approve`);
618
836
  set((s) => ({ pendingApprovals: s.pendingApprovals.filter((a) => a.id !== id) }));
619
837
  get().addToast('success', 'Approved');
620
838
  } catch (err) {
@@ -624,7 +842,7 @@ export const useGrooveStore = create((set, get) => ({
624
842
 
625
843
  async rejectRequest(id, reason = '') {
626
844
  try {
627
- await api.post(`/approvals/${id}/reject`, { reason });
845
+ await api.post(`/approvals/${encodeURIComponent(id)}/reject`, { reason });
628
846
  set((s) => ({ pendingApprovals: s.pendingApprovals.filter((a) => a.id !== id) }));
629
847
  get().addToast('info', 'Rejected');
630
848
  } catch (err) {
@@ -652,19 +870,26 @@ export const useGrooveStore = create((set, get) => ({
652
870
  const allExist = phase1Roles.every((role) => teamAgents.some((a) => a.role === role));
653
871
 
654
872
  if (allExist && phase1Roles.length > 0) {
655
- // Auto-delegate all agents already exist in the team
656
- set({ recommendedTeam: null });
657
- const result = await api.post('/recommended-team/launch');
658
- const agents = result.agents || [];
659
- const names = agents.map((a) => a.name).join(', ') || '';
660
- get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
661
- // Set thinking indicator for all delegated agents so the UI shows activity
662
- if (agents.length > 0) {
663
- set((s) => ({
664
- thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
665
- }));
873
+ // Guard: skip if already delegating for this team (poll race)
874
+ if (get()._delegatingTeamIds.has(teamId)) return;
875
+ set((s) => ({ recommendedTeam: null, _delegatingTeamIds: new Set([...s._delegatingTeamIds, teamId]) }));
876
+ try {
877
+ const result = await api.post('/recommended-team/launch', { teamId });
878
+ const agents = result.agents || [];
879
+ const names = agents.map((a) => a.name).join(', ') || '';
880
+ get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
881
+ if (agents.length > 0) {
882
+ set((s) => ({
883
+ thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
884
+ }));
885
+ }
886
+ } finally {
887
+ set((s) => {
888
+ const next = new Set(s._delegatingTeamIds);
889
+ next.delete(teamId);
890
+ return { _delegatingTeamIds: next };
891
+ });
666
892
  }
667
- api.post('/cleanup').catch(() => {});
668
893
  return;
669
894
  }
670
895
  }
@@ -729,12 +954,12 @@ export const useGrooveStore = create((set, get) => ({
729
954
  },
730
955
 
731
956
  async softRemoveRepo(importId) {
732
- await api.delete('/repos/' + importId + '/remove');
957
+ await api.delete(`/repos/${encodeURIComponent(importId)}/remove`);
733
958
  get().fetchImportedRepos();
734
959
  },
735
960
 
736
961
  async hardNukeRepo(importId, deleteFiles = true) {
737
- await api.delete('/repos/' + importId + '/nuke?deleteFiles=' + deleteFiles);
962
+ await api.delete(`/repos/${encodeURIComponent(importId)}/nuke?deleteFiles=${deleteFiles}`);
738
963
  get().fetchImportedRepos();
739
964
  },
740
965
 
@@ -754,22 +979,22 @@ export const useGrooveStore = create((set, get) => ({
754
979
  },
755
980
 
756
981
  async updateTunnel(id, config) {
757
- const result = await api.patch('/tunnels/' + id, config);
982
+ const result = await api.patch(`/tunnels/${encodeURIComponent(id)}`, config);
758
983
  get().fetchTunnels();
759
984
  return result;
760
985
  },
761
986
 
762
987
  async deleteTunnel(id) {
763
- await api.delete('/tunnels/' + id);
988
+ await api.delete(`/tunnels/${encodeURIComponent(id)}`);
764
989
  get().fetchTunnels();
765
990
  },
766
991
 
767
992
  async testTunnel(id) {
768
- return api.post('/tunnels/' + id + '/test');
993
+ return api.post(`/tunnels/${encodeURIComponent(id)}/test`);
769
994
  },
770
995
 
771
996
  async connectTunnel(id) {
772
- const result = await api.post('/tunnels/' + id + '/connect');
997
+ const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
773
998
  set({ activeTunnelId: id });
774
999
  get().fetchTunnels();
775
1000
  if (result.url) window.open(result.url, '_blank');
@@ -777,17 +1002,17 @@ export const useGrooveStore = create((set, get) => ({
777
1002
  },
778
1003
 
779
1004
  async disconnectTunnel(id) {
780
- await api.post('/tunnels/' + id + '/disconnect');
1005
+ await api.post(`/tunnels/${encodeURIComponent(id)}/disconnect`);
781
1006
  set({ activeTunnelId: null });
782
1007
  get().fetchTunnels();
783
1008
  },
784
1009
 
785
1010
  async installTunnel(id) {
786
- return api.post('/tunnels/' + id + '/install');
1011
+ return api.post(`/tunnels/${encodeURIComponent(id)}/install`);
787
1012
  },
788
1013
 
789
1014
  async startTunnel(id) {
790
- return api.post('/tunnels/' + id + '/start');
1015
+ return api.post(`/tunnels/${encodeURIComponent(id)}/start`);
791
1016
  },
792
1017
 
793
1018
  // ── Journalist ────────────────────────────────────────────
@@ -828,7 +1053,7 @@ export const useGrooveStore = create((set, get) => ({
828
1053
 
829
1054
  async killAgent(id, purge = false) {
830
1055
  try {
831
- await api.delete(`/agents/${id}?purge=${purge}`);
1056
+ await api.delete(`/agents/${encodeURIComponent(id)}?purge=${purge}`);
832
1057
  } catch (err) {
833
1058
  get().addToast('error', 'Kill failed', err.message);
834
1059
  }
@@ -836,7 +1061,7 @@ export const useGrooveStore = create((set, get) => ({
836
1061
 
837
1062
  async rotateAgent(id) {
838
1063
  try {
839
- return await api.post(`/agents/${id}/rotate`);
1064
+ return await api.post(`/agents/${encodeURIComponent(id)}/rotate`);
840
1065
  } catch (err) {
841
1066
  get().addToast('error', 'Rotation failed', err.message);
842
1067
  throw err;
@@ -847,6 +1072,48 @@ export const useGrooveStore = create((set, get) => ({
847
1072
  return api.get('/providers');
848
1073
  },
849
1074
 
1075
+ // ── Onboarding ────────────────────────────────────────────
1076
+
1077
+ async fetchOnboardingStatus() {
1078
+ try {
1079
+ const data = await api.get('/onboarding/status');
1080
+ if (data?.complete) {
1081
+ set({ onboardingComplete: true });
1082
+ localStorage.setItem('groove:onboardingComplete', 'true');
1083
+ }
1084
+ return data;
1085
+ } catch {
1086
+ return null;
1087
+ }
1088
+ },
1089
+
1090
+ dismissOnboarding() {
1091
+ set({ onboardingComplete: true });
1092
+ localStorage.setItem('groove:onboardingComplete', 'true');
1093
+ api.post('/onboarding/dismiss').catch(() => {});
1094
+ },
1095
+
1096
+ async installProvider(providerId) {
1097
+ try {
1098
+ const data = await api.post('/onboarding/install-provider', { provider: providerId });
1099
+ get().addToast('success', `${providerId} installed`);
1100
+ return data;
1101
+ } catch (err) {
1102
+ get().addToast('error', `Install failed: ${providerId}`, err.message);
1103
+ throw err;
1104
+ }
1105
+ },
1106
+
1107
+ async setDefaultProvider(provider, model) {
1108
+ try {
1109
+ await api.post('/onboarding/set-default', { provider, model });
1110
+ get().addToast('success', `Default set to ${provider} (${model})`);
1111
+ } catch (err) {
1112
+ get().addToast('error', 'Failed to set default', err.message);
1113
+ throw err;
1114
+ }
1115
+ },
1116
+
850
1117
  // ── Chat ──────────────────────────────────────────────────
851
1118
 
852
1119
  addChatMessage(agentId, from, text, isQuery = false) {
@@ -864,7 +1131,7 @@ export const useGrooveStore = create((set, get) => ({
864
1131
 
865
1132
  async stopAgent(id) {
866
1133
  try {
867
- await api.post(`/agents/${id}/stop`);
1134
+ await api.post(`/agents/${encodeURIComponent(id)}/stop`);
868
1135
  // Clear thinking indicator
869
1136
  set((s) => {
870
1137
  const next = new Set(s.thinkingAgents);
@@ -878,41 +1145,24 @@ export const useGrooveStore = create((set, get) => ({
878
1145
  },
879
1146
 
880
1147
  async instructAgent(id, message) {
881
- const agent = get().agents.find((a) => a.id === id);
882
- const isAlive = agent && (agent.status === 'running' || agent.status === 'starting');
1148
+ get().addChatMessage(id, 'user', message, false);
1149
+ set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
1150
+ try {
1151
+ const data = await api.post(`/agents/${encodeURIComponent(id)}/instruct`, { message });
883
1152
 
884
- // Running agent: use query (non-destructive) instead of killing it
885
- if (isAlive) {
886
- get().addChatMessage(id, 'user', message, false);
887
- try {
888
- const data = await api.post(`/agents/${id}/query`, { message });
889
- // Agent loop agents: response comes via WebSocket, show thinking indicator
890
- if (data.status === 'pending' || data.response === 'Message sent to agent') {
891
- set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
892
- return data;
893
- }
894
- get().addChatMessage(id, 'agent', data.response);
1153
+ // Agent loop: message sent directly to running agent — response comes via WebSocket
1154
+ if (data.status === 'message_sent') {
895
1155
  return data;
896
- } catch (err) {
897
- get().addChatMessage(id, 'system', `failed: ${err.message}`);
898
- throw err;
899
1156
  }
900
- }
901
1157
 
902
- // Completed/stopped agent: resume with full context
903
- get().addChatMessage(id, 'user', message, false);
904
- // Show thinking indicator immediately — stays until first WebSocket output
905
- set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, id]) }));
906
- try {
907
- const newAgent = await api.post(`/agents/${id}/instruct`, { message });
908
- // Carry history + thinking state to new agent ID
1158
+ // CLI agent: was stopped + resumed/rotated — transfer state to new agent ID
1159
+ const newAgent = data;
909
1160
  for (const key of ['chatHistory', 'activityLog', 'tokenTimeline']) {
910
1161
  const old = get()[key][id];
911
1162
  if (old?.length) {
912
1163
  set((s) => ({ [key]: { ...s[key], [newAgent.id]: [...old] } }));
913
1164
  }
914
1165
  }
915
- // Transfer thinking indicator to the new agent
916
1166
  set((s) => {
917
1167
  const next = new Set(s.thinkingAgents);
918
1168
  next.delete(id);
@@ -924,7 +1174,6 @@ export const useGrooveStore = create((set, get) => ({
924
1174
  get().selectAgent(newAgent.id);
925
1175
  return newAgent;
926
1176
  } catch (err) {
927
- // Clear thinking indicator on failure
928
1177
  set((s) => {
929
1178
  const next = new Set(s.thinkingAgents);
930
1179
  next.delete(id);
@@ -938,7 +1187,7 @@ export const useGrooveStore = create((set, get) => ({
938
1187
  async queryAgent(id, message) {
939
1188
  get().addChatMessage(id, 'user', message, true);
940
1189
  try {
941
- const data = await api.post(`/agents/${id}/query`, { message });
1190
+ const data = await api.post(`/agents/${encodeURIComponent(id)}/query`, { message });
942
1191
  get().addChatMessage(id, 'agent', data.response);
943
1192
  return data;
944
1193
  } catch (err) {
@@ -998,6 +1247,11 @@ export const useGrooveStore = create((set, get) => ({
998
1247
 
999
1248
  setActiveFile(path) { set({ editorActiveFile: path }); },
1000
1249
 
1250
+ setEditorSidebarWidth(width) {
1251
+ set({ editorSidebarWidth: width });
1252
+ localStorage.setItem('groove:editorSidebarWidth', String(width));
1253
+ },
1254
+
1001
1255
  updateFileContent(path, content) {
1002
1256
  set((s) => ({ editorFiles: { ...s.editorFiles, [path]: { ...s.editorFiles[path], content } } }));
1003
1257
  },
@@ -1081,6 +1335,67 @@ export const useGrooveStore = create((set, get) => ({
1081
1335
  }
1082
1336
  },
1083
1337
 
1338
+ // ── Federation ────────────────────────────────────────────
1339
+
1340
+ async fetchFederationStatus() {
1341
+ try {
1342
+ const data = await api.get('/federation');
1343
+ set((s) => ({
1344
+ federation: {
1345
+ ...s.federation,
1346
+ peers: data.peers || [],
1347
+ whitelist: data.whitelist || [],
1348
+ connections: data.connections || [],
1349
+ ambassadors: data.ambassadors?.ambassadors || data.ambassadors || [],
1350
+ },
1351
+ }));
1352
+ return data;
1353
+ } catch { return null; }
1354
+ },
1355
+
1356
+ async addToWhitelist(ip, port = 31415, name) {
1357
+ try {
1358
+ await api.post('/federation/whitelist', { ip, port, ...(name && { name }) });
1359
+ get().addToast('success', `Added ${ip} to whitelist`);
1360
+ get().fetchFederationStatus();
1361
+ } catch (err) {
1362
+ get().addToast('error', 'Whitelist failed', err.message);
1363
+ throw err;
1364
+ }
1365
+ },
1366
+
1367
+ async removeFromWhitelist(ip) {
1368
+ try {
1369
+ await api.delete(`/federation/whitelist/${encodeURIComponent(ip)}`);
1370
+ get().addToast('info', `Removed ${ip}`);
1371
+ get().fetchFederationStatus();
1372
+ } catch (err) {
1373
+ get().addToast('error', 'Remove failed', err.message);
1374
+ }
1375
+ },
1376
+
1377
+ setSelectedPeer(peerId) {
1378
+ set((s) => ({ federation: { ...s.federation, selectedPeerId: peerId } }));
1379
+ },
1380
+
1381
+ async fetchPouchLog(peerId) {
1382
+ try {
1383
+ const data = await api.get(`/federation/pouch/log${peerId ? `?peerId=${encodeURIComponent(peerId)}` : ''}`);
1384
+ set((s) => ({ federation: { ...s.federation, pouchLog: data || [] } }));
1385
+ } catch { /* ignore */ }
1386
+ },
1387
+
1388
+ async sendPouch(peerId, contract) {
1389
+ try {
1390
+ const result = await api.post('/federation/pouch/send', { peerId, contract });
1391
+ get().addToast('success', 'Pouch sent');
1392
+ return result;
1393
+ } catch (err) {
1394
+ get().addToast('error', 'Pouch send failed', err.message);
1395
+ throw err;
1396
+ }
1397
+ },
1398
+
1084
1399
  async renameFile(oldPath, newPath) {
1085
1400
  try {
1086
1401
  await api.post('/files/rename', { oldPath, newPath });