groove-dev 0.27.164 → 0.27.166
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 +0 -7
- 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/tunnel-manager.js +28 -21
- package/node_modules/@groove-dev/gui/dist/assets/{index-CkCFf4Fl.js → index-B_kpnfOu.js} +89 -89
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +57 -3
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/stores/groove.js +12 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/tunnel-manager.js +28 -21
- package/packages/gui/dist/assets/{index-CkCFf4Fl.js → index-B_kpnfOu.js} +89 -89
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/editor/terminal.jsx +57 -3
- package/packages/gui/src/components/fleet/fleet-content.jsx +4 -1
- package/packages/gui/src/components/fleet/fleet-pane.jsx +11 -6
- package/packages/gui/src/stores/groove.js +12 -3
- package/terminal/Screenshot_2026-05-19_at_12.20.15_PM.png +0 -0
|
@@ -6,7 +6,7 @@
|
|
|
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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-B_kpnfOu.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">
|
|
@@ -85,6 +85,7 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
const requestId = `spawn-${++spawnSeq}`;
|
|
88
|
+
const initialCols = term.cols;
|
|
88
89
|
ws.send(JSON.stringify({ type: 'terminal:spawn', cols: term.cols, rows: term.rows, requestId }));
|
|
89
90
|
|
|
90
91
|
function onMessage(event) {
|
|
@@ -93,6 +94,25 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
93
94
|
|
|
94
95
|
if (msg.type === 'terminal:spawned' && msg.requestId === requestId && !termIdRef.current) {
|
|
95
96
|
termIdRef.current = msg.id;
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
try { fitRef.current?.fit(); } catch {}
|
|
99
|
+
const c = term.cols, r = term.rows;
|
|
100
|
+
if (c > 1 && r > 1 && termIdRef.current) {
|
|
101
|
+
const w = useGrooveStore.getState().ws;
|
|
102
|
+
if (w?.readyState === WebSocket.OPEN) {
|
|
103
|
+
w.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
|
|
104
|
+
lastSizeRef.current = { cols: c, rows: r };
|
|
105
|
+
if (c !== initialCols) {
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
term.clear();
|
|
108
|
+
if (w.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
109
|
+
w.send(JSON.stringify({ type: 'terminal:input', id: termIdRef.current, data: '\x0c' }));
|
|
110
|
+
}
|
|
111
|
+
}, 80);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}, 50);
|
|
96
116
|
} else if (msg.type === 'terminal:output' && msg.id === termIdRef.current) {
|
|
97
117
|
term.write(msg.data);
|
|
98
118
|
} else if (msg.type === 'terminal:exit' && msg.id === termIdRef.current) {
|
|
@@ -114,26 +134,50 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
114
134
|
term.onResize(({ cols, rows }) => {
|
|
115
135
|
if (cols === lastSizeRef.current.cols && rows === lastSizeRef.current.rows) return;
|
|
116
136
|
if (cols < 2 || rows < 2) return;
|
|
117
|
-
lastSizeRef.current = { cols, rows };
|
|
118
137
|
const ws = useGrooveStore.getState().ws;
|
|
119
138
|
if (ws?.readyState === WebSocket.OPEN && termIdRef.current) {
|
|
139
|
+
lastSizeRef.current = { cols, rows };
|
|
120
140
|
ws.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows, cols }));
|
|
121
141
|
}
|
|
122
142
|
});
|
|
123
143
|
}
|
|
124
144
|
|
|
145
|
+
let hasSpawned = false;
|
|
146
|
+
function tryInitSpawn() {
|
|
147
|
+
if (hasSpawned) return;
|
|
148
|
+
if (term.cols >= 10 && term.rows >= 2) {
|
|
149
|
+
hasSpawned = true;
|
|
150
|
+
trySpawn();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
125
154
|
requestAnimationFrame(() => {
|
|
126
155
|
try { fitAddon.fit(); } catch {}
|
|
127
|
-
|
|
156
|
+
requestAnimationFrame(() => {
|
|
157
|
+
try { fitAddon.fit(); } catch {}
|
|
158
|
+
tryInitSpawn();
|
|
159
|
+
});
|
|
128
160
|
});
|
|
129
161
|
|
|
162
|
+
const spawnFallback = setTimeout(() => {
|
|
163
|
+
if (!hasSpawned) {
|
|
164
|
+
try { fitAddon.fit(); } catch {}
|
|
165
|
+
hasSpawned = true;
|
|
166
|
+
trySpawn();
|
|
167
|
+
}
|
|
168
|
+
}, 3000);
|
|
169
|
+
|
|
130
170
|
const observer = new ResizeObserver(() => {
|
|
131
171
|
if (!visibleRef.current) return;
|
|
132
|
-
requestAnimationFrame(() => {
|
|
172
|
+
requestAnimationFrame(() => {
|
|
173
|
+
try { fitAddon.fit(); } catch {}
|
|
174
|
+
tryInitSpawn();
|
|
175
|
+
});
|
|
133
176
|
});
|
|
134
177
|
observer.observe(containerRef.current);
|
|
135
178
|
|
|
136
179
|
return () => {
|
|
180
|
+
clearTimeout(spawnFallback);
|
|
137
181
|
observer.disconnect();
|
|
138
182
|
if (handlerRef.current) {
|
|
139
183
|
handlerRef.current.ws.removeEventListener('message', handlerRef.current.handler);
|
|
@@ -149,6 +193,16 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
|
149
193
|
if (visible && fitRef.current) {
|
|
150
194
|
requestAnimationFrame(() => {
|
|
151
195
|
try { fitRef.current.fit(); } catch {}
|
|
196
|
+
if (termRef.current && termIdRef.current) {
|
|
197
|
+
const c = termRef.current.cols, r = termRef.current.rows;
|
|
198
|
+
if (c > 1 && r > 1) {
|
|
199
|
+
const ws = useGrooveStore.getState().ws;
|
|
200
|
+
if (ws?.readyState === WebSocket.OPEN) {
|
|
201
|
+
ws.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
|
|
202
|
+
lastSizeRef.current = { cols: c, rows: r };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
152
206
|
});
|
|
153
207
|
}
|
|
154
208
|
}, [visible]);
|
|
@@ -5,7 +5,10 @@ import { useGrooveStore } from '../../stores/groove';
|
|
|
5
5
|
import { FleetPane } from './fleet-pane';
|
|
6
6
|
|
|
7
7
|
export function FleetContent() {
|
|
8
|
-
const
|
|
8
|
+
const rawSelected = useGrooveStore((s) => s.fleetSelectedAgents);
|
|
9
|
+
const lastSelectedRef = useRef(rawSelected);
|
|
10
|
+
if (rawSelected[0] || rawSelected[1]) lastSelectedRef.current = rawSelected;
|
|
11
|
+
const selected = (rawSelected[0] || rawSelected[1]) ? rawSelected : lastSelectedRef.current;
|
|
9
12
|
const splitMode = useGrooveStore((s) => s.fleetSplitMode);
|
|
10
13
|
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
11
14
|
|
|
@@ -28,7 +28,12 @@ const STATUS_LABEL = {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export function FleetPane({ agentId, paneIndex }) {
|
|
31
|
-
const
|
|
31
|
+
const effectiveId = agentId || null;
|
|
32
|
+
const lastIdRef = useRef(effectiveId);
|
|
33
|
+
if (effectiveId) lastIdRef.current = effectiveId;
|
|
34
|
+
const resolvedId = effectiveId || lastIdRef.current;
|
|
35
|
+
|
|
36
|
+
const liveAgent = useGrooveStore((s) => resolvedId ? s.agents.find((a) => a.id === resolvedId) : null);
|
|
32
37
|
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
33
38
|
const fleetMarkRead = useGrooveStore((s) => s.fleetMarkRead);
|
|
34
39
|
|
|
@@ -43,15 +48,15 @@ export function FleetPane({ agentId, paneIndex }) {
|
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
useEffect(() => {
|
|
46
|
-
if (!liveAgent &&
|
|
47
|
-
goneTimer.current = setTimeout(() => setGone(true),
|
|
51
|
+
if (!liveAgent && resolvedId && !gone) {
|
|
52
|
+
goneTimer.current = setTimeout(() => setGone(true), 3000);
|
|
48
53
|
return () => { if (goneTimer.current) clearTimeout(goneTimer.current); };
|
|
49
54
|
}
|
|
50
|
-
}, [liveAgent,
|
|
55
|
+
}, [liveAgent, resolvedId, gone]);
|
|
51
56
|
|
|
52
57
|
useEffect(() => {
|
|
53
|
-
if (
|
|
54
|
-
}, [
|
|
58
|
+
if (effectiveId) fleetMarkRead(effectiveId);
|
|
59
|
+
}, [effectiveId, fleetMarkRead]);
|
|
55
60
|
|
|
56
61
|
const agent = liveAgent || lastAgentRef.current;
|
|
57
62
|
|
|
@@ -199,8 +199,10 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
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
|
|
203
|
-
|
|
202
|
+
if (removed.length > 0) {
|
|
203
|
+
if (st.detailPanel?.type === 'agent' && removed.includes(st.detailPanel.agentId)) {
|
|
204
|
+
updates.detailPanel = null;
|
|
205
|
+
}
|
|
204
206
|
}
|
|
205
207
|
set(updates);
|
|
206
208
|
break;
|
|
@@ -527,8 +529,15 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
527
529
|
const tid = get().activeTeamId;
|
|
528
530
|
teamDetailPanels = { ...s.teamDetailPanels, [tid]: newPanel };
|
|
529
531
|
}
|
|
532
|
+
let fleetSelectedAgents = s.fleetSelectedAgents;
|
|
533
|
+
if (fleetSelectedAgents[0] === oldId || fleetSelectedAgents[1] === oldId) {
|
|
534
|
+
fleetSelectedAgents = [
|
|
535
|
+
fleetSelectedAgents[0] === oldId ? newId : fleetSelectedAgents[0],
|
|
536
|
+
fleetSelectedAgents[1] === oldId ? newId : fleetSelectedAgents[1],
|
|
537
|
+
];
|
|
538
|
+
}
|
|
530
539
|
try { localStorage.setItem('groove:chatHistory', JSON.stringify(chatHistory)); } catch {}
|
|
531
|
-
return { chatHistory, tokenTimeline, activityLog, chatInputs, detailPanel, teamDetailPanels };
|
|
540
|
+
return { chatHistory, tokenTimeline, activityLog, chatInputs, detailPanel, teamDetailPanels, fleetSelectedAgents };
|
|
532
541
|
});
|
|
533
542
|
break;
|
|
534
543
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.166",
|
|
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)",
|
|
@@ -479,6 +479,10 @@ export class TunnelManager {
|
|
|
479
479
|
// Remote is behind npm — upgrade
|
|
480
480
|
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: remoteVer, to: npmVer } });
|
|
481
481
|
|
|
482
|
+
const target = `${config.user}@${config.host}`;
|
|
483
|
+
const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
|
|
484
|
+
const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
|
|
485
|
+
|
|
482
486
|
const installCmd = npmGlobalInstall(`groove-dev@${npmVer}`, config.user);
|
|
483
487
|
const cleanupCmd = 'rm -rf $(npm root -g)/.groove-dev-* $(npm root -g)/groove-dev 2>/dev/null || true';
|
|
484
488
|
|
|
@@ -496,40 +500,43 @@ export class TunnelManager {
|
|
|
496
500
|
}
|
|
497
501
|
}
|
|
498
502
|
|
|
499
|
-
// Restart remote daemon
|
|
503
|
+
// Restart remote daemon — fire and forget the SSH, verify through the tunnel
|
|
500
504
|
const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
507
|
-
});
|
|
505
|
+
try {
|
|
506
|
+
execFileSync('ssh', [...sshBase, sshCmd(`kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 1; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown`)], {
|
|
507
|
+
encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
508
|
+
});
|
|
509
|
+
} catch { /* SSH may close before nohup finishes — that's fine */ }
|
|
508
510
|
|
|
509
|
-
//
|
|
511
|
+
// Wait for daemon to come back up through the existing tunnel
|
|
512
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
510
513
|
let daemonVer = null;
|
|
511
|
-
for (let i = 0; i <
|
|
514
|
+
for (let i = 0; i < 8; i++) {
|
|
515
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
512
516
|
try {
|
|
513
|
-
const check = await fetch(`http://localhost:${localPort}/api/status`, {
|
|
514
|
-
signal: AbortSignal.timeout(3000),
|
|
515
|
-
});
|
|
517
|
+
const check = await fetch(`http://localhost:${localPort}/api/status`, { signal: AbortSignal.timeout(3000) });
|
|
516
518
|
if (check.ok) {
|
|
517
519
|
daemonVer = (await check.json()).version || null;
|
|
518
520
|
break;
|
|
519
521
|
}
|
|
520
|
-
} catch { /*
|
|
521
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
522
|
+
} catch { /* not up yet */ }
|
|
522
523
|
}
|
|
523
524
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
525
|
+
if (config.projectDir && daemonVer) {
|
|
526
|
+
try {
|
|
527
|
+
await fetch(`http://localhost:${localPort}/api/project-dir`, {
|
|
528
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
529
|
+
body: JSON.stringify({ path: config.projectDir }),
|
|
530
|
+
signal: AbortSignal.timeout(3000),
|
|
531
|
+
});
|
|
532
|
+
} catch { /* best effort */ }
|
|
529
533
|
}
|
|
530
534
|
|
|
535
|
+
const localVer = getLocalVersion();
|
|
536
|
+
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: daemonVer || npmVer, match: (daemonVer || npmVer) === localVer } });
|
|
531
537
|
this.daemon.audit.log('tunnel.upgrade', { id, from: remoteVer, to: daemonVer || npmVer });
|
|
532
538
|
} catch (err) {
|
|
539
|
+
// Upgrade failed but tunnel may still work — check before reporting failure
|
|
533
540
|
try {
|
|
534
541
|
const verify = await fetch(`http://localhost:${localPort}/api/status`, { signal: AbortSignal.timeout(5000) });
|
|
535
542
|
if (verify.ok) {
|
|
@@ -537,7 +544,7 @@ export class TunnelManager {
|
|
|
537
544
|
this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: getLocalVersion(), remoteVersion: verifyData.version, match: false } });
|
|
538
545
|
return;
|
|
539
546
|
}
|
|
540
|
-
} catch { /* tunnel
|
|
547
|
+
} catch { /* tunnel down */ }
|
|
541
548
|
this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: err.message } });
|
|
542
549
|
}
|
|
543
550
|
}
|