groove-dev 0.27.140 → 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 +82 -16
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
- package/node_modules/@groove-dev/daemon/src/keeper.js +3 -3
- 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 +8 -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/components/agents/agent-chat.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +7 -2
- 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 -49
- 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 +10 -10
- 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 +110 -32
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
- 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 +82 -16
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/journalist.js +169 -0
- package/packages/daemon/src/keeper.js +3 -3
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +76 -0
- package/packages/daemon/src/validate.js +8 -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/components/agents/agent-chat.jsx +3 -3
- package/packages/gui/src/components/agents/agent-file-tree.jsx +7 -2
- 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 -49
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
- 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 +110 -32
- package/packages/gui/src/views/agents.jsx +114 -56
- package/packages/gui/src/views/memory.jsx +9 -9
- 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 +53 -42
- package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
- package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
|
@@ -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>
|
|
@@ -39,9 +39,9 @@ function parseSegments(text) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function highlightKeeper(text) {
|
|
42
|
-
const parts = text.split(/(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|#[\w/.-]+)/gi);
|
|
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
43
|
return parts.map((part, i) => {
|
|
44
|
-
if (/^\[(?:save|append|update|delete|view|doc|link|read|instruct)\]
|
|
44
|
+
if (/^\[?(?:save|append|update|delete|view|doc|link|read|instruct)\]?$/i.test(part)) {
|
|
45
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
46
|
}
|
|
47
47
|
if (/^#[\w/.-]+$/.test(part)) {
|
|
@@ -288,7 +288,7 @@ export function AgentChat({ agent }) {
|
|
|
288
288
|
'focus:outline-none focus:ring-1',
|
|
289
289
|
'min-h-[32px] max-h-[120px]',
|
|
290
290
|
'border-border focus:ring-accent/40',
|
|
291
|
-
input && /(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]
|
|
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
292
|
? 'text-transparent caret-text-0'
|
|
293
293
|
: 'text-text-0',
|
|
294
294
|
)}
|
|
@@ -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([]);
|
|
@@ -366,7 +367,11 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
366
367
|
if (workingDir && absPath.startsWith(workingDir + '/')) {
|
|
367
368
|
return absPath.slice(workingDir.length + 1);
|
|
368
369
|
}
|
|
369
|
-
|
|
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;
|
|
370
375
|
}
|
|
371
376
|
|
|
372
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
|
)}
|
|
@@ -5,8 +5,8 @@ import { cn } from '../../lib/cn';
|
|
|
5
5
|
import { api } from '../../lib/api';
|
|
6
6
|
import {
|
|
7
7
|
ChevronRight, ChevronDown, File, Folder, FolderOpen,
|
|
8
|
-
|
|
9
|
-
ChevronsDownUp, PanelLeftClose,
|
|
8
|
+
FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
|
|
9
|
+
ChevronsDownUp, PanelLeftClose,
|
|
10
10
|
} from 'lucide-react';
|
|
11
11
|
import { ScrollArea } from '../ui/scroll-area';
|
|
12
12
|
|
|
@@ -240,25 +240,6 @@ function TreeDir({ dirPath, depth, activePath, onFileClick, expanded, onDirToggl
|
|
|
240
240
|
|
|
241
241
|
// ── Main FileTree ────────────────────────────────────────────
|
|
242
242
|
|
|
243
|
-
// ── Collapsible Section ──────────────────────────────────────
|
|
244
|
-
function CollapsibleSection({ title, icon: Icon, count, defaultOpen = true, children }) {
|
|
245
|
-
const [open, setOpen] = useState(defaultOpen);
|
|
246
|
-
return (
|
|
247
|
-
<div className="border-b border-border-subtle">
|
|
248
|
-
<button
|
|
249
|
-
onClick={() => setOpen(!open)}
|
|
250
|
-
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-2xs font-sans font-medium text-text-2 uppercase tracking-wide hover:bg-surface-4 transition-colors cursor-pointer"
|
|
251
|
-
>
|
|
252
|
-
{open ? <ChevronDown size={10} /> : <ChevronRight size={10} />}
|
|
253
|
-
<Icon size={11} className="text-text-3" />
|
|
254
|
-
<span className="flex-1 text-left">{title}</span>
|
|
255
|
-
{count > 0 && <span className="text-text-4">{count}</span>}
|
|
256
|
-
</button>
|
|
257
|
-
{open && children}
|
|
258
|
-
</div>
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
243
|
export function FileTree({ rootDir, onCollapse }) {
|
|
263
244
|
const treeCache = useGrooveStore((s) => s.editorTreeCache);
|
|
264
245
|
const activeFile = useGrooveStore((s) => s.editorActiveFile);
|
|
@@ -508,34 +489,6 @@ export function FileTree({ rootDir, onCollapse }) {
|
|
|
508
489
|
|
|
509
490
|
{/* Tree */}
|
|
510
491
|
<ScrollArea className="flex-1">
|
|
511
|
-
{/* Git Changes section */}
|
|
512
|
-
{gitChanges.length > 0 && (
|
|
513
|
-
<CollapsibleSection title="Git Changes" icon={GitBranch} count={gitChanges.length} defaultOpen={true}>
|
|
514
|
-
<div className="py-0.5">
|
|
515
|
-
{gitChanges.map((entry) => {
|
|
516
|
-
const name = entry.path.split('/').pop();
|
|
517
|
-
const statusColor = entry.status === 'A' || entry.status === '?' ? 'text-success' : entry.status === 'D' ? 'text-danger' : 'text-warning';
|
|
518
|
-
return (
|
|
519
|
-
<button
|
|
520
|
-
key={entry.path}
|
|
521
|
-
onClick={() => openFile(entry.path)}
|
|
522
|
-
className={cn(
|
|
523
|
-
'w-full flex items-center gap-1.5 px-3 py-[3px] text-xs font-sans cursor-pointer',
|
|
524
|
-
'hover:bg-surface-5 transition-colors text-left',
|
|
525
|
-
activeFile === entry.path ? 'bg-accent/10 text-text-0' : 'text-text-1',
|
|
526
|
-
)}
|
|
527
|
-
>
|
|
528
|
-
<File size={12} className={cn('flex-shrink-0', getFileColor(name))} />
|
|
529
|
-
<span className="truncate flex-1">{name}</span>
|
|
530
|
-
<span className={cn('text-2xs font-mono flex-shrink-0', statusColor)}>{entry.status}</span>
|
|
531
|
-
</button>
|
|
532
|
-
);
|
|
533
|
-
})}
|
|
534
|
-
</div>
|
|
535
|
-
</CollapsibleSection>
|
|
536
|
-
)}
|
|
537
|
-
|
|
538
|
-
{/* File Explorer */}
|
|
539
492
|
<div
|
|
540
493
|
className="py-1"
|
|
541
494
|
onDragOver={(e) => { if (!dragState.draggingPath) return; e.preventDefault(); setDragOverDir(null); }}
|
|
@@ -22,7 +22,7 @@ const THEME = {
|
|
|
22
22
|
let tabCounter = 0;
|
|
23
23
|
let spawnSeq = 0;
|
|
24
24
|
|
|
25
|
-
function TerminalInstance({ tabId, visible, registerKill }) {
|
|
25
|
+
function TerminalInstance({ tabId, visible, registerKill, onSelectionChange }) {
|
|
26
26
|
const containerRef = useRef(null);
|
|
27
27
|
const termRef = useRef(null);
|
|
28
28
|
const fitRef = useRef(null);
|
|
@@ -70,6 +70,11 @@ function TerminalInstance({ tabId, visible, registerKill }) {
|
|
|
70
70
|
termRef.current = term;
|
|
71
71
|
fitRef.current = fitAddon;
|
|
72
72
|
|
|
73
|
+
term.onSelectionChange(() => {
|
|
74
|
+
const text = term.getSelection();
|
|
75
|
+
onSelectionChange?.(text || '');
|
|
76
|
+
});
|
|
77
|
+
|
|
73
78
|
let spawnAttempts = 0;
|
|
74
79
|
function trySpawn() {
|
|
75
80
|
spawnAttempts++;
|
|
@@ -117,7 +122,6 @@ function TerminalInstance({ tabId, visible, registerKill }) {
|
|
|
117
122
|
});
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
// Fit first, then spawn — ensures PTY gets correct column count
|
|
121
125
|
requestAnimationFrame(() => {
|
|
122
126
|
try { fitAddon.fit(); } catch {}
|
|
123
127
|
trySpawn();
|
|
@@ -169,6 +173,7 @@ export function TerminalManager() {
|
|
|
169
173
|
|
|
170
174
|
const [tabs, setTabs] = useState([{ id: 'term-0', label: 'Terminal' }]);
|
|
171
175
|
const [activeTab, setActiveTab] = useState('term-0');
|
|
176
|
+
const [selectedText, setSelectedText] = useState('');
|
|
172
177
|
const killFns = useRef({});
|
|
173
178
|
|
|
174
179
|
const registerKill = useCallback((tabId, fn) => { killFns.current[tabId] = fn; }, []);
|
|
@@ -217,13 +222,19 @@ export function TerminalManager() {
|
|
|
217
222
|
onToggleFullHeight={() => setFullHeight(true)}
|
|
218
223
|
onMinimize={() => setFullHeight(false)}
|
|
219
224
|
onClose={() => setTerminalVisible(false)}
|
|
225
|
+
selectedText={selectedText}
|
|
220
226
|
>
|
|
221
227
|
{tabs.map((tab) => (
|
|
222
|
-
<TerminalInstance
|
|
228
|
+
<TerminalInstance
|
|
229
|
+
key={tab.id}
|
|
230
|
+
tabId={tab.id}
|
|
231
|
+
visible={tab.id === activeTab}
|
|
232
|
+
registerKill={registerKill}
|
|
233
|
+
onSelectionChange={tab.id === activeTab ? setSelectedText : undefined}
|
|
234
|
+
/>
|
|
223
235
|
))}
|
|
224
236
|
</TerminalPanel>
|
|
225
237
|
);
|
|
226
238
|
}
|
|
227
239
|
|
|
228
|
-
// Keep backward-compat export for existing imports
|
|
229
240
|
export { TerminalManager as TerminalEmulator };
|