groove-dev 0.27.15 → 0.27.18

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 (172) hide show
  1. package/CLAUDE.md +0 -10
  2. package/README.md +37 -1
  3. package/developerID_application.cer +0 -0
  4. package/node_modules/@groove-dev/daemon/src/api.js +586 -67
  5. package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
  6. package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
  7. package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
  8. package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
  9. package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
  10. package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
  11. package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
  12. package/node_modules/@groove-dev/daemon/src/index.js +172 -19
  13. package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
  14. package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
  15. package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
  16. package/node_modules/@groove-dev/daemon/src/process.js +140 -23
  17. package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
  18. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +14 -0
  19. package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
  20. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
  21. package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
  22. package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
  23. package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
  24. package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
  25. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
  26. package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
  27. package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
  28. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
  29. package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
  30. package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
  31. package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
  32. package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
  33. package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
  34. package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
  35. package/node_modules/@groove-dev/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  37. package/node_modules/@groove-dev/gui/dist/index.html +3 -2
  38. package/node_modules/@groove-dev/gui/index.html +1 -0
  39. package/node_modules/@groove-dev/gui/src/app.css +7 -0
  40. package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
  45. package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
  46. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
  47. package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
  48. package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
  49. package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
  50. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
  51. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +15 -3
  52. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
  53. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  54. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
  55. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +11 -1
  56. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
  57. package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
  58. package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
  59. package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  60. package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
  61. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
  62. package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
  63. package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
  64. package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
  65. package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
  66. package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
  67. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
  68. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
  69. package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
  70. package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
  71. package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
  73. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
  74. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
  75. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
  76. package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
  77. package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
  78. package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
  79. package/node_modules/@groove-dev/gui/src/stores/groove.js +388 -63
  80. package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
  81. package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
  82. package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
  83. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
  84. package/node_modules/@groove-dev/gui/src/views/settings.jsx +35 -134
  85. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
  86. package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
  87. package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
  88. package/package.json +1 -1
  89. package/packages/daemon/src/api.js +586 -67
  90. package/packages/daemon/src/classifier.js +24 -0
  91. package/packages/daemon/src/credentials.js +12 -2
  92. package/packages/daemon/src/federation/ambassador.js +204 -0
  93. package/packages/daemon/src/federation/connection.js +359 -0
  94. package/packages/daemon/src/federation/contracts.js +112 -0
  95. package/packages/daemon/src/federation/whitelist.js +190 -0
  96. package/packages/daemon/src/federation.js +166 -7
  97. package/packages/daemon/src/index.js +172 -19
  98. package/packages/daemon/src/introducer.js +52 -7
  99. package/packages/daemon/src/journalist.js +46 -1
  100. package/packages/daemon/src/memory.js +36 -16
  101. package/packages/daemon/src/process.js +140 -23
  102. package/packages/daemon/src/providers/base.js +1 -0
  103. package/packages/daemon/src/providers/claude-code.js +14 -0
  104. package/packages/daemon/src/providers/codex.js +124 -28
  105. package/packages/daemon/src/providers/gemini.js +104 -17
  106. package/packages/daemon/src/providers/index.js +17 -0
  107. package/packages/daemon/src/registry.js +10 -1
  108. package/packages/daemon/src/rotator.js +93 -30
  109. package/packages/daemon/src/skills.js +33 -3
  110. package/packages/daemon/src/terminal-pty.js +9 -1
  111. package/packages/daemon/src/tool-executor.js +11 -5
  112. package/packages/daemon/src/toys.js +69 -0
  113. package/packages/daemon/src/tunnel-manager.js +24 -5
  114. package/packages/daemon/templates/toys-catalog.json +242 -0
  115. package/packages/gui/dist/assets/index-Bg6_D2xK.css +1 -0
  116. package/packages/gui/dist/assets/index-D3rvwTHD.js +8607 -0
  117. package/packages/gui/dist/index.html +3 -2
  118. package/packages/gui/index.html +1 -0
  119. package/packages/gui/src/app.css +7 -0
  120. package/packages/gui/src/app.jsx +37 -10
  121. package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
  122. package/packages/gui/src/components/agents/agent-config.jsx +11 -6
  123. package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
  124. package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
  125. package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
  126. package/packages/gui/src/components/editor/code-editor.jsx +33 -2
  127. package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
  128. package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
  129. package/packages/gui/src/components/editor/goto-line.jsx +35 -0
  130. package/packages/gui/src/components/editor/terminal.jsx +12 -6
  131. package/packages/gui/src/components/layout/activity-bar.jsx +15 -3
  132. package/packages/gui/src/components/layout/app-shell.jsx +0 -1
  133. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
  134. package/packages/gui/src/components/layout/command-palette.jsx +6 -2
  135. package/packages/gui/src/components/layout/status-bar.jsx +11 -1
  136. package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
  137. package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
  138. package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
  139. package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
  140. package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
  141. package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
  142. package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
  143. package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
  144. package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
  145. package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
  146. package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
  147. package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
  148. package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
  149. package/packages/gui/src/components/settings/server-detail.jsx +310 -0
  150. package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
  151. package/packages/gui/src/components/settings/server-list.jsx +59 -0
  152. package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
  153. package/packages/gui/src/components/toys/toy-card.jsx +78 -0
  154. package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
  155. package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
  156. package/packages/gui/src/components/ui/toast.jsx +2 -2
  157. package/packages/gui/src/lib/electron.js +15 -0
  158. package/packages/gui/src/lib/format.js +1 -0
  159. package/packages/gui/src/stores/groove.js +388 -63
  160. package/packages/gui/src/views/agents.jsx +148 -42
  161. package/packages/gui/src/views/editor.jsx +92 -2
  162. package/packages/gui/src/views/federation.jsx +37 -0
  163. package/packages/gui/src/views/marketplace.jsx +2 -42
  164. package/packages/gui/src/views/settings.jsx +35 -134
  165. package/packages/gui/src/views/subscription-panel.jsx +327 -0
  166. package/packages/gui/src/views/teams.jsx +3 -3
  167. package/packages/gui/src/views/toys.jsx +162 -0
  168. package/plans/chat-persistence-refactor.md +154 -0
  169. package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
  170. package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
  171. package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
  172. package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
@@ -2,15 +2,16 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
7
  <link rel="icon" type="image/png" href="/favicon.png" />
7
8
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-zdzOLAZM.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-D3rvwTHD.js"></script>
9
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-BE6lYcd7.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Bg6_D2xK.css">
14
15
  </head>
15
16
  <body>
16
17
  <div id="root"></div>
@@ -2,6 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; connect-src 'self' ws://localhost:* ws://127.0.0.1:* http://localhost:* http://127.0.0.1:*; font-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none';">
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
7
  <link rel="icon" type="image/png" href="/favicon.png" />
7
8
  <title>Groove GUI</title>
@@ -201,6 +201,13 @@ html {
201
201
  filter: drop-shadow(0 0 4px rgba(97, 175, 239, 0.4));
202
202
  }
203
203
 
204
+ /* ── Onboarding Wizard ───────────────────────────────────── */
205
+
206
+ @keyframes wizard-checkmark {
207
+ 0% { stroke-dashoffset: 24; }
208
+ 100% { stroke-dashoffset: 0; }
209
+ }
210
+
204
211
  /* ── Electron Desktop App ────────────────────────────────── */
205
212
 
206
213
  .electron-drag {
@@ -1,9 +1,12 @@
1
1
  // GROOVE GUI v2 — App Root
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useEffect } from 'react';
4
+ import React, { useEffect, useMemo } from 'react';
5
5
  import { useGrooveStore } from './stores/groove';
6
6
  import { AppShell } from './components/layout/app-shell';
7
+ import { SetupWizard } from './components/onboarding/setup-wizard';
8
+ import { useKeyboard } from './lib/hooks/use-keyboard';
9
+ import { UpgradeModal } from './components/pro/upgrade-modal';
7
10
 
8
11
  // Views
9
12
  import AgentsView from './views/agents';
@@ -13,6 +16,8 @@ import MarketplaceView from './views/marketplace';
13
16
  import TeamsView from './views/teams';
14
17
  import SettingsView from './views/settings';
15
18
  import ModelsView from './views/models';
19
+ import FederationView from './views/federation';
20
+ import ToysView from './views/toys';
16
21
 
17
22
  // Agent components
18
23
  import { AgentPanel } from './components/agents/agent-panel';
@@ -57,8 +62,10 @@ function ViewRouter() {
57
62
  case 'editor': content = <EditorView />; break;
58
63
  case 'dashboard': content = <DashboardView />; break;
59
64
  case 'marketplace': content = <MarketplaceView />; break;
65
+ case 'toys': content = <ToysView />; break;
60
66
  case 'teams': content = <TeamsView />; break;
61
67
  case 'models': content = <ModelsView />; break;
68
+ case 'federation': content = <FederationView />; break;
62
69
  case 'settings': content = <SettingsView />; break;
63
70
  default: content = <AgentsView />;
64
71
  }
@@ -100,25 +107,45 @@ export default function App() {
100
107
  const connect = useGrooveStore((s) => s.connect);
101
108
  const hydrated = useGrooveStore((s) => s.hydrated);
102
109
  const tunneled = useGrooveStore((s) => s.tunneled);
110
+ const onboardingComplete = useGrooveStore((s) => s.onboardingComplete);
103
111
  useEffect(() => { connect(); }, [connect]);
104
112
 
105
113
  useEffect(() => {
106
- const params = new URLSearchParams(window.location.search);
107
- const instance = params.get('instance');
108
- if (instance) {
109
- document.title = `${instance} — Groove`;
110
- } else if (tunneled) {
111
- document.title = 'Remote — Groove';
112
- } else {
113
- document.title = 'Groove';
114
+ async function setTitle() {
115
+ if (window.groove?.getInstanceInfo) {
116
+ const info = await window.groove.getInstanceInfo();
117
+ if (info?.name) {
118
+ document.title = `${info.name} Groove`;
119
+ return;
120
+ }
121
+ }
122
+ const params = new URLSearchParams(window.location.search);
123
+ const instance = params.get('instance');
124
+ if (instance) {
125
+ const sanitized = instance.replace(/[\x00-\x1F]/g, '').slice(0, 50);
126
+ document.title = `${sanitized} — Groove`;
127
+ } else if (tunneled) {
128
+ document.title = 'Remote — Groove';
129
+ } else {
130
+ document.title = 'Groove';
131
+ }
114
132
  }
133
+ setTitle();
115
134
  }, [tunneled]);
116
135
 
136
+ const openFolderShortcuts = useMemo(() =>
137
+ window.groove?.openFolder
138
+ ? [{ key: 'o', meta: true, handler: () => window.groove.openFolder() }]
139
+ : [],
140
+ []);
141
+ useKeyboard(openFolderShortcuts);
142
+
117
143
  if (!hydrated) return <LoadingScreen />;
118
144
 
119
145
  return (
120
146
  <ErrorBoundary>
121
- <ViewRouter />
147
+ {onboardingComplete ? <ViewRouter /> : <SetupWizard />}
148
+ <UpgradeModal />
122
149
  </ErrorBoundary>
123
150
  );
124
151
  }
@@ -105,7 +105,6 @@ export function AgentChat({ agent }) {
105
105
  const chatHistory = useGrooveStore((s) => s.chatHistory[agent.id]) || EMPTY;
106
106
  const activityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
107
107
  const instructAgent = useGrooveStore((s) => s.instructAgent);
108
- const queryAgent = useGrooveStore((s) => s.queryAgent);
109
108
  const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
110
109
 
111
110
  const storeInput = useGrooveStore((s) => s.chatInputs[agent.id] || '');
@@ -143,11 +142,7 @@ export function AgentChat({ agent }) {
143
142
  setAttachedFiles([]);
144
143
  setSending(true);
145
144
  try {
146
- if (text.startsWith('?')) {
147
- await queryAgent(agent.id, text.slice(1).trim());
148
- } else {
149
- await instructAgent(agent.id, text);
150
- }
145
+ await instructAgent(agent.id, text);
151
146
  } catch { /* toast handles */ }
152
147
  setSending(false);
153
148
  inputRef.current?.focus();
@@ -160,7 +155,6 @@ export function AgentChat({ agent }) {
160
155
  }
161
156
  }
162
157
 
163
- const isQuery = input.startsWith('?');
164
158
  const isAlive = agent.status === 'running' || agent.status === 'starting';
165
159
 
166
160
  // Build merged timeline
@@ -183,7 +177,7 @@ export function AgentChat({ agent }) {
183
177
  </p>
184
178
  <p className="text-xs text-text-3 font-sans mt-1 max-w-[240px]">
185
179
  {isAlive
186
- ? 'Send an instruction to guide this agent, or prefix with ? to query without disrupting its work.'
180
+ ? 'Send a message to guide this agent. Use the stop button to interrupt.'
187
181
  : 'Reply to continue the conversation — a new session starts with full context.'}
188
182
  </p>
189
183
  </div>
@@ -200,14 +194,11 @@ export function AgentChat({ agent }) {
200
194
  <div className="border-t border-border-subtle px-4 py-3 bg-surface-1">
201
195
  {/* Mode indicator */}
202
196
  <div className="flex items-center gap-2 mb-2">
203
- <span className={cn(
204
- 'text-2xs font-semibold font-sans px-2 py-0.5 rounded-full',
205
- isQuery ? 'bg-info/12 text-info' : 'bg-accent/12 text-accent',
206
- )}>
207
- {isQuery ? 'Query' : isAlive ? 'Instruct' : 'Continue'}
197
+ <span className="text-2xs font-semibold font-sans px-2 py-0.5 rounded-full bg-accent/12 text-accent">
198
+ {isAlive ? 'Instruct' : 'Continue'}
208
199
  </span>
209
200
  <span className="text-2xs text-text-4 font-sans">
210
- {isQuery ? 'Read-only question agent keeps running' : isAlive ? 'Guides the agent\'s next action' : 'Resumes with full context'}
201
+ {isAlive ? 'Message goes directly to this agent' : 'Resumes with full context'}
211
202
  </span>
212
203
  </div>
213
204
 
@@ -233,7 +224,7 @@ export function AgentChat({ agent }) {
233
224
  value={input}
234
225
  onChange={(e) => setInput(e.target.value)}
235
226
  onKeyDown={onKeyDown}
236
- placeholder={isAlive ? 'Instruct agent... (? to query)' : 'Continue conversation...'}
227
+ placeholder={isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
237
228
  rows={1}
238
229
  className={cn(
239
230
  'flex-1 resize-y rounded-xl px-4 py-2.5 text-sm',
@@ -241,10 +232,10 @@ export function AgentChat({ agent }) {
241
232
  'placeholder:text-text-4',
242
233
  'focus:outline-none focus:ring-1',
243
234
  'min-h-[40px]',
244
- isQuery ? 'border-info/30 focus:ring-info/40' : 'border-border focus:ring-accent/40',
235
+ 'border-border focus:ring-accent/40',
245
236
  )}
246
237
  />
247
- {isThinking ? (
238
+ {isAlive && (
248
239
  <button
249
240
  onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
250
241
  title="Stop agent"
@@ -252,21 +243,20 @@ export function AgentChat({ agent }) {
252
243
  >
253
244
  <Square size={14} fill="currentColor" />
254
245
  </button>
255
- ) : (
256
- <button
257
- onClick={handleSend}
258
- disabled={!input.trim() || sending}
259
- className={cn(
260
- 'w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer',
261
- 'disabled:opacity-20 disabled:cursor-not-allowed',
262
- input.trim()
263
- ? 'bg-accent text-surface-0 hover:bg-accent/90 shadow-lg shadow-accent/20'
264
- : 'bg-surface-4 text-text-4',
265
- )}
266
- >
267
- {sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
268
- </button>
269
246
  )}
247
+ <button
248
+ onClick={handleSend}
249
+ disabled={!input.trim() || sending}
250
+ className={cn(
251
+ 'w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer',
252
+ 'disabled:opacity-20 disabled:cursor-not-allowed',
253
+ input.trim()
254
+ ? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
255
+ : 'bg-surface-4 text-text-4',
256
+ )}
257
+ >
258
+ {sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
259
+ </button>
270
260
  </div>
271
261
  </div>
272
262
  </div>
@@ -79,7 +79,7 @@ function AgentActions({ agent }) {
79
79
  async function handleKill() {
80
80
  if (!confirmKill) { setConfirmKill(true); setTimeout(() => setConfirmKill(false), 3000); return; }
81
81
  setLoading('kill');
82
- try { await killAgent(agent.id); closeDetail(); } catch {}
82
+ try { await killAgent(agent.id, !isAlive); closeDetail(); } catch {}
83
83
  setLoading(null);
84
84
  setConfirmKill(false);
85
85
  }
@@ -189,13 +189,18 @@ export function AgentConfig({ agent }) {
189
189
 
190
190
  useEffect(() => {
191
191
  setPersonalityLoaded(false);
192
- api.get(`/personalities/${agent.name}`).then((data) => {
193
- setPersonalityContent(data?.content || '');
194
- setPersonalityLoaded(true);
195
- }).catch(() => {
192
+ if (agent.personality) {
193
+ api.get(`/personalities/${agent.name}`).then((data) => {
194
+ setPersonalityContent(data?.content || '');
195
+ setPersonalityLoaded(true);
196
+ }).catch(() => {
197
+ setPersonalityContent('');
198
+ setPersonalityLoaded(true);
199
+ });
200
+ } else {
196
201
  setPersonalityContent('');
197
202
  setPersonalityLoaded(true);
198
- });
203
+ }
199
204
  api.get('/personalities').then((data) => {
200
205
  setPersonalities(Array.isArray(data) ? data : data.personalities || []);
201
206
  }).catch(() => {});
@@ -755,8 +755,8 @@ export function AgentFeed({ agent }) {
755
755
  'disabled:opacity-15 disabled:cursor-not-allowed',
756
756
  input.trim()
757
757
  ? mode === 'query'
758
- ? 'bg-info text-white hover:bg-info/85'
759
- : 'bg-accent text-white hover:bg-accent/85'
758
+ ? 'bg-info/15 text-info hover:bg-info/25 border border-info/25'
759
+ : 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
760
760
  : 'bg-transparent text-text-4',
761
761
  )}
762
762
  >
@@ -11,7 +11,7 @@ import {
11
11
  Server, Monitor, Code2, TestTube, Cloud, FileText,
12
12
  Shield, Database, Megaphone, Calculator, UserCheck,
13
13
  Headphones, BarChart3, Rocket, ChevronDown, Pen, Presentation,
14
- Sparkles, X, Search, AlertTriangle, Plug, MessageCircle, GitBranch,
14
+ Sparkles, X, Search, AlertTriangle, Plug, MessageCircle, GitBranch, Globe,
15
15
  } from 'lucide-react';
16
16
  import { api } from '../../lib/api';
17
17
  import { Dialog, DialogContent } from '../ui/dialog';
@@ -70,6 +70,7 @@ const ROLE_PRESETS = [
70
70
  { id: 'analyst', label: 'Analyst', desc: 'Data analysis, insights', icon: BarChart3, tier: 'Medium' },
71
71
  { id: 'creative', label: 'Writer', desc: 'Copy, articles, proposals', icon: Pen, tier: 'Heavy', skillHint: true },
72
72
  { id: 'slides', label: 'Slides', desc: 'Pitch decks, presentations', icon: Presentation, tier: 'Heavy', skillHint: true },
73
+ { id: 'ambassador', label: 'Ambassador', desc: 'Bridge to federated server', icon: Globe, tier: 'Light' },
73
74
  ];
74
75
 
75
76
  function CheckMark() {
@@ -112,6 +113,8 @@ export function SpawnWizard() {
112
113
  const [selectedPersonality, setSelectedPersonality] = useState('');
113
114
  const [showAdvanced, setShowAdvanced] = useState(false);
114
115
  const [spawning, setSpawning] = useState(false);
116
+ const [selectedPeerId, setSelectedPeerId] = useState('');
117
+ const federation = useGrooveStore((s) => s.federation);
115
118
 
116
119
  useEffect(() => {
117
120
  if (open) {
@@ -144,6 +147,7 @@ export function SpawnWizard() {
144
147
  setIntegrationApproval('manual');
145
148
  setSelectedRepos([]);
146
149
  setSelectedPersonality('');
150
+ setSelectedPeerId('');
147
151
  setShowAdvanced(false);
148
152
  }
149
153
  }, [open, fetchProviders]);
@@ -168,6 +172,7 @@ export function SpawnWizard() {
168
172
  ...(selectedIntegrations.length > 0 && { integrationApproval }),
169
173
  ...(selectedRepos.length > 0 && { repos: selectedRepos }),
170
174
  ...(selectedPersonality && { personality: selectedPersonality }),
175
+ ...(selectedRole === 'ambassador' && selectedPeerId && { peerId: selectedPeerId }),
171
176
  };
172
177
  await spawnAgent(config);
173
178
  closeDetail();
@@ -232,6 +237,42 @@ export function SpawnWizard() {
232
237
  </div>
233
238
  </div>
234
239
 
240
+ {/* Ambassador server picker */}
241
+ {selectedRole === 'ambassador' && (() => {
242
+ const eligible = federation.whitelist.filter((e) => typeof e === 'object' && (e.status === 'mutual' || e.status === 'connected'));
243
+ if (eligible.length === 0) {
244
+ return (
245
+ <div className="rounded-lg border border-dashed border-border-subtle bg-surface-1/50 px-4 py-4 text-center">
246
+ <Globe size={18} className="text-text-4 mx-auto mb-1.5" />
247
+ <p className="text-2xs text-text-3 font-sans mb-2">No federated servers connected. Add one in the Federation view.</p>
248
+ <Button variant="ghost" size="sm" className="text-2xs text-accent" onClick={() => { closeDetail(); useGrooveStore.getState().setActiveView('federation'); }}>
249
+ Go to Federation
250
+ </Button>
251
+ </div>
252
+ );
253
+ }
254
+ return (
255
+ <div>
256
+ <label className="text-xs font-semibold text-text-2 font-sans uppercase tracking-wider block mb-2">
257
+ Target Server
258
+ </label>
259
+ <div className="relative">
260
+ <select
261
+ value={selectedPeerId}
262
+ onChange={(e) => setSelectedPeerId(e.target.value)}
263
+ className="w-full h-8 px-3 pr-8 text-sm rounded-md bg-surface-1 border border-border text-text-0 font-sans appearance-none cursor-pointer focus:outline-none focus:ring-1 focus:ring-accent"
264
+ >
265
+ <option value="">Select a server...</option>
266
+ {eligible.map((e) => (
267
+ <option key={e.ip} value={e.ip}>{e.name || `${e.ip}:${e.port || 31415}`}</option>
268
+ ))}
269
+ </select>
270
+ <ChevronDown size={14} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-3 pointer-events-none" />
271
+ </div>
272
+ </div>
273
+ );
274
+ })()}
275
+
235
276
  {/* Section 2: Configuration */}
236
277
  {selectedRole && (
237
278
  <div className="space-y-4">
@@ -0,0 +1,30 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { ChevronRight } from 'lucide-react';
3
+
4
+ export function Breadcrumbs({ path }) {
5
+ if (!path) return null;
6
+
7
+ const segments = path.split('/').filter(Boolean);
8
+
9
+ return (
10
+ <div className="flex items-center h-7 px-3 bg-surface-2 border-b border-border-subtle text-2xs font-sans text-text-3 overflow-hidden flex-shrink-0 select-none">
11
+ {segments.map((segment, i) => {
12
+ const isLast = i === segments.length - 1;
13
+ return (
14
+ <span key={i} className="flex items-center min-w-0">
15
+ {i > 0 && <ChevronRight size={10} className="mx-0.5 flex-shrink-0 text-text-4" />}
16
+ <span
17
+ className={
18
+ isLast
19
+ ? 'text-text-1 font-medium truncate'
20
+ : 'hover:text-text-1 cursor-pointer truncate transition-colors'
21
+ }
22
+ >
23
+ {segment}
24
+ </span>
25
+ </span>
26
+ );
27
+ })}
28
+ </div>
29
+ );
30
+ }
@@ -34,16 +34,41 @@ const grooveTheme = EditorView.theme({
34
34
  '.cm-activeLineGutter': { backgroundColor: '#2c313a' },
35
35
  '.cm-activeLine': { backgroundColor: 'rgba(44, 49, 58, 0.5)' },
36
36
  '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { backgroundColor: 'rgba(51, 175, 188, 0.15)' },
37
+ // Search panel styling
38
+ '.cm-panels': { backgroundColor: '#24282f', borderBottom: '1px solid #3e4451' },
39
+ '.cm-panels.cm-panels-top': { borderBottom: '1px solid #3e4451' },
40
+ '.cm-panels.cm-panels-bottom': { borderTop: '1px solid #3e4451' },
41
+ '.cm-search': { padding: '6px 8px', gap: '4px', fontFamily: 'var(--font-sans)', fontSize: '12px', display: 'flex', flexWrap: 'wrap', alignItems: 'center' },
42
+ '.cm-search label': { display: 'flex', alignItems: 'center', gap: '4px', color: '#8b929e', fontSize: '11px' },
43
+ '.cm-search input, .cm-search .cm-textfield': {
44
+ backgroundColor: '#1a1e25', border: '1px solid #2c313a', borderRadius: '4px', color: '#e6e6e6',
45
+ padding: '2px 6px', fontSize: '12px', fontFamily: 'var(--font-mono)', outline: 'none',
46
+ },
47
+ '.cm-search input:focus, .cm-search .cm-textfield:focus': { borderColor: '#33afbc' },
48
+ '.cm-search .cm-button, .cm-button': {
49
+ backgroundColor: '#2c313a', border: '1px solid #3e4451', borderRadius: '4px', color: '#bcc2cd',
50
+ padding: '2px 8px', fontSize: '11px', fontFamily: 'var(--font-sans)', cursor: 'pointer',
51
+ backgroundImage: 'none',
52
+ },
53
+ '.cm-search .cm-button:hover, .cm-button:hover': { backgroundColor: '#333842', color: '#e6e6e6' },
54
+ '.cm-search .cm-button:active': { backgroundColor: '#3a3f4b' },
55
+ '.cm-search br': { display: 'none' },
56
+ '.cm-panel.cm-search [name=close]': { color: '#6e7681', cursor: 'pointer', padding: '0 4px' },
57
+ '.cm-panel.cm-search [name=close]:hover': { color: '#e6e6e6' },
58
+ '.cm-searchMatch': { backgroundColor: 'rgba(51, 175, 188, 0.2)', outline: '1px solid rgba(51, 175, 188, 0.4)' },
59
+ '.cm-searchMatch-selected': { backgroundColor: 'rgba(51, 175, 188, 0.35)' },
37
60
  }, { dark: true });
38
61
 
39
- export function CodeEditor({ content, language, onChange, onSave }) {
62
+ export function CodeEditor({ content, language, onChange, onSave, onCursorChange, viewRef: externalViewRef }) {
40
63
  const containerRef = useRef(null);
41
64
  const viewRef = useRef(null);
42
65
  const langCompartment = useRef(new Compartment());
43
66
  const onChangeRef = useRef(onChange);
44
67
  const onSaveRef = useRef(onSave);
68
+ const onCursorChangeRef = useRef(onCursorChange);
45
69
  onChangeRef.current = onChange;
46
70
  onSaveRef.current = onSave;
71
+ onCursorChangeRef.current = onCursorChange;
47
72
 
48
73
  useEffect(() => {
49
74
  if (!containerRef.current) return;
@@ -74,14 +99,20 @@ export function CodeEditor({ content, language, onChange, onSave }) {
74
99
  if (update.docChanged) {
75
100
  onChangeRef.current?.(update.state.doc.toString());
76
101
  }
102
+ if (update.selectionSet || update.docChanged) {
103
+ const pos = update.state.selection.main.head;
104
+ const line = update.state.doc.lineAt(pos);
105
+ onCursorChangeRef.current?.({ line: line.number, col: pos - line.from + 1 });
106
+ }
77
107
  }),
78
108
  ],
79
109
  });
80
110
 
81
111
  const view = new EditorView({ state, parent: containerRef.current });
82
112
  viewRef.current = view;
113
+ if (externalViewRef) externalViewRef.current = view;
83
114
 
84
- return () => { view.destroy(); viewRef.current = null; };
115
+ return () => { view.destroy(); viewRef.current = null; if (externalViewRef) externalViewRef.current = null; };
85
116
  }, []); // mount once
86
117
 
87
118
  // Update content when file changes externally
@@ -0,0 +1,26 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ const LANG_LABELS = {
4
+ javascript: 'JavaScript',
5
+ typescript: 'TypeScript',
6
+ css: 'CSS',
7
+ html: 'HTML',
8
+ json: 'JSON',
9
+ markdown: 'Markdown',
10
+ python: 'Python',
11
+ };
12
+
13
+ export function EditorStatusBar({ cursorPos, language }) {
14
+ return (
15
+ <div className="flex items-center justify-between h-6 px-3 bg-surface-1 border-t border-border-subtle text-2xs font-sans text-text-3 flex-shrink-0 select-none">
16
+ <div className="flex items-center gap-3">
17
+ <span>Ln {cursorPos.line}, Col {cursorPos.col}</span>
18
+ </div>
19
+ <div className="flex items-center gap-3">
20
+ <span className="cursor-default">{LANG_LABELS[language] || language || 'Plain Text'}</span>
21
+ <span>Spaces: 2</span>
22
+ <span>UTF-8</span>
23
+ </div>
24
+ </div>
25
+ );
26
+ }