groove-dev 0.27.55 → 0.27.57

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 (42) hide show
  1. package/ai-chat/CHAT_MASTER_PLAN.md +184 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +169 -0
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +423 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  7. package/node_modules/@groove-dev/gui/dist/assets/index-C5WTeZO4.css +1 -0
  8. package/node_modules/@groove-dev/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
  9. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  10. package/node_modules/@groove-dev/gui/package.json +1 -1
  11. package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
  12. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +138 -0
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +112 -0
  14. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +347 -0
  15. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +165 -0
  16. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +154 -0
  17. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +143 -0
  18. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
  19. package/node_modules/@groove-dev/gui/src/stores/groove.js +220 -0
  20. package/node_modules/@groove-dev/gui/src/views/chat.jsx +6 -0
  21. package/package.json +1 -1
  22. package/packages/cli/package.json +1 -1
  23. package/packages/daemon/package.json +1 -1
  24. package/packages/daemon/src/api.js +169 -0
  25. package/packages/daemon/src/conversations.js +423 -0
  26. package/packages/daemon/src/index.js +2 -0
  27. package/packages/gui/dist/assets/index-C5WTeZO4.css +1 -0
  28. package/packages/gui/dist/assets/{index-De-OWmBX.js → index-X58BAjGp.js} +1752 -1745
  29. package/packages/gui/dist/index.html +2 -2
  30. package/packages/gui/package.json +1 -1
  31. package/packages/gui/src/app.jsx +2 -0
  32. package/packages/gui/src/components/chat/chat-header.jsx +138 -0
  33. package/packages/gui/src/components/chat/chat-input.jsx +112 -0
  34. package/packages/gui/src/components/chat/chat-messages.jsx +347 -0
  35. package/packages/gui/src/components/chat/chat-view.jsx +165 -0
  36. package/packages/gui/src/components/chat/conversation-list.jsx +154 -0
  37. package/packages/gui/src/components/chat/model-picker.jsx +143 -0
  38. package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
  39. package/packages/gui/src/stores/groove.js +220 -0
  40. package/packages/gui/src/views/chat.jsx +6 -0
  41. package/node_modules/@groove-dev/gui/dist/assets/index-CyVj0fHl.css +0 -1
  42. package/packages/gui/dist/assets/index-CyVj0fHl.css +0 -1
@@ -75,6 +75,13 @@ export const useGrooveStore = create((set, get) => ({
75
75
  chatInputs: {}, // Per-agent draft input text — persists across tab switches
76
76
  tokenTimeline: {},
77
77
 
78
+ // ── Conversations (Chat view) ────────────────────────────
79
+ conversations: [],
80
+ activeConversationId: localStorage.getItem('groove:activeConversationId') || null,
81
+ conversationMessages: loadJSON('groove:conversationMessages'),
82
+ sendingMessage: false,
83
+ streamingConversationId: null,
84
+
78
85
  // ── Approvals ─────────────────────────────────────────────
79
86
  pendingApprovals: [],
80
87
  resolvedApprovals: [],
@@ -163,6 +170,7 @@ export const useGrooveStore = create((set, get) => ({
163
170
  if (isTunneled) get().fetchProjectDir();
164
171
  }).catch(() => {});
165
172
  get().fetchTeams();
173
+ get().fetchConversations();
166
174
  get().fetchApprovals();
167
175
  get().checkMarketplaceAuth();
168
176
  get().fetchTunnels();
@@ -342,6 +350,28 @@ export const useGrooveStore = create((set, get) => ({
342
350
  set({ chatHistory: history });
343
351
  persistJSON('groove:chatHistory', history);
344
352
  }
353
+
354
+ // Mirror to conversation messages if this agent belongs to a conversation
355
+ const conv = get().conversations.find((c) => c.agentId === agentId);
356
+ if (conv) {
357
+ const convMsgs = { ...get().conversationMessages };
358
+ if (!convMsgs[conv.id]) convMsgs[conv.id] = [];
359
+ const convArr = [...convMsgs[conv.id]];
360
+ const lastConv = convArr[convArr.length - 1];
361
+ const isRecentConv = lastConv && lastConv.from === 'assistant' && (Date.now() - lastConv.timestamp) < 8000;
362
+ const isConvDupe = isRecentConv && (lastConv.text === trimmed || lastConv.text.endsWith(trimmed));
363
+ if (!isConvDupe) {
364
+ if (isRecentConv) {
365
+ const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
366
+ convArr[convArr.length - 1] = { ...lastConv, text: lastConv.text + sep + trimmed, timestamp: Date.now() };
367
+ } else {
368
+ convArr.push({ from: 'assistant', text: trimmed, timestamp: Date.now() });
369
+ }
370
+ convMsgs[conv.id] = convArr.slice(-200);
371
+ set({ conversationMessages: convMsgs, streamingConversationId: conv.id });
372
+ persistJSON('groove:conversationMessages', convMsgs);
373
+ }
374
+ }
345
375
  }
346
376
 
347
377
  // Tool calls → activity log (shown in streaming bar, not as chat bubbles)
@@ -379,6 +409,12 @@ export const useGrooveStore = create((set, get) => ({
379
409
  });
380
410
  }
381
411
 
412
+ // Clear conversation streaming state
413
+ const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
414
+ if (exitConv && get().streamingConversationId === exitConv.id) {
415
+ set({ sendingMessage: false, streamingConversationId: null });
416
+ }
417
+
382
418
  // Log crash error to agent chat so user can see what happened
383
419
  if (msg.error && msg.agentId) {
384
420
  get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
@@ -736,6 +772,76 @@ export const useGrooveStore = create((set, get) => ({
736
772
  get().fetchBetaStatus();
737
773
  get().fetchNetworkInstallStatus();
738
774
  break;
775
+
776
+ case 'conversation:created': {
777
+ const conv = msg.data;
778
+ if (conv) set((s) => ({ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)] }));
779
+ break;
780
+ }
781
+
782
+ case 'conversation:updated': {
783
+ const conv = msg.data;
784
+ if (conv) set((s) => ({ conversations: s.conversations.map((c) => c.id === conv.id ? { ...c, ...conv } : c) }));
785
+ break;
786
+ }
787
+
788
+ case 'conversation:deleted': {
789
+ const id = msg.data?.id || msg.id;
790
+ if (id) {
791
+ set((s) => {
792
+ const conversations = s.conversations.filter((c) => c.id !== id);
793
+ const conversationMessages = { ...s.conversationMessages };
794
+ delete conversationMessages[id];
795
+ const activeConversationId = s.activeConversationId === id ? null : s.activeConversationId;
796
+ if (activeConversationId !== s.activeConversationId) localStorage.setItem('groove:activeConversationId', '');
797
+ return { conversations, conversationMessages, activeConversationId };
798
+ });
799
+ }
800
+ break;
801
+ }
802
+
803
+ case 'conversation:chunk': {
804
+ const { conversationId, text } = msg.data || msg;
805
+ if (!conversationId || !text) break;
806
+ set((s) => {
807
+ const msgs = { ...s.conversationMessages };
808
+ if (!msgs[conversationId]) msgs[conversationId] = [];
809
+ const arr = [...msgs[conversationId]];
810
+ const last = arr[arr.length - 1];
811
+ if (last && last.from === 'assistant' && (Date.now() - last.timestamp) < 30000) {
812
+ arr[arr.length - 1] = { ...last, text: last.text + text, timestamp: Date.now() };
813
+ } else {
814
+ arr.push({ from: 'assistant', text, timestamp: Date.now() });
815
+ }
816
+ msgs[conversationId] = arr.slice(-200);
817
+ persistJSON('groove:conversationMessages', msgs);
818
+ return { conversationMessages: msgs, streamingConversationId: conversationId };
819
+ });
820
+ break;
821
+ }
822
+
823
+ case 'conversation:complete': {
824
+ const { conversationId } = msg.data || msg;
825
+ if (conversationId) {
826
+ set({ sendingMessage: false, streamingConversationId: null });
827
+ persistJSON('groove:conversationMessages', get().conversationMessages);
828
+ }
829
+ break;
830
+ }
831
+
832
+ case 'conversation:error': {
833
+ const { conversationId, error } = msg.data || msg;
834
+ if (conversationId) {
835
+ set((s) => {
836
+ const msgs = { ...s.conversationMessages };
837
+ if (!msgs[conversationId]) msgs[conversationId] = [];
838
+ msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Error: ${error || 'Unknown error'}`, timestamp: Date.now() }];
839
+ persistJSON('groove:conversationMessages', msgs);
840
+ return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
841
+ });
842
+ }
843
+ break;
844
+ }
739
845
  }
740
846
  };
741
847
 
@@ -1532,6 +1638,120 @@ export const useGrooveStore = create((set, get) => ({
1532
1638
  }
1533
1639
  },
1534
1640
 
1641
+ // ── Conversations (Chat view) ────────────────────────────
1642
+
1643
+ async fetchConversations() {
1644
+ try {
1645
+ const data = await api.get('/conversations');
1646
+ set({ conversations: data.conversations || data || [] });
1647
+ } catch { /* endpoint may not exist yet */ }
1648
+ },
1649
+
1650
+ async createConversation(provider, model, mode = 'api') {
1651
+ try {
1652
+ const conv = await api.post('/conversations', { provider, model, mode });
1653
+ set((s) => ({
1654
+ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)],
1655
+ activeConversationId: conv.id,
1656
+ }));
1657
+ localStorage.setItem('groove:activeConversationId', conv.id);
1658
+ return conv;
1659
+ } catch (err) {
1660
+ get().addToast('error', 'Failed to create conversation', err.message);
1661
+ throw err;
1662
+ }
1663
+ },
1664
+
1665
+ async setConversationMode(id, mode) {
1666
+ try {
1667
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { mode });
1668
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
1669
+ } catch (err) {
1670
+ get().addToast('error', 'Mode change failed', err.message);
1671
+ }
1672
+ },
1673
+
1674
+ async stopChatStreaming(conversationId) {
1675
+ try {
1676
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/stop`);
1677
+ set({ sendingMessage: false, streamingConversationId: null });
1678
+ } catch { /* ignore */ }
1679
+ },
1680
+
1681
+ async deleteConversation(id) {
1682
+ try {
1683
+ await api.delete(`/conversations/${encodeURIComponent(id)}`);
1684
+ set((s) => {
1685
+ const conversations = s.conversations.filter((c) => c.id !== id);
1686
+ const conversationMessages = { ...s.conversationMessages };
1687
+ delete conversationMessages[id];
1688
+ persistJSON('groove:conversationMessages', conversationMessages);
1689
+ const activeConversationId = s.activeConversationId === id
1690
+ ? (conversations[0]?.id || null)
1691
+ : s.activeConversationId;
1692
+ localStorage.setItem('groove:activeConversationId', activeConversationId || '');
1693
+ return { conversations, conversationMessages, activeConversationId };
1694
+ });
1695
+ } catch (err) {
1696
+ get().addToast('error', 'Delete failed', err.message);
1697
+ }
1698
+ },
1699
+
1700
+ async renameConversation(id, title) {
1701
+ try {
1702
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { title });
1703
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
1704
+ } catch (err) {
1705
+ get().addToast('error', 'Rename failed', err.message);
1706
+ }
1707
+ },
1708
+
1709
+ async pinConversation(id, pinned) {
1710
+ try {
1711
+ const conv = await api.patch(`/conversations/${encodeURIComponent(id)}`, { pinned });
1712
+ set((s) => ({ conversations: s.conversations.map((c) => c.id === id ? { ...c, ...conv } : c) }));
1713
+ } catch (err) {
1714
+ get().addToast('error', 'Pin failed', err.message);
1715
+ }
1716
+ },
1717
+
1718
+ setActiveConversation(id) {
1719
+ set({ activeConversationId: id });
1720
+ localStorage.setItem('groove:activeConversationId', id || '');
1721
+ },
1722
+
1723
+ async sendChatMessage(conversationId, message) {
1724
+ const conv = get().conversations.find((c) => c.id === conversationId);
1725
+ if (!conv) return;
1726
+
1727
+ // Add user message to local state immediately
1728
+ set((s) => {
1729
+ const msgs = { ...s.conversationMessages };
1730
+ if (!msgs[conversationId]) msgs[conversationId] = [];
1731
+ msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: message, timestamp: Date.now() }];
1732
+ persistJSON('groove:conversationMessages', msgs);
1733
+ return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
1734
+ });
1735
+
1736
+ try {
1737
+ const body = { message };
1738
+ if (conv.mode === 'api' || !conv.mode) {
1739
+ const history = get().conversationMessages[conversationId] || [];
1740
+ body.history = history.slice(0, -1);
1741
+ }
1742
+ await api.post(`/conversations/${encodeURIComponent(conversationId)}/message`, body);
1743
+ } catch (err) {
1744
+ set((s) => {
1745
+ const msgs = { ...s.conversationMessages };
1746
+ if (!msgs[conversationId]) msgs[conversationId] = [];
1747
+ msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Failed: ${err.message}`, timestamp: Date.now() }];
1748
+ persistJSON('groove:conversationMessages', msgs);
1749
+ return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
1750
+ });
1751
+ get().addToast('error', 'Message failed', err.message);
1752
+ }
1753
+ },
1754
+
1535
1755
  // ── Editor ────────────────────────────────────────────────
1536
1756
 
1537
1757
  async openFile(path) {
@@ -0,0 +1,6 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { ChatView } from '../components/chat/chat-view';
3
+
4
+ export default function ChatViewEntry() {
5
+ return <ChatView />;
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.55",
3
+ "version": "0.27.57",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.55",
3
+ "version": "0.27.57",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.55",
3
+ "version": "0.27.57",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -810,6 +810,175 @@ export function createApi(app, daemon) {
810
810
  }
811
811
  });
812
812
 
813
+ // --- Conversations ---
814
+
815
+ app.get('/api/conversations', (req, res) => {
816
+ res.json({ conversations: daemon.conversations.list() });
817
+ });
818
+
819
+ app.post('/api/conversations', async (req, res) => {
820
+ try {
821
+ const { provider, model, title, mode } = req.body;
822
+ if (!provider || typeof provider !== 'string') {
823
+ return res.status(400).json({ error: 'provider is required' });
824
+ }
825
+ if (mode && mode !== 'api' && mode !== 'agent') {
826
+ return res.status(400).json({ error: 'mode must be "api" or "agent"' });
827
+ }
828
+ const conversation = await daemon.conversations.create(provider, model, title, mode || 'api');
829
+ daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
830
+ res.status(201).json(conversation);
831
+ } catch (err) {
832
+ res.status(400).json({ error: err.message });
833
+ }
834
+ });
835
+
836
+ app.get('/api/conversations/:id', (req, res) => {
837
+ const conversation = daemon.conversations.get(req.params.id);
838
+ if (!conversation) return res.status(404).json({ error: 'Conversation not found' });
839
+ res.json(conversation);
840
+ });
841
+
842
+ app.patch('/api/conversations/:id', async (req, res) => {
843
+ try {
844
+ const conv = daemon.conversations.get(req.params.id);
845
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
846
+ if (req.body.title !== undefined) daemon.conversations.rename(req.params.id, req.body.title);
847
+ if (req.body.pinned !== undefined) daemon.conversations.pin(req.params.id, req.body.pinned);
848
+ if (req.body.archived !== undefined) daemon.conversations.archive(req.params.id, req.body.archived);
849
+ if (req.body.mode !== undefined) {
850
+ if (req.body.mode !== 'api' && req.body.mode !== 'agent') {
851
+ return res.status(400).json({ error: 'mode must be "api" or "agent"' });
852
+ }
853
+ await daemon.conversations.setMode(req.params.id, req.body.mode);
854
+ }
855
+ daemon.audit.log('conversation.update', { id: req.params.id, mode: req.body.mode });
856
+ res.json(daemon.conversations.get(req.params.id));
857
+ } catch (err) {
858
+ res.status(400).json({ error: err.message });
859
+ }
860
+ });
861
+
862
+ app.delete('/api/conversations/:id', async (req, res) => {
863
+ try {
864
+ const conv = daemon.conversations.get(req.params.id);
865
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
866
+ await daemon.conversations.delete(req.params.id);
867
+ daemon.audit.log('conversation.delete', { id: req.params.id });
868
+ res.json({ ok: true });
869
+ } catch (err) {
870
+ res.status(400).json({ error: err.message });
871
+ }
872
+ });
873
+
874
+ app.post('/api/conversations/:id/message', async (req, res) => {
875
+ try {
876
+ const { message, history } = req.body;
877
+ if (!message || typeof message !== 'string' || !message.trim()) {
878
+ return res.status(400).json({ error: 'message is required' });
879
+ }
880
+ const conv = daemon.conversations.get(req.params.id);
881
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
882
+
883
+ daemon.conversations.autoTitle(req.params.id, message.trim());
884
+ daemon.conversations.touchUpdatedAt(req.params.id);
885
+
886
+ // API mode — lightweight headless streaming, no agent spawned
887
+ if (conv.mode === 'api' || !conv.agentId) {
888
+ await daemon.conversations.sendMessage(req.params.id, message.trim(), history || []);
889
+ daemon.audit.log('conversation.message', { id: req.params.id, mode: 'api' });
890
+ return res.json({ status: 'streaming', mode: 'api' });
891
+ }
892
+
893
+ // Agent mode — existing behavior
894
+ const agent = daemon.registry.get(conv.agentId);
895
+ if (!agent) return res.status(400).json({ error: 'Agent no longer exists' });
896
+
897
+ // Record user feedback for journalist context
898
+ if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
899
+
900
+ // Agent loop path — send message directly to the running loop
901
+ if (daemon.processes.hasAgentLoop(conv.agentId)) {
902
+ const sent = await daemon.processes.sendMessage(conv.agentId, message.trim());
903
+ if (sent) {
904
+ daemon.audit.log('conversation.message', { id: req.params.id, agentId: conv.agentId });
905
+ return res.json({ id: conv.agentId, status: 'message_sent' });
906
+ }
907
+ }
908
+
909
+ // One-shot providers: kill and respawn with the message as prompt
910
+ const provider = getProvider(agent.provider);
911
+ if (provider?.constructor?.isOneShot) {
912
+ const oldConfig = { ...agent };
913
+ if (daemon.processes.isRunning(conv.agentId)) {
914
+ await daemon.processes.kill(conv.agentId);
915
+ }
916
+ daemon.registry.remove(conv.agentId);
917
+ daemon.locks.release(conv.agentId);
918
+
919
+ const newAgent = await daemon.processes.spawn({
920
+ role: 'chat',
921
+ scope: oldConfig.scope,
922
+ provider: oldConfig.provider,
923
+ model: oldConfig.model,
924
+ prompt: message.trim(),
925
+ permission: oldConfig.permission || 'full',
926
+ workingDir: oldConfig.workingDir,
927
+ name: oldConfig.name,
928
+ teamId: oldConfig.teamId,
929
+ });
930
+
931
+ // Update conversation to point to new agent
932
+ const convObj = daemon.conversations.conversations.get(req.params.id);
933
+ if (convObj) {
934
+ convObj.agentId = newAgent.id;
935
+ daemon.conversations._save();
936
+ }
937
+ daemon.audit.log('conversation.message', { id: req.params.id, agentId: newAgent.id, oneShot: true });
938
+ return res.json({ id: newAgent.id, status: 'respawned' });
939
+ }
940
+
941
+ // Running CLI agent — queue the message
942
+ if (daemon.processes.isRunning(conv.agentId)) {
943
+ daemon.processes.queueMessage(conv.agentId, message.trim());
944
+ daemon.audit.log('conversation.message', { id: req.params.id, agentId: conv.agentId, queued: true });
945
+ return res.json({ id: conv.agentId, status: 'message_queued' });
946
+ }
947
+
948
+ // CLI agent — session resume or rotation
949
+ const SESSION_RESUME_CEILING = 5_000_000;
950
+ const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
951
+ const newAgent = resumed
952
+ ? await daemon.processes.resume(conv.agentId, message.trim())
953
+ : await daemon.rotator.rotate(conv.agentId, { additionalPrompt: message.trim() });
954
+
955
+ // Update conversation to point to new agent if rotated
956
+ if (newAgent.id !== conv.agentId) {
957
+ const convObj = daemon.conversations.conversations.get(req.params.id);
958
+ if (convObj) {
959
+ convObj.agentId = newAgent.id;
960
+ daemon.conversations._save();
961
+ }
962
+ }
963
+
964
+ daemon.audit.log('conversation.message', { id: req.params.id, agentId: newAgent.id, resumed });
965
+ res.json({ id: newAgent.id, status: resumed ? 'resumed' : 'rotated' });
966
+ } catch (err) {
967
+ res.status(400).json({ error: err.message });
968
+ }
969
+ });
970
+
971
+ app.post('/api/conversations/:id/stop', (req, res) => {
972
+ try {
973
+ const conv = daemon.conversations.get(req.params.id);
974
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
975
+ daemon.conversations.stopStreaming(req.params.id);
976
+ res.json({ ok: true });
977
+ } catch (err) {
978
+ res.status(400).json({ error: err.message });
979
+ }
980
+ });
981
+
813
982
  // --- Approvals ---
814
983
 
815
984
  app.get('/api/approvals', (req, res) => {