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,227 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { api } from '../../lib/api';
4
+ import { loadJSON, persistJSON } from '../helpers.js';
5
+
6
+ const _modeChangePending = new Set();
7
+
8
+ export const createChatSlice = (set, get) => ({
9
+ // ── Conversations (Chat view) ────────────────────────────
10
+ conversations: [],
11
+ activeConversationId: localStorage.getItem('groove:activeConversationId') || null,
12
+ conversationMessages: loadJSON('groove:conversationMessages'),
13
+ sendingMessage: false,
14
+ streamingConversationId: null,
15
+ conversationRoles: loadJSON('groove:conversationRoles'),
16
+ conversationReasoningEffort: loadJSON('groove:conversationReasoningEffort'),
17
+ conversationVerbosity: loadJSON('groove:conversationVerbosity'),
18
+
19
+ // ── Conversations (Chat view) ────────────────────────────
20
+
21
+ async fetchConversations() {
22
+ try {
23
+ const data = await api.get('/conversations');
24
+ set({ conversations: data.conversations || data || [] });
25
+ } catch { /* endpoint may not exist yet */ }
26
+ },
27
+
28
+ async createConversation(provider, model, mode = 'api') {
29
+ try {
30
+ const conv = await api.post('/conversations', { provider, model, mode });
31
+ set((s) => ({
32
+ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)],
33
+ activeConversationId: conv.id,
34
+ }));
35
+ localStorage.setItem('groove:activeConversationId', conv.id);
36
+ return conv;
37
+ } catch (err) {
38
+ get().addToast('error', 'Failed to create conversation', err.message);
39
+ throw err;
40
+ }
41
+ },
42
+
43
+ async setConversationMode(id, mode) {
44
+ if (_modeChangePending.has(id)) return;
45
+ _modeChangePending.add(id);
46
+ try {
47
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { mode });
48
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
49
+ } catch (err) {
50
+ get().addToast('error', 'Mode change failed', err.message);
51
+ } finally {
52
+ _modeChangePending.delete(id);
53
+ }
54
+ },
55
+
56
+ async setConversationModel(id, provider, model) {
57
+ try {
58
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { provider, model });
59
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
60
+ } catch (err) {
61
+ get().addToast('error', 'Model change failed', err.message);
62
+ }
63
+ },
64
+
65
+ async stopChatStreaming(conversationId) {
66
+ try {
67
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/stop`);
68
+ set({ sendingMessage: false, streamingConversationId: null });
69
+ } catch { /* ignore */ }
70
+ },
71
+
72
+ async deleteConversation(id) {
73
+ try {
74
+ await api.delete(`/conversations/${encodeURIComponent(id)}`);
75
+ set((s) => {
76
+ const conversations = s.conversations.filter((c) => c.id !== id);
77
+ const conversationMessages = { ...s.conversationMessages };
78
+ delete conversationMessages[id];
79
+ persistJSON('groove:conversationMessages', conversationMessages);
80
+ const activeConversationId = s.activeConversationId === id
81
+ ? (conversations[0]?.id || null)
82
+ : s.activeConversationId;
83
+ localStorage.setItem('groove:activeConversationId', activeConversationId || '');
84
+ const conversationRoles = { ...s.conversationRoles };
85
+ delete conversationRoles[id];
86
+ persistJSON('groove:conversationRoles', conversationRoles);
87
+ const conversationReasoningEffort = { ...s.conversationReasoningEffort };
88
+ delete conversationReasoningEffort[id];
89
+ persistJSON('groove:conversationReasoningEffort', conversationReasoningEffort);
90
+ const conversationVerbosity = { ...s.conversationVerbosity };
91
+ delete conversationVerbosity[id];
92
+ persistJSON('groove:conversationVerbosity', conversationVerbosity);
93
+ return { conversations, conversationMessages, conversationRoles, conversationReasoningEffort, conversationVerbosity, activeConversationId };
94
+ });
95
+ } catch (err) {
96
+ get().addToast('error', 'Delete failed', err.message);
97
+ }
98
+ },
99
+
100
+ async renameConversation(id, title) {
101
+ try {
102
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { title });
103
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
104
+ } catch (err) {
105
+ get().addToast('error', 'Rename failed', err.message);
106
+ }
107
+ },
108
+
109
+ async pinConversation(id, pinned) {
110
+ try {
111
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { pinned });
112
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
113
+ } catch (err) {
114
+ get().addToast('error', 'Pin failed', err.message);
115
+ }
116
+ },
117
+
118
+ setActiveConversation(id) {
119
+ set({ activeConversationId: id });
120
+ localStorage.setItem('groove:activeConversationId', id || '');
121
+ },
122
+
123
+ setConversationRole(id, role) {
124
+ set((s) => {
125
+ const roles = { ...s.conversationRoles };
126
+ if (role) {
127
+ roles[id] = role;
128
+ } else {
129
+ delete roles[id];
130
+ }
131
+ persistJSON('groove:conversationRoles', roles);
132
+ return { conversationRoles: roles };
133
+ });
134
+ },
135
+
136
+ setConversationReasoningEffort(id, effort) {
137
+ set((s) => {
138
+ const map = { ...s.conversationReasoningEffort };
139
+ map[id] = effort || 'medium';
140
+ persistJSON('groove:conversationReasoningEffort', map);
141
+ return { conversationReasoningEffort: map };
142
+ });
143
+ },
144
+
145
+ setConversationVerbosity(id, verbosity) {
146
+ set((s) => {
147
+ const map = { ...s.conversationVerbosity };
148
+ map[id] = verbosity || 'medium';
149
+ persistJSON('groove:conversationVerbosity', map);
150
+ return { conversationVerbosity: map };
151
+ });
152
+ },
153
+
154
+ async sendChatMessage(conversationId, message) {
155
+ const conv = get().conversations.find((c) => c.id === conversationId);
156
+ if (!conv) return;
157
+
158
+ // Add user message to local state immediately
159
+ set((s) => {
160
+ const msgs = { ...s.conversationMessages };
161
+ if (!msgs[conversationId]) msgs[conversationId] = [];
162
+ msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: message, timestamp: Date.now() }];
163
+ persistJSON('groove:conversationMessages', msgs);
164
+ return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
165
+ });
166
+
167
+ try {
168
+ const body = { message };
169
+ if (conv.mode === 'api' || !conv.mode) {
170
+ const history = get().conversationMessages[conversationId] || [];
171
+ body.history = history.slice(0, -1);
172
+
173
+ const role = get().conversationRoles?.[conversationId];
174
+ const rules = ['Never use emojis in your responses.', 'Be professional, concise, and direct.'];
175
+ if (role) rules.unshift(`You are a professional ${role}. Respond with deep expertise in that domain.`);
176
+ const systemCtx = rules.join(' ');
177
+ body.history = [
178
+ { from: 'user', text: `Instructions: ${systemCtx}` },
179
+ { from: 'assistant', text: 'Understood.' },
180
+ ...body.history,
181
+ ];
182
+ }
183
+ const effort = get().conversationReasoningEffort?.[conversationId] || 'medium';
184
+ const verbosity = get().conversationVerbosity?.[conversationId] || 'medium';
185
+ if (conv.provider === 'codex') {
186
+ body.reasoning_effort = effort;
187
+ body.verbosity = verbosity;
188
+ }
189
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
190
+ } catch (err) {
191
+ set((s) => {
192
+ const msgs = { ...s.conversationMessages };
193
+ if (!msgs[conversationId]) msgs[conversationId] = [];
194
+ msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Failed: ${err.message}`, timestamp: Date.now() }];
195
+ persistJSON('groove:conversationMessages', msgs);
196
+ return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
197
+ });
198
+ get().addToast('error', 'Message failed', err.message);
199
+ }
200
+ },
201
+
202
+ async sendImageMessage(conversationId, prompt, { model, size, quality } = {}) {
203
+ const conv = get().conversations.find((c) => c.id === conversationId);
204
+ if (!conv) return;
205
+
206
+ set((s) => {
207
+ const msgs = { ...s.conversationMessages };
208
+ if (!msgs[conversationId]) msgs[conversationId] = [];
209
+ msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: prompt, timestamp: Date.now() }];
210
+ persistJSON('groove:conversationMessages', msgs);
211
+ return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
212
+ });
213
+
214
+ try {
215
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/generate-image`, { prompt, model, size, quality });
216
+ } catch (err) {
217
+ set((s) => {
218
+ const msgs = { ...s.conversationMessages };
219
+ if (!msgs[conversationId]) msgs[conversationId] = [];
220
+ msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Image failed: ${err.message}`, timestamp: Date.now() }];
221
+ persistJSON('groove:conversationMessages', msgs);
222
+ return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
223
+ });
224
+ get().addToast('error', 'Image generation failed', err.message);
225
+ }
226
+ },
227
+ });
@@ -0,0 +1,285 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { api } from '../../lib/api';
4
+
5
+ export const createEditorSlice = (set, get) => ({
6
+ // ── Editor state ──────────────────────────────────────────
7
+ editorFiles: {},
8
+ editorActiveFile: null,
9
+ editorOpenTabs: [],
10
+ editorTreeCache: {},
11
+ editorChangedFiles: {},
12
+ editorRecentSaves: {},
13
+ editorSidebarWidth: Number(localStorage.getItem('groove:editorSidebarWidth')) || 240,
14
+ editorTheme: localStorage.getItem('groove:editorTheme') || 'vscodeDark',
15
+
16
+ // ── Editor (Cursor-style) ────────────────────────────────
17
+ editorSelectedAgent: null,
18
+ editorPendingSnippet: null,
19
+ editorViewMode: 'code',
20
+ editorAiPanelOpen: false,
21
+ editorAiPanelWidth: Number(localStorage.getItem('groove:editorAiPanelWidth')) || 360,
22
+ editorGitStatus: null,
23
+ editorGitBranch: null,
24
+ editorGitDiff: null,
25
+ editorQuickSearchOpen: false,
26
+
27
+ // ── Workspace Snapshots ────────────────────────────────────
28
+ workspaceSnapshots: {},
29
+
30
+ // ── Editor ────────────────────────────────────────────────
31
+
32
+ async openFile(path) {
33
+ if (get().editorFiles[path] || get().editorOpenTabs.includes(path)) {
34
+ set((s) => ({
35
+ editorActiveFile: path,
36
+ editorOpenTabs: s.editorOpenTabs.includes(path) ? s.editorOpenTabs : [...s.editorOpenTabs, path],
37
+ }));
38
+ return;
39
+ }
40
+ const ext = path.split('.').pop()?.toLowerCase();
41
+ const MEDIA = ['png','jpg','jpeg','gif','svg','webp','ico','bmp','avif','mp4','webm','mov','avi','mkv','ogv'];
42
+ if (MEDIA.includes(ext)) {
43
+ set((s) => ({ editorActiveFile: path, editorOpenTabs: [...s.editorOpenTabs, path] }));
44
+ return;
45
+ }
46
+ try {
47
+ const data = await api.get(`/files/read?path=${encodeURIComponent(path)}`);
48
+ if (data.binary) { get().addToast('warning', 'Binary file — cannot open'); return; }
49
+ set((s) => ({
50
+ editorFiles: { ...s.editorFiles, [path]: { content: data.content, originalContent: data.content, language: data.language, loadedAt: Date.now() } },
51
+ editorActiveFile: path,
52
+ editorOpenTabs: s.editorOpenTabs.includes(path) ? s.editorOpenTabs : [...s.editorOpenTabs, path],
53
+ }));
54
+ const ws = get().ws;
55
+ if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watch', path }));
56
+ } catch (err) {
57
+ get().addToast('error', 'Failed to open file', err.message);
58
+ }
59
+ },
60
+
61
+ closeFile(path) {
62
+ set((s) => {
63
+ const tabs = s.editorOpenTabs.filter((t) => t !== path);
64
+ const files = { ...s.editorFiles };
65
+ delete files[path];
66
+ const changed = { ...s.editorChangedFiles };
67
+ delete changed[path];
68
+ let active = s.editorActiveFile;
69
+ if (active === path) {
70
+ const idx = s.editorOpenTabs.indexOf(path);
71
+ active = tabs[Math.min(idx, tabs.length - 1)] || null;
72
+ }
73
+ return { editorOpenTabs: tabs, editorFiles: files, editorChangedFiles: changed, editorActiveFile: active };
74
+ });
75
+ const ws = get().ws;
76
+ if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:unwatch', path }));
77
+ },
78
+
79
+ setActiveFile(path) { set({ editorActiveFile: path }); },
80
+
81
+ setEditorSidebarWidth(width) {
82
+ set({ editorSidebarWidth: width });
83
+ localStorage.setItem('groove:editorSidebarWidth', String(width));
84
+ },
85
+ setEditorTheme(theme) {
86
+ set({ editorTheme: theme });
87
+ localStorage.setItem('groove:editorTheme', theme);
88
+ },
89
+
90
+ updateFileContent(path, content) {
91
+ set((s) => ({ editorFiles: { ...s.editorFiles, [path]: { ...s.editorFiles[path], content } } }));
92
+ },
93
+
94
+ async saveFile(path) {
95
+ const file = get().editorFiles[path];
96
+ if (!file) return;
97
+ try {
98
+ await api.post('/files/write', { path, content: file.content });
99
+ set((s) => ({
100
+ editorFiles: { ...s.editorFiles, [path]: { ...s.editorFiles[path], originalContent: file.content } },
101
+ editorChangedFiles: (() => { const c = { ...s.editorChangedFiles }; delete c[path]; return c; })(),
102
+ editorRecentSaves: { ...s.editorRecentSaves, [path]: Date.now() },
103
+ }));
104
+ get().addToast('success', 'File saved');
105
+ } catch (err) {
106
+ get().addToast('error', 'Save failed', err.message);
107
+ }
108
+ },
109
+
110
+ async reloadFile(path) {
111
+ try {
112
+ const data = await api.get(`/files/read?path=${encodeURIComponent(path)}`);
113
+ if (data.binary) return;
114
+ set((s) => ({
115
+ editorFiles: { ...s.editorFiles, [path]: { content: data.content, originalContent: data.content, language: data.language, loadedAt: Date.now() } },
116
+ editorChangedFiles: (() => { const c = { ...s.editorChangedFiles }; delete c[path]; return c; })(),
117
+ }));
118
+ } catch { /* ignore */ }
119
+ },
120
+
121
+ dismissFileChange(path) {
122
+ set((s) => { const c = { ...s.editorChangedFiles }; delete c[path]; return { editorChangedFiles: c }; });
123
+ },
124
+
125
+ async fetchTreeDir(dirPath) {
126
+ try {
127
+ const data = await api.get(`/files/tree?path=${encodeURIComponent(dirPath)}`);
128
+ set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: data.entries || [] } }));
129
+ const ws = get().ws;
130
+ if (ws?.readyState === 1) ws.send(JSON.stringify({ type: 'editor:watchdir', path: dirPath }));
131
+ } catch (err) {
132
+ console.error('[file-tree] fetchTreeDir failed for', dirPath, err.message);
133
+ set((s) => ({ editorTreeCache: { ...s.editorTreeCache, [dirPath]: [] } }));
134
+ }
135
+ },
136
+
137
+ async createFile(relPath) {
138
+ try {
139
+ await api.post('/files/create', { path: relPath });
140
+ const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
141
+ await get().fetchTreeDir(parent);
142
+ get().addToast('success', 'File created');
143
+ return true;
144
+ } catch (err) {
145
+ get().addToast('error', 'Create failed', err.message);
146
+ return false;
147
+ }
148
+ },
149
+
150
+ async createDir(relPath) {
151
+ try {
152
+ await api.post('/files/mkdir', { path: relPath });
153
+ const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
154
+ await get().fetchTreeDir(parent);
155
+ get().addToast('success', 'Folder created');
156
+ return true;
157
+ } catch (err) {
158
+ get().addToast('error', 'Create failed', err.message);
159
+ return false;
160
+ }
161
+ },
162
+
163
+ async deleteFile(relPath) {
164
+ try {
165
+ await api.delete(`/files/delete?path=${encodeURIComponent(relPath)}`);
166
+ if (get().editorOpenTabs.includes(relPath)) get().closeFile(relPath);
167
+ const parent = relPath.includes('/') ? relPath.split('/').slice(0, -1).join('/') : '';
168
+ await get().fetchTreeDir(parent);
169
+ set((s) => { const cache = { ...s.editorTreeCache }; delete cache[relPath]; return { editorTreeCache: cache }; });
170
+ get().addToast('success', 'Deleted');
171
+ return true;
172
+ } catch (err) {
173
+ get().addToast('error', 'Delete failed', err.message);
174
+ return false;
175
+ }
176
+ },
177
+
178
+ async renameFile(oldPath, newPath) {
179
+ try {
180
+ await api.post('/files/rename', { oldPath, newPath });
181
+ set((s) => {
182
+ const tabs = s.editorOpenTabs.map((t) => t === oldPath ? newPath : t);
183
+ const files = { ...s.editorFiles };
184
+ if (files[oldPath]) { files[newPath] = files[oldPath]; delete files[oldPath]; }
185
+ const active = s.editorActiveFile === oldPath ? newPath : s.editorActiveFile;
186
+ return { editorOpenTabs: tabs, editorFiles: files, editorActiveFile: active };
187
+ });
188
+ const oldParent = oldPath.includes('/') ? oldPath.split('/').slice(0, -1).join('/') : '';
189
+ const newParent = newPath.includes('/') ? newPath.split('/').slice(0, -1).join('/') : '';
190
+ await get().fetchTreeDir(oldParent);
191
+ if (newParent !== oldParent) await get().fetchTreeDir(newParent);
192
+ get().addToast('success', 'Renamed');
193
+ return true;
194
+ } catch (err) {
195
+ get().addToast('error', 'Rename failed', err.message);
196
+ return false;
197
+ }
198
+ },
199
+
200
+ captureSnapshot(path, content) {
201
+ set((s) => {
202
+ if (s.workspaceSnapshots[path]) return s;
203
+ const next = { ...s.workspaceSnapshots, [path]: content };
204
+ const keys = Object.keys(next);
205
+ if (keys.length > 200) {
206
+ delete next[keys[0]];
207
+ }
208
+ return { workspaceSnapshots: next };
209
+ });
210
+ },
211
+
212
+ // ── Editor (Cursor-style) ──────────────────────────────────
213
+
214
+ setEditorAgent(id) {
215
+ set({ editorSelectedAgent: id });
216
+ },
217
+
218
+ setEditorViewMode(mode) {
219
+ set({ editorViewMode: mode });
220
+ },
221
+
222
+ toggleAiPanel() {
223
+ set((s) => {
224
+ const open = !s.editorAiPanelOpen;
225
+ return { editorAiPanelOpen: open };
226
+ });
227
+ },
228
+
229
+ setEditorAiPanelWidth(width) {
230
+ set({ editorAiPanelWidth: width });
231
+ localStorage.setItem('groove:editorAiPanelWidth', String(width));
232
+ },
233
+
234
+ setEditorQuickSearchOpen(open) {
235
+ set({ editorQuickSearchOpen: open });
236
+ },
237
+
238
+ attachSnippet(snippet) {
239
+ set({ editorPendingSnippet: snippet });
240
+ if (!get().editorAiPanelOpen) {
241
+ set({ editorAiPanelOpen: true });
242
+ }
243
+ },
244
+
245
+ clearSnippet() {
246
+ set({ editorPendingSnippet: null });
247
+ },
248
+
249
+ async sendCodeToAgent(agentId, instruction, filePath, lineStart, lineEnd, selectedCode) {
250
+ if (!agentId) return;
251
+ get().attachSnippet({
252
+ type: 'code',
253
+ instruction,
254
+ filePath,
255
+ lineStart,
256
+ lineEnd,
257
+ code: selectedCode,
258
+ });
259
+ },
260
+
261
+ async fetchGitStatus() {
262
+ try {
263
+ const data = await api.get('/files/git-status');
264
+ set({ editorGitStatus: data });
265
+ return data;
266
+ } catch { return null; }
267
+ },
268
+
269
+ async fetchGitBranch() {
270
+ try {
271
+ const data = await api.get('/files/git-branch');
272
+ set({ editorGitBranch: data });
273
+ return data;
274
+ } catch { return null; }
275
+ },
276
+
277
+ async fetchGitDiff(path) {
278
+ try {
279
+ const url = path ? `/files/git-diff?path=${encodeURIComponent(path)}` : '/files/git-diff';
280
+ const data = await api.get(url);
281
+ set({ editorGitDiff: data });
282
+ return data;
283
+ } catch { return null; }
284
+ },
285
+ });