groove-dev 0.27.157 → 0.27.161
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.
- package/CLAUDE.md +7 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +130 -2
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +68 -60
- package/node_modules/@groove-dev/gui/dist/assets/index-DpRdb7o1.js +1020 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -2
- package/node_modules/@groove-dev/gui/src/app.css +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +17 -2
- package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +19 -7
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/node_modules/@groove-dev/gui/src/stores/groove.js +9 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +69 -38
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +75 -30
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/process.js +130 -2
- package/packages/daemon/src/tunnel-manager.js +68 -60
- package/packages/gui/dist/assets/index-DpRdb7o1.js +1020 -0
- package/packages/gui/dist/assets/index-Dzofq3wS.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -2
- package/packages/gui/src/app.css +2 -2
- package/packages/gui/src/components/agents/agent-feed.jsx +8 -8
- package/packages/gui/src/components/dashboard/cache-ring.jsx +2 -2
- package/packages/gui/src/components/dashboard/token-chart.jsx +2 -2
- package/packages/gui/src/components/editor/terminal.jsx +1 -1
- package/packages/gui/src/components/layout/welcome-splash.jsx +17 -2
- package/packages/gui/src/components/network/activity-chart.jsx +4 -4
- package/packages/gui/src/components/network/performance-dashboard.jsx +1 -1
- package/packages/gui/src/components/settings/quick-connect.jsx +19 -7
- package/packages/gui/src/components/settings/ssh-wizard.jsx +122 -17
- package/packages/gui/src/stores/groove.js +9 -1
- package/packages/gui/src/stores/slices/agents-slice.js +69 -38
- package/packages/gui/src/views/memory.jsx +75 -30
- package/node_modules/@fontsource-variable/jetbrains-mono/CHANGELOG.md +0 -2
- package/node_modules/@fontsource-variable/jetbrains-mono/LICENSE +0 -93
- package/node_modules/@fontsource-variable/jetbrains-mono/README.md +0 -48
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-cyrillic-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-greek-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-italic.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-vietnamese-wght-normal.woff2 +0 -0
- package/node_modules/@fontsource-variable/jetbrains-mono/index.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/metadata.json +0 -29
- package/node_modules/@fontsource-variable/jetbrains-mono/package.json +0 -47
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/metadata.scss +0 -46
- package/node_modules/@fontsource-variable/jetbrains-mono/scss/mixins.scss +0 -193
- package/node_modules/@fontsource-variable/jetbrains-mono/unicode.json +0 -8
- package/node_modules/@fontsource-variable/jetbrains-mono/wght-italic.css +0 -59
- package/node_modules/@fontsource-variable/jetbrains-mono/wght.css +0 -59
- package/node_modules/@groove-dev/gui/dist/assets/index-B6taUF7J.js +0 -1015
- package/node_modules/@groove-dev/gui/dist/assets/index-BAM0QzR0.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/packages/gui/dist/assets/index-B6taUF7J.js +0 -1015
- package/packages/gui/dist/assets/index-BAM0QzR0.css +0 -1
- package/packages/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/packages/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
|
@@ -7,7 +7,7 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
7
7
|
import { cn } from '../../lib/cn';
|
|
8
8
|
import {
|
|
9
9
|
FolderSearch, Check, X, AlertTriangle, Loader2,
|
|
10
|
-
ExternalLink, Server, KeyRound, Settings, Plug,
|
|
10
|
+
ExternalLink, Server, KeyRound, Settings, Plug, Terminal, Copy, RefreshCw,
|
|
11
11
|
} from 'lucide-react';
|
|
12
12
|
|
|
13
13
|
const STEPS = [
|
|
@@ -110,6 +110,111 @@ function InfoCard({ icon: Icon, title, iconColor, children }) {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function CopyableCommand({ command }) {
|
|
114
|
+
const [copied, setCopied] = useState(false);
|
|
115
|
+
return (
|
|
116
|
+
<div className="flex items-center gap-1.5 group">
|
|
117
|
+
<code className="flex-1 text-2xs font-mono text-text-1 bg-surface-0 px-2.5 py-1.5 rounded-md border border-border-subtle truncate">
|
|
118
|
+
{command}
|
|
119
|
+
</code>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => { navigator.clipboard.writeText(command); setCopied(true); setTimeout(() => setCopied(false), 1500); }}
|
|
122
|
+
className="p-1.5 text-text-4 hover:text-text-1 cursor-pointer transition-colors flex-shrink-0"
|
|
123
|
+
title="Copy"
|
|
124
|
+
>
|
|
125
|
+
{copied ? <Check size={11} className="text-success" /> : <Copy size={11} />}
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function SetupStatus({ testResult, user, host, sshPort, testLoading, onRetest }) {
|
|
132
|
+
const allGood = testResult.nodeInstalled && testResult.grooveInstalled && testResult.daemonRunning;
|
|
133
|
+
const needsNode = !testResult.nodeInstalled;
|
|
134
|
+
const needsGroove = testResult.nodeInstalled && !testResult.grooveInstalled;
|
|
135
|
+
const needsDaemon = testResult.grooveInstalled && !testResult.daemonRunning;
|
|
136
|
+
const sshTarget = `${user}@${host}${sshPort !== 22 ? ` -p ${sshPort}` : ''}`;
|
|
137
|
+
|
|
138
|
+
if (allGood) {
|
|
139
|
+
return (
|
|
140
|
+
<InfoCard icon={Check} title="Ready to Connect" iconColor="bg-success/10 text-success">
|
|
141
|
+
<div className="space-y-2.5">
|
|
142
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
143
|
+
<StatusDot status="running" size="sm" />
|
|
144
|
+
<span className="text-text-1">Node.js {testResult.nodeVersion}</span>
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
147
|
+
<StatusDot status="running" size="sm" />
|
|
148
|
+
<span className="text-text-1">Groove Installed{testResult.remoteVersion ? ` (v${testResult.remoteVersion})` : ''}</span>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
151
|
+
<StatusDot status="running" size="sm" />
|
|
152
|
+
<span className="text-text-1">Daemon Running</span>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</InfoCard>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="rounded-xl border border-warning/25 bg-gradient-to-br from-warning/[0.04] to-transparent px-5 py-4">
|
|
161
|
+
<div className="flex items-center justify-between mb-3">
|
|
162
|
+
<div className="flex items-center gap-2.5">
|
|
163
|
+
<div className="w-7 h-7 rounded-lg bg-warning/10 flex items-center justify-center flex-shrink-0">
|
|
164
|
+
<Terminal size={13} className="text-warning" />
|
|
165
|
+
</div>
|
|
166
|
+
<span className="text-sm font-semibold text-text-0 font-sans">Remote Setup Required</span>
|
|
167
|
+
</div>
|
|
168
|
+
<button
|
|
169
|
+
onClick={onRetest}
|
|
170
|
+
disabled={testLoading}
|
|
171
|
+
className="flex items-center gap-1 text-2xs text-text-3 hover:text-text-1 font-sans cursor-pointer transition-colors disabled:opacity-50"
|
|
172
|
+
>
|
|
173
|
+
<RefreshCw size={10} className={testLoading ? 'animate-spin' : ''} />
|
|
174
|
+
Re-test
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="space-y-2.5 mb-3">
|
|
179
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
180
|
+
<StatusDot status={testResult.reachable ? 'running' : 'crashed'} size="sm" />
|
|
181
|
+
<span className="text-text-1">Reachable</span>
|
|
182
|
+
</div>
|
|
183
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
184
|
+
<StatusDot status={testResult.nodeInstalled ? 'running' : 'stopped'} size="sm" />
|
|
185
|
+
<span className="text-text-1">Node.js{testResult.nodeVersion ? ` ${testResult.nodeVersion}` : ''}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
188
|
+
<StatusDot status={testResult.grooveInstalled ? 'running' : 'stopped'} size="sm" />
|
|
189
|
+
<span className="text-text-1">Groove{testResult.remoteVersion ? ` v${testResult.remoteVersion}` : ''}</span>
|
|
190
|
+
</div>
|
|
191
|
+
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
192
|
+
<StatusDot status={testResult.daemonRunning ? 'running' : 'stopped'} size="sm" />
|
|
193
|
+
<span className="text-text-1">Daemon</span>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="border-t border-border-subtle pt-3 space-y-2">
|
|
198
|
+
<p className="text-2xs text-text-3 font-sans">SSH in and run{needsNode ? '' : needsGroove ? '' : ''}:</p>
|
|
199
|
+
<CopyableCommand command={`ssh ${sshTarget}`} />
|
|
200
|
+
{needsNode && (
|
|
201
|
+
<>
|
|
202
|
+
<CopyableCommand command='curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash' />
|
|
203
|
+
<CopyableCommand command="source ~/.bashrc && nvm install 20" />
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
{(needsNode || needsGroove) && (
|
|
207
|
+
<CopyableCommand command="npm i -g groove-dev" />
|
|
208
|
+
)}
|
|
209
|
+
{(needsNode || needsGroove || needsDaemon) && (
|
|
210
|
+
<CopyableCommand command="groove start" />
|
|
211
|
+
)}
|
|
212
|
+
<p className="text-2xs text-text-4 font-sans mt-1">Then click Re-test above.</p>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
113
218
|
export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
|
|
114
219
|
const remoteHomedir = useGrooveStore((s) => s.remoteHomedir);
|
|
115
220
|
const [step, setStep] = useState(0);
|
|
@@ -140,6 +245,13 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
|
|
|
140
245
|
setAutoConnect(server.autoConnect || false);
|
|
141
246
|
setCompletedSteps([0, 1]);
|
|
142
247
|
setStep(2);
|
|
248
|
+
setTestResult(null);
|
|
249
|
+
setTestLoading(true);
|
|
250
|
+
onTest().then((result) => {
|
|
251
|
+
setTestResult(result);
|
|
252
|
+
}).catch((err) => {
|
|
253
|
+
setTestResult({ error: err.message || 'Test failed' });
|
|
254
|
+
}).finally(() => setTestLoading(false));
|
|
143
255
|
} else {
|
|
144
256
|
setName('');
|
|
145
257
|
setHost('');
|
|
@@ -150,6 +262,7 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
|
|
|
150
262
|
setAutoConnect(false);
|
|
151
263
|
setCompletedSteps([]);
|
|
152
264
|
setStep(0);
|
|
265
|
+
setTestResult(null);
|
|
153
266
|
}
|
|
154
267
|
}, [server]);
|
|
155
268
|
|
|
@@ -402,22 +515,14 @@ export function SSHWizard({ server, onSave, onTest, onConnect, onCancel }) {
|
|
|
402
515
|
</FieldCard>
|
|
403
516
|
|
|
404
517
|
{testResult && !testResult.error ? (
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<span className="text-text-1">Groove Installed</span>
|
|
414
|
-
</div>
|
|
415
|
-
<div className="flex items-center gap-2 text-2xs font-sans">
|
|
416
|
-
<StatusDot status={testResult.daemonRunning ? 'running' : 'stopped'} size="sm" />
|
|
417
|
-
<span className="text-text-1">Daemon Running</span>
|
|
418
|
-
</div>
|
|
419
|
-
</div>
|
|
420
|
-
</InfoCard>
|
|
518
|
+
<SetupStatus
|
|
519
|
+
testResult={testResult}
|
|
520
|
+
user={user}
|
|
521
|
+
host={host}
|
|
522
|
+
sshPort={sshPort}
|
|
523
|
+
testLoading={testLoading}
|
|
524
|
+
onRetest={handleTest}
|
|
525
|
+
/>
|
|
421
526
|
) : (
|
|
422
527
|
<InfoCard icon={Server} title={name || 'Server'}>
|
|
423
528
|
<div className="space-y-2 text-2xs font-sans">
|
|
@@ -198,7 +198,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
for (const id of removed) delete timeline[id];
|
|
201
|
-
|
|
201
|
+
const updates = { agents, tokenTimeline: timeline, hydrated: true };
|
|
202
|
+
if (removed.length > 0 && st.detailPanel?.type === 'agent' && removed.includes(st.detailPanel.agentId)) {
|
|
203
|
+
updates.detailPanel = null;
|
|
204
|
+
}
|
|
205
|
+
set(updates);
|
|
202
206
|
break;
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -366,6 +370,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
366
370
|
break;
|
|
367
371
|
}
|
|
368
372
|
|
|
373
|
+
case 'recommended-team:ready':
|
|
374
|
+
if (!get().recommendedTeam) get().checkRecommendedTeam();
|
|
375
|
+
break;
|
|
376
|
+
|
|
369
377
|
case 'phase2:spawned':
|
|
370
378
|
get().addToast('info', `QC agent ${msg.name} auto-spawned`, 'Auditing phase 1 work');
|
|
371
379
|
break;
|
|
@@ -169,10 +169,11 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
169
169
|
return data;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
// CLI agent: was stopped + resumed/rotated — transfer state to new agent ID
|
|
172
|
+
// CLI agent: was stopped + resumed/rotated — transfer state to new agent ID.
|
|
173
|
+
// Only transfer if the rotation:complete WebSocket handler hasn't already done it.
|
|
173
174
|
const newAgent = data;
|
|
174
175
|
for (const key of ['chatHistory', 'activityLog', 'tokenTimeline']) {
|
|
175
|
-
if (snapshot[key]?.length) {
|
|
176
|
+
if (snapshot[key]?.length && !get()[key]?.[newAgent.id]?.length) {
|
|
176
177
|
set((s) => ({ [key]: { ...s[key], [newAgent.id]: [...snapshot[key]] } }));
|
|
177
178
|
}
|
|
178
179
|
}
|
|
@@ -380,6 +381,13 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
380
381
|
get().addChatMessage(agentId, 'system', text);
|
|
381
382
|
};
|
|
382
383
|
|
|
384
|
+
const startThinking = () => {
|
|
385
|
+
set((s) => ({ thinkingAgents: new Set([...s.thinkingAgents, agentId]) }));
|
|
386
|
+
};
|
|
387
|
+
const stopThinking = () => {
|
|
388
|
+
set((s) => { const next = new Set(s.thinkingAgents); next.delete(agentId); return { thinkingAgents: next }; });
|
|
389
|
+
};
|
|
390
|
+
|
|
383
391
|
try {
|
|
384
392
|
switch (command) {
|
|
385
393
|
case 'instruct': {
|
|
@@ -391,8 +399,11 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
391
399
|
if (tags.length === 0) { addSystemMsg('Usage: save #tag your message here'); return true; }
|
|
392
400
|
const content = rest.replace(/#[\w/.-]+/g, '').trim();
|
|
393
401
|
if (!content) { addSystemMsg('Usage: save #tag your message here'); return true; }
|
|
394
|
-
|
|
395
|
-
|
|
402
|
+
startThinking();
|
|
403
|
+
try {
|
|
404
|
+
await get().saveKeeperItem(tags[0], content);
|
|
405
|
+
addSystemMsg(`Saved to #${tags[0]}`);
|
|
406
|
+
} finally { stopThinking(); }
|
|
396
407
|
return { passthrough: content };
|
|
397
408
|
}
|
|
398
409
|
|
|
@@ -400,36 +411,48 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
400
411
|
if (tags.length === 0) { addSystemMsg('Usage: append #tag content to add'); return true; }
|
|
401
412
|
const content = rest.replace(/#[\w/.-]+/g, '').trim();
|
|
402
413
|
if (!content) { addSystemMsg('Usage: append #tag content to add'); return true; }
|
|
403
|
-
|
|
404
|
-
|
|
414
|
+
startThinking();
|
|
415
|
+
try {
|
|
416
|
+
await get().appendKeeperItem(tags[0], content);
|
|
417
|
+
addSystemMsg(`Appended to #${tags[0]}`);
|
|
418
|
+
} finally { stopThinking(); }
|
|
405
419
|
return { passthrough: content };
|
|
406
420
|
}
|
|
407
421
|
|
|
408
422
|
case 'update': {
|
|
409
423
|
if (tags.length === 0) { addSystemMsg('Usage: [update] #tag'); return true; }
|
|
410
424
|
get().addChatMessage(agentId, 'user', message, false);
|
|
411
|
-
|
|
412
|
-
|
|
425
|
+
startThinking();
|
|
426
|
+
try {
|
|
427
|
+
const existing = await get().getKeeperItem(tags[0]);
|
|
428
|
+
set({ keeperEditing: { tag: tags[0], content: existing?.content || '', isNew: !existing } });
|
|
429
|
+
} finally { stopThinking(); }
|
|
413
430
|
return true;
|
|
414
431
|
}
|
|
415
432
|
|
|
416
433
|
case 'delete': {
|
|
417
434
|
if (tags.length === 0) { addSystemMsg('Usage: [delete] #tag'); return true; }
|
|
418
435
|
get().addChatMessage(agentId, 'user', message, false);
|
|
419
|
-
|
|
420
|
-
|
|
436
|
+
startThinking();
|
|
437
|
+
try {
|
|
438
|
+
await get().deleteKeeperItem(tags[0]);
|
|
439
|
+
addSystemMsg(`Deleted #${tags[0]}`);
|
|
440
|
+
} finally { stopThinking(); }
|
|
421
441
|
return true;
|
|
422
442
|
}
|
|
423
443
|
|
|
424
444
|
case 'view': {
|
|
425
445
|
if (tags.length === 0) { addSystemMsg('Usage: [view] #tag'); return true; }
|
|
426
446
|
get().addChatMessage(agentId, 'user', message, false);
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
447
|
+
startThinking();
|
|
448
|
+
try {
|
|
449
|
+
const item = await get().getKeeperItem(tags[0]);
|
|
450
|
+
if (item) {
|
|
451
|
+
set({ keeperEditing: { tag: tags[0], content: item.content, isNew: false, readOnly: true } });
|
|
452
|
+
} else {
|
|
453
|
+
addSystemMsg(`#${tags[0]} not found`);
|
|
454
|
+
}
|
|
455
|
+
} finally { stopThinking(); }
|
|
433
456
|
return true;
|
|
434
457
|
}
|
|
435
458
|
|
|
@@ -437,21 +460,22 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
437
460
|
if (tags.length === 0) { addSystemMsg('Usage: [read] #tag1 #tag2 ...'); return true; }
|
|
438
461
|
const userText = rest.replace(/#[\w/.-]+/g, '').trim();
|
|
439
462
|
get().addChatMessage(agentId, 'user', message, false);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
463
|
+
startThinking();
|
|
464
|
+
try {
|
|
465
|
+
const readBrief = await api.post('/keeper/pull', { tags });
|
|
466
|
+
if (readBrief?.brief) {
|
|
467
|
+
const memoryBlock = `\n\n---\nContext from memories (${tags.map(t => '#' + t).join(', ')}):\n\n${readBrief.brief}`;
|
|
468
|
+
await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, {
|
|
469
|
+
message: userText ? `${userText}${memoryBlock}` : `Here is context from my tagged memories:\n\n${readBrief.brief}`,
|
|
470
|
+
});
|
|
471
|
+
addSystemMsg(`Sent ${tags.map(t => '#' + t).join(', ')} to agent`);
|
|
472
|
+
} else {
|
|
473
|
+
addSystemMsg(`No memories found for ${tags.map(t => '#' + t).join(', ')}`);
|
|
474
|
+
if (userText) {
|
|
475
|
+
await api.post(`/agents/${encodeURIComponent(agentId)}/instruct`, { message: userText });
|
|
476
|
+
}
|
|
453
477
|
}
|
|
454
|
-
}
|
|
478
|
+
} finally { stopThinking(); }
|
|
455
479
|
return true;
|
|
456
480
|
}
|
|
457
481
|
|
|
@@ -459,12 +483,15 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
459
483
|
if (tags.length === 0) { addSystemMsg('Usage: [doc] #tag'); return true; }
|
|
460
484
|
get().addChatMessage(agentId, 'user', message, false);
|
|
461
485
|
addSystemMsg(`Generating doc for #${tags[0]}...`);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
486
|
+
startThinking();
|
|
487
|
+
try {
|
|
488
|
+
const history = get().chatHistory[agentId] || [];
|
|
489
|
+
const result = await api.post('/keeper/doc', { tag: tags[0], chatHistory: history, agentId });
|
|
490
|
+
if (result?.content) {
|
|
491
|
+
addSystemMsg(`Doc #${tags[0]} generated (${result.size}B)`);
|
|
492
|
+
set({ keeperEditing: { tag: tags[0], content: result.content, isNew: false } });
|
|
493
|
+
}
|
|
494
|
+
} finally { stopThinking(); }
|
|
468
495
|
return true;
|
|
469
496
|
}
|
|
470
497
|
|
|
@@ -473,12 +500,16 @@ export const createAgentsSlice = (set, get) => ({
|
|
|
473
500
|
if (!linkMatch || tags.length === 0) { addSystemMsg('Usage: [link] #tag path/to/doc'); return true; }
|
|
474
501
|
const docPath = linkMatch[2].trim();
|
|
475
502
|
get().addChatMessage(agentId, 'user', message, false);
|
|
476
|
-
|
|
477
|
-
|
|
503
|
+
startThinking();
|
|
504
|
+
try {
|
|
505
|
+
await api.post('/keeper/link', { tag: tags[0], docPath });
|
|
506
|
+
addSystemMsg(`Linked #${tags[0]} → ${docPath}`);
|
|
507
|
+
} finally { stopThinking(); }
|
|
478
508
|
return true;
|
|
479
509
|
}
|
|
480
510
|
}
|
|
481
511
|
} catch (err) {
|
|
512
|
+
stopThinking();
|
|
482
513
|
addSystemMsg(`Keeper error: ${err.message}`);
|
|
483
514
|
return true;
|
|
484
515
|
}
|
|
@@ -4,7 +4,7 @@ import { useGrooveStore } from '../stores/groove';
|
|
|
4
4
|
import { Button } from '../components/ui/button';
|
|
5
5
|
import { ScrollArea } from '../components/ui/scroll-area';
|
|
6
6
|
import { Dialog, DialogContent } from '../components/ui/dialog';
|
|
7
|
-
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical } from 'lucide-react';
|
|
7
|
+
import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle, GripVertical, CornerLeftUp } from 'lucide-react';
|
|
8
8
|
|
|
9
9
|
const COMMANDS = [
|
|
10
10
|
{ cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
|
|
@@ -114,7 +114,7 @@ function MemoryCard({ item, onEdit, onDelete }) {
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
117
|
+
function EditorModal({ open, onOpenChange, editing, onSave, onRename }) {
|
|
118
118
|
const [tag, setTag] = useState('');
|
|
119
119
|
const [content, setContent] = useState('');
|
|
120
120
|
const textareaRef = useRef(null);
|
|
@@ -132,9 +132,15 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
132
132
|
}
|
|
133
133
|
}, [open]);
|
|
134
134
|
|
|
135
|
-
const handleSave = () => {
|
|
135
|
+
const handleSave = async () => {
|
|
136
136
|
if (!tag.trim() || editing?.readOnly) return;
|
|
137
|
-
|
|
137
|
+
const originalTag = editing?.tag || '';
|
|
138
|
+
const newTag = tag.trim();
|
|
139
|
+
if (!editing?.isNew && newTag !== originalTag) {
|
|
140
|
+
await onRename(originalTag, newTag, content);
|
|
141
|
+
} else {
|
|
142
|
+
onSave(newTag, content);
|
|
143
|
+
}
|
|
138
144
|
onOpenChange(false);
|
|
139
145
|
};
|
|
140
146
|
|
|
@@ -148,27 +154,30 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
148
154
|
const isNew = editing?.isNew;
|
|
149
155
|
const readOnly = editing?.readOnly;
|
|
150
156
|
const title = readOnly ? `#${editing?.tag || ''}` : isNew ? 'New Memory' : `Edit #${editing?.tag || ''}`;
|
|
157
|
+
const tagChanged = !isNew && tag.trim() !== (editing?.tag || '');
|
|
151
158
|
|
|
152
159
|
return (
|
|
153
160
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
154
161
|
<DialogContent title={title} description="Memory content" className="max-w-2xl">
|
|
155
162
|
<div className="p-5 space-y-4" onKeyDown={handleKeyDown}>
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
163
|
+
<div>
|
|
164
|
+
<label className="block text-xs font-medium text-text-2 mb-1.5">
|
|
165
|
+
{isNew ? 'Tag' : 'Title'}
|
|
166
|
+
{tagChanged && <span className="ml-2 text-2xs text-warning">(will rename)</span>}
|
|
167
|
+
</label>
|
|
168
|
+
<div className="flex items-center gap-1">
|
|
169
|
+
<span className="text-sm text-text-3">#</span>
|
|
170
|
+
<input
|
|
171
|
+
type="text"
|
|
172
|
+
value={tag}
|
|
173
|
+
onChange={(e) => !readOnly && setTag(e.target.value.replace(/[^a-zA-Z0-9/_-]/g, '').toLowerCase())}
|
|
174
|
+
readOnly={readOnly}
|
|
175
|
+
placeholder="project/feature-name"
|
|
176
|
+
className="flex-1 px-2 py-1.5 text-sm font-mono rounded-md bg-surface-0 border border-border text-text-0 placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
177
|
+
/>
|
|
170
178
|
</div>
|
|
171
|
-
|
|
179
|
+
{isNew && <p className="text-2xs text-text-4 mt-1">Use / for hierarchy: groove/memory-system</p>}
|
|
180
|
+
</div>
|
|
172
181
|
<div>
|
|
173
182
|
{!readOnly && <label className="block text-xs font-medium text-text-2 mb-1.5">Content</label>}
|
|
174
183
|
<textarea
|
|
@@ -196,7 +205,7 @@ function EditorModal({ open, onOpenChange, editing, onSave }) {
|
|
|
196
205
|
{!readOnly && (
|
|
197
206
|
<Button variant="primary" size="sm" onClick={handleSave} disabled={!tag.trim()}>
|
|
198
207
|
<Save size={14} />
|
|
199
|
-
Save
|
|
208
|
+
{tagChanged ? 'Rename & Save' : 'Save'}
|
|
200
209
|
</Button>
|
|
201
210
|
)}
|
|
202
211
|
</div>
|
|
@@ -249,7 +258,7 @@ function InstructModal({ open, onOpenChange }) {
|
|
|
249
258
|
);
|
|
250
259
|
}
|
|
251
260
|
|
|
252
|
-
function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
261
|
+
function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onEdit, onDelete, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
253
262
|
return (
|
|
254
263
|
<div
|
|
255
264
|
draggable
|
|
@@ -257,19 +266,34 @@ function TreeItem({ tag, label, isDoc, indent, isDragOver, onSelect, onDragStart
|
|
|
257
266
|
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; onDragOver?.(tag); }}
|
|
258
267
|
onDragLeave={() => onDragLeave?.()}
|
|
259
268
|
onDrop={(e) => { e.preventDefault(); onDrop?.(e.dataTransfer.getData('text/plain'), tag); }}
|
|
260
|
-
onClick={() => onSelect({ tag })}
|
|
261
269
|
className={`flex items-center gap-1.5 w-full px-2 py-1.5 rounded-md text-xs transition-colors cursor-pointer group ${isDragOver ? 'bg-accent/15 border border-accent/30 border-dashed' : 'hover:bg-surface-2'}`}
|
|
262
270
|
style={indent ? { paddingLeft: `${8 + indent * 16}px` } : undefined}
|
|
263
271
|
>
|
|
264
272
|
<GripVertical size={10} className="text-text-4 opacity-0 group-hover:opacity-50 flex-shrink-0 cursor-grab" />
|
|
265
273
|
<Hash size={11} className="text-text-4 flex-shrink-0" />
|
|
266
|
-
<span className="font-medium text-text-2 truncate">{label}</span>
|
|
274
|
+
<span className="font-medium text-text-2 truncate flex-1" onClick={() => onSelect({ tag })}>{label}</span>
|
|
267
275
|
{isDoc && <Sparkles size={9} className="text-purple flex-shrink-0" />}
|
|
276
|
+
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
|
277
|
+
<button
|
|
278
|
+
onClick={(e) => { e.stopPropagation(); onEdit?.({ tag }); }}
|
|
279
|
+
className="p-1 rounded text-text-4 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
|
|
280
|
+
title="Edit"
|
|
281
|
+
>
|
|
282
|
+
<Pencil size={11} />
|
|
283
|
+
</button>
|
|
284
|
+
<button
|
|
285
|
+
onClick={(e) => { e.stopPropagation(); onDelete?.(tag); }}
|
|
286
|
+
className="p-1 rounded text-text-4 hover:text-danger hover:bg-danger/10 transition-colors cursor-pointer"
|
|
287
|
+
title="Delete"
|
|
288
|
+
>
|
|
289
|
+
<Trash2 size={11} />
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
268
292
|
</div>
|
|
269
293
|
);
|
|
270
294
|
}
|
|
271
295
|
|
|
272
|
-
function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
296
|
+
function TreeGroup({ node, onSelect, onEdit, onDelete, dragOverTag, onDragStart, onDragOver, onDragLeave, onDrop }) {
|
|
273
297
|
const [expanded, setExpanded] = useState(true);
|
|
274
298
|
const hasChildren = node.children && node.children.length > 0;
|
|
275
299
|
|
|
@@ -278,7 +302,8 @@ function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDra
|
|
|
278
302
|
<TreeItem
|
|
279
303
|
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'}
|
|
280
304
|
isDragOver={dragOverTag === node.tag}
|
|
281
|
-
onSelect={onSelect}
|
|
305
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
306
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
282
307
|
/>
|
|
283
308
|
);
|
|
284
309
|
}
|
|
@@ -305,14 +330,16 @@ function TreeGroup({ node, onSelect, dragOverTag, onDragStart, onDragOver, onDra
|
|
|
305
330
|
<TreeItem
|
|
306
331
|
tag={node.tag} label={node.tag} isDoc={node.type === 'doc'} indent={1}
|
|
307
332
|
isDragOver={false}
|
|
308
|
-
onSelect={onSelect}
|
|
333
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
334
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
309
335
|
/>
|
|
310
336
|
)}
|
|
311
337
|
{node.children.map((child) => (
|
|
312
338
|
<TreeItem
|
|
313
339
|
key={child.tag} tag={child.tag} label={child.tag.split('/').pop()} isDoc={child.type === 'doc'} indent={1}
|
|
314
340
|
isDragOver={dragOverTag === child.tag}
|
|
315
|
-
onSelect={onSelect}
|
|
341
|
+
onSelect={onSelect} onEdit={onEdit} onDelete={onDelete}
|
|
342
|
+
onDragStart={onDragStart} onDragOver={onDragOver} onDragLeave={onDragLeave} onDrop={onDrop}
|
|
316
343
|
/>
|
|
317
344
|
))}
|
|
318
345
|
</div>
|
|
@@ -335,7 +362,7 @@ export default function MemoryView() {
|
|
|
335
362
|
const setKeeperEditing = useGrooveStore((s) => s.setKeeperEditing);
|
|
336
363
|
|
|
337
364
|
const [search, setSearch] = useState('');
|
|
338
|
-
const [viewMode, setViewMode] = useState('
|
|
365
|
+
const [viewMode, setViewMode] = useState('tree');
|
|
339
366
|
const [editorOpen, setEditorOpen] = useState(false);
|
|
340
367
|
const [dragOverTag, setDragOverTag] = useState(null);
|
|
341
368
|
const [draggingTag, setDraggingTag] = useState(null);
|
|
@@ -383,6 +410,12 @@ export default function MemoryView() {
|
|
|
383
410
|
setKeeperEditing(null);
|
|
384
411
|
};
|
|
385
412
|
|
|
413
|
+
const handleRename = async (oldTag, newTag, content) => {
|
|
414
|
+
await moveKeeperItem(oldTag, newTag);
|
|
415
|
+
await updateKeeperItem(newTag, content);
|
|
416
|
+
setKeeperEditing(null);
|
|
417
|
+
};
|
|
418
|
+
|
|
386
419
|
const handleEditorClose = (open) => {
|
|
387
420
|
setEditorOpen(open);
|
|
388
421
|
if (!open) setKeeperEditing(null);
|
|
@@ -397,10 +430,9 @@ export default function MemoryView() {
|
|
|
397
430
|
setDragOverTag(null);
|
|
398
431
|
setDraggingTag(null);
|
|
399
432
|
if (!sourceTag || !targetTag || sourceTag === targetTag) return;
|
|
400
|
-
// Don't drop onto self or own children
|
|
401
433
|
if (targetTag.startsWith(sourceTag + '/')) return;
|
|
402
434
|
const sourceName = sourceTag.split('/').pop();
|
|
403
|
-
const newTag = targetTag + '/' + sourceName;
|
|
435
|
+
const newTag = targetTag === '__root__' ? sourceName : targetTag + '/' + sourceName;
|
|
404
436
|
if (sourceTag === newTag) return;
|
|
405
437
|
try {
|
|
406
438
|
await moveKeeperItem(sourceTag, newTag);
|
|
@@ -487,6 +519,7 @@ export default function MemoryView() {
|
|
|
487
519
|
{filteredTree.map((node) => (
|
|
488
520
|
<TreeGroup
|
|
489
521
|
key={node.tag} node={node} onSelect={handleTreeSelect}
|
|
522
|
+
onEdit={handleEdit} onDelete={(tag) => deleteKeeperItem(tag)}
|
|
490
523
|
dragOverTag={dragOverTag}
|
|
491
524
|
onDragStart={(tag) => setDraggingTag(tag)}
|
|
492
525
|
onDragOver={(tag) => { if (tag !== draggingTag) setDragOverTag(tag); }}
|
|
@@ -494,6 +527,17 @@ export default function MemoryView() {
|
|
|
494
527
|
onDrop={handleDrop}
|
|
495
528
|
/>
|
|
496
529
|
))}
|
|
530
|
+
{draggingTag && draggingTag.includes('/') && (
|
|
531
|
+
<div
|
|
532
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverTag('__root__'); }}
|
|
533
|
+
onDragLeave={() => setDragOverTag(null)}
|
|
534
|
+
onDrop={(e) => { e.preventDefault(); handleDrop(e.dataTransfer.getData('text/plain'), '__root__'); }}
|
|
535
|
+
className={`flex items-center gap-2 px-3 py-2 mt-2 rounded-md border border-dashed text-xs transition-colors ${dragOverTag === '__root__' ? 'border-accent/50 bg-accent/10 text-accent' : 'border-border text-text-4'}`}
|
|
536
|
+
>
|
|
537
|
+
<CornerLeftUp size={12} />
|
|
538
|
+
<span>Drop here to move to root</span>
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
497
541
|
</div>
|
|
498
542
|
)
|
|
499
543
|
) : (
|
|
@@ -518,6 +562,7 @@ export default function MemoryView() {
|
|
|
518
562
|
onOpenChange={handleEditorClose}
|
|
519
563
|
editing={keeperEditing}
|
|
520
564
|
onSave={handleSave}
|
|
565
|
+
onRename={handleRename}
|
|
521
566
|
/>
|
|
522
567
|
|
|
523
568
|
{/* Instruct Modal (command reference) */}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.161",
|
|
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)",
|