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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +100 -23
- package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +171 -1
- package/node_modules/@groove-dev/daemon/src/keeper.js +2 -2
- package/node_modules/@groove-dev/daemon/src/memory.js +8 -5
- package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
- package/node_modules/@groove-dev/daemon/src/process.js +65 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
- package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
- package/node_modules/@groove-dev/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
- package/node_modules/@groove-dev/gui/dist/assets/index-Bjd91ufV.js +984 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BqdwIFn4.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -3
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.jsx +0 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +8 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +12 -8
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +109 -12
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +111 -0
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +70 -33
- package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +2 -68
- package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
- package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +1 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +151 -3
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/node_modules/@groove-dev/gui/src/stores/groove.js +107 -29
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +3 -71
- package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
- package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/integrations-registry.json +12 -44
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +100 -23
- package/packages/daemon/src/integrations.js +10 -0
- package/packages/daemon/src/introducer.js +1 -1
- package/packages/daemon/src/journalist.js +171 -1
- package/packages/daemon/src/keeper.js +2 -2
- package/packages/daemon/src/memory.js +8 -5
- package/packages/daemon/src/model-lab.js +11 -0
- package/packages/daemon/src/process.js +65 -0
- package/packages/daemon/src/rotator.js +25 -8
- package/packages/daemon/src/validate.js +8 -0
- package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
- package/packages/gui/dist/assets/index-Bjd91ufV.js +984 -0
- package/packages/gui/dist/assets/index-BqdwIFn4.css +1 -0
- package/packages/gui/dist/index.html +3 -3
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.jsx +0 -2
- package/packages/gui/src/components/agents/agent-chat.jsx +3 -4
- package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
- package/packages/gui/src/components/agents/agent-file-tree.jsx +12 -8
- package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
- package/packages/gui/src/components/agents/code-review.jsx +5 -4
- package/packages/gui/src/components/agents/workspace-mode.jsx +109 -12
- package/packages/gui/src/components/dashboard/context-gauges.jsx +111 -0
- package/packages/gui/src/components/dashboard/routing-chart.jsx +70 -33
- package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
- package/packages/gui/src/components/editor/code-editor.jsx +2 -68
- package/packages/gui/src/components/editor/file-tree.jsx +2 -49
- package/packages/gui/src/components/editor/terminal.jsx +15 -4
- package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
- package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +151 -3
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
- package/packages/gui/src/stores/groove.js +107 -29
- package/packages/gui/src/views/agents.jsx +114 -56
- package/packages/gui/src/views/dashboard.jsx +2 -0
- package/packages/gui/src/views/marketplace.jsx +3 -71
- package/packages/gui/src/views/memory.jsx +9 -9
- package/packages/gui/src/views/model-lab.jsx +1 -6
- package/packages/gui/src/views/models.jsx +658 -565
- package/plan_files/keeper-manual.md +53 -42
- package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +0 -78
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +0 -144
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +0 -187
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +0 -162
- package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
- package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
- package/packages/gui/src/components/toys/toy-card.jsx +0 -78
- package/packages/gui/src/components/toys/toy-creator.jsx +0 -144
- package/packages/gui/src/components/toys/toy-launcher.jsx +0 -187
- 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-
|
|
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-
|
|
80
|
+
'border-r border-border-subtle transition-colors duration-75 flex-shrink-0',
|
|
78
81
|
isActive
|
|
79
|
-
? 'bg-surface-
|
|
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
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
87
|
+
{/* Tier donut + legend */}
|
|
30
88
|
{total > 0 && (
|
|
31
|
-
<div className="
|
|
32
|
-
<
|
|
33
|
-
|
|
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">{
|
|
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
|
-
<
|
|
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,
|
|
135
|
+
await instructAgent(agentId, message);
|
|
72
136
|
} catch { /* toast handles */ }
|
|
73
137
|
setSending(false);
|
|
74
138
|
inputRef.current?.focus();
|
|
@@ -85,6 +149,7 @@ export function AiPanel() {
|
|
|
85
149
|
from: 'agent', text: a.text, timestamp: a.timestamp, key: `act-${i}`,
|
|
86
150
|
}));
|
|
87
151
|
const messages = chatHistory.length > 0 ? chatHistory : recentActivity;
|
|
152
|
+
const canSend = (input.trim() || pendingSnippet) && !sending;
|
|
88
153
|
|
|
89
154
|
return (
|
|
90
155
|
<div className="flex flex-col h-full bg-surface-1 border-l border-border">
|
|
@@ -168,22 +233,28 @@ export function AiPanel() {
|
|
|
168
233
|
|
|
169
234
|
{/* Input */}
|
|
170
235
|
<div className="border-t border-border-subtle px-3 py-2 flex-shrink-0">
|
|
236
|
+
{/* Snippet tag */}
|
|
237
|
+
{pendingSnippet && (
|
|
238
|
+
<div className="mb-1.5">
|
|
239
|
+
<SnippetTag snippet={pendingSnippet} onRemove={clearSnippet} />
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
171
242
|
<div className="flex items-end gap-1.5">
|
|
172
243
|
<textarea
|
|
173
244
|
ref={inputRef}
|
|
174
245
|
value={input}
|
|
175
246
|
onChange={(e) => setInput(e.target.value)}
|
|
176
247
|
onKeyDown={onKeyDown}
|
|
177
|
-
placeholder=
|
|
248
|
+
placeholder={pendingSnippet ? 'Add a message (optional)...' : 'Message agent...'}
|
|
178
249
|
rows={1}
|
|
179
250
|
className="flex-1 resize-none rounded-lg px-3 py-1.5 text-xs bg-surface-0 border border-border text-text-0 font-sans placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent/40 min-h-[32px] max-h-[100px]"
|
|
180
251
|
/>
|
|
181
252
|
<button
|
|
182
253
|
onClick={handleSend}
|
|
183
|
-
disabled={!
|
|
254
|
+
disabled={!canSend}
|
|
184
255
|
className={cn(
|
|
185
256
|
'w-8 h-8 flex items-center justify-center rounded-lg transition-colors cursor-pointer flex-shrink-0',
|
|
186
|
-
|
|
257
|
+
canSend
|
|
187
258
|
? 'bg-accent text-surface-0 hover:bg-accent/80'
|
|
188
259
|
: 'bg-surface-3 text-text-4',
|
|
189
260
|
)}
|