groove-dev 0.27.142 → 0.27.144

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 (187) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  4. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  7. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  9. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  10. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  12. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  13. package/node_modules/@groove-dev/daemon/src/routes/agents.js +889 -0
  14. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  15. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  21. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  22. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  23. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  24. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  25. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  26. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  27. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  28. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  29. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  31. package/node_modules/@groove-dev/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  32. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  33. package/node_modules/@groove-dev/gui/package.json +1 -1
  34. package/{packages/gui/src/app.jsx → node_modules/@groove-dev/gui/src/App.jsx} +0 -2
  35. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  36. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  37. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +144 -31
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  39. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  40. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  41. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  43. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  44. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  49. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  50. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  54. package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
  55. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  56. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  57. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +195 -14
  58. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +286 -102
  59. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  60. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  61. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  62. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  63. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  64. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  65. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  66. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  67. package/node_modules/@groove-dev/gui/src/lib/status.js +24 -24
  68. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  69. package/node_modules/@groove-dev/gui/src/stores/groove.js +34 -3144
  70. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  71. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +452 -0
  72. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  73. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +227 -0
  74. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  75. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  76. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  77. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  78. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  79. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  81. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  82. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  84. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +17 -6
  85. package/node_modules/@groove-dev/gui/src/views/models.jsx +410 -509
  86. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  88. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  89. package/package.json +1 -1
  90. package/packages/cli/package.json +1 -1
  91. package/packages/daemon/package.json +1 -1
  92. package/packages/daemon/src/api.js +1086 -6532
  93. package/packages/daemon/src/gateways/manager.js +35 -1
  94. package/packages/daemon/src/index.js +3 -0
  95. package/packages/daemon/src/journalist.js +23 -13
  96. package/packages/daemon/src/mlx-server.js +365 -0
  97. package/packages/daemon/src/model-lab.js +308 -12
  98. package/packages/daemon/src/pm.js +1 -1
  99. package/packages/daemon/src/process.js +2 -2
  100. package/packages/daemon/src/providers/local.js +36 -8
  101. package/packages/daemon/src/registry.js +21 -5
  102. package/packages/daemon/src/routes/agents.js +889 -0
  103. package/packages/daemon/src/routes/coordination.js +318 -0
  104. package/packages/daemon/src/routes/files.js +751 -0
  105. package/packages/daemon/src/routes/integrations.js +485 -0
  106. package/packages/daemon/src/routes/network.js +1784 -0
  107. package/packages/daemon/src/routes/providers.js +755 -0
  108. package/packages/daemon/src/routes/schedules.js +110 -0
  109. package/packages/daemon/src/routes/teams.js +650 -0
  110. package/packages/daemon/src/scheduler.js +456 -24
  111. package/packages/daemon/src/teams.js +1 -1
  112. package/packages/daemon/src/validate.js +38 -1
  113. package/packages/daemon/templates/mlx-setup.json +12 -0
  114. package/packages/daemon/templates/tgi-setup.json +1 -1
  115. package/packages/daemon/templates/vllm-setup.json +1 -1
  116. package/packages/gui/dist/assets/index-BcoF6_eF.js +1012 -0
  117. package/packages/gui/dist/assets/index-Dd7qhiEd.css +1 -0
  118. package/packages/gui/dist/index.html +2 -2
  119. package/packages/gui/package.json +1 -1
  120. package/{node_modules/@groove-dev/gui/src/app.jsx → packages/gui/src/App.jsx} +0 -2
  121. package/packages/gui/src/app.css +35 -0
  122. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  123. package/packages/gui/src/components/agents/agent-feed.jsx +144 -31
  124. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  125. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  126. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  127. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  128. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  129. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  130. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  131. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  132. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  133. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  134. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  135. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  136. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  137. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  138. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  139. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  140. package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
  141. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  142. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  143. package/packages/gui/src/components/lab/parameter-panel.jsx +195 -14
  144. package/packages/gui/src/components/lab/runtime-config.jsx +286 -102
  145. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  146. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  147. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  148. package/packages/gui/src/components/network/network-health.jsx +2 -2
  149. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  150. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  151. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  152. package/packages/gui/src/lib/cron.js +64 -0
  153. package/packages/gui/src/lib/status.js +24 -24
  154. package/packages/gui/src/lib/theme-hex.js +1 -0
  155. package/packages/gui/src/stores/groove.js +34 -3144
  156. package/packages/gui/src/stores/helpers.js +10 -0
  157. package/packages/gui/src/stores/slices/agents-slice.js +452 -0
  158. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  159. package/packages/gui/src/stores/slices/chat-slice.js +227 -0
  160. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  161. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  162. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  163. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  164. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  165. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  166. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  167. package/packages/gui/src/views/agents.jsx +5 -5
  168. package/packages/gui/src/views/dashboard.jsx +12 -13
  169. package/packages/gui/src/views/marketplace.jsx +191 -3
  170. package/packages/gui/src/views/model-lab.jsx +17 -6
  171. package/packages/gui/src/views/models.jsx +410 -509
  172. package/packages/gui/src/views/network.jsx +3 -3
  173. package/packages/gui/src/views/settings.jsx +81 -94
  174. package/packages/gui/src/views/teams.jsx +40 -483
  175. package/SECURITY_SWEEP.md +0 -228
  176. package/TRAINING_DATA_v4.md +0 -6
  177. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +0 -984
  178. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +0 -1
  179. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -322
  180. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  181. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  182. package/packages/gui/dist/assets/index-Bjd91ufV.js +0 -984
  183. package/packages/gui/dist/assets/index-BqdwIFn4.css +0 -1
  184. package/packages/gui/src/components/agents/agent-chat.jsx +0 -322
  185. package/packages/gui/src/views/preview.jsx +0 -6
  186. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  187. package/test.py +0 -571
@@ -0,0 +1,413 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { api } from '../../lib/api';
4
+
5
+ export const createTeamsSlice = (set, get) => ({
6
+ // ── Teams ─────────────────────────────────────────────────
7
+ teams: [],
8
+ archivedTeams: [],
9
+ activeTeamId: localStorage.getItem('groove:activeTeamId') || null,
10
+
11
+ // ── Team Launch Config (set during planner spawn, cascades to team) ──
12
+ teamLaunchConfig: null, // { provider, model, reasoningEffort, temperature, verbosity, mode }
13
+
14
+ // ── Team Builder ──────────────────────────────────────────
15
+ teamBuilderOpen: false,
16
+ teamBuilderRoles: [],
17
+ teamBuilderSettings: { provider: null, model: null, reasoningEffort: 50, temperature: 0.5 },
18
+ teamBuilderTask: '',
19
+ teamTemplates: { builtIn: [], custom: [] },
20
+
21
+ // ── Recommended Team ──────────────────────────────────────
22
+ recommendedTeam: null, // { name, agents: [...] } from planner
23
+ _delegatingTeamIds: new Set(),
24
+
25
+ // ── Teams ─────────────────────────────────────────────────
26
+
27
+ async fetchTeams() {
28
+ try {
29
+ const data = await api.get('/teams');
30
+ let teams = data.teams || [];
31
+ const defaultTeamId = data.defaultTeamId;
32
+ try {
33
+ const saved = JSON.parse(localStorage.getItem('groove:teamOrder') || '[]');
34
+ if (saved.length) {
35
+ const byId = Object.fromEntries(teams.map((t) => [t.id, t]));
36
+ const ordered = saved.filter((id) => byId[id]).map((id) => byId[id]);
37
+ const remaining = teams.filter((t) => !saved.includes(t.id));
38
+ teams = [...ordered, ...remaining];
39
+ }
40
+ } catch {}
41
+ const { activeTeamId } = get();
42
+ const ids = teams.map((t) => t.id);
43
+ const resolved = ids.includes(activeTeamId) ? activeTeamId : defaultTeamId;
44
+ set({ teams, activeTeamId: resolved });
45
+ if (resolved) localStorage.setItem('groove:activeTeamId', resolved);
46
+ } catch { /* ignore */ }
47
+ },
48
+
49
+ switchTeam(id) {
50
+ const { activeTeamId, detailPanel, teamDetailPanels, teamPreviews } = get();
51
+ const updated = { ...teamDetailPanels };
52
+ if (activeTeamId) updated[activeTeamId] = detailPanel;
53
+ const restored = updated[id] || null;
54
+ const tp = teamPreviews[id];
55
+ const previewUpdate = tp
56
+ ? { previewState: { url: tp.url, teamId: id, kind: tp.kind, deviceSize: 'desktop', screenshotMode: false } }
57
+ : {};
58
+ set({ activeTeamId: id, detailPanel: restored, teamDetailPanels: updated, ...previewUpdate });
59
+ localStorage.setItem('groove:activeTeamId', id);
60
+ },
61
+
62
+ async createTeam(name, workingDir, mode) {
63
+ try {
64
+ const body = { name };
65
+ if (workingDir) body.workingDir = workingDir;
66
+ if (mode) body.mode = mode;
67
+ const team = await api.post('/teams', body);
68
+ // Only set activeTeamId — the WS team:created handler adds to the teams array
69
+ set({ activeTeamId: team.id });
70
+ localStorage.setItem('groove:activeTeamId', team.id);
71
+ get().addToast('success', `Team "${name}" created`);
72
+ return team;
73
+ } catch (err) {
74
+ get().addToast('error', 'Failed to create team', err.message);
75
+ throw err;
76
+ }
77
+ },
78
+
79
+ async archiveTeam(id) {
80
+ const team = get().teams.find((t) => t.id === id);
81
+ try {
82
+ await api.delete(`/teams/${encodeURIComponent(id)}`);
83
+ const wiped = team?.isDefault ? 'wiped' : 'archived';
84
+ get().addToast('success', `Team "${team?.name}" ${wiped}`, wiped === 'archived' ? 'Files preserved — restore anytime from Archived Teams' : undefined);
85
+ get().fetchArchivedTeams();
86
+ } catch (err) {
87
+ get().addToast('error', 'Failed to archive team', err.message);
88
+ }
89
+ },
90
+
91
+ async deleteTeamPermanently(id) {
92
+ const team = get().teams.find((t) => t.id === id);
93
+ try {
94
+ await api.delete(`/teams/${encodeURIComponent(id)}?permanent=true`);
95
+ get().addToast('success', `Team "${team?.name}" permanently deleted`);
96
+ } catch (err) {
97
+ get().addToast('error', 'Failed to delete team', err.message);
98
+ }
99
+ },
100
+
101
+ async deleteTeam(id) {
102
+ return get().archiveTeam(id);
103
+ },
104
+
105
+ reorderTeams(fromIndex, toIndex) {
106
+ const teams = [...get().teams];
107
+ const [moved] = teams.splice(fromIndex, 1);
108
+ teams.splice(toIndex, 0, moved);
109
+ set({ teams });
110
+ try { localStorage.setItem('groove:teamOrder', JSON.stringify(teams.map((t) => t.id))); } catch {}
111
+ },
112
+
113
+ async fetchArchivedTeams() {
114
+ try {
115
+ const data = await api.get('/teams/archived');
116
+ set({ archivedTeams: data.archived || data.teams || [] });
117
+ } catch { /* endpoint may not exist yet */ }
118
+ },
119
+
120
+ async restoreTeam(archivedId) {
121
+ try {
122
+ await api.post(`/teams/archived/${encodeURIComponent(archivedId)}/restore`);
123
+ get().addToast('success', 'Team restored');
124
+ get().fetchArchivedTeams();
125
+ } catch (err) {
126
+ get().addToast('error', 'Failed to restore team', err.message);
127
+ }
128
+ },
129
+
130
+ async purgeTeam(archivedId) {
131
+ try {
132
+ await api.delete(`/teams/archived/${encodeURIComponent(archivedId)}`);
133
+ get().addToast('info', 'Archived team permanently deleted');
134
+ get().fetchArchivedTeams();
135
+ } catch (err) {
136
+ get().addToast('error', 'Failed to purge team', err.message);
137
+ }
138
+ },
139
+
140
+ async cloneTeam(id) {
141
+ const team = get().teams.find((t) => t.id === id);
142
+ if (!team) return;
143
+ const sourceAgents = get().agents.filter((a) => a.teamId === id);
144
+ try {
145
+ const newTeam = await api.post('/teams', { name: `${team.name} (copy)` });
146
+ set({ activeTeamId: newTeam.id });
147
+ localStorage.setItem('groove:activeTeamId', newTeam.id);
148
+ for (const agent of sourceAgents) {
149
+ await api.post('/agents', {
150
+ role: agent.role,
151
+ name: agent.name,
152
+ provider: agent.provider,
153
+ model: agent.model,
154
+ scope: agent.scope,
155
+ teamId: newTeam.id,
156
+ });
157
+ }
158
+ get().addToast('success', `Cloned "${team.name}" with ${sourceAgents.length} agent${sourceAgents.length !== 1 ? 's' : ''}`);
159
+ return newTeam;
160
+ } catch (err) {
161
+ get().addToast('error', 'Failed to clone team', err.message);
162
+ }
163
+ },
164
+
165
+ async renameTeam(id, name) {
166
+ try {
167
+ const team = await api.patch(`/teams/${encodeURIComponent(id)}`, { name });
168
+ set((s) => ({ teams: s.teams.map((t) => (t.id === id ? team : t)) }));
169
+ return team;
170
+ } catch (err) {
171
+ get().addToast('error', 'Failed to rename team', err.message);
172
+ throw err;
173
+ }
174
+ },
175
+
176
+ async promoteTeam(id) {
177
+ try {
178
+ const team = await api.post(`/teams/${encodeURIComponent(id)}/promote`);
179
+ set((s) => ({ teams: s.teams.filter((t) => t.id !== id) }));
180
+ get().addToast('success', 'Team promoted — files moved to project directory');
181
+ return team;
182
+ } catch (err) {
183
+ get().addToast('error', 'Failed to promote team', err.message);
184
+ throw err;
185
+ }
186
+ },
187
+
188
+ // ── Recommended Team ──────────────────────────────────────
189
+
190
+ async checkRecommendedTeam() {
191
+ try {
192
+ const data = await api.get('/recommended-team');
193
+ if (!data || !data.agents?.length) {
194
+ set({ recommendedTeam: null });
195
+ return;
196
+ }
197
+
198
+ // Check if all recommended roles already exist in the planner's team.
199
+ // If so, auto-delegate instead of showing the "Launch Team" modal.
200
+ const teamId = data.teamId || null;
201
+
202
+ if (teamId) {
203
+ const teamAgents = get().agents.filter((a) => a.teamId === teamId && a.role !== 'planner');
204
+ const phase1Roles = data.agents.filter((a) => !a.phase || a.phase === 1).map((a) => a.role);
205
+ const allExist = phase1Roles.every((role) => teamAgents.some((a) => a.role === role));
206
+
207
+ if (allExist && phase1Roles.length > 0) {
208
+ // Guard: skip if already delegating for this team (poll race)
209
+ if (get()._delegatingTeamIds.has(teamId)) return;
210
+ set((s) => ({ recommendedTeam: null, _delegatingTeamIds: new Set([...s._delegatingTeamIds, teamId]) }));
211
+ try {
212
+ const tlc = get().teamLaunchConfig;
213
+ const result = await api.post('/recommended-team/launch', {
214
+ teamId,
215
+ ...(tlc?.provider && { teamProvider: tlc.provider }),
216
+ ...(tlc?.model && { teamModel: tlc.model }),
217
+ ...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
218
+ ...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
219
+ });
220
+ const agents = result.agents || [];
221
+ const failures = result.failed || [];
222
+ const names = agents.map((a) => a.name).join(', ') || '';
223
+
224
+ if (agents.length === 0 && failures.length > 0) {
225
+ get().addToast('error', 'Delegation failed', failures.map(f => f.role + ': ' + f.error).join(', '));
226
+ } else {
227
+ get().addToast('success', 'Planner delegated work', names ? `→ ${names}` : undefined);
228
+ if (failures.length > 0) {
229
+ get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
230
+ }
231
+ }
232
+ if (agents.length > 0) {
233
+ set((s) => ({
234
+ thinkingAgents: new Set([...s.thinkingAgents, ...agents.map((a) => a.id)]),
235
+ }));
236
+ }
237
+ } finally {
238
+ set((s) => {
239
+ const next = new Set(s._delegatingTeamIds);
240
+ next.delete(teamId);
241
+ return { _delegatingTeamIds: next };
242
+ });
243
+ }
244
+ return;
245
+ }
246
+ }
247
+
248
+ // New agents needed — show the modal for approval
249
+ set({ recommendedTeam: { ...data, teamId: data.teamId || null } });
250
+ } catch {
251
+ set({ recommendedTeam: null });
252
+ }
253
+ },
254
+
255
+ async launchRecommendedTeam(modifiedAgents) {
256
+ try {
257
+ const teamId = get().recommendedTeam?.teamId || null;
258
+ const tlc = get().teamLaunchConfig;
259
+ set({ recommendedTeam: null }); // Dismiss modal immediately
260
+ get().addToast('info', 'Launching team...');
261
+ const body = {
262
+ ...(modifiedAgents && { agents: modifiedAgents }),
263
+ ...(teamId && { teamId }),
264
+ ...(tlc?.provider && { teamProvider: tlc.provider }),
265
+ ...(tlc?.model && { teamModel: tlc.model }),
266
+ ...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
267
+ ...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
268
+ ...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
269
+ };
270
+ const result = await api.post('/recommended-team/launch', body);
271
+ const totalOk = (result.launched || 0) + (result.reused || 0);
272
+ const failures = result.failed || [];
273
+
274
+ if (totalOk === 0 && failures.length > 0) {
275
+ get().addToast('error', 'Team launch failed', failures.map(f => f.role + ': ' + f.error).join(', '));
276
+ } else {
277
+ const sub = [
278
+ result.phase2Pending ? `${result.phase2Pending} QC queued` : '',
279
+ result.projectDir ? `→ ${result.projectDir}/` : '',
280
+ ].filter(Boolean).join(' · ');
281
+ get().addToast('success', `Launched ${totalOk} agents`, sub || undefined);
282
+ if (failures.length > 0) {
283
+ get().addToast('warning', `${failures.length} agent(s) failed to spawn`, failures.map(f => f.role + ': ' + f.error).join(', '));
284
+ }
285
+ }
286
+ // Set thinking indicator for all launched/reused agents
287
+ const launchedAgents = result.agents || [];
288
+ if (launchedAgents.length > 0) {
289
+ set((s) => ({
290
+ thinkingAgents: new Set([...s.thinkingAgents, ...launchedAgents.map((a) => a.id)]),
291
+ }));
292
+ }
293
+ // Clean up stale files — scoped to the launched team so plans in other
294
+ // teams' workspaces survive. The launch endpoint already unlinks the
295
+ // exact plan it read; this is a belt-and-suspenders sweep.
296
+ const launchedTeamId = body?.teamId || result?.teamId || null;
297
+ if (launchedTeamId) {
298
+ api.post('/cleanup', { teamId: launchedTeamId }).catch(() => {});
299
+ }
300
+ return result;
301
+ } catch (err) {
302
+ get().addToast('error', 'Launch failed', err.message);
303
+ throw err;
304
+ }
305
+ },
306
+
307
+ // ── Team Builder ──────────────────────────────────────────
308
+
309
+ openTeamBuilder() { set({ teamBuilderOpen: true }); },
310
+ closeTeamBuilder() {
311
+ set({
312
+ teamBuilderOpen: false,
313
+ teamBuilderRoles: [],
314
+ teamBuilderSettings: { provider: null, model: null, reasoningEffort: 50, temperature: 0.5 },
315
+ teamBuilderTask: '',
316
+ });
317
+ },
318
+ addTeamBuilderRole(role) {
319
+ set((s) => ({
320
+ teamBuilderRoles: [...s.teamBuilderRoles, {
321
+ role, name: '', provider: null, model: null,
322
+ reasoningEffort: null, temperature: null, prompt: '',
323
+ }],
324
+ }));
325
+ },
326
+ removeTeamBuilderRole(index) {
327
+ set((s) => ({ teamBuilderRoles: s.teamBuilderRoles.filter((_, i) => i !== index) }));
328
+ },
329
+ updateTeamBuilderRole(index, updates) {
330
+ set((s) => ({
331
+ teamBuilderRoles: s.teamBuilderRoles.map((r, i) => i === index ? { ...r, ...updates } : r),
332
+ }));
333
+ },
334
+ applyTemplate(template) {
335
+ set({
336
+ teamBuilderRoles: (template.roles || []).map((r) => ({
337
+ role: typeof r === 'string' ? r : r.role,
338
+ name: '', provider: null, model: null,
339
+ reasoningEffort: null, temperature: null, prompt: '',
340
+ })),
341
+ });
342
+ },
343
+ setTeamBuilderSettings(settings) {
344
+ set((s) => ({ teamBuilderSettings: { ...s.teamBuilderSettings, ...settings } }));
345
+ },
346
+ setTeamBuilderTask(task) { set({ teamBuilderTask: task }); },
347
+
348
+ async fetchTeamTemplates() {
349
+ try {
350
+ const data = await api.get('/team-templates');
351
+ const builtIn = [];
352
+ const custom = [];
353
+ for (const [key, tmpl] of Object.entries(data || {})) {
354
+ const entry = { ...tmpl, name: key };
355
+ if (tmpl.builtIn) builtIn.push(entry);
356
+ else custom.push(entry);
357
+ }
358
+ set({ teamTemplates: { builtIn, custom } });
359
+ } catch { /* endpoint may not exist yet */ }
360
+ },
361
+
362
+ async saveTeamTemplate(name) {
363
+ try {
364
+ const { teamBuilderRoles, teamBuilderSettings } = get();
365
+ await api.post('/team-templates', {
366
+ name,
367
+ roles: teamBuilderRoles.map((r) => r.role),
368
+ settings: teamBuilderSettings,
369
+ });
370
+ get().addToast('success', `Template "${name}" saved`);
371
+ get().fetchTeamTemplates();
372
+ } catch (err) {
373
+ get().addToast('error', 'Failed to save template', err.message);
374
+ }
375
+ },
376
+
377
+ async deleteTeamTemplate(name) {
378
+ try {
379
+ await api.delete(`/team-templates/${encodeURIComponent(name)}`);
380
+ get().addToast('info', `Template "${name}" deleted`);
381
+ get().fetchTeamTemplates();
382
+ } catch (err) {
383
+ get().addToast('error', 'Failed to delete template', err.message);
384
+ }
385
+ },
386
+
387
+ async launchTeamBuilder() {
388
+ const { teamBuilderRoles, teamBuilderSettings, teamBuilderTask, activeTeamId } = get();
389
+ if (teamBuilderRoles.length === 0) return;
390
+ set({ teamLaunchConfig: {
391
+ provider: teamBuilderSettings.provider || null,
392
+ model: teamBuilderSettings.model || null,
393
+ reasoningEffort: teamBuilderSettings.reasoningEffort,
394
+ temperature: teamBuilderSettings.temperature,
395
+ }});
396
+ get().closeTeamBuilder();
397
+ try {
398
+ const body = {
399
+ task: teamBuilderTask,
400
+ roles: teamBuilderRoles,
401
+ settings: teamBuilderSettings,
402
+ launchMode: 'plan-first',
403
+ teamId: activeTeamId,
404
+ };
405
+ const result = await api.post('/team-builder/launch', body);
406
+ get().addToast('success', 'Planner spawned — team will build automatically');
407
+ return result;
408
+ } catch (err) {
409
+ get().addToast('error', 'Team launch failed', err.message);
410
+ throw err;
411
+ }
412
+ },
413
+ });
@@ -0,0 +1,98 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { loadJSON, persistJSON } from '../helpers.js';
4
+
5
+ let toastCounter = 0;
6
+
7
+ export const createUiSlice = (set, get) => ({
8
+ // ── Navigation ────────────────────────────────────────────
9
+ activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
10
+ detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
11
+ teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
12
+ commandPaletteOpen: false,
13
+ quickConnectOpen: false,
14
+ upgradeModalOpen: false,
15
+
16
+ // ── Node expansion (click-to-open persistent panels) ───────
17
+ expandedNodes: loadJSON('groove:expandedNodes'),
18
+
19
+ // ── Layout persistence ────────────────────────────────────
20
+ detailPanelWidth: Number(localStorage.getItem('groove:detailWidth')) || 480,
21
+ terminalVisible: localStorage.getItem('groove:terminalVisible') === 'true',
22
+ terminalHeight: Number(localStorage.getItem('groove:terminalHeight')) || 260,
23
+ terminalFullHeight: false,
24
+
25
+ // ── Toasts ────────────────────────────────────────────────
26
+ toasts: [],
27
+
28
+ // ── Version / Auto-Update ──────────────────────────────────
29
+ version: null,
30
+ updateReady: null,
31
+ updateProgress: null,
32
+ updateModalOpen: false,
33
+
34
+ // ── Navigation ────────────────────────────────────────────
35
+
36
+ setActiveView(view) { set({ activeView: view }); },
37
+
38
+ openDetail(descriptor) {
39
+ const tid = get().activeTeamId;
40
+ set((s) => ({ detailPanel: descriptor, teamDetailPanels: { ...s.teamDetailPanels, [tid]: descriptor } }));
41
+ },
42
+ closeDetail() {
43
+ const tid = get().activeTeamId;
44
+ set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
45
+ },
46
+ clearSelection() {
47
+ const tid = get().activeTeamId;
48
+ set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
49
+ },
50
+ toggleCommandPalette() { set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })); },
51
+ toggleQuickConnect() { set((s) => ({ quickConnectOpen: !s.quickConnectOpen })); },
52
+ setUpgradeModalOpen: (open) => set({ upgradeModalOpen: open }),
53
+
54
+ setDetailPanelWidth(w) {
55
+ set({ detailPanelWidth: w });
56
+ localStorage.setItem('groove:detailWidth', String(w));
57
+ },
58
+ setTerminalVisible(v) {
59
+ set({ terminalVisible: v });
60
+ localStorage.setItem('groove:terminalVisible', String(v));
61
+ },
62
+ setTerminalHeight(h) {
63
+ set({ terminalHeight: h });
64
+ localStorage.setItem('groove:terminalHeight', String(h));
65
+ },
66
+ setTerminalFullHeight(v) { set({ terminalFullHeight: v }); },
67
+
68
+ toggleNodeExpanded(id) {
69
+ const expanded = { ...get().expandedNodes };
70
+ expanded[id] = !expanded[id];
71
+ if (!expanded[id]) delete expanded[id];
72
+ set({ expandedNodes: expanded });
73
+ persistJSON('groove:expandedNodes', expanded);
74
+ },
75
+
76
+ // ── Toasts ────────────────────────────────────────────────
77
+
78
+ addToast(type, message, detail, action, options = {}) {
79
+ const id = ++toastCounter;
80
+ const persistent = !!options.persistent;
81
+ const duration = options.duration;
82
+ const actions = options.actions;
83
+ set((s) => ({ toasts: [...s.toasts, { id, type, message, detail, action, actions, persistent, duration }] }));
84
+ },
85
+ removeToast(id) {
86
+ set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }));
87
+ },
88
+
89
+ installUpdate() {
90
+ window.groove?.update?.installUpdate();
91
+ },
92
+ setUpdateModalOpen(open) {
93
+ set({ updateModalOpen: open });
94
+ },
95
+ checkForUpdate() {
96
+ window.groove?.update?.checkForUpdate();
97
+ },
98
+ });
@@ -79,7 +79,7 @@ function saveTeamViewport(teamId, viewport) {
79
79
  /* ── Team Tab Bar (IDE-style) ──────────────────────────────── */
80
80
 
81
81
  function teamStatus(agents, teamId) {
82
- const ta = agents.filter((a) => a.teamId === teamId);
82
+ const ta = agents.filter((a) => a.teamId === teamId && !a.metadata?.scheduled);
83
83
  if (ta.length === 0) return 'idle';
84
84
  const running = ta.some((a) => a.status === 'running' || a.status === 'starting');
85
85
  if (running) return 'working';
@@ -173,10 +173,10 @@ export function TeamTabBar() {
173
173
  style={{ scrollbarWidth: 'none', msOverflowStyle: 'none', WebkitOverflowScrolling: 'touch' }}
174
174
  >
175
175
  {teams.map((team) => {
176
- const count = agents.filter((a) => a.teamId === team.id).length;
176
+ const count = agents.filter((a) => a.teamId === team.id && !a.metadata?.scheduled).length;
177
177
  const isActive = team.id === activeTeamId;
178
178
  const isRenaming = renamingId === team.id;
179
- const running = agents.filter((a) => a.teamId === team.id && (a.status === 'running' || a.status === 'starting')).length;
179
+ const running = agents.filter((a) => a.teamId === team.id && !a.metadata?.scheduled && (a.status === 'running' || a.status === 'starting')).length;
180
180
 
181
181
  return (
182
182
  <ContextMenu key={team.id}>
@@ -332,7 +332,7 @@ function AgentTreeInner() {
332
332
  const prevAgentsRef = useRef([]);
333
333
  const agents = useMemo(() => {
334
334
  const next = allAgents
335
- .filter((a) => a.teamId === activeTeamId)
335
+ .filter((a) => a.teamId === activeTeamId && !a.metadata?.scheduled)
336
336
  .sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id));
337
337
  const prev = prevAgentsRef.current;
338
338
  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;
@@ -1579,7 +1579,7 @@ export default function AgentsView() {
1579
1579
  } catch { /* toast handles */ }
1580
1580
  }
1581
1581
 
1582
- const teamAgents = allAgents.filter((a) => a.teamId === activeTeamId);
1582
+ const teamAgents = allAgents.filter((a) => a.teamId === activeTeamId && !a.metadata?.scheduled);
1583
1583
  const hydrated = useGrooveStore((s) => s.hydrated);
1584
1584
  const [showLoader, setShowLoader] = useState(true);
1585
1585
 
@@ -18,10 +18,9 @@ import { BarChart3 } from 'lucide-react';
18
18
 
19
19
  function DashboardSkeleton() {
20
20
  return (
21
- <div className="flex-1 grid gap-px p-0" style={{
21
+ <div className="flex-1 grid gap-px p-0 bg-surface-3" style={{
22
22
  gridTemplateRows: 'auto minmax(0, 1fr) minmax(0, 1fr)',
23
23
  gridTemplateColumns: '2fr 2.5fr 1.5fr',
24
- background: '#282c34',
25
24
  }}>
26
25
  <div className="col-span-3"><Skeleton className="h-[72px] rounded-none" /></div>
27
26
  <Skeleton className="rounded-none" />
@@ -90,14 +89,14 @@ export default function DashboardView() {
90
89
  const events = timeline.events || data.events || [];
91
90
 
92
91
  const kpis = [
93
- { label: 'Tokens Used', value: fmtNum(tokens.totalTokens), sparkData: kpiHistory.tokens, color: HEX.accent, hint: 'Total tokens consumed across all agents — input, output, and cache tokens combined.' },
94
- { label: 'Total Cost', value: fmtDollar(tokens.totalCostUsd), sparkData: kpiHistory.cost, color: HEX.warning, hint: 'Actual cost reported by providers. Claude Code reports real billing; other providers use estimated rates.' },
95
- { label: 'Quality', value: avgQuality != null ? `${avgQuality}` : '—', sparkData: kpiHistory.saved, color: avgQuality >= 70 ? HEX.success : avgQuality >= 40 ? HEX.warning : HEX.danger, hint: 'Average session quality score (0-100) across running agents. Based on error rate, repetitions, file churn, and tool success. Below 40 triggers auto-rotation.' },
96
- { label: 'Cache Rate', value: fmtPct(tokens.cacheHitRate * 100), sparkData: kpiHistory.cache, color: HEX.info, hint: 'Percentage of input tokens served from prompt cache. Higher = faster responses and lower cost. Managed by your AI provider.' },
97
- { label: 'Rotations', value: `${totalRotations}`, sparkData: kpiHistory.efficiency, color: HEX.purple, hint: 'Total context rotations — includes quality-based (auto), context threshold, natural compaction (provider-managed), and manual rotations.' },
98
- { label: 'I/O Ratio', value: `${ioRatio}:1`, sparkData: kpiHistory.inputOutput, color: HEX.orange, hint: 'Ratio of input to output tokens. High ratios mean agents are reading more than writing — common for analysis tasks.' },
99
- { label: 'Agents', value: `${runningCount}/${agents.length}`, sparkData: kpiHistory.agents, color: HEX.accent, hint: 'Running agents out of total spawned this session (including completed and crashed).' },
100
- { label: 'Turns', value: fmtNum(tokens.totalTurns), sparkData: kpiHistory.turns, color: HEX.text2, hint: 'Total conversation turns across all agents. Each turn is one request-response cycle with the AI provider.' },
92
+ { label: 'Tokens Used', value: fmtNum(tokens.totalTokens), sparkData: kpiHistory.tokens, color: HEX.text3, hint: 'Total tokens consumed across all agents — input, output, and cache tokens combined.' },
93
+ { label: 'Total Cost', value: fmtDollar(tokens.totalCostUsd), sparkData: kpiHistory.cost, color: HEX.text3, hint: 'Actual cost reported by providers. Claude Code reports real billing; other providers use estimated rates.' },
94
+ { label: 'Quality', value: avgQuality != null ? `${avgQuality}` : '—', sparkData: kpiHistory.saved, color: HEX.text3, hint: 'Average session quality score (0-100) across running agents. Based on error rate, repetitions, file churn, and tool success. Below 40 triggers auto-rotation.' },
95
+ { label: 'Cache Rate', value: fmtPct(tokens.cacheHitRate * 100), sparkData: kpiHistory.cache, color: HEX.text3, hint: 'Percentage of input tokens served from prompt cache. Higher = faster responses and lower cost. Managed by your AI provider.' },
96
+ { label: 'Rotations', value: `${totalRotations}`, sparkData: kpiHistory.efficiency, color: HEX.text3, hint: 'Total context rotations — includes quality-based (auto), context threshold, natural compaction (provider-managed), and manual rotations.' },
97
+ { label: 'I/O Ratio', value: `${ioRatio}:1`, sparkData: kpiHistory.inputOutput, color: HEX.text3, hint: 'Ratio of input to output tokens. High ratios mean agents are reading more than writing — common for analysis tasks.' },
98
+ { label: 'Agents', value: `${runningCount}/${agents.length}`, sparkData: kpiHistory.agents, color: HEX.text3, hint: 'Running agents out of total spawned this session (including completed and crashed).' },
99
+ { label: 'Turns', value: fmtNum(tokens.totalTurns), sparkData: kpiHistory.turns, color: HEX.text3, hint: 'Total conversation turns across all agents. Each turn is one request-response cycle with the AI provider.' },
101
100
  ];
102
101
 
103
102
  return (
@@ -113,8 +112,8 @@ export default function DashboardView() {
113
112
 
114
113
  <KpiStrip kpis={kpis} />
115
114
 
116
- <div className="flex-1 min-h-0 flex flex-col" style={{ background: '#282c34', gap: '1px' }}>
117
- <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '3fr 1.5fr 1.5fr', gap: '0 1px' }}>
115
+ <div className="flex-1 min-h-0 flex flex-col bg-surface-3 gap-px">
116
+ <div className="min-h-0 flex-1 grid gap-x-px" style={{ gridTemplateColumns: '3fr 1.5fr 1.5fr' }}>
118
117
  <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 relative">
119
118
  <TokenChart data={snapshots} />
120
119
  </div>
@@ -138,7 +137,7 @@ export default function DashboardView() {
138
137
  </div>
139
138
  </div>
140
139
 
141
- <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '2fr 2.5fr 1.5fr', gap: '0 1px' }}>
140
+ <div className="min-h-0 flex-1 grid gap-x-px" style={{ gridTemplateColumns: '2fr 2.5fr 1.5fr' }}>
142
141
  <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-border">
143
142
  <div className="px-3 pt-2.5 pb-1 flex-shrink-0">
144
143
  <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>