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.
Files changed (75) 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 +225 -16
  5. package/node_modules/@groove-dev/daemon/src/index.js +2 -0
  6. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  7. package/node_modules/@groove-dev/daemon/src/introducer.js +17 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  9. package/node_modules/@groove-dev/daemon/src/keeper.js +277 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  12. package/node_modules/@groove-dev/daemon/src/validate.js +9 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  15. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  16. package/node_modules/@groove-dev/gui/package.json +1 -1
  17. package/node_modules/@groove-dev/gui/src/app.jsx +4 -0
  18. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +41 -17
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +10 -3
  20. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  22. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  23. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -86
  24. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  25. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +177 -0
  26. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +11 -6
  27. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  28. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +302 -20
  30. package/node_modules/@groove-dev/gui/src/views/agents.jsx +118 -60
  31. package/node_modules/@groove-dev/gui/src/views/editor.jsx +67 -219
  32. package/node_modules/@groove-dev/gui/src/views/memory.jsx +460 -0
  33. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  34. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/integrations-registry.json +12 -44
  38. package/packages/daemon/package.json +1 -1
  39. package/packages/daemon/src/api.js +225 -16
  40. package/packages/daemon/src/index.js +2 -0
  41. package/packages/daemon/src/integrations.js +10 -0
  42. package/packages/daemon/src/introducer.js +17 -1
  43. package/packages/daemon/src/journalist.js +169 -0
  44. package/packages/daemon/src/keeper.js +277 -0
  45. package/packages/daemon/src/model-lab.js +11 -0
  46. package/packages/daemon/src/process.js +76 -0
  47. package/packages/daemon/src/validate.js +9 -0
  48. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  49. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  50. package/packages/gui/dist/index.html +2 -2
  51. package/packages/gui/package.json +1 -1
  52. package/packages/gui/src/app.jsx +4 -0
  53. package/packages/gui/src/components/agents/agent-chat.jsx +41 -17
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +10 -3
  55. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  56. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  57. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  58. package/packages/gui/src/components/editor/file-tree.jsx +2 -86
  59. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  60. package/packages/gui/src/components/keeper/global-modals.jsx +177 -0
  61. package/packages/gui/src/components/layout/activity-bar.jsx +11 -6
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  63. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  64. package/packages/gui/src/stores/groove.js +302 -20
  65. package/packages/gui/src/views/agents.jsx +118 -60
  66. package/packages/gui/src/views/editor.jsx +67 -219
  67. package/packages/gui/src/views/memory.jsx +460 -0
  68. package/packages/gui/src/views/model-lab.jsx +1 -6
  69. package/packages/gui/src/views/models.jsx +658 -565
  70. package/plan_files/keeper-manual.md +295 -0
  71. package/plan_files/keeper-memory-system.md +223 -0
  72. package/node_modules/@groove-dev/gui/dist/assets/index-AkOtskHS.css +0 -1
  73. package/node_modules/@groove-dev/gui/dist/assets/index-B4uYLR57.js +0 -8694
  74. package/packages/gui/dist/assets/index-AkOtskHS.css +0 -1
  75. 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-B4uYLR57.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-AkOtskHS.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.139",
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",
@@ -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
- <textarea
258
- ref={inputRef}
259
- value={input}
260
- onChange={(e) => setInput(e.target.value)}
261
- onKeyDown={onKeyDown}
262
- placeholder={isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
263
- rows={1}
264
- className={cn(
265
- 'flex-1 resize-none rounded-lg px-3 py-1.5 text-xs',
266
- 'bg-surface-0 border text-text-0 font-sans',
267
- 'placeholder:text-text-4',
268
- 'focus:outline-none focus:ring-1',
269
- 'min-h-[32px] max-h-[120px]',
270
- 'border-border focus:ring-accent/40',
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) setTouchedFiles(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
- 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;
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 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
  )}