groove-dev 0.27.165 → 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.
@@ -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-SZBexPhJ.js"></script>
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">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.165",
3
+ "version": "0.27.166",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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) {
@@ -101,6 +102,14 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
101
102
  if (w?.readyState === WebSocket.OPEN) {
102
103
  w.send(JSON.stringify({ type: 'terminal:resize', id: termIdRef.current, rows: r, cols: c }));
103
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
+ }
104
113
  }
105
114
  }
106
115
  }, 50);
@@ -133,21 +142,42 @@ function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
133
142
  });
134
143
  }
135
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
+
136
154
  requestAnimationFrame(() => {
137
155
  try { fitAddon.fit(); } catch {}
138
156
  requestAnimationFrame(() => {
139
157
  try { fitAddon.fit(); } catch {}
140
- trySpawn();
158
+ tryInitSpawn();
141
159
  });
142
160
  });
143
161
 
162
+ const spawnFallback = setTimeout(() => {
163
+ if (!hasSpawned) {
164
+ try { fitAddon.fit(); } catch {}
165
+ hasSpawned = true;
166
+ trySpawn();
167
+ }
168
+ }, 3000);
169
+
144
170
  const observer = new ResizeObserver(() => {
145
171
  if (!visibleRef.current) return;
146
- requestAnimationFrame(() => { try { fitAddon.fit(); } catch {} });
172
+ requestAnimationFrame(() => {
173
+ try { fitAddon.fit(); } catch {}
174
+ tryInitSpawn();
175
+ });
147
176
  });
148
177
  observer.observe(containerRef.current);
149
178
 
150
179
  return () => {
180
+ clearTimeout(spawnFallback);
151
181
  observer.disconnect();
152
182
  if (handlerRef.current) {
153
183
  handlerRef.current.ws.removeEventListener('message', handlerRef.current.handler);
@@ -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 selected = useGrooveStore((s) => s.fleetSelectedAgents);
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 liveAgent = useGrooveStore((s) => s.agents.find((a) => a.id === agentId));
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 && agentId && !gone) {
47
- goneTimer.current = setTimeout(() => setGone(true), 2000);
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, agentId, gone]);
55
+ }, [liveAgent, resolvedId, gone]);
51
56
 
52
57
  useEffect(() => {
53
- if (agentId) fleetMarkRead(agentId);
54
- }, [agentId, fleetMarkRead]);
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 && st.detailPanel?.type === 'agent' && removed.includes(st.detailPanel.agentId)) {
203
- updates.detailPanel = null;
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.165",
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)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.165",
3
+ "version": "0.27.166",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.165",
3
+ "version": "0.27.166",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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
- const setProjectDir = config.projectDir
502
- ? `curl -sf -X POST -H 'Content-Type: application/json' --data '{"path":"${config.projectDir}"}' http://localhost:${REMOTE_PORT}/api/project-dir > /dev/null 2>&1 || true; `
503
- : '';
504
- const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/status && (${setProjectDir}true) || true`;
505
- execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
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
- // Verify through tunnel
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 < 3; 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 { /* retry */ }
521
- await new Promise(r => setTimeout(r, 2000));
522
+ } catch { /* not up yet */ }
522
523
  }
523
524
 
524
- const localVer = getLocalVersion();
525
- if (daemonVer) {
526
- this.daemon.broadcast({ type: 'tunnel.version-info', data: { id, localVersion: localVer, remoteVersion: daemonVer, match: daemonVer === localVer } });
527
- } else {
528
- this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: 'Daemon did not respond after restart', from: remoteVer, attempted: npmVer } });
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 verification failed */ }
547
+ } catch { /* tunnel down */ }
541
548
  this.daemon.broadcast({ type: 'tunnel.upgrade-failed', data: { id, error: err.message } });
542
549
  }
543
550
  }