groove-dev 0.27.140 → 0.27.142

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 (98) 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 +100 -23
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
  7. package/node_modules/@groove-dev/daemon/src/journalist.js +171 -1
  8. package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
  9. package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  11. package/node_modules/@groove-dev/daemon/src/process.js +65 -0
  12. package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
  13. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  14. package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  15. package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  18. package/node_modules/@groove-dev/gui/package.json +1 -1
  19. package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -4
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
  22. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +12 -8
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
  24. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  25. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +109 -12
  26. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
  27. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
  28. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  29. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
  30. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  31. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  32. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  33. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
  34. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +151 -3
  35. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  36. package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -29
  37. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  38. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
  39. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
  40. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  41. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  42. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  43. package/package.json +1 -1
  44. package/packages/cli/package.json +1 -1
  45. package/packages/daemon/integrations-registry.json +12 -44
  46. package/packages/daemon/package.json +1 -1
  47. package/packages/daemon/src/api.js +100 -23
  48. package/packages/daemon/src/integrations.js +10 -0
  49. package/packages/daemon/src/introducer.js +1 -1
  50. package/packages/daemon/src/journalist.js +171 -1
  51. package/packages/daemon/src/keeper.js +2 -2
  52. package/packages/daemon/src/memory.js +8 -5
  53. package/packages/daemon/src/model-lab.js +11 -0
  54. package/packages/daemon/src/process.js +65 -0
  55. package/packages/daemon/src/rotator.js +25 -8
  56. package/packages/daemon/src/validate.js +8 -0
  57. package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
  58. package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
  59. package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
  60. package/packages/gui/dist/index.html +3 -3
  61. package/packages/gui/package.json +1 -1
  62. package/packages/gui/src/app.jsx +0 -2
  63. package/packages/gui/src/components/agents/agent-chat.jsx +3 -4
  64. package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
  65. package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -8
  66. package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
  67. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  68. package/packages/gui/src/components/agents/workspace-mode.jsx +109 -12
  69. package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
  70. package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
  71. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  72. package/packages/gui/src/components/editor/code-editor.jsx +2 -68
  73. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  74. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  75. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  76. package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
  77. package/packages/gui/src/components/layout/terminal-panel.jsx +151 -3
  78. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  79. package/packages/gui/src/stores/groove.js +107 -29
  80. package/packages/gui/src/views/agents.jsx +114 -56
  81. package/packages/gui/src/views/dashboard.jsx +2 -0
  82. package/packages/gui/src/views/marketplace.jsx +3 -71
  83. package/packages/gui/src/views/memory.jsx +9 -9
  84. package/packages/gui/src/views/model-lab.jsx +1 -6
  85. package/packages/gui/src/views/models.jsx +658 -565
  86. package/plan_files/keeper-manual.md +53 -42
  87. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  88. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  89. package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
  90. package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
  91. package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
  92. package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
  93. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  94. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  95. package/packages/gui/src/components/toys/toy-card.jsx +0 -78
  96. package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
  97. package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
  98. package/packages/gui/src/views/toys.jsx +0 -162
@@ -6,12 +6,15 @@ 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 { SelectionMenu } from '../editor/selection-menu';
10
+ import { InlinePrompt } from '../editor/inline-prompt';
11
+ import { QuickSearch } from '../editor/quick-search';
9
12
  import { Tooltip } from '../ui/tooltip';
10
13
  import { roleColor } from '../../lib/status';
11
14
  import { MediaViewer, isMediaFile } from '../editor/media-viewer';
12
15
  import {
13
16
  X, Code2, FileCode, GitCompareArrows,
14
- ClipboardCheck, Users, PanelLeftOpen,
17
+ ClipboardCheck, Users, PanelLeftOpen, Search,
15
18
  } from 'lucide-react';
16
19
 
17
20
  const TREE_DEFAULT = 220;
@@ -57,11 +60,11 @@ function AgentRail({ agents, activeId, onSelect }) {
57
60
  );
58
61
  }
59
62
 
60
- function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots, onBackToTeam, onToggleReview, reviewMode }) {
63
+ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots, onBackToTeam, onToggleReview, reviewMode, onCmdP }) {
61
64
  const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
62
65
 
63
66
  return (
64
- <div className="flex items-stretch h-8 bg-[#1a1e25] border-b border-[#1e2229] flex-shrink-0">
67
+ <div className="flex items-stretch h-8 bg-surface-2 border-b border-border flex-shrink-0">
65
68
  <div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
66
69
  {tabs.map((path) => {
67
70
  const isActive = path === activeFile;
@@ -74,9 +77,9 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
74
77
  key={path}
75
78
  className={cn(
76
79
  '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',
80
+ 'border-r border-border-subtle transition-colors duration-75 flex-shrink-0',
78
81
  isActive
79
- ? 'bg-surface-0 text-text-1 border-b border-b-accent'
82
+ ? 'bg-surface-1 text-text-1 border-b border-b-accent'
80
83
  : 'text-text-4 hover:text-text-2 hover:bg-surface-3 border-b border-b-transparent',
81
84
  )}
82
85
  onClick={() => onSelect(path)}
@@ -94,6 +97,14 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
94
97
  })}
95
98
  </div>
96
99
  <div className="flex items-center gap-0.5 px-2 border-l border-border-subtle flex-shrink-0">
100
+ <Tooltip content="Quick Search (Cmd+P)" side="bottom">
101
+ <button
102
+ onClick={onCmdP}
103
+ className="p-1.5 text-text-3 hover:text-text-1 hover:bg-surface-3 rounded cursor-pointer transition-colors"
104
+ >
105
+ <Search size={12} />
106
+ </button>
107
+ </Tooltip>
97
108
  {hasSnapshot && (
98
109
  <>
99
110
  <button
@@ -136,7 +147,6 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
136
147
  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
148
  >
138
149
  <Users size={12} />
139
- <span className="text-2xs">Team</span>
140
150
  </button>
141
151
  </Tooltip>
142
152
  </div>
@@ -153,6 +163,7 @@ export function WorkspaceMode() {
153
163
  const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
154
164
  const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
155
165
  const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
166
+ const setQuickSearchOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
156
167
 
157
168
  const editorFiles = useGrooveStore((s) => s.editorFiles);
158
169
  const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
@@ -168,7 +179,11 @@ export function WorkspaceMode() {
168
179
  const [treeWidth, setTreeWidth] = useState(TREE_DEFAULT);
169
180
  const [diffMode, setDiffMode] = useState(false);
170
181
  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
182
+ const [selection, setSelection] = useState(null);
183
+ const [inlinePrompt, setInlinePrompt] = useState(null);
184
+ const [cursorPos, setCursorPos] = useState({ line: 1, col: 1 });
171
185
 
186
+ const editorViewRef = useRef(null);
172
187
  const treeDragging = useRef(false);
173
188
  const startX = useRef(0);
174
189
  const startW = useRef(0);
@@ -177,6 +192,13 @@ export function WorkspaceMode() {
177
192
  setDiffMode(false);
178
193
  }, [editorActiveFile]);
179
194
 
195
+ // Set the selected agent in the store so AI features use it
196
+ useEffect(() => {
197
+ if (agent?.id) {
198
+ useGrooveStore.setState({ editorSelectedAgent: agent.id });
199
+ }
200
+ }, [agent?.id]);
201
+
180
202
  const onTreeMouseDown = useCallback((e) => {
181
203
  e.preventDefault();
182
204
  treeDragging.current = true;
@@ -195,6 +217,47 @@ export function WorkspaceMode() {
195
217
  document.addEventListener('mouseup', onUp);
196
218
  }, [treeWidth]);
197
219
 
220
+
221
+ const handleEditorMouseUp = useCallback(() => {
222
+ const view = editorViewRef.current;
223
+ if (!view) return;
224
+ const sel = view.state.selection.main;
225
+ if (sel.empty) { setSelection(null); return; }
226
+ const text = view.state.sliceDoc(sel.from, sel.to);
227
+ if (!text.trim()) { setSelection(null); return; }
228
+ const fromLine = view.state.doc.lineAt(sel.from);
229
+ const toLine = view.state.doc.lineAt(sel.to);
230
+ const coords = view.coordsAtPos(sel.to);
231
+ if (coords) {
232
+ setSelection({
233
+ x: Math.min(coords.left + 10, window.innerWidth - 220),
234
+ y: coords.bottom + 4,
235
+ lineStart: fromLine.number,
236
+ lineEnd: toLine.number,
237
+ selectedCode: text,
238
+ });
239
+ }
240
+ }, []);
241
+
242
+ const handleEditorContextMenu = useCallback((e) => {
243
+ const view = editorViewRef.current;
244
+ if (!view) return;
245
+ const sel = view.state.selection.main;
246
+ if (sel.empty) return;
247
+ const text = view.state.sliceDoc(sel.from, sel.to);
248
+ if (!text.trim()) return;
249
+ e.preventDefault();
250
+ const fromLine = view.state.doc.lineAt(sel.from);
251
+ const toLine = view.state.doc.lineAt(sel.to);
252
+ setSelection({
253
+ x: Math.min(e.clientX, window.innerWidth - 220),
254
+ y: e.clientY + 4,
255
+ lineStart: fromLine.number,
256
+ lineEnd: toLine.number,
257
+ selectedCode: text,
258
+ });
259
+ }, []);
260
+
198
261
  if (!agent) {
199
262
  return (
200
263
  <div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
@@ -240,7 +303,7 @@ export function WorkspaceMode() {
240
303
  {/* Editor Area */}
241
304
  <div className="flex-1 flex flex-col min-w-0 bg-surface-1">
242
305
  {workspaceReviewMode ? (
243
- <CodeReview agentId={agent.id} />
306
+ <CodeReview agentId={agent.id} onBack={toggleReviewMode} />
244
307
  ) : (
245
308
  <>
246
309
  <TabBar
@@ -255,6 +318,7 @@ export function WorkspaceMode() {
255
318
  onBackToTeam={() => setWorkspaceMode(false)}
256
319
  onToggleReview={toggleReviewMode}
257
320
  reviewMode={workspaceReviewMode}
321
+ onCmdP={() => setQuickSearchOpen(true)}
258
322
  />
259
323
 
260
324
  <div className="flex-1 relative min-h-0">
@@ -277,18 +341,51 @@ export function WorkspaceMode() {
277
341
  )}
278
342
 
279
343
  {editorActiveFile && !diffMode && !isMedia && file && (
280
- <CodeEditor
281
- content={file.content}
282
- language={file.language}
283
- onChange={(content) => updateFileContent(editorActiveFile, content)}
284
- onSave={() => saveFile(editorActiveFile)}
344
+ <div className="w-full h-full" onMouseUp={handleEditorMouseUp} onContextMenu={handleEditorContextMenu}>
345
+ <CodeEditor
346
+ content={file.content}
347
+ language={file.language}
348
+ filePath={editorActiveFile}
349
+ onChange={(content) => updateFileContent(editorActiveFile, content)}
350
+ onSave={() => saveFile(editorActiveFile)}
351
+ onCursorChange={setCursorPos}
352
+ onCmdK={({ line, coords }) => setInlinePrompt({ line, coords })}
353
+ viewRef={editorViewRef}
354
+ />
355
+ </div>
356
+ )}
357
+
358
+ {/* Selection menu */}
359
+ {selection && !diffMode && (
360
+ <SelectionMenu
361
+ x={selection.x}
362
+ y={selection.y}
363
+ filePath={editorActiveFile}
364
+ lineStart={selection.lineStart}
365
+ lineEnd={selection.lineEnd}
366
+ selectedCode={selection.selectedCode}
367
+ onClose={() => setSelection(null)}
368
+ />
369
+ )}
370
+
371
+ {/* Inline prompt (Cmd+K) */}
372
+ {inlinePrompt && !diffMode && (
373
+ <InlinePrompt
374
+ line={inlinePrompt.line}
375
+ coords={inlinePrompt.coords}
376
+ filePath={editorActiveFile}
377
+ onClose={() => setInlinePrompt(null)}
285
378
  />
286
379
  )}
287
380
  </div>
288
381
  </>
289
382
  )}
290
383
  </div>
384
+
291
385
  </div>
386
+
387
+ {/* Quick Search modal */}
388
+ <QuickSearch />
292
389
  </div>
293
390
  );
294
391
  }
@@ -0,0 +1,111 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { memo } from 'react';
3
+ import { HEX } from '../../lib/theme-hex';
4
+
5
+ const SIZE = 36;
6
+ const STROKE = 3;
7
+ const RADIUS = (SIZE - STROKE) / 2;
8
+ const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
9
+ const START_ANGLE = -90;
10
+
11
+ function gaugeColor(pct) {
12
+ if (pct > 80) return HEX.danger;
13
+ if (pct > 60) return HEX.warning;
14
+ return HEX.success;
15
+ }
16
+
17
+ function MiniGauge({ name, pct, threshold }) {
18
+ const color = gaugeColor(pct);
19
+ const dashLen = (pct / 100) * CIRCUMFERENCE;
20
+
21
+ return (
22
+ <div className="flex flex-col items-center gap-0.5" title={`${name}: ${pct}% context used`}>
23
+ <svg width={SIZE} height={SIZE} viewBox={`0 0 ${SIZE} ${SIZE}`}>
24
+ <circle
25
+ cx={SIZE / 2} cy={SIZE / 2} r={RADIUS}
26
+ fill="none" strokeWidth={STROKE}
27
+ className="stroke-surface-4"
28
+ />
29
+ <circle
30
+ cx={SIZE / 2} cy={SIZE / 2} r={RADIUS}
31
+ fill="none" strokeWidth={STROKE}
32
+ strokeLinecap="round"
33
+ style={{
34
+ stroke: color,
35
+ strokeDasharray: `${dashLen} ${CIRCUMFERENCE - dashLen}`,
36
+ strokeDashoffset: 0,
37
+ transition: 'stroke-dasharray 0.5s ease',
38
+ }}
39
+ transform={`rotate(${START_ANGLE} ${SIZE / 2} ${SIZE / 2})`}
40
+ />
41
+ {threshold && (
42
+ <circle
43
+ cx={SIZE / 2} cy={SIZE / 2} r={RADIUS}
44
+ fill="none" strokeWidth={1}
45
+ strokeLinecap="butt"
46
+ style={{
47
+ stroke: HEX.purple,
48
+ strokeDasharray: `1 ${CIRCUMFERENCE - 1}`,
49
+ strokeDashoffset: -(threshold / 100) * CIRCUMFERENCE,
50
+ }}
51
+ transform={`rotate(${START_ANGLE} ${SIZE / 2} ${SIZE / 2})`}
52
+ />
53
+ )}
54
+ <text
55
+ x={SIZE / 2} y={SIZE / 2 + 1}
56
+ textAnchor="middle" dominantBaseline="central"
57
+ className="fill-text-1 font-mono font-semibold"
58
+ style={{ fontSize: 9 }}
59
+ >
60
+ {pct}
61
+ </text>
62
+ </svg>
63
+ <span className="text-2xs font-mono text-text-3 truncate max-w-[40px] leading-none">{name}</span>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ function FleetSummary({ zones }) {
69
+ return (
70
+ <div className="flex items-center gap-2 text-2xs font-mono">
71
+ <span className="text-success">{zones.healthy}</span>
72
+ <span className="text-text-4">/</span>
73
+ <span className="text-warning">{zones.warning}</span>
74
+ <span className="text-text-4">/</span>
75
+ <span className="text-danger">{zones.critical}</span>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ const ContextGauges = memo(function ContextGauges({ agentBreakdown }) {
81
+ const alive = (agentBreakdown || []).filter(
82
+ (a) => a.status === 'running' || a.status === 'starting',
83
+ );
84
+ if (alive.length === 0) return null;
85
+
86
+ const zones = { healthy: 0, warning: 0, critical: 0 };
87
+ for (const a of alive) {
88
+ const pct = Math.round((a.contextUsage || 0) * 100);
89
+ if (pct > 80) zones.critical++;
90
+ else if (pct > 60) zones.warning++;
91
+ else zones.healthy++;
92
+ }
93
+
94
+ return (
95
+ <div className="px-3 py-2 flex-shrink-0 border-b border-border">
96
+ <div className="flex items-center justify-between mb-1.5">
97
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Context Health</span>
98
+ <FleetSummary zones={zones} />
99
+ </div>
100
+ <div className="flex items-start gap-2 overflow-x-auto">
101
+ {alive.map((a) => {
102
+ const pct = Math.round((a.contextUsage || 0) * 100);
103
+ const threshold = a.rotationThreshold ? Math.round(a.rotationThreshold * 100) : null;
104
+ return <MiniGauge key={a.id} name={a.name} pct={pct} threshold={threshold} />;
105
+ })}
106
+ </div>
107
+ </div>
108
+ );
109
+ });
110
+
111
+ export { ContextGauges };
@@ -6,6 +6,65 @@ import { fmtNum, fmtPct } from '../../lib/format';
6
6
  const TIER_COLORS = { heavy: HEX.danger, medium: HEX.warning, light: HEX.success };
7
7
  const TIER_LABELS = { heavy: 'Heavy', medium: 'Medium', light: 'Light' };
8
8
 
9
+ const DONUT_SIZE = 80;
10
+ const DONUT_STROKE = 6;
11
+ const DONUT_RADIUS = (DONUT_SIZE - DONUT_STROKE) / 2;
12
+ const DONUT_CIRCUMFERENCE = 2 * Math.PI * DONUT_RADIUS;
13
+
14
+ function TierDonut({ byTier, total, tiers }) {
15
+ let offset = 0;
16
+ const segments = [];
17
+ for (const tier of tiers) {
18
+ const count = byTier[tier] || 0;
19
+ if (count === 0) continue;
20
+ const pct = count / total;
21
+ const dashLen = pct * DONUT_CIRCUMFERENCE;
22
+ segments.push({ tier, dashLen, offset });
23
+ offset += dashLen;
24
+ }
25
+
26
+ const heavyPct = total > 0 ? Math.round(((byTier.heavy || 0) / total) * 100) : 0;
27
+
28
+ return (
29
+ <svg width={DONUT_SIZE} height={DONUT_SIZE} viewBox={`0 0 ${DONUT_SIZE} ${DONUT_SIZE}`} className="flex-shrink-0">
30
+ <circle
31
+ cx={DONUT_SIZE / 2} cy={DONUT_SIZE / 2} r={DONUT_RADIUS}
32
+ fill="none" strokeWidth={DONUT_STROKE}
33
+ className="stroke-surface-4"
34
+ />
35
+ {segments.map((seg) => (
36
+ <circle
37
+ key={seg.tier}
38
+ cx={DONUT_SIZE / 2} cy={DONUT_SIZE / 2} r={DONUT_RADIUS}
39
+ fill="none" strokeWidth={DONUT_STROKE}
40
+ strokeLinecap="butt"
41
+ style={{
42
+ stroke: TIER_COLORS[seg.tier],
43
+ strokeDasharray: `${seg.dashLen} ${DONUT_CIRCUMFERENCE - seg.dashLen}`,
44
+ strokeDashoffset: -seg.offset,
45
+ }}
46
+ transform={`rotate(-90 ${DONUT_SIZE / 2} ${DONUT_SIZE / 2})`}
47
+ />
48
+ ))}
49
+ <text
50
+ x={DONUT_SIZE / 2} y={DONUT_SIZE / 2 - 2}
51
+ textAnchor="middle" dominantBaseline="central"
52
+ className="fill-text-0 text-sm font-mono font-semibold"
53
+ >
54
+ {fmtNum(total)}
55
+ </text>
56
+ <text
57
+ x={DONUT_SIZE / 2} y={DONUT_SIZE / 2 + 11}
58
+ textAnchor="middle" dominantBaseline="central"
59
+ className="fill-text-3 font-mono"
60
+ style={{ fontSize: 7 }}
61
+ >
62
+ decisions
63
+ </text>
64
+ </svg>
65
+ );
66
+ }
67
+
9
68
  const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
10
69
  if (!routing) return null;
11
70
 
@@ -13,7 +72,6 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
13
72
  const tiers = ['heavy', 'medium', 'light'];
14
73
  const total = tiers.reduce((s, t) => s + (byTier[t] || 0), 0);
15
74
 
16
- // Build model usage from agent breakdown
17
75
  const modelUsage = {};
18
76
  for (const a of (agentBreakdown || [])) {
19
77
  const model = a.model || 'default';
@@ -26,43 +84,27 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
26
84
 
27
85
  return (
28
86
  <div className="flex flex-col h-full px-3 py-3 overflow-y-auto">
29
- {/* Tier distribution bar */}
87
+ {/* Tier donut + legend */}
30
88
  {total > 0 && (
31
- <div className="space-y-1.5 mb-3">
32
- <div className="flex items-center gap-2">
33
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Tier Distribution</span>
34
- <span className="text-2xs font-mono text-text-4 ml-auto tabular-nums">{fmtNum(total)} decisions</span>
35
- </div>
36
- {/* Stacked horizontal bar */}
37
- <div className="h-0.5 bg-surface-2 rounded-sm overflow-hidden flex">
89
+ <div className="flex items-center gap-3 mb-3">
90
+ <TierDonut byTier={byTier} total={total} tiers={tiers} />
91
+ <div className="flex flex-col gap-1.5 flex-1 min-w-0">
38
92
  {tiers.map((tier) => {
39
93
  const count = byTier[tier] || 0;
40
94
  if (count === 0) return null;
41
95
  const pct = (count / total) * 100;
42
- return (
43
- <div
44
- key={tier}
45
- className="h-full transition-all duration-500"
46
- style={{ width: `${pct}%`, background: TIER_COLORS[tier] }}
47
- title={`${TIER_LABELS[tier]}: ${count} (${Math.round(pct)}%)`}
48
- />
49
- );
50
- })}
51
- </div>
52
- {/* Tier legend */}
53
- <div className="flex items-center gap-3">
54
- {tiers.map((tier) => {
55
- const count = byTier[tier] || 0;
56
- if (count === 0) return null;
57
- const pct = total > 0 ? (count / total) * 100 : 0;
58
96
  return (
59
97
  <div key={tier} className="flex items-center gap-1.5">
60
98
  <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ background: TIER_COLORS[tier] }} />
61
- <span className="text-2xs font-mono text-text-2">{TIER_LABELS[tier]}</span>
62
- <span className="text-2xs font-mono text-text-4 tabular-nums">{fmtPct(pct)}</span>
99
+ <span className="text-2xs font-mono text-text-2 flex-1">{TIER_LABELS[tier]}</span>
100
+ <span className="text-2xs font-mono text-text-4 tabular-nums">{count}</span>
101
+ <span className="text-2xs font-mono text-text-4 tabular-nums w-8 text-right">{fmtPct(pct)}</span>
63
102
  </div>
64
103
  );
65
104
  })}
105
+ {autoRoutedCount > 0 && (
106
+ <div className="text-2xs font-mono text-text-4 mt-0.5">{autoRoutedCount} auto-routed</div>
107
+ )}
66
108
  </div>
67
109
  </div>
68
110
  )}
@@ -70,12 +112,7 @@ const RoutingChart = memo(function RoutingChart({ routing, agentBreakdown }) {
70
112
  {/* Model usage breakdown */}
71
113
  {modelEntries.length > 0 && (
72
114
  <div className="space-y-1.5 flex-1">
73
- <div className="flex items-center gap-2">
74
- <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Models in Use</span>
75
- {autoRoutedCount > 0 && (
76
- <span className="text-2xs font-mono text-text-4 ml-auto">{autoRoutedCount} auto</span>
77
- )}
78
- </div>
115
+ <span className="text-2xs font-mono text-text-3 uppercase tracking-wider">Models in Use</span>
79
116
  <div className="space-y-1.5">
80
117
  {modelEntries.map(([model, usage]) => {
81
118
  const barPct = maxModelTokens > 0 ? (usage.tokens / maxModelTokens) * 100 : 0;
@@ -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
  )}