groove-dev 0.27.139 → 0.27.141
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/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +225 -16
- package/node_modules/@groove-dev/daemon/src/index.js +2 -0
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +76 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +225 -16
- package/packages/daemon/src/index.js +2 -0
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +17 -1
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +277 -0
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +9 -0
- package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
- package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +4 -0
- package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
- package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/file-tree.jsx +2 -86
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
- package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +302 -20
- package/packages/gui/src/views/agents.jsx +118 -60
- package/packages/gui/src/views/editor.jsx +67 -219
- package/packages/gui/src/views/memory.jsx +460 -0
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +295 -0
- package/plan_files/keeper-memory-system.md +223 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
- package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
- package/packages/gui/dist/assets/index-B4uYLR57.js +0 -8694
|
@@ -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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-P1hsM27-.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-BQqYnZfL.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-A4e1gIDh.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -25,11 +25,13 @@ import ModelLabView from './views/model-lab';
|
|
|
25
25
|
import NetworkView from './views/network';
|
|
26
26
|
import ChatView from './views/chat';
|
|
27
27
|
import PreviewView from './views/preview';
|
|
28
|
+
import MemoryView from './views/memory';
|
|
28
29
|
|
|
29
30
|
// Agent components
|
|
30
31
|
import { AgentPanel } from './components/agents/agent-panel';
|
|
31
32
|
import { SpawnWizard } from './components/agents/spawn-wizard';
|
|
32
33
|
import { JournalistPanel } from './components/agents/journalist-panel';
|
|
34
|
+
import { KeeperGlobalModals } from './components/keeper/global-modals';
|
|
33
35
|
|
|
34
36
|
// Terminal
|
|
35
37
|
import { TerminalManager } from './components/editor/terminal';
|
|
@@ -79,6 +81,7 @@ function ViewRouter() {
|
|
|
79
81
|
case 'settings': content = <SettingsView />; break;
|
|
80
82
|
case 'chat': content = <ChatView />; break;
|
|
81
83
|
case 'preview': content = <PreviewView />; break;
|
|
84
|
+
case 'memory': content = <MemoryView />; break;
|
|
82
85
|
case 'network': content = networkUnlocked ? <NetworkView /> : <AgentsView />; break;
|
|
83
86
|
default: content = <AgentsView />;
|
|
84
87
|
}
|
|
@@ -186,6 +189,7 @@ export default function App() {
|
|
|
186
189
|
<ViewRouter />
|
|
187
190
|
<UpgradeModal />
|
|
188
191
|
<DataSharingModal />
|
|
192
|
+
<KeeperGlobalModals />
|
|
189
193
|
</ErrorBoundary>
|
|
190
194
|
);
|
|
191
195
|
}
|
|
@@ -38,6 +38,19 @@ function parseSegments(text) {
|
|
|
38
38
|
return segments;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function highlightKeeper(text) {
|
|
42
|
+
const parts = text.split(/(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|\b(?:save|append|update|delete|view|doc|link|read|instruct)\b(?=\s+#)|#[\w/.-]+)/gi);
|
|
43
|
+
return parts.map((part, i) => {
|
|
44
|
+
if (/^\[?(?:save|append|update|delete|view|doc|link|read|instruct)\]?$/i.test(part)) {
|
|
45
|
+
return <span key={i} className="px-1 py-0.5 rounded bg-accent/15 text-accent font-semibold font-mono text-2xs">{part}</span>;
|
|
46
|
+
}
|
|
47
|
+
if (/^#[\w/.-]+$/.test(part)) {
|
|
48
|
+
return <span key={i} className="text-accent font-medium">{part}</span>;
|
|
49
|
+
}
|
|
50
|
+
return part;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
function FormattedText({ text }) {
|
|
42
55
|
if (!text) return null;
|
|
43
56
|
const segments = parseSegments(text);
|
|
@@ -61,7 +74,7 @@ function FormattedText({ text }) {
|
|
|
61
74
|
return <span key={i}>{part.split(/(\*\*[^*]+\*\*)/g).map((s, j) =>
|
|
62
75
|
s.startsWith('**') && s.endsWith('**')
|
|
63
76
|
? <strong key={j} className="font-semibold text-text-0">{s.slice(2, -2)}</strong>
|
|
64
|
-
: s
|
|
77
|
+
: <span key={j}>{highlightKeeper(s)}</span>
|
|
65
78
|
)}</span>;
|
|
66
79
|
})}
|
|
67
80
|
</span>
|
|
@@ -254,22 +267,33 @@ export function AgentChat({ agent }) {
|
|
|
254
267
|
>
|
|
255
268
|
<Paperclip size={14} />
|
|
256
269
|
</button>
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
'
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
270
|
+
<div className="flex-1 relative">
|
|
271
|
+
<div
|
|
272
|
+
aria-hidden
|
|
273
|
+
className="absolute inset-0 px-3 py-1.5 text-xs font-sans pointer-events-none whitespace-pre-wrap break-words overflow-hidden min-h-[32px] max-h-[120px] leading-[1.625]"
|
|
274
|
+
>
|
|
275
|
+
{input ? highlightKeeper(input) : null}
|
|
276
|
+
</div>
|
|
277
|
+
<textarea
|
|
278
|
+
ref={inputRef}
|
|
279
|
+
value={input}
|
|
280
|
+
onChange={(e) => setInput(e.target.value)}
|
|
281
|
+
onKeyDown={onKeyDown}
|
|
282
|
+
placeholder={isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
|
|
283
|
+
rows={1}
|
|
284
|
+
className={cn(
|
|
285
|
+
'w-full resize-none rounded-lg px-3 py-1.5 text-xs',
|
|
286
|
+
'bg-surface-0 border font-sans',
|
|
287
|
+
'placeholder:text-text-4',
|
|
288
|
+
'focus:outline-none focus:ring-1',
|
|
289
|
+
'min-h-[32px] max-h-[120px]',
|
|
290
|
+
'border-border focus:ring-accent/40',
|
|
291
|
+
input && /(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|\b(?:save|append|update|delete|view|doc|link|read|instruct)\b\s+#|#[\w/.-]+)/i.test(input)
|
|
292
|
+
? 'text-transparent caret-text-0'
|
|
293
|
+
: 'text-text-0',
|
|
294
|
+
)}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
273
297
|
{isAlive && (
|
|
274
298
|
<button
|
|
275
299
|
onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
|
|
@@ -182,10 +182,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
182
182
|
const createFile = useGrooveStore((s) => s.createFile);
|
|
183
183
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
184
184
|
const fetchTreeDir = useGrooveStore((s) => s.fetchTreeDir);
|
|
185
|
+
const projectDir = useGrooveStore((s) => s.projectDir);
|
|
185
186
|
|
|
186
187
|
const agent = agents.find((a) => a.id === agentId);
|
|
187
188
|
const scope = agent?.scope || [];
|
|
188
|
-
const workingDir = agent?.workingDir || '';
|
|
189
|
+
const workingDir = agent?.workingDir || projectDir || '';
|
|
189
190
|
const isRunning = agent?.status === 'running' || agent?.status === 'starting';
|
|
190
191
|
|
|
191
192
|
const [treeData, setTreeData] = useState([]);
|
|
@@ -202,7 +203,9 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
202
203
|
async function poll() {
|
|
203
204
|
try {
|
|
204
205
|
const data = await api.get(`/agents/${encodeURIComponent(agentId)}/files-touched`);
|
|
205
|
-
if (!cancelled && data.files)
|
|
206
|
+
if (!cancelled && data.files) {
|
|
207
|
+
setTouchedFiles(data.files.filter((f) => f.exists !== false && f.writes > 0));
|
|
208
|
+
}
|
|
206
209
|
} catch { /* agent may not exist yet */ }
|
|
207
210
|
}
|
|
208
211
|
poll();
|
|
@@ -364,7 +367,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
364
367
|
if (workingDir && absPath.startsWith(workingDir + '/')) {
|
|
365
368
|
return absPath.slice(workingDir.length + 1);
|
|
366
369
|
}
|
|
367
|
-
|
|
370
|
+
if (projectDir && absPath.startsWith(projectDir + '/')) {
|
|
371
|
+
return absPath.slice(projectDir.length + 1);
|
|
372
|
+
}
|
|
373
|
+
const lastSlash = absPath.lastIndexOf('/');
|
|
374
|
+
return lastSlash >= 0 ? absPath.slice(lastSlash + 1) : absPath;
|
|
368
375
|
}
|
|
369
376
|
|
|
370
377
|
function parentDir(path) {
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
XCircle, Send, FilePlus, FileMinus, FileEdit,
|
|
12
12
|
} from 'lucide-react';
|
|
13
13
|
|
|
14
|
-
export function CodeReview() {
|
|
15
|
-
const
|
|
14
|
+
export function CodeReview({ agentId: agentIdProp, onBack }) {
|
|
15
|
+
const storeAgentId = useGrooveStore((s) => s.editorSelectedAgent);
|
|
16
|
+
const agentId = agentIdProp || storeAgentId;
|
|
16
17
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
17
18
|
const openFile = useGrooveStore((s) => s.openFile);
|
|
18
19
|
const setViewMode = useGrooveStore((s) => s.setEditorViewMode);
|
|
@@ -84,7 +85,7 @@ export function CodeReview() {
|
|
|
84
85
|
if (reviewComments.length > 0) {
|
|
85
86
|
await instructAgent(agentId, `Code review feedback:\n${reviewComments.join('\n')}`);
|
|
86
87
|
}
|
|
87
|
-
setViewMode('code');
|
|
88
|
+
if (onBack) onBack(); else setViewMode('code');
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
const approved = Object.values(statuses).filter((s) => s === 'approved').length;
|
|
@@ -112,7 +113,7 @@ export function CodeReview() {
|
|
|
112
113
|
return (
|
|
113
114
|
<div className="flex flex-col h-full">
|
|
114
115
|
<div className="flex items-center gap-3 px-4 py-3 bg-surface-1 border-b border-border flex-shrink-0">
|
|
115
|
-
<button onClick={() => setViewMode('code')} className="p-1 rounded hover:bg-surface-4 text-text-3 hover:text-text-1 cursor-pointer" title="Back to Editor">
|
|
116
|
+
<button onClick={() => onBack ? onBack() : setViewMode('code')} className="p-1 rounded hover:bg-surface-4 text-text-3 hover:text-text-1 cursor-pointer" title="Back to Editor">
|
|
116
117
|
<ChevronLeft size={16} />
|
|
117
118
|
</button>
|
|
118
119
|
<span className="text-sm font-semibold text-text-0 font-sans flex-1">Review Changes</span>
|
|
@@ -6,12 +6,16 @@ import { AgentFileTree } from './agent-file-tree';
|
|
|
6
6
|
import { DiffViewer } from './diff-viewer';
|
|
7
7
|
import { CodeReview } from './code-review';
|
|
8
8
|
import { CodeEditor } from '../editor/code-editor';
|
|
9
|
+
import { AiPanel } from '../editor/ai-panel';
|
|
10
|
+
import { SelectionMenu } from '../editor/selection-menu';
|
|
11
|
+
import { InlinePrompt } from '../editor/inline-prompt';
|
|
12
|
+
import { QuickSearch } from '../editor/quick-search';
|
|
9
13
|
import { Tooltip } from '../ui/tooltip';
|
|
10
14
|
import { roleColor } from '../../lib/status';
|
|
11
15
|
import { MediaViewer, isMediaFile } from '../editor/media-viewer';
|
|
12
16
|
import {
|
|
13
17
|
X, Code2, FileCode, GitCompareArrows,
|
|
14
|
-
ClipboardCheck, Users, PanelLeftOpen,
|
|
18
|
+
ClipboardCheck, Users, PanelLeftOpen, Sparkles, Search,
|
|
15
19
|
} from 'lucide-react';
|
|
16
20
|
|
|
17
21
|
const TREE_DEFAULT = 220;
|
|
@@ -57,11 +61,11 @@ function AgentRail({ agents, activeId, onSelect }) {
|
|
|
57
61
|
);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots, onBackToTeam, onToggleReview, reviewMode }) {
|
|
64
|
+
function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots, onBackToTeam, onToggleReview, reviewMode, onToggleAi, aiOpen, onCmdP }) {
|
|
61
65
|
const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
|
|
62
66
|
|
|
63
67
|
return (
|
|
64
|
-
<div className="flex items-stretch h-8 bg-
|
|
68
|
+
<div className="flex items-stretch h-8 bg-surface-2 border-b border-border flex-shrink-0">
|
|
65
69
|
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
|
|
66
70
|
{tabs.map((path) => {
|
|
67
71
|
const isActive = path === activeFile;
|
|
@@ -74,9 +78,9 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
74
78
|
key={path}
|
|
75
79
|
className={cn(
|
|
76
80
|
'flex items-center gap-1.5 px-3 text-2xs font-sans cursor-pointer select-none',
|
|
77
|
-
'border-r border-
|
|
81
|
+
'border-r border-border-subtle transition-colors duration-75 flex-shrink-0',
|
|
78
82
|
isActive
|
|
79
|
-
? 'bg-surface-
|
|
83
|
+
? 'bg-surface-1 text-text-1 border-b border-b-accent'
|
|
80
84
|
: 'text-text-4 hover:text-text-2 hover:bg-surface-3 border-b border-b-transparent',
|
|
81
85
|
)}
|
|
82
86
|
onClick={() => onSelect(path)}
|
|
@@ -94,6 +98,14 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
94
98
|
})}
|
|
95
99
|
</div>
|
|
96
100
|
<div className="flex items-center gap-0.5 px-2 border-l border-border-subtle flex-shrink-0">
|
|
101
|
+
<Tooltip content="Quick Search (Cmd+P)" side="bottom">
|
|
102
|
+
<button
|
|
103
|
+
onClick={onCmdP}
|
|
104
|
+
className="p-1.5 text-text-3 hover:text-text-1 hover:bg-surface-3 rounded cursor-pointer transition-colors"
|
|
105
|
+
>
|
|
106
|
+
<Search size={12} />
|
|
107
|
+
</button>
|
|
108
|
+
</Tooltip>
|
|
97
109
|
{hasSnapshot && (
|
|
98
110
|
<>
|
|
99
111
|
<button
|
|
@@ -130,13 +142,25 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
130
142
|
<ClipboardCheck size={12} />
|
|
131
143
|
</button>
|
|
132
144
|
</Tooltip>
|
|
145
|
+
<Tooltip content="AI Panel" side="bottom">
|
|
146
|
+
<button
|
|
147
|
+
onClick={onToggleAi}
|
|
148
|
+
className={cn(
|
|
149
|
+
'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
150
|
+
aiOpen
|
|
151
|
+
? 'bg-accent/15 text-accent'
|
|
152
|
+
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
<Sparkles size={12} />
|
|
156
|
+
</button>
|
|
157
|
+
</Tooltip>
|
|
133
158
|
<Tooltip content="Back to Team View" side="bottom">
|
|
134
159
|
<button
|
|
135
160
|
onClick={onBackToTeam}
|
|
136
161
|
className="flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors text-text-3 hover:text-text-1 hover:bg-surface-3"
|
|
137
162
|
>
|
|
138
163
|
<Users size={12} />
|
|
139
|
-
<span className="text-2xs">Team</span>
|
|
140
164
|
</button>
|
|
141
165
|
</Tooltip>
|
|
142
166
|
</div>
|
|
@@ -153,6 +177,11 @@ export function WorkspaceMode() {
|
|
|
153
177
|
const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
|
|
154
178
|
const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
|
|
155
179
|
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
180
|
+
const aiPanelOpen = useGrooveStore((s) => s.editorAiPanelOpen);
|
|
181
|
+
const toggleAiPanel = useGrooveStore((s) => s.toggleAiPanel);
|
|
182
|
+
const aiPanelWidth = useGrooveStore((s) => s.editorAiPanelWidth);
|
|
183
|
+
const setAiPanelWidth = useGrooveStore((s) => s.setEditorAiPanelWidth);
|
|
184
|
+
const setQuickSearchOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
|
|
156
185
|
|
|
157
186
|
const editorFiles = useGrooveStore((s) => s.editorFiles);
|
|
158
187
|
const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
|
|
@@ -168,15 +197,29 @@ export function WorkspaceMode() {
|
|
|
168
197
|
const [treeWidth, setTreeWidth] = useState(TREE_DEFAULT);
|
|
169
198
|
const [diffMode, setDiffMode] = useState(false);
|
|
170
199
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
200
|
+
const [selection, setSelection] = useState(null);
|
|
201
|
+
const [inlinePrompt, setInlinePrompt] = useState(null);
|
|
202
|
+
const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
|
|
171
203
|
|
|
204
|
+
const editorViewRef = useRef(null);
|
|
172
205
|
const treeDragging = useRef(false);
|
|
173
206
|
const startX = useRef(0);
|
|
174
207
|
const startW = useRef(0);
|
|
208
|
+
const aiDragging = useRef(false);
|
|
209
|
+
const aiStartX = useRef(0);
|
|
210
|
+
const aiStartW = useRef(0);
|
|
175
211
|
|
|
176
212
|
useEffect(() => {
|
|
177
213
|
setDiffMode(false);
|
|
178
214
|
}, [editorActiveFile]);
|
|
179
215
|
|
|
216
|
+
// Set the selected agent in the store so AI features use it
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (agent?.id) {
|
|
219
|
+
useGrooveStore.setState({ editorSelectedAgent: agent.id });
|
|
220
|
+
}
|
|
221
|
+
}, [agent?.id]);
|
|
222
|
+
|
|
180
223
|
const onTreeMouseDown = useCallback((e) => {
|
|
181
224
|
e.preventDefault();
|
|
182
225
|
treeDragging.current = true;
|
|
@@ -195,6 +238,65 @@ export function WorkspaceMode() {
|
|
|
195
238
|
document.addEventListener('mouseup', onUp);
|
|
196
239
|
}, [treeWidth]);
|
|
197
240
|
|
|
241
|
+
const onAiPanelMouseDown = useCallback((e) => {
|
|
242
|
+
e.preventDefault();
|
|
243
|
+
aiDragging.current = true;
|
|
244
|
+
aiStartX.current = e.clientX;
|
|
245
|
+
aiStartW.current = aiPanelWidth;
|
|
246
|
+
function onMove(e) {
|
|
247
|
+
if (!aiDragging.current) return;
|
|
248
|
+
const delta = aiStartX.current - e.clientX;
|
|
249
|
+
setAiPanelWidth(Math.min(Math.max(aiStartW.current + delta, 280), 600));
|
|
250
|
+
}
|
|
251
|
+
function onUp() {
|
|
252
|
+
aiDragging.current = false;
|
|
253
|
+
document.removeEventListener('mousemove', onMove);
|
|
254
|
+
document.removeEventListener('mouseup', onUp);
|
|
255
|
+
}
|
|
256
|
+
document.addEventListener('mousemove', onMove);
|
|
257
|
+
document.addEventListener('mouseup', onUp);
|
|
258
|
+
}, [aiPanelWidth, setAiPanelWidth]);
|
|
259
|
+
|
|
260
|
+
const handleEditorMouseUp = useCallback(() => {
|
|
261
|
+
const view = editorViewRef.current;
|
|
262
|
+
if (!view) return;
|
|
263
|
+
const sel = view.state.selection.main;
|
|
264
|
+
if (sel.empty) { setSelection(null); return; }
|
|
265
|
+
const text = view.state.sliceDoc(sel.from, sel.to);
|
|
266
|
+
if (!text.trim()) { setSelection(null); return; }
|
|
267
|
+
const fromLine = view.state.doc.lineAt(sel.from);
|
|
268
|
+
const toLine = view.state.doc.lineAt(sel.to);
|
|
269
|
+
const coords = view.coordsAtPos(sel.to);
|
|
270
|
+
if (coords) {
|
|
271
|
+
setSelection({
|
|
272
|
+
x: Math.min(coords.left + 10, window.innerWidth - 220),
|
|
273
|
+
y: coords.bottom + 4,
|
|
274
|
+
lineStart: fromLine.number,
|
|
275
|
+
lineEnd: toLine.number,
|
|
276
|
+
selectedCode: text,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
const handleEditorContextMenu = useCallback((e) => {
|
|
282
|
+
const view = editorViewRef.current;
|
|
283
|
+
if (!view) return;
|
|
284
|
+
const sel = view.state.selection.main;
|
|
285
|
+
if (sel.empty) return;
|
|
286
|
+
const text = view.state.sliceDoc(sel.from, sel.to);
|
|
287
|
+
if (!text.trim()) return;
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
const fromLine = view.state.doc.lineAt(sel.from);
|
|
290
|
+
const toLine = view.state.doc.lineAt(sel.to);
|
|
291
|
+
setSelection({
|
|
292
|
+
x: Math.min(e.clientX, window.innerWidth - 220),
|
|
293
|
+
y: e.clientY + 4,
|
|
294
|
+
lineStart: fromLine.number,
|
|
295
|
+
lineEnd: toLine.number,
|
|
296
|
+
selectedCode: text,
|
|
297
|
+
});
|
|
298
|
+
}, []);
|
|
299
|
+
|
|
198
300
|
if (!agent) {
|
|
199
301
|
return (
|
|
200
302
|
<div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
|
|
@@ -240,7 +342,7 @@ export function WorkspaceMode() {
|
|
|
240
342
|
{/* Editor Area */}
|
|
241
343
|
<div className="flex-1 flex flex-col min-w-0 bg-surface-1">
|
|
242
344
|
{workspaceReviewMode ? (
|
|
243
|
-
<CodeReview agentId={agent.id} />
|
|
345
|
+
<CodeReview agentId={agent.id} onBack={toggleReviewMode} />
|
|
244
346
|
) : (
|
|
245
347
|
<>
|
|
246
348
|
<TabBar
|
|
@@ -255,6 +357,9 @@ export function WorkspaceMode() {
|
|
|
255
357
|
onBackToTeam={() => setWorkspaceMode(false)}
|
|
256
358
|
onToggleReview={toggleReviewMode}
|
|
257
359
|
reviewMode={workspaceReviewMode}
|
|
360
|
+
onToggleAi={toggleAiPanel}
|
|
361
|
+
aiOpen={aiPanelOpen}
|
|
362
|
+
onCmdP={() => setQuickSearchOpen(true)}
|
|
258
363
|
/>
|
|
259
364
|
|
|
260
365
|
<div className="flex-1 relative min-h-0">
|
|
@@ -277,18 +382,61 @@ export function WorkspaceMode() {
|
|
|
277
382
|
)}
|
|
278
383
|
|
|
279
384
|
{editorActiveFile && !diffMode && !isMedia && file && (
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
385
|
+
<div className="w-full h-full" onMouseUp={handleEditorMouseUp} onContextMenu={handleEditorContextMenu}>
|
|
386
|
+
<CodeEditor
|
|
387
|
+
content={file.content}
|
|
388
|
+
language={file.language}
|
|
389
|
+
filePath={editorActiveFile}
|
|
390
|
+
onChange={(content) => updateFileContent(editorActiveFile, content)}
|
|
391
|
+
onSave={() => saveFile(editorActiveFile)}
|
|
392
|
+
onCursorChange={setCursorPos}
|
|
393
|
+
onCmdK={({ line, coords }) => setInlinePrompt({ line, coords })}
|
|
394
|
+
viewRef={editorViewRef}
|
|
395
|
+
/>
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* Selection menu */}
|
|
400
|
+
{selection && !diffMode && (
|
|
401
|
+
<SelectionMenu
|
|
402
|
+
x={selection.x}
|
|
403
|
+
y={selection.y}
|
|
404
|
+
filePath={editorActiveFile}
|
|
405
|
+
lineStart={selection.lineStart}
|
|
406
|
+
lineEnd={selection.lineEnd}
|
|
407
|
+
selectedCode={selection.selectedCode}
|
|
408
|
+
onClose={() => setSelection(null)}
|
|
409
|
+
/>
|
|
410
|
+
)}
|
|
411
|
+
|
|
412
|
+
{/* Inline prompt (Cmd+K) */}
|
|
413
|
+
{inlinePrompt && !diffMode && (
|
|
414
|
+
<InlinePrompt
|
|
415
|
+
line={inlinePrompt.line}
|
|
416
|
+
coords={inlinePrompt.coords}
|
|
417
|
+
filePath={editorActiveFile}
|
|
418
|
+
onClose={() => setInlinePrompt(null)}
|
|
285
419
|
/>
|
|
286
420
|
)}
|
|
287
421
|
</div>
|
|
288
422
|
</>
|
|
289
423
|
)}
|
|
290
424
|
</div>
|
|
425
|
+
|
|
426
|
+
{/* AI Panel */}
|
|
427
|
+
{aiPanelOpen && !workspaceReviewMode && (
|
|
428
|
+
<div className="relative flex-shrink-0" style={{ width: aiPanelWidth }}>
|
|
429
|
+
<div
|
|
430
|
+
className="absolute top-0 left-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
431
|
+
onMouseDown={onAiPanelMouseDown}
|
|
432
|
+
/>
|
|
433
|
+
<AiPanel />
|
|
434
|
+
</div>
|
|
435
|
+
)}
|
|
291
436
|
</div>
|
|
437
|
+
|
|
438
|
+
{/* Quick Search modal */}
|
|
439
|
+
<QuickSearch />
|
|
292
440
|
</div>
|
|
293
441
|
);
|
|
294
442
|
}
|
|
@@ -3,7 +3,7 @@ import { useState, useRef, useEffect } from 'react';
|
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { ScrollArea } from '../ui/scroll-area';
|
|
6
|
-
import { Send, Bot, MessageSquare, X, ArrowRight } from 'lucide-react';
|
|
6
|
+
import { Send, Bot, MessageSquare, X, ArrowRight, FileCode, Terminal } from 'lucide-react';
|
|
7
7
|
import { timeAgo } from '../../lib/format';
|
|
8
8
|
|
|
9
9
|
function FormattedText({ text }) {
|
|
@@ -29,6 +29,38 @@ function FormattedText({ text }) {
|
|
|
29
29
|
);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function SnippetTag({ snippet, onRemove }) {
|
|
33
|
+
const isCode = snippet.type === 'code';
|
|
34
|
+
const Icon = isCode ? FileCode : Terminal;
|
|
35
|
+
const lines = snippet.code.split('\n').length;
|
|
36
|
+
|
|
37
|
+
let label;
|
|
38
|
+
if (isCode && snippet.filePath) {
|
|
39
|
+
const fileName = snippet.filePath.split('/').pop();
|
|
40
|
+
label = `${fileName}:${snippet.lineStart}-${snippet.lineEnd}`;
|
|
41
|
+
} else if (isCode) {
|
|
42
|
+
label = `${lines} line${lines !== 1 ? 's' : ''}`;
|
|
43
|
+
} else {
|
|
44
|
+
label = `Terminal · ${lines} line${lines !== 1 ? 's' : ''}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-accent/10 border border-accent/20 text-accent">
|
|
49
|
+
<Icon size={11} className="flex-shrink-0" />
|
|
50
|
+
<span className="text-2xs font-sans font-medium truncate max-w-[160px]">{label}</span>
|
|
51
|
+
{snippet.instruction && (
|
|
52
|
+
<span className="text-2xs text-accent/60 truncate max-w-[100px]">· {snippet.instruction}</span>
|
|
53
|
+
)}
|
|
54
|
+
<button
|
|
55
|
+
onClick={onRemove}
|
|
56
|
+
className="p-0.5 rounded hover:bg-accent/20 cursor-pointer flex-shrink-0"
|
|
57
|
+
>
|
|
58
|
+
<X size={9} />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
32
64
|
export function AiPanel() {
|
|
33
65
|
const agentId = useGrooveStore((s) => s.editorSelectedAgent);
|
|
34
66
|
const agents = useGrooveStore((s) => s.agents);
|
|
@@ -37,6 +69,8 @@ export function AiPanel() {
|
|
|
37
69
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
38
70
|
const isThinking = useGrooveStore((s) => agentId ? s.thinkingAgents?.has(agentId) : false);
|
|
39
71
|
const toggleAiPanel = useGrooveStore((s) => s.toggleAiPanel);
|
|
72
|
+
const pendingSnippet = useGrooveStore((s) => s.editorPendingSnippet);
|
|
73
|
+
const clearSnippet = useGrooveStore((s) => s.clearSnippet);
|
|
40
74
|
|
|
41
75
|
const agent = agents.find((a) => a.id === agentId);
|
|
42
76
|
const [input, setInput] = useState('');
|
|
@@ -45,6 +79,12 @@ export function AiPanel() {
|
|
|
45
79
|
const inputRef = useRef(null);
|
|
46
80
|
const isAtBottomRef = useRef(true);
|
|
47
81
|
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (pendingSnippet) {
|
|
84
|
+
inputRef.current?.focus();
|
|
85
|
+
}
|
|
86
|
+
}, [pendingSnippet]);
|
|
87
|
+
|
|
48
88
|
useEffect(() => {
|
|
49
89
|
const el = scrollRef.current;
|
|
50
90
|
if (!el) return;
|
|
@@ -61,14 +101,38 @@ export function AiPanel() {
|
|
|
61
101
|
}
|
|
62
102
|
}, [chatHistory.length, activityLog.length, sending, isThinking]);
|
|
63
103
|
|
|
104
|
+
function buildMessage(userText) {
|
|
105
|
+
const parts = [];
|
|
106
|
+
|
|
107
|
+
if (userText) parts.push(userText);
|
|
108
|
+
|
|
109
|
+
if (pendingSnippet) {
|
|
110
|
+
const s = pendingSnippet;
|
|
111
|
+
if (s.type === 'code' && s.filePath) {
|
|
112
|
+
if (s.instruction && !userText) parts.push(s.instruction);
|
|
113
|
+
parts.push(`File: ${s.filePath} (lines ${s.lineStart}-${s.lineEnd})`);
|
|
114
|
+
parts.push('```\n' + s.code + '\n```');
|
|
115
|
+
} else if (s.type === 'terminal') {
|
|
116
|
+
if (!userText) parts.push('Terminal output:');
|
|
117
|
+
parts.push('```\n' + s.code + '\n```');
|
|
118
|
+
} else {
|
|
119
|
+
parts.push('```\n' + s.code + '\n```');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return parts.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
64
126
|
async function handleSend() {
|
|
65
127
|
const text = input.trim();
|
|
66
|
-
if (!text || sending || !agentId) return;
|
|
128
|
+
if ((!text && !pendingSnippet) || sending || !agentId) return;
|
|
129
|
+
const message = buildMessage(text);
|
|
67
130
|
setInput('');
|
|
131
|
+
clearSnippet();
|
|
68
132
|
setSending(true);
|
|
69
133
|
isAtBottomRef.current = true;
|
|
70
134
|
try {
|
|
71
|
-
await instructAgent(agentId,
|
|
135
|
+
await instructAgent(agentId, message);
|
|
72
136
|
} catch { /* toast handles */ }
|
|
73
137
|
setSending(false);
|
|
74
138
|
inputRef.current?.focus();
|
|
@@ -85,6 +149,7 @@ export function AiPanel() {
|
|
|
85
149
|
from: 'agent', text: a.text, timestamp: a.timestamp, key: `act-${i}`,
|
|
86
150
|
}));
|
|
87
151
|
const messages = chatHistory.length > 0 ? chatHistory : recentActivity;
|
|
152
|
+
const canSend = (input.trim() || pendingSnippet) && !sending;
|
|
88
153
|
|
|
89
154
|
return (
|
|
90
155
|
<div className="flex flex-col h-full bg-surface-1 border-l border-border">
|
|
@@ -168,22 +233,28 @@ export function AiPanel() {
|
|
|
168
233
|
|
|
169
234
|
{/* Input */}
|
|
170
235
|
<div className="border-t border-border-subtle px-3 py-2 flex-shrink-0">
|
|
236
|
+
{/* Snippet tag */}
|
|
237
|
+
{pendingSnippet && (
|
|
238
|
+
<div className="mb-1.5">
|
|
239
|
+
<SnippetTag snippet={pendingSnippet} onRemove={clearSnippet} />
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
171
242
|
<div className="flex items-end gap-1.5">
|
|
172
243
|
<textarea
|
|
173
244
|
ref={inputRef}
|
|
174
245
|
value={input}
|
|
175
246
|
onChange={(e) => setInput(e.target.value)}
|
|
176
247
|
onKeyDown={onKeyDown}
|
|
177
|
-
placeholder=
|
|
248
|
+
placeholder={pendingSnippet ? 'Add a message (optional)...' : 'Message agent...'}
|
|
178
249
|
rows={1}
|
|
179
250
|
className="flex-1 resize-none rounded-lg px-3 py-1.5 text-xs bg-surface-0 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent/40 min-h-[32px] max-h-[100px]"
|
|
180
251
|
/>
|
|
181
252
|
<button
|
|
182
253
|
onClick={handleSend}
|
|
183
|
-
disabled={!
|
|
254
|
+
disabled={!canSend}
|
|
184
255
|
className={cn(
|
|
185
256
|
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors cursor-pointer flex-shrink-0',
|
|
186
|
-
|
|
257
|
+
canSend
|
|
187
258
|
? 'bg-accent text-surface-0 hover:bg-accent/80'
|
|
188
259
|
: 'bg-surface-3 text-text-4',
|
|
189
260
|
)}
|