groove-dev 0.27.30 → 0.27.33

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 (46) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/cli/src/commands/start.js +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +32 -2
  5. package/node_modules/@groove-dev/daemon/src/firstrun.js +1 -0
  6. package/node_modules/@groove-dev/daemon/src/index.js +14 -0
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +16 -4
  8. package/node_modules/@groove-dev/daemon/src/memory.js +6 -1
  9. package/node_modules/@groove-dev/daemon/src/process.js +44 -28
  10. package/node_modules/@groove-dev/daemon/src/providers/local.js +4 -2
  11. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +35 -3
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +1 -0
  13. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -7
  14. package/node_modules/@groove-dev/daemon/test/rotator.test.js +1 -0
  15. package/node_modules/@groove-dev/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
  16. package/node_modules/@groove-dev/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
  17. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +2 -0
  20. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
  21. package/node_modules/@groove-dev/gui/src/components/layout/project-picker.jsx +127 -0
  22. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +22 -15
  23. package/node_modules/@groove-dev/gui/src/stores/groove.js +39 -2
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/cli/src/commands/start.js +1 -1
  27. package/packages/daemon/package.json +1 -1
  28. package/packages/daemon/src/api.js +32 -2
  29. package/packages/daemon/src/firstrun.js +1 -0
  30. package/packages/daemon/src/index.js +14 -0
  31. package/packages/daemon/src/journalist.js +16 -4
  32. package/packages/daemon/src/memory.js +6 -1
  33. package/packages/daemon/src/process.js +44 -28
  34. package/packages/daemon/src/providers/local.js +4 -2
  35. package/packages/daemon/src/providers/ollama.js +35 -3
  36. package/packages/daemon/src/rotator.js +1 -0
  37. package/packages/daemon/src/tunnel-manager.js +19 -7
  38. package/packages/gui/dist/assets/{index-BwNjgBny.css → index-BnLiWvrh.css} +1 -1
  39. package/packages/gui/dist/assets/{index-PxWmJjcJ.js → index-BoU6IhQI.js} +1635 -1635
  40. package/packages/gui/dist/index.html +2 -2
  41. package/packages/gui/package.json +1 -1
  42. package/packages/gui/src/components/layout/app-shell.jsx +2 -0
  43. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +16 -1
  44. package/packages/gui/src/components/layout/project-picker.jsx +127 -0
  45. package/packages/gui/src/components/settings/quick-connect.jsx +22 -15
  46. package/packages/gui/src/stores/groove.js +39 -2
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-PxWmJjcJ.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BoU6IhQI.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-BwNjgBny.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BnLiWvrh.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.30",
3
+ "version": "0.27.33",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -13,6 +13,7 @@ import { DetailPanel } from './detail-panel';
13
13
  import { CommandPalette } from './command-palette';
14
14
  import { ApprovalModal } from '../ui/approval-modal';
15
15
  import { QuickConnect } from '../settings/quick-connect';
16
+ import { ProjectPicker } from './project-picker';
16
17
  import { TeamTabBar } from '../../views/agents';
17
18
 
18
19
  export function AppShell({ children, detailContent, terminalContent }) {
@@ -117,6 +118,7 @@ export function AppShell({ children, detailContent, terminalContent }) {
117
118
 
118
119
  <CommandPalette />
119
120
  <QuickConnect />
121
+ <ProjectPicker />
120
122
  <ApprovalModal />
121
123
  <ToastContainer />
122
124
  </div>
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useEffect, useRef } from 'react';
3
- import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown } from 'lucide-react';
3
+ import { Search, ChevronRight, LogIn, LogOut, User, ExternalLink, BookOpen, ChevronDown, FolderOpen } from 'lucide-react';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { useGrooveStore } from '../../stores/groove';
6
6
  import { isElectron, getPlatform } from '../../lib/electron';
@@ -130,10 +130,13 @@ const VIEW_LABELS = {
130
130
  export function BreadcrumbBar({
131
131
  activeView,
132
132
  connected,
133
+ tunneled,
133
134
  daemonHost,
134
135
  editorActiveFile,
135
136
  onOpenCommandPalette,
136
137
  }) {
138
+ const projectDir = useGrooveStore((s) => s.projectDir);
139
+ const toggleProjectPicker = useGrooveStore((s) => s.toggleProjectPicker);
137
140
  const crumbs = ['Groove', VIEW_LABELS[activeView] || activeView];
138
141
  if (activeView === 'editor' && editorActiveFile) {
139
142
  crumbs.push(editorActiveFile.split('/').pop());
@@ -182,6 +185,18 @@ export function BreadcrumbBar({
182
185
  </span>
183
186
  )}
184
187
 
188
+ {/* Project dir badge — remote sessions, clickable to change */}
189
+ {tunneled && projectDir && (
190
+ <button
191
+ onClick={toggleProjectPicker}
192
+ className="flex items-center gap-1 text-2xs font-mono font-medium text-text-2 bg-surface-5 px-1.5 py-0.5 rounded flex-shrink-0 hover:bg-surface-4 hover:text-text-0 transition-colors cursor-pointer"
193
+ title={projectDir}
194
+ >
195
+ <FolderOpen size={11} />
196
+ {projectDir.split('/').pop() || '/'}
197
+ </button>
198
+ )}
199
+
185
200
  <div className="flex-1 min-w-4" />
186
201
 
187
202
  {/* Breadcrumbs */}
@@ -0,0 +1,127 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { FolderBrowser } from '../agents/folder-browser';
5
+ import { cn } from '../../lib/cn';
6
+ import {
7
+ FolderOpen, FolderClosed, Clock, ChevronRight, Plus, Monitor,
8
+ } from 'lucide-react';
9
+
10
+ function formatTimeAgo(iso) {
11
+ if (!iso) return '';
12
+ const diff = Date.now() - new Date(iso).getTime();
13
+ const mins = Math.floor(diff / 60000);
14
+ if (mins < 1) return 'just now';
15
+ if (mins < 60) return `${mins}m ago`;
16
+ const hours = Math.floor(mins / 60);
17
+ if (hours < 24) return `${hours}h ago`;
18
+ const days = Math.floor(hours / 24);
19
+ return `${days}d ago`;
20
+ }
21
+
22
+ export function ProjectPicker() {
23
+ const show = useGrooveStore((s) => s.showProjectPicker);
24
+ const recentProjects = useGrooveStore((s) => s.recentProjects);
25
+ const setProjectDir = useGrooveStore((s) => s.setProjectDir);
26
+ const [browserOpen, setBrowserOpen] = useState(false);
27
+ const [loading, setLoading] = useState(null);
28
+
29
+ if (!show) return null;
30
+
31
+ async function handleSelect(path) {
32
+ setLoading(path);
33
+ try {
34
+ await setProjectDir(path);
35
+ } catch {
36
+ setLoading(null);
37
+ }
38
+ }
39
+
40
+ async function handleBrowseSelect(path) {
41
+ setBrowserOpen(false);
42
+ await handleSelect(path);
43
+ }
44
+
45
+ return (
46
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-surface-0/90 backdrop-blur-sm">
47
+ <div className="w-full max-w-lg mx-4">
48
+ {/* Header */}
49
+ <div className="text-center mb-8">
50
+ <div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-accent/10 mb-4">
51
+ <Monitor size={28} className="text-accent" />
52
+ </div>
53
+ <h1 className="text-xl font-semibold text-text-0 mb-1">Open a project</h1>
54
+ <p className="text-sm text-text-3">Select a working directory for this session</p>
55
+ </div>
56
+
57
+ {/* Recent projects */}
58
+ {recentProjects.length > 0 && (
59
+ <div className="mb-4">
60
+ <div className="flex items-center gap-2 mb-2 px-1">
61
+ <Clock size={13} className="text-text-4" />
62
+ <span className="text-xs font-medium text-text-3 uppercase tracking-wider">Recent</span>
63
+ </div>
64
+ <div className="bg-surface-2 rounded-xl border border-border overflow-hidden">
65
+ {recentProjects.map((project, i) => (
66
+ <button
67
+ key={project.path}
68
+ onClick={() => handleSelect(project.path)}
69
+ disabled={loading !== null}
70
+ className={cn(
71
+ 'w-full flex items-center gap-3 px-4 py-3 text-left cursor-pointer',
72
+ 'hover:bg-surface-4 transition-colors',
73
+ 'disabled:opacity-50 disabled:cursor-wait',
74
+ i < recentProjects.length - 1 && 'border-b border-border-subtle',
75
+ )}
76
+ >
77
+ <FolderClosed size={18} className="text-warning flex-shrink-0" />
78
+ <div className="flex-1 min-w-0">
79
+ <div className="text-sm font-medium text-text-0 truncate">{project.name}</div>
80
+ <div className="text-xs text-text-3 font-mono truncate">{project.path}</div>
81
+ </div>
82
+ <div className="flex items-center gap-2 flex-shrink-0">
83
+ {project.openedAt && (
84
+ <span className="text-[11px] text-text-4">{formatTimeAgo(project.openedAt)}</span>
85
+ )}
86
+ {loading === project.path ? (
87
+ <div className="w-4 h-4 border-2 border-accent border-t-transparent rounded-full animate-spin" />
88
+ ) : (
89
+ <ChevronRight size={14} className="text-text-4" />
90
+ )}
91
+ </div>
92
+ </button>
93
+ ))}
94
+ </div>
95
+ </div>
96
+ )}
97
+
98
+ {/* Open folder button */}
99
+ <button
100
+ onClick={() => setBrowserOpen(true)}
101
+ disabled={loading !== null}
102
+ className={cn(
103
+ 'w-full flex items-center gap-3 px-4 py-3.5 rounded-xl cursor-pointer',
104
+ 'bg-surface-2 border border-border border-dashed',
105
+ 'hover:bg-surface-4 hover:border-accent/30 transition-colors',
106
+ 'disabled:opacity-50 disabled:cursor-wait',
107
+ )}
108
+ >
109
+ <div className="flex items-center justify-center w-9 h-9 rounded-lg bg-accent/10">
110
+ <Plus size={18} className="text-accent" />
111
+ </div>
112
+ <div className="text-left">
113
+ <div className="text-sm font-medium text-text-0">Open Folder</div>
114
+ <div className="text-xs text-text-3">Browse the filesystem</div>
115
+ </div>
116
+ </button>
117
+
118
+ <FolderBrowser
119
+ open={browserOpen}
120
+ onOpenChange={setBrowserOpen}
121
+ currentPath="/home"
122
+ onSelect={handleBrowseSelect}
123
+ />
124
+ </div>
125
+ </div>
126
+ );
127
+ }
@@ -1,5 +1,5 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState } from 'react';
2
+ import { useState, useRef } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { AnimatePresence, motion } from 'framer-motion';
@@ -17,6 +17,7 @@ export function QuickConnect() {
17
17
  const addToast = useGrooveStore((s) => s.addToast);
18
18
  const [connectingId, setConnectingId] = useState(null);
19
19
  const [showWizard, setShowWizard] = useState(false);
20
+ const wizardTunnelId = useRef(null);
20
21
 
21
22
  if (!open) return null;
22
23
 
@@ -25,7 +26,9 @@ export function QuickConnect() {
25
26
  try {
26
27
  await useGrooveStore.getState().connectTunnel(id);
27
28
  toggle();
28
- } catch {}
29
+ } catch (err) {
30
+ addToast('error', 'Connection failed', err?.message || 'Unknown error');
31
+ }
29
32
  setConnectingId(null);
30
33
  }
31
34
 
@@ -81,24 +84,28 @@ export function QuickConnect() {
81
84
  <SSHWizard
82
85
  server={null}
83
86
  onSave={async (data) => {
84
- if (data.id) {
85
- await useGrooveStore.getState().updateTunnel(data.id, data);
87
+ const existingId = data.id || wizardTunnelId.current;
88
+ if (existingId) {
89
+ await useGrooveStore.getState().updateTunnel(existingId, data);
90
+ addToast('success', 'Server updated');
86
91
  } else {
87
- await useGrooveStore.getState().saveTunnel(data);
92
+ const result = await useGrooveStore.getState().saveTunnel(data);
93
+ if (result?.id) wizardTunnelId.current = result.id;
94
+ addToast('success', 'Server added');
88
95
  }
89
- addToast('success', data.id ? 'Server updated' : 'Server added');
90
96
  }}
91
97
  onTest={() => {
92
- const tunnels = useGrooveStore.getState().savedTunnels;
93
- const last = tunnels[tunnels.length - 1];
94
- if (last?.id) return useGrooveStore.getState().testTunnel(last.id);
98
+ const id = wizardTunnelId.current;
99
+ if (id) return useGrooveStore.getState().testTunnel(id);
95
100
  }}
96
101
  onConnect={() => {
97
- const tunnels = useGrooveStore.getState().savedTunnels;
98
- const last = tunnels[tunnels.length - 1];
99
- if (last?.id) return useGrooveStore.getState().connectTunnel(last.id);
102
+ const id = wizardTunnelId.current;
103
+ if (id) return useGrooveStore.getState().connectTunnel(id);
104
+ }}
105
+ onCancel={() => {
106
+ wizardTunnelId.current = null;
107
+ setShowWizard(false);
100
108
  }}
101
- onCancel={() => setShowWizard(false)}
102
109
  />
103
110
  ) : (
104
111
  <>
@@ -112,7 +119,7 @@ export function QuickConnect() {
112
119
  <Button
113
120
  variant="primary"
114
121
  size="sm"
115
- onClick={() => setShowWizard(true)}
122
+ onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
116
123
  className="h-8 text-xs gap-1.5 mt-3"
117
124
  >
118
125
  <Plus size={12} /> Add Connection
@@ -157,7 +164,7 @@ export function QuickConnect() {
157
164
  {/* Footer with Add button */}
158
165
  <div className="px-4 py-2.5 border-t border-border-subtle">
159
166
  <button
160
- onClick={() => setShowWizard(true)}
167
+ onClick={() => { wizardTunnelId.current = null; setShowWizard(true); }}
161
168
  className="flex items-center gap-1.5 text-2xs text-accent hover:text-accent/80 font-sans font-medium cursor-pointer transition-colors"
162
169
  >
163
170
  <Plus size={10} /> Add new connection
@@ -107,6 +107,11 @@ export const useGrooveStore = create((set, get) => ({
107
107
  // ── Toasts ────────────────────────────────────────────────
108
108
  toasts: [],
109
109
 
110
+ // ── Project Directory ───────────────────────────────────────
111
+ projectDir: null,
112
+ recentProjects: [],
113
+ showProjectPicker: false,
114
+
110
115
  // ── Tunnels ────────────────────────────────────────────────
111
116
  savedTunnels: [],
112
117
  activeTunnelId: null,
@@ -140,9 +145,11 @@ export const useGrooveStore = create((set, get) => ({
140
145
  const updates = {};
141
146
  if (s.host && s.host !== '127.0.0.1') updates.daemonHost = s.host;
142
147
  const browserPort = window.location.port || '80';
143
- if (String(s.port) !== browserPort) updates.tunneled = true;
148
+ const isTunneled = String(s.port) !== browserPort;
149
+ if (isTunneled) updates.tunneled = true;
144
150
  if (s.version) updates.version = s.version;
145
151
  if (Object.keys(updates).length > 0) set(updates);
152
+ if (isTunneled) get().fetchProjectDir();
146
153
  }).catch(() => {});
147
154
  get().fetchTeams();
148
155
  get().fetchApprovals();
@@ -502,6 +509,10 @@ export const useGrooveStore = create((set, get) => ({
502
509
  }));
503
510
  break;
504
511
 
512
+ case 'project-dir:changed':
513
+ set({ projectDir: msg.data?.projectDir, showProjectPicker: false });
514
+ break;
515
+
505
516
  case 'tunnel.connected':
506
517
  set({ activeTunnelId: msg.data?.id || null });
507
518
  get().fetchTunnels();
@@ -1034,6 +1045,33 @@ export const useGrooveStore = create((set, get) => ({
1034
1045
  get().fetchImportedRepos();
1035
1046
  },
1036
1047
 
1048
+ // ── Project Directory ────────────────────────────────────
1049
+
1050
+ async fetchProjectDir() {
1051
+ try {
1052
+ const data = await api.get('/project-dir');
1053
+ const isHome = /^\/home\/[^/]+$/.test(data.projectDir) || data.projectDir === '/root';
1054
+ set({
1055
+ projectDir: data.projectDir,
1056
+ recentProjects: data.recentProjects || [],
1057
+ showProjectPicker: isHome || (data.recentProjects || []).length === 0,
1058
+ });
1059
+ } catch {}
1060
+ },
1061
+
1062
+ async setProjectDir(path) {
1063
+ const data = await api.post('/project-dir', { path });
1064
+ set({
1065
+ projectDir: data.projectDir,
1066
+ recentProjects: data.recentProjects || [],
1067
+ showProjectPicker: false,
1068
+ });
1069
+ },
1070
+
1071
+ toggleProjectPicker() {
1072
+ set((s) => ({ showProjectPicker: !s.showProjectPicker }));
1073
+ },
1074
+
1037
1075
  // ── Tunnels ──────────────────────────────────────────────
1038
1076
 
1039
1077
  async fetchTunnels() {
@@ -1068,7 +1106,6 @@ export const useGrooveStore = create((set, get) => ({
1068
1106
  const result = await api.post(`/tunnels/${encodeURIComponent(id)}/connect`);
1069
1107
  set({ activeTunnelId: id });
1070
1108
  get().fetchTunnels();
1071
- if (result.url) window.open(result.url, '_blank');
1072
1109
  return result;
1073
1110
  },
1074
1111