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.
Files changed (64) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +82 -16
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  7. package/node_modules/@groove-dev/daemon/src/keeper.js +3 -3
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  9. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  10. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -3
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +7 -2
  17. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  18. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  19. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  20. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  21. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  22. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  23. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  24. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +110 -32
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  27. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  28. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  29. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/integrations-registry.json +12 -44
  33. package/packages/daemon/package.json +1 -1
  34. package/packages/daemon/src/api.js +82 -16
  35. package/packages/daemon/src/integrations.js +10 -0
  36. package/packages/daemon/src/journalist.js +169 -0
  37. package/packages/daemon/src/keeper.js +3 -3
  38. package/packages/daemon/src/model-lab.js +11 -0
  39. package/packages/daemon/src/process.js +76 -0
  40. package/packages/daemon/src/validate.js +8 -0
  41. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  42. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  43. package/packages/gui/dist/index.html +2 -2
  44. package/packages/gui/package.json +1 -1
  45. package/packages/gui/src/components/agents/agent-chat.jsx +3 -3
  46. package/packages/gui/src/components/agents/agent-file-tree.jsx +7 -2
  47. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  48. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  49. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  50. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  51. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  52. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  53. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  54. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  55. package/packages/gui/src/stores/groove.js +110 -32
  56. package/packages/gui/src/views/agents.jsx +114 -56
  57. package/packages/gui/src/views/memory.jsx +9 -9
  58. package/packages/gui/src/views/model-lab.jsx +1 -6
  59. package/packages/gui/src/views/models.jsx +658 -565
  60. package/plan_files/keeper-manual.md +53 -42
  61. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  62. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  63. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  64. 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-DK6UIz0n.js"></script>
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-BV9CAiw1.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-A4e1gIDh.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.140",
3
+ "version": "0.27.141",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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)\]$/i.test(part)) {
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)\]|#[\w/.-]+)/i.test(input)
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
- return absPath;
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 agentId = useGrooveStore((s) => s.editorSelectedAgent);
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-[#1a1e25] border-b border-[#1e2229] flex-shrink-0">
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-white/5 transition-colors duration-75 flex-shrink-0',
81
+ 'border-r border-border-subtle transition-colors duration-75 flex-shrink-0',
78
82
  isActive
79
- ? 'bg-surface-0 text-text-1 border-b border-b-accent'
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
- <CodeEditor
281
- content={file.content}
282
- language={file.language}
283
- onChange={(content) => updateFileContent(editorActiveFile, content)}
284
- onSave={() => saveFile(editorActiveFile)}
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, text);
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="Message agent..."
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={!input.trim() || sending}
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
- input.trim() && !sending
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
- Plus, FolderPlus, Search, RefreshCw, Trash2, Pencil, FilePlus,
9
- ChevronsDownUp, PanelLeftClose, GitBranch,
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 key={tab.id} tabId={tab.id} visible={tab.id === activeTab} registerKill={registerKill} />
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 };