groove-dev 0.27.196 → 0.27.198

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 (106) hide show
  1. package/axom-integration/Screenshot_2026-07-25_at_5.25.30_PM.png +0 -0
  2. package/axom-integration/Screenshot_2026-07-25_at_5.33.26_PM.png +0 -0
  3. package/axom-integration/Screenshot_2026-07-25_at_6.11.53_PM.png +0 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/api.js +5 -5
  7. package/node_modules/@groove-dev/daemon/src/axom-connector.js +340 -0
  8. package/node_modules/@groove-dev/daemon/src/axom-install.js +140 -0
  9. package/node_modules/@groove-dev/daemon/src/axom-server.js +229 -0
  10. package/node_modules/@groove-dev/daemon/src/chatstore.js +104 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +6 -0
  12. package/node_modules/@groove-dev/daemon/src/index.js +14 -2
  13. package/node_modules/@groove-dev/daemon/src/innerchat-docs.js +8 -1
  14. package/node_modules/@groove-dev/daemon/src/innerchat-relay.js +89 -0
  15. package/node_modules/@groove-dev/daemon/src/innerchat.js +261 -1
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  17. package/node_modules/@groove-dev/daemon/src/network-guard.js +56 -0
  18. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  19. package/node_modules/@groove-dev/daemon/src/providers/axom.js +66 -0
  20. package/node_modules/@groove-dev/daemon/src/providers/index.js +2 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/agents.js +1 -175
  22. package/node_modules/@groove-dev/daemon/src/routes/axom.js +196 -0
  23. package/node_modules/@groove-dev/daemon/src/routes/chat-history.js +34 -0
  24. package/node_modules/@groove-dev/daemon/src/routes/innerchat.js +152 -20
  25. package/node_modules/@groove-dev/daemon/test/axom-connector.test.js +412 -0
  26. package/node_modules/@groove-dev/daemon/test/axom-server.test.js +187 -0
  27. package/node_modules/@groove-dev/daemon/test/chatstore.test.js +86 -0
  28. package/node_modules/@groove-dev/daemon/test/innerchat-relay.test.js +251 -0
  29. package/node_modules/@groove-dev/gui/dist/assets/index-RbtaI6l7.css +1 -0
  30. package/node_modules/@groove-dev/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  31. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  32. package/node_modules/@groove-dev/gui/package.json +1 -1
  33. package/node_modules/@groove-dev/gui/src/App.jsx +2 -2
  34. package/node_modules/@groove-dev/gui/src/app.css +53 -0
  35. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +2 -2
  36. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +3 -3
  37. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  38. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  39. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +1 -1
  40. package/node_modules/@groove-dev/gui/src/stores/groove.js +24 -163
  41. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +52 -2
  42. package/node_modules/@groove-dev/gui/src/stores/slices/axom-slice.js +197 -0
  43. package/node_modules/@groove-dev/gui/src/views/axom.jsx +1060 -0
  44. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/views/settings.jsx +178 -64
  46. package/package.json +2 -2
  47. package/packages/cli/package.json +1 -1
  48. package/packages/daemon/package.json +1 -1
  49. package/packages/daemon/src/api.js +5 -5
  50. package/packages/daemon/src/axom-connector.js +340 -0
  51. package/packages/daemon/src/axom-install.js +140 -0
  52. package/packages/daemon/src/axom-server.js +229 -0
  53. package/packages/daemon/src/chatstore.js +104 -0
  54. package/packages/daemon/src/federation.js +6 -0
  55. package/packages/daemon/src/index.js +14 -2
  56. package/packages/daemon/src/innerchat-docs.js +8 -1
  57. package/packages/daemon/src/innerchat-relay.js +89 -0
  58. package/packages/daemon/src/innerchat.js +261 -1
  59. package/packages/daemon/src/introducer.js +1 -1
  60. package/packages/daemon/src/network-guard.js +56 -0
  61. package/packages/daemon/src/process.js +2 -2
  62. package/packages/daemon/src/providers/axom.js +66 -0
  63. package/packages/daemon/src/providers/index.js +2 -0
  64. package/packages/daemon/src/routes/agents.js +1 -175
  65. package/packages/daemon/src/routes/axom.js +196 -0
  66. package/packages/daemon/src/routes/chat-history.js +34 -0
  67. package/packages/daemon/src/routes/innerchat.js +152 -20
  68. package/packages/gui/dist/assets/index-RbtaI6l7.css +1 -0
  69. package/packages/gui/dist/assets/index-b9dKN6cq.js +1074 -0
  70. package/packages/gui/dist/index.html +2 -2
  71. package/packages/gui/package.json +1 -1
  72. package/packages/gui/src/App.jsx +2 -2
  73. package/packages/gui/src/app.css +53 -0
  74. package/packages/gui/src/components/agents/agent-config.jsx +2 -2
  75. package/packages/gui/src/components/agents/spawn-wizard.jsx +3 -3
  76. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  77. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +1 -1
  78. package/packages/gui/src/components/layout/command-palette.jsx +1 -1
  79. package/packages/gui/src/stores/groove.js +24 -163
  80. package/packages/gui/src/stores/slices/agents-slice.js +52 -2
  81. package/packages/gui/src/stores/slices/axom-slice.js +197 -0
  82. package/packages/gui/src/views/axom.jsx +1060 -0
  83. package/packages/gui/src/views/marketplace.jsx +1 -1
  84. package/packages/gui/src/views/settings.jsx +178 -64
  85. package/node_modules/@groove-dev/daemon/src/conversations.js +0 -576
  86. package/node_modules/@groove-dev/gui/dist/assets/index-Cat5pJUx.css +0 -1
  87. package/node_modules/@groove-dev/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  88. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +0 -198
  89. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +0 -367
  90. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +0 -527
  91. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +0 -212
  92. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +0 -163
  93. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +0 -201
  94. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +0 -227
  95. package/node_modules/@groove-dev/gui/src/views/chat.jsx +0 -6
  96. package/packages/daemon/src/conversations.js +0 -576
  97. package/packages/gui/dist/assets/index-Cat5pJUx.css +0 -1
  98. package/packages/gui/dist/assets/index-L8xSnvHj.js +0 -1070
  99. package/packages/gui/src/components/chat/chat-header.jsx +0 -198
  100. package/packages/gui/src/components/chat/chat-input.jsx +0 -367
  101. package/packages/gui/src/components/chat/chat-messages.jsx +0 -527
  102. package/packages/gui/src/components/chat/chat-view.jsx +0 -212
  103. package/packages/gui/src/components/chat/conversation-list.jsx +0 -163
  104. package/packages/gui/src/components/chat/model-picker.jsx +0 -201
  105. package/packages/gui/src/stores/slices/chat-slice.js +0 -227
  106. package/packages/gui/src/views/chat.jsx +0 -6
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-L8xSnvHj.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-b9dKN6cq.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-Cat5pJUx.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-RbtaI6l7.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.196",
3
+ "version": "0.27.198",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -22,10 +22,10 @@ import ModelsView from './views/models';
22
22
  import FederationView from './views/federation';
23
23
  import ModelLabView from './views/model-lab';
24
24
  import NetworkView from './views/network';
25
- import ChatView from './views/chat';
26
25
  import MemoryView from './views/memory';
27
26
  import FleetView from './views/fleet';
28
27
  import AutoAgentsView from './views/auto-agents';
28
+ import AxomView from './views/axom';
29
29
 
30
30
  // Agent components
31
31
  import { AgentPanel } from './components/agents/agent-panel';
@@ -79,8 +79,8 @@ function ViewRouter() {
79
79
  case 'model-lab': content = <ModelLabView />; break;
80
80
  case 'federation': content = <FederationView />; break;
81
81
  case 'settings': content = <SettingsView />; break;
82
- case 'chat': content = <ChatView />; break;
83
82
  case 'fleet': content = <FleetView />; break;
83
+ case 'axom': content = <AxomView />; break;
84
84
  case 'memory': content = <MemoryView />; break;
85
85
  case 'network': content = networkUnlocked ? <NetworkView /> : <AgentsView />; break;
86
86
  default: content = <AgentsView />;
@@ -309,6 +309,59 @@ html {
309
309
  box-shadow: 0 0 60px rgba(51,175,188,0.15), 0 0 120px rgba(51,175,188,0.04);
310
310
  }
311
311
 
312
+ /* ── Axom Onboarding ─────────────────────────────────────── */
313
+
314
+ /* Front door backdrop — one cool bloom behind the mark on the identity pane,
315
+ a faint cold wash under the operations pane. Nothing else. */
316
+ .axom-hero-bg {
317
+ background:
318
+ radial-gradient(ellipse 760px 620px at 20% 38%, rgba(51, 175, 188, 0.11) 0%, transparent 68%),
319
+ radial-gradient(ellipse 900px 700px at 88% 62%, rgba(122, 176, 223, 0.045) 0%, transparent 70%),
320
+ var(--color-surface-1);
321
+ }
322
+
323
+ /* Panels read as instrument faces: light at the top edge, settling downward. */
324
+ .axom-panel {
325
+ background: linear-gradient(180deg, var(--color-surface-3) 0%, var(--color-surface-1) 100%);
326
+ }
327
+
328
+ /* Hairline that carries the accent only where it matters — a top edge. */
329
+ .axom-edge-accent {
330
+ background: linear-gradient(90deg, transparent 0%, rgba(51, 175, 188, 0.6) 50%, transparent 100%);
331
+ }
332
+ .axom-edge-muted {
333
+ background: linear-gradient(90deg, transparent 0%, var(--color-border) 50%, transparent 100%);
334
+ }
335
+
336
+ /* Display type — a soft vertical falloff, no color, just depth. */
337
+ .axom-display {
338
+ background: linear-gradient(180deg, #f2f4f7 0%, #9aa3b0 100%);
339
+ -webkit-background-clip: text;
340
+ background-clip: text;
341
+ color: transparent;
342
+ }
343
+
344
+ .axom-ring {
345
+ border-radius: 50%;
346
+ border: 1px solid rgba(51, 175, 188, 0.22);
347
+ }
348
+
349
+ @keyframes axom-orbit { to { transform: rotate(360deg); } }
350
+ .animate-axom-orbit { animation: axom-orbit 26s linear infinite; }
351
+ .animate-axom-orbit-slow { animation: axom-orbit 44s linear infinite reverse; }
352
+
353
+ @keyframes axom-breathe {
354
+ 0%, 100% { opacity: 0.45; }
355
+ 50% { opacity: 0.85; }
356
+ }
357
+ .animate-axom-breathe { animation: axom-breathe 6s ease-in-out infinite; }
358
+
359
+ @media (prefers-reduced-motion: reduce) {
360
+ .animate-axom-orbit,
361
+ .animate-axom-orbit-slow,
362
+ .animate-axom-breathe { animation: none; }
363
+ }
364
+
312
365
  /* ── React Flow Overrides ─────────────────────────────────── */
313
366
 
314
367
  /* Suppress node fly-in animation — nodes appear in position instantly */
@@ -704,7 +704,7 @@ export function AgentConfig({ agent }) {
704
704
  </div>
705
705
  )}
706
706
  {(agent.integrations || []).length === 0 && installedIntegrations.length === 0 && (
707
- <span className="text-2xs text-text-4 font-sans">No integrations installed — browse the Marketplace</span>
707
+ <span className="text-2xs text-text-4 font-sans">No integrations installed — browse the Integrations tab</span>
708
708
  )}
709
709
  </div>
710
710
  {(agent.integrations || []).length > 0 && (
@@ -775,7 +775,7 @@ export function AgentConfig({ agent }) {
775
775
  </div>
776
776
  )}
777
777
  {(agent.repos || []).length === 0 && (
778
- <span className="text-2xs text-text-4 font-sans">No repos attached — import one from the Marketplace</span>
778
+ <span className="text-2xs text-text-4 font-sans">No repos attached — import one from the Integrations tab</span>
779
779
  )}
780
780
  </div>
781
781
  </ConfigSection>
@@ -368,7 +368,7 @@ export function SpawnWizard() {
368
368
  }}
369
369
  className="ml-auto text-2xs text-accent hover:underline font-sans cursor-pointer"
370
370
  >
371
- Install in Marketplace
371
+ Install in Integrations
372
372
  </button>
373
373
  </div>
374
374
  );
@@ -778,7 +778,7 @@ export function SpawnWizard() {
778
778
  <div className="text-2xs text-text-3 font-sans truncate">{integration.description}</div>
779
779
  )}
780
780
  {!configured && (
781
- <div className="text-2xs text-text-4 font-sans">Configure in Marketplace</div>
781
+ <div className="text-2xs text-text-4 font-sans">Configure in Integrations</div>
782
782
  )}
783
783
  </div>
784
784
  {active && <CheckMark />}
@@ -787,7 +787,7 @@ export function SpawnWizard() {
787
787
  })}
788
788
  {installedIntegrations.length === 0 && (
789
789
  <div className="text-center py-6 text-xs text-text-3 font-sans">
790
- No integrations installed. Visit the Marketplace to install integrations.
790
+ No integrations installed. Visit the Integrations tab to install some.
791
791
  </div>
792
792
  )}
793
793
  </div>
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, MessageCircle, BookOpen, LayoutList, Bot } from 'lucide-react';
2
+ import { Network, Code2, ChartSpline, Puzzle, Users, Box, FlaskConical, Newspaper, Settings, Globe, BookOpen, LayoutList, Bot, Atom } from 'lucide-react';
3
3
  import { cn } from '../../lib/cn';
4
4
  import { Tooltip } from '../ui/tooltip';
5
5
  import { useGrooveStore } from '../../stores/groove';
@@ -7,14 +7,14 @@ import { isElectron, getPlatform } from '../../lib/electron';
7
7
 
8
8
  const BASE_NAV_ITEMS = [
9
9
  { id: 'agents', icon: Network, label: 'Agents' },
10
+ { id: 'axom', icon: Atom, label: 'Axom' },
10
11
  { id: 'fleet', icon: LayoutList, label: 'Fleet' },
11
- { id: 'chat', icon: MessageCircle, label: 'Chat' },
12
12
  { id: 'editor', icon: Code2, label: 'Editor' },
13
13
  { id: 'dashboard', icon: ChartSpline, label: 'Dashboard' },
14
14
  { id: 'memory', icon: BookOpen, label: 'Memory' },
15
15
  { id: 'auto-agents', icon: Bot, label: 'Auto Agents' },
16
16
  { id: 'teams', icon: Users, label: 'Teams' },
17
- { id: 'marketplace', icon: Puzzle, label: 'Marketplace' },
17
+ { id: 'marketplace', icon: Puzzle, label: 'Integrations' },
18
18
  { id: 'models', icon: Box, label: 'Models' },
19
19
  { id: 'model-lab', icon: FlaskConical, label: 'Model Lab' },
20
20
  ];
@@ -115,7 +115,7 @@ const VIEW_LABELS = {
115
115
  agents: 'Agents',
116
116
  editor: 'Editor',
117
117
  dashboard: 'Dashboard',
118
- marketplace: 'Marketplace',
118
+ marketplace: 'Integrations',
119
119
  teams: 'Teams',
120
120
  };
121
121
 
@@ -13,7 +13,7 @@ const STATIC_COMMANDS = [
13
13
  { id: 'nav:agents', label: 'Go to Agents', icon: Network, category: 'Navigation', action: (s) => { s.setActiveView('agents'); } },
14
14
  { id: 'nav:editor', label: 'Go to Editor', icon: Code2, category: 'Navigation', action: (s) => { s.setActiveView('editor'); } },
15
15
  { id: 'nav:dashboard', label: 'Go to Dashboard', icon: ChartSpline, category: 'Navigation', action: (s) => { s.setActiveView('dashboard'); } },
16
- { id: 'nav:marketplace', label: 'Go to Marketplace', icon: Puzzle, category: 'Navigation', action: (s) => { s.setActiveView('marketplace'); } },
16
+ { id: 'nav:marketplace', label: 'Go to Integrations', icon: Puzzle, category: 'Navigation', action: (s) => { s.setActiveView('marketplace'); } },
17
17
  { id: 'nav:teams', label: 'Go to Teams', icon: Users, category: 'Navigation', action: (s) => { s.setActiveView('teams'); } },
18
18
  { id: 'nav:federation', label: 'Go to Federation', icon: Globe, category: 'Navigation', action: (s) => { s.setActiveView('federation'); } },
19
19
  { id: 'action:whitelist', label: 'Add to Federation Whitelist', icon: Shield, category: 'Federation', action: (s) => { s.setActiveView('federation'); } },
@@ -7,7 +7,6 @@ import { persistJSON, persistChatHistory } from './helpers.js';
7
7
  import { createUiSlice } from './slices/ui-slice.js';
8
8
  import { createAgentsSlice } from './slices/agents-slice.js';
9
9
  import { createTeamsSlice } from './slices/teams-slice.js';
10
- import { createChatSlice } from './slices/chat-slice.js';
11
10
  import { createEditorSlice } from './slices/editor-slice.js';
12
11
  import { createProvidersSlice } from './slices/providers-slice.js';
13
12
  import { createNetworkSlice } from './slices/network-slice.js';
@@ -15,6 +14,7 @@ import { createPreviewSlice } from './slices/preview-slice.js';
15
14
  import { createMarketplaceSlice } from './slices/marketplace-slice.js';
16
15
  import { createAutomationsSlice } from './slices/automations-slice.js';
17
16
  import { createAutoAgentsSlice } from './slices/auto-agents-slice.js';
17
+ import { createAxomSlice } from './slices/axom-slice.js';
18
18
 
19
19
  const WS_URL = `ws://${window.location.hostname}:${window.location.port || 31415}`;
20
20
 
@@ -33,7 +33,6 @@ export const useGrooveStore = create((set, get) => ({
33
33
  ...createUiSlice(set, get),
34
34
  ...createAgentsSlice(set, get),
35
35
  ...createTeamsSlice(set, get),
36
- ...createChatSlice(set, get),
37
36
  ...createEditorSlice(set, get),
38
37
  ...createProvidersSlice(set, get),
39
38
  ...createNetworkSlice(set, get),
@@ -41,6 +40,7 @@ export const useGrooveStore = create((set, get) => ({
41
40
  ...createMarketplaceSlice(set, get),
42
41
  ...createAutomationsSlice(set, get),
43
42
  ...createAutoAgentsSlice(set, get),
43
+ ...createAxomSlice(set, get),
44
44
 
45
45
  // ── Connection ────────────────────────────────────────────
46
46
  connected: false,
@@ -69,12 +69,13 @@ export const useGrooveStore = create((set, get) => ({
69
69
  if (isTunneled) get().fetchProjectDir();
70
70
  }).catch(() => {});
71
71
  get().fetchTeams();
72
- get().fetchConversations();
72
+ get().fetchChatHistory();
73
73
  get().fetchApprovals();
74
74
  get().checkMarketplaceAuth();
75
75
  get().fetchTunnels();
76
76
  get().fetchBetaStatus();
77
77
  get().fetchNetworkInstallStatus();
78
+ get().fetchAxomStatus();
78
79
  get().fetchTrainingStatus();
79
80
  api.get('/config').then((cfg) => {
80
81
  if (cfg?.dataSharingDismissed) set({ dataSharingDismissed: true });
@@ -266,27 +267,6 @@ export const useGrooveStore = create((set, get) => ({
266
267
  set({ chatHistory: history });
267
268
  persistChatHistory(history);
268
269
  }
269
-
270
- const conv = get().conversations.find((c) => c.agentId === agentId);
271
- if (conv) {
272
- const convMsgs = { ...get().conversationMessages };
273
- if (!convMsgs[conv.id]) convMsgs[conv.id] = [];
274
- const convArr = [...convMsgs[conv.id]];
275
- const lastConv = convArr[convArr.length - 1];
276
- const isRecentConv = lastConv && lastConv.from === 'assistant' && (Date.now() - lastConv.timestamp) < 8000;
277
- const isConvDupe = isRecentConv && (lastConv.text === trimmed || lastConv.text.endsWith(trimmed));
278
- if (!isConvDupe) {
279
- if (isRecentConv) {
280
- const sep = data.subtype === 'assistant' ? '\n\n' : ' ';
281
- convArr[convArr.length - 1] = { ...lastConv, text: lastConv.text + sep + trimmed, timestamp: Date.now() };
282
- } else {
283
- convArr.push({ from: 'assistant', text: trimmed, timestamp: Date.now() });
284
- }
285
- convMsgs[conv.id] = convArr.slice(-200);
286
- set({ conversationMessages: convMsgs, streamingConversationId: conv.id });
287
- persistJSON('groove:conversationMessages', convMsgs);
288
- }
289
- }
290
270
  }
291
271
 
292
272
  if (activityText && activityText.trim()) {
@@ -347,11 +327,6 @@ export const useGrooveStore = create((set, get) => ({
347
327
  });
348
328
  }
349
329
 
350
- const exitConv = get().conversations.find((c) => c.agentId === msg.agentId);
351
- if (exitConv && get().streamingConversationId === exitConv.id) {
352
- set({ sendingMessage: false, streamingConversationId: null });
353
- }
354
-
355
330
  if (msg.error && msg.agentId) {
356
331
  get().addChatMessage(msg.agentId, 'system', `Crashed: ${msg.error}`);
357
332
  }
@@ -385,8 +360,10 @@ export const useGrooveStore = create((set, get) => ({
385
360
  isQuery: false,
386
361
  innerchat: { turnId: turn.id, threadId: thread.id, kind: turn.kind, peer, direction },
387
362
  });
363
+ const pushed = [];
388
364
  const push = (agentId, item) => {
389
365
  history[agentId] = [...(history[agentId] || []), item].slice(-100);
366
+ pushed.push([agentId, item]);
390
367
  };
391
368
 
392
369
  if (turn.kind === 'ask') {
@@ -403,6 +380,8 @@ export const useGrooveStore = create((set, get) => ({
403
380
  }
404
381
 
405
382
  persistChatHistory(history);
383
+ // Mirror to the daemon so these survive an origin/port change too.
384
+ for (const [agentId, item] of pushed) get().persistMessageRemote(agentId, item);
406
385
 
407
386
  const answers = { ...s.innerchatAnswers };
408
387
  if (turn.kind === 'answer') {
@@ -677,6 +656,22 @@ export const useGrooveStore = create((set, get) => ({
677
656
  set({ gateways: msg.data || [] });
678
657
  break;
679
658
 
659
+ case 'axom:status':
660
+ set({ axomStatus: msg.data || { endpoints: [] } });
661
+ break;
662
+
663
+ case 'axom:event':
664
+ get().ingestAxomEvent(msg.endpoint, msg.session, msg.envelope);
665
+ break;
666
+
667
+ case 'axom:instances':
668
+ set({ axomInstances: msg.data || [] });
669
+ break;
670
+
671
+ case 'axom:install:progress':
672
+ set({ axomInstall: msg.data });
673
+ break;
674
+
680
675
  case 'provider:status-changed':
681
676
  set({ _providerRefreshTick: Date.now() });
682
677
  break;
@@ -967,140 +962,6 @@ export const useGrooveStore = create((set, get) => ({
967
962
  get().fetchNetworkInstallStatus();
968
963
  break;
969
964
 
970
- case 'conversation:created': {
971
- const conv = msg.data;
972
- if (conv) set((s) => ({ conversations: [conv, ...s.conversations.filter((c) => c.id !== conv.id)] }));
973
- break;
974
- }
975
-
976
- case 'conversation:updated': {
977
- const conv = msg.data;
978
- if (conv) set((s) => ({ conversations: s.conversations.map((c) => c.id === conv.id ? { ...c, ...conv } : c) }));
979
- break;
980
- }
981
-
982
- case 'conversation:deleted': {
983
- const id = msg.data?.id || msg.id;
984
- if (id) {
985
- set((s) => {
986
- const conversations = s.conversations.filter((c) => c.id !== id);
987
- const conversationMessages = { ...s.conversationMessages };
988
- delete conversationMessages[id];
989
- const activeConversationId = s.activeConversationId === id ? null : s.activeConversationId;
990
- if (activeConversationId !== s.activeConversationId) localStorage.setItem('groove:activeConversationId', '');
991
- return { conversations, conversationMessages, activeConversationId };
992
- });
993
- }
994
- break;
995
- }
996
-
997
- case 'conversation:tool': {
998
- const { conversationId, name, summary } = msg.data || msg;
999
- if (!conversationId) break;
1000
- set((s) => {
1001
- const tools = { ...s.conversationActiveTools };
1002
- tools[conversationId] = { name: name || 'Tool', summary: summary || null, timestamp: Date.now() };
1003
- return { conversationActiveTools: tools };
1004
- });
1005
- break;
1006
- }
1007
-
1008
- case 'conversation:chunk': {
1009
- const { conversationId, text } = msg.data || msg;
1010
- if (!conversationId || !text) break;
1011
- set((s) => {
1012
- const msgs = { ...s.conversationMessages };
1013
- if (!msgs[conversationId]) msgs[conversationId] = [];
1014
- const arr = [...msgs[conversationId]];
1015
- const last = arr[arr.length - 1];
1016
- if (last && last.from === 'assistant' && (Date.now() - last.timestamp) < 30000) {
1017
- arr[arr.length - 1] = { ...last, text: last.text + text, timestamp: Date.now() };
1018
- } else {
1019
- arr.push({ from: 'assistant', text, timestamp: Date.now() });
1020
- }
1021
- msgs[conversationId] = arr.slice(-200);
1022
- const tools = { ...s.conversationActiveTools };
1023
- delete tools[conversationId];
1024
- return { conversationMessages: msgs, streamingConversationId: conversationId, conversationActiveTools: tools };
1025
- });
1026
- break;
1027
- }
1028
-
1029
- case 'conversation:complete': {
1030
- const { conversationId } = msg.data || msg;
1031
- if (conversationId && get().streamingConversationId === conversationId) {
1032
- const tools = { ...get().conversationActiveTools };
1033
- delete tools[conversationId];
1034
- set({ sendingMessage: false, streamingConversationId: null, conversationActiveTools: tools });
1035
- }
1036
- if (conversationId) persistJSON('groove:conversationMessages', get().conversationMessages);
1037
- break;
1038
- }
1039
-
1040
- case 'conversation:image': {
1041
- const { conversationId, prompt, url, b64_json, mimeType, model: imgModel, provider: imgProvider } = msg.data || msg;
1042
- if (!conversationId) break;
1043
- const imageUrl = url || (b64_json ? `data:${mimeType || 'image/png'};base64,${b64_json}` : null);
1044
- set((s) => {
1045
- const msgs = { ...s.conversationMessages };
1046
- if (!msgs[conversationId]) msgs[conversationId] = [];
1047
- const arr = [...msgs[conversationId]];
1048
- const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading' && m.prompt === prompt);
1049
- if (loadingIdx >= 0) {
1050
- arr[loadingIdx] = { from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() };
1051
- } else {
1052
- arr.push({ from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() });
1053
- }
1054
- msgs[conversationId] = arr.slice(-200);
1055
- persistJSON('groove:conversationMessages', msgs);
1056
- const isActive = s.streamingConversationId === conversationId;
1057
- return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
1058
- });
1059
- break;
1060
- }
1061
-
1062
- case 'conversation:image-progress': {
1063
- const { conversationId, status, prompt: imgPrompt, error: imgError } = msg.data || msg;
1064
- if (!conversationId) break;
1065
- if (status === 'generating') {
1066
- set((s) => {
1067
- const msgs = { ...s.conversationMessages };
1068
- if (!msgs[conversationId]) msgs[conversationId] = [];
1069
- msgs[conversationId] = [...msgs[conversationId], { from: 'assistant', type: 'image-loading', prompt: imgPrompt, timestamp: Date.now() }];
1070
- return { conversationMessages: msgs, streamingConversationId: conversationId };
1071
- });
1072
- } else if (status === 'error') {
1073
- set((s) => {
1074
- const msgs = { ...s.conversationMessages };
1075
- if (!msgs[conversationId]) msgs[conversationId] = [];
1076
- const arr = [...msgs[conversationId]];
1077
- const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading');
1078
- if (loadingIdx >= 0) arr.splice(loadingIdx, 1);
1079
- arr.push({ from: 'system', text: `Image generation failed: ${imgError || 'Unknown error'}`, timestamp: Date.now() });
1080
- msgs[conversationId] = arr;
1081
- persistJSON('groove:conversationMessages', msgs);
1082
- const isActive = s.streamingConversationId === conversationId;
1083
- return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
1084
- });
1085
- }
1086
- break;
1087
- }
1088
-
1089
- case 'conversation:error': {
1090
- const { conversationId, error } = msg.data || msg;
1091
- if (conversationId) {
1092
- set((s) => {
1093
- const msgs = { ...s.conversationMessages };
1094
- if (!msgs[conversationId]) msgs[conversationId] = [];
1095
- msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Error: ${error || 'Unknown error'}`, timestamp: Date.now() }];
1096
- persistJSON('groove:conversationMessages', msgs);
1097
- const isActive = s.streamingConversationId === conversationId;
1098
- return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
1099
- });
1100
- }
1101
- break;
1102
- }
1103
-
1104
965
  case 'network:token:timing': {
1105
966
  const { __proto__: _a, constructor: _b, prototype: _c, ...td } = msg.data || {};
1106
967
  const updates = {
@@ -89,6 +89,7 @@ export const createAgentsSlice = (set, get) => ({
89
89
  persistJSON('groove:activityLog', activityLog);
90
90
  return { chatHistory, activityLog, tokenTimeline };
91
91
  });
92
+ api.delete(`/chat-history/${encodeURIComponent(id)}`).catch(() => {});
92
93
  }
93
94
  } catch (err) {
94
95
  get().addToast('error', 'Kill failed', err.message);
@@ -166,15 +167,64 @@ export const createAgentsSlice = (set, get) => ({
166
167
  // ── Chat ──────────────────────────────────────────────────
167
168
 
168
169
  addChatMessage(agentId, from, text, isQuery = false, attachments = undefined) {
170
+ const msg = { from, text, timestamp: Date.now(), isQuery };
171
+ if (attachments?.length) msg.attachments = attachments;
169
172
  set((s) => {
170
173
  const history = { ...s.chatHistory };
171
174
  if (!history[agentId]) history[agentId] = [];
172
- const msg = { from, text, timestamp: Date.now(), isQuery };
173
- if (attachments?.length) msg.attachments = attachments;
174
175
  history[agentId] = [...history[agentId].slice(-100), msg];
175
176
  persistChatHistory(history);
176
177
  return { chatHistory: history };
177
178
  });
179
+ get().persistMessageRemote(agentId, msg);
180
+ },
181
+
182
+ // Persist one message to the daemon so history survives on the server, not
183
+ // just in this browser's per-origin localStorage. Attachment payloads are
184
+ // stripped; the daemon keeps metadata only.
185
+ persistMessageRemote(agentId, msg) {
186
+ if (!agentId || !msg) return;
187
+ const clean = msg.attachments?.length
188
+ ? { ...msg, attachments: msg.attachments.map(({ dataUrl, ...rest }) => rest) }
189
+ : msg;
190
+ api.post(`/chat-history/${encodeURIComponent(agentId)}`, { message: clean }).catch(() => {});
191
+ },
192
+
193
+ // Load server-side history on (re)connect and merge it in — the daemon is the
194
+ // source of truth, so this restores chats regardless of which tunnel port
195
+ // (and thus browser origin) we came up on.
196
+ //
197
+ // Also runs a ONE-TIME, per-origin seed: whatever chat history is stranded in
198
+ // THIS origin's localStorage gets pushed to the daemon for any agent the
199
+ // daemon doesn't yet know. Visiting each old origin once migrates its history
200
+ // server-side without clobbering anything already there.
201
+ async fetchChatHistory() {
202
+ try {
203
+ const { history } = await api.get('/chat-history');
204
+ const remote = (history && typeof history === 'object') ? history : {};
205
+
206
+ if (localStorage.getItem('groove:chatImported') !== '1') {
207
+ const local = get().chatHistory;
208
+ for (const [agentId, msgs] of Object.entries(local)) {
209
+ // Only seed agents the daemon has nothing for — never overwrite
210
+ // server history with an older local copy.
211
+ if (Array.isArray(msgs) && msgs.length && !(remote[agentId]?.length)) {
212
+ api.put(`/chat-history/${encodeURIComponent(agentId)}`, { messages: msgs }).catch(() => {});
213
+ remote[agentId] = msgs; // reflect so the merge below keeps it
214
+ }
215
+ }
216
+ localStorage.setItem('groove:chatImported', '1');
217
+ }
218
+
219
+ set((s) => {
220
+ const merged = { ...s.chatHistory };
221
+ for (const [agentId, msgs] of Object.entries(remote)) {
222
+ if (Array.isArray(msgs) && msgs.length) merged[agentId] = msgs;
223
+ }
224
+ persistChatHistory(merged);
225
+ return { chatHistory: merged };
226
+ });
227
+ } catch { /* offline or old daemon without the endpoint — keep local */ }
178
228
  },
179
229
 
180
230
  async stopAgent(id) {