groove-dev 0.27.141 → 0.27.143
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/CLAUDE.md +7 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +18 -7
- package/node_modules/@groove-dev/daemon/src/introducer.js +1 -1
- package/node_modules/@groove-dev/daemon/src/journalist.js +3 -2
- 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/process.js +5 -16
- package/node_modules/@groove-dev/daemon/src/rotator.js +25 -8
- 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-CCVvAoQn.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +984 -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 +73 -17
- 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 +5 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +79 -5
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -53
- 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/code-editor.jsx +2 -68
- package/node_modules/@groove-dev/gui/src/components/editor/selection-menu.jsx +2 -0
- 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 +0 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +3 -3
- 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/models.jsx +5 -6
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +18 -7
- package/packages/daemon/src/introducer.js +1 -1
- package/packages/daemon/src/journalist.js +3 -2
- package/packages/daemon/src/keeper.js +2 -2
- package/packages/daemon/src/memory.js +8 -5
- package/packages/daemon/src/process.js +5 -16
- package/packages/daemon/src/rotator.js +25 -8
- package/packages/gui/dist/assets/{codemirror-BQqYnZfL.js → codemirror-BYKpdS2W.js} +10 -10
- package/packages/gui/dist/assets/index-CCVvAoQn.css +1 -0
- package/packages/gui/dist/assets/index-DGIv_TRm.js +984 -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 +73 -17
- package/packages/gui/src/components/agents/agent-feed.jsx +8 -2
- package/packages/gui/src/components/agents/agent-file-tree.jsx +5 -6
- package/packages/gui/src/components/agents/agent-panel.jsx +79 -5
- package/packages/gui/src/components/agents/workspace-mode.jsx +2 -53
- 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/code-editor.jsx +2 -68
- package/packages/gui/src/components/editor/selection-menu.jsx +2 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +1 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +0 -1
- package/packages/gui/src/stores/groove.js +3 -3
- package/packages/gui/src/views/dashboard.jsx +2 -0
- package/packages/gui/src/views/marketplace.jsx +3 -71
- package/packages/gui/src/views/models.jsx +5 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +0 -8696
- 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-A4e1gIDh.css +0 -1
- package/packages/gui/dist/assets/index-P1hsM27-.js +0 -8696
- 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,12 @@
|
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
8
8
|
<title>Groove GUI</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DGIv_TRm.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
|
-
<link rel="modulepreload" crossorigin href="/assets/codemirror-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
|
|
14
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CCVvAoQn.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -20,7 +20,6 @@ import TeamsView from './views/teams';
|
|
|
20
20
|
import SettingsView from './views/settings';
|
|
21
21
|
import ModelsView from './views/models';
|
|
22
22
|
import FederationView from './views/federation';
|
|
23
|
-
import ToysView from './views/toys';
|
|
24
23
|
import ModelLabView from './views/model-lab';
|
|
25
24
|
import NetworkView from './views/network';
|
|
26
25
|
import ChatView from './views/chat';
|
|
@@ -73,7 +72,6 @@ function ViewRouter() {
|
|
|
73
72
|
case 'editor': content = <EditorView />; break;
|
|
74
73
|
case 'dashboard': content = <DashboardView />; break;
|
|
75
74
|
case 'marketplace': content = <MarketplaceView />; break;
|
|
76
|
-
case 'toys': content = <ToysView />; break;
|
|
77
75
|
case 'teams': content = <TeamsView />; break;
|
|
78
76
|
case 'models': content = <ModelsView />; break;
|
|
79
77
|
case 'model-lab': content = <ModelLabView />; break;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useRef, useEffect } from 'react';
|
|
3
|
-
import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip, Square } from 'lucide-react';
|
|
3
|
+
import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip, Square, FileCode, Terminal as TerminalIcon, X } from 'lucide-react';
|
|
4
4
|
import { useGrooveStore } from '../../stores/groove';
|
|
5
5
|
import { cn } from '../../lib/cn';
|
|
6
6
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
@@ -39,9 +39,9 @@ function parseSegments(text) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function highlightKeeper(text) {
|
|
42
|
-
const parts = text.split(/(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]
|
|
42
|
+
const parts = text.split(/(\[(?:save|append|update|delete|view|doc|link|read|instruct)\]|#[\w/.-]+)/gi);
|
|
43
43
|
return parts.map((part, i) => {
|
|
44
|
-
if (/^\[
|
|
44
|
+
if (/^\[(?:save|append|update|delete|view|doc|link|read|instruct)\]$/i.test(part)) {
|
|
45
45
|
return <span key={i} className="px-1 py-0.5 rounded bg-accent/15 text-accent font-semibold font-mono text-2xs">{part}</span>;
|
|
46
46
|
}
|
|
47
47
|
if (/^#[\w/.-]+$/.test(part)) {
|
|
@@ -145,12 +145,40 @@ function TypingIndicator() {
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function SnippetTag({ snippet, onRemove }) {
|
|
149
|
+
const isCode = snippet.type === 'code';
|
|
150
|
+
const Icon = isCode ? FileCode : TerminalIcon;
|
|
151
|
+
const lines = snippet.code.split('\n').length;
|
|
152
|
+
let label;
|
|
153
|
+
if (isCode && snippet.filePath) {
|
|
154
|
+
const fileName = snippet.filePath.split('/').pop();
|
|
155
|
+
label = `${fileName}:${snippet.lineStart}-${snippet.lineEnd}`;
|
|
156
|
+
} else {
|
|
157
|
+
label = `${isCode ? '' : 'Terminal · '}${lines} line${lines !== 1 ? 's' : ''}`;
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
<div className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-accent/10 border border-accent/20 text-accent">
|
|
161
|
+
<Icon size={11} className="flex-shrink-0" />
|
|
162
|
+
<span className="text-2xs font-sans font-medium truncate max-w-[160px]">{label}</span>
|
|
163
|
+
{snippet.instruction && (
|
|
164
|
+
<span className="text-2xs text-accent/60 truncate max-w-[100px]">· {snippet.instruction}</span>
|
|
165
|
+
)}
|
|
166
|
+
<button onClick={onRemove} className="p-0.5 rounded hover:bg-accent/20 cursor-pointer flex-shrink-0">
|
|
167
|
+
<X size={9} />
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
148
173
|
export function AgentChat({ agent }) {
|
|
149
174
|
const chatHistory = useGrooveStore((s) => s.chatHistory[agent.id]) || EMPTY;
|
|
150
175
|
const activityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
|
|
151
176
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
152
177
|
const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
|
|
153
178
|
|
|
179
|
+
const pendingSnippet = useGrooveStore((s) => s.editorPendingSnippet);
|
|
180
|
+
const clearSnippet = useGrooveStore((s) => s.clearSnippet);
|
|
181
|
+
|
|
154
182
|
const storeInput = useGrooveStore((s) => s.chatInputs[agent.id] || '');
|
|
155
183
|
const setStoreInput = (val) => useGrooveStore.setState((s) => ({ chatInputs: { ...s.chatInputs, [agent.id]: val } }));
|
|
156
184
|
const input = storeInput;
|
|
@@ -162,6 +190,10 @@ export function AgentChat({ agent }) {
|
|
|
162
190
|
const fileInputRef = useRef(null);
|
|
163
191
|
const isAtBottomRef = useRef(true);
|
|
164
192
|
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
if (pendingSnippet) inputRef.current?.focus();
|
|
195
|
+
}, [pendingSnippet]);
|
|
196
|
+
|
|
165
197
|
useEffect(() => {
|
|
166
198
|
const el = scrollRef.current;
|
|
167
199
|
if (!el) return;
|
|
@@ -192,16 +224,30 @@ export function AgentChat({ agent }) {
|
|
|
192
224
|
|
|
193
225
|
async function handleSend() {
|
|
194
226
|
const text = input.trim();
|
|
195
|
-
if (!text || sending) return;
|
|
227
|
+
if ((!text && !pendingSnippet) || sending) return;
|
|
228
|
+
const parts = [];
|
|
229
|
+
if (text) parts.push(text);
|
|
230
|
+
if (pendingSnippet) {
|
|
231
|
+
const s = pendingSnippet;
|
|
232
|
+
if (s.type === 'code' && s.filePath) {
|
|
233
|
+
if (s.instruction && !text) parts.push(s.instruction);
|
|
234
|
+
parts.push(`File: ${s.filePath} (lines ${s.lineStart}-${s.lineEnd})`);
|
|
235
|
+
parts.push('```\n' + s.code + '\n```');
|
|
236
|
+
} else if (s.code) {
|
|
237
|
+
parts.push('```\n' + s.code + '\n```');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const message = parts.join('\n\n');
|
|
196
241
|
setInput('');
|
|
197
242
|
setAttachedFiles([]);
|
|
243
|
+
clearSnippet();
|
|
198
244
|
setSending(true);
|
|
199
245
|
isAtBottomRef.current = true;
|
|
200
246
|
requestAnimationFrame(() => {
|
|
201
247
|
if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
202
248
|
});
|
|
203
249
|
try {
|
|
204
|
-
await instructAgent(agent.id,
|
|
250
|
+
await instructAgent(agent.id, message);
|
|
205
251
|
} catch { /* toast handles */ }
|
|
206
252
|
setSending(false);
|
|
207
253
|
inputRef.current?.focus();
|
|
@@ -251,6 +297,22 @@ export function AgentChat({ agent }) {
|
|
|
251
297
|
|
|
252
298
|
{/* ── Input area ──────────────────────────────────── */}
|
|
253
299
|
<div className="border-t border-border-subtle px-3 py-2 bg-surface-1">
|
|
300
|
+
{pendingSnippet && (
|
|
301
|
+
<div className="mb-1.5">
|
|
302
|
+
<SnippetTag snippet={pendingSnippet} onRemove={clearSnippet} />
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
{input && /\[(?:save|append|update|delete|view|doc|link|read|instruct)\]/i.test(input) && (() => {
|
|
306
|
+
const cmdMatch = input.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
|
|
307
|
+
const tags = (input.match(/#[\w/.-]+/g) || []);
|
|
308
|
+
return (
|
|
309
|
+
<div className="flex items-center gap-1.5 px-3 py-1 mb-1.5 rounded-md bg-accent/5 border border-accent/10">
|
|
310
|
+
<span className="px-1.5 py-0.5 rounded bg-accent/15 text-accent font-semibold font-mono text-2xs">{cmdMatch[0]}</span>
|
|
311
|
+
{tags.map((tag, i) => <span key={i} className="text-accent font-medium text-2xs">{tag}</span>)}
|
|
312
|
+
<span className="text-2xs text-text-4 ml-auto">memory command</span>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
})()}
|
|
254
316
|
<div className="flex items-end gap-1.5">
|
|
255
317
|
<input
|
|
256
318
|
ref={fileInputRef}
|
|
@@ -267,19 +329,13 @@ export function AgentChat({ agent }) {
|
|
|
267
329
|
>
|
|
268
330
|
<Paperclip size={14} />
|
|
269
331
|
</button>
|
|
270
|
-
<div className="flex-1
|
|
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>
|
|
332
|
+
<div className="flex-1">
|
|
277
333
|
<textarea
|
|
278
334
|
ref={inputRef}
|
|
279
335
|
value={input}
|
|
280
336
|
onChange={(e) => setInput(e.target.value)}
|
|
281
337
|
onKeyDown={onKeyDown}
|
|
282
|
-
placeholder={isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
|
|
338
|
+
placeholder={pendingSnippet ? 'Add a message (optional)...' : isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
|
|
283
339
|
rows={1}
|
|
284
340
|
className={cn(
|
|
285
341
|
'w-full resize-none rounded-lg px-3 py-1.5 text-xs',
|
|
@@ -288,8 +344,8 @@ export function AgentChat({ agent }) {
|
|
|
288
344
|
'focus:outline-none focus:ring-1',
|
|
289
345
|
'min-h-[32px] max-h-[120px]',
|
|
290
346
|
'border-border focus:ring-accent/40',
|
|
291
|
-
input &&
|
|
292
|
-
? 'text-
|
|
347
|
+
input && /\[(?:save|append|update|delete|view|doc|link|read|instruct)\]/i.test(input)
|
|
348
|
+
? 'text-accent'
|
|
293
349
|
: 'text-text-0',
|
|
294
350
|
)}
|
|
295
351
|
/>
|
|
@@ -305,11 +361,11 @@ export function AgentChat({ agent }) {
|
|
|
305
361
|
)}
|
|
306
362
|
<button
|
|
307
363
|
onClick={handleSend}
|
|
308
|
-
disabled={!input.trim() || sending}
|
|
364
|
+
disabled={(!input.trim() && !pendingSnippet) || sending}
|
|
309
365
|
className={cn(
|
|
310
366
|
'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
|
|
311
367
|
'disabled:opacity-20 disabled:cursor-not-allowed',
|
|
312
|
-
input.trim()
|
|
368
|
+
(input.trim() || pendingSnippet)
|
|
313
369
|
? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
|
|
314
370
|
: 'bg-surface-4 text-text-4',
|
|
315
371
|
)}
|
|
@@ -483,11 +483,17 @@ function BootSequence({ agent }) {
|
|
|
483
483
|
// ── Main Feed ────────────────────────────────────────────────
|
|
484
484
|
|
|
485
485
|
export function AgentFeed({ agent }) {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
486
|
+
const rawChatHistory = useGrooveStore((s) => s.chatHistory[agent.id]) || EMPTY;
|
|
487
|
+
const rawActivityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
|
|
488
488
|
const instructAgent = useGrooveStore((s) => s.instructAgent);
|
|
489
489
|
const queryAgent = useGrooveStore((s) => s.queryAgent);
|
|
490
490
|
const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
|
|
491
|
+
const cachedChatRef = useRef(EMPTY);
|
|
492
|
+
const cachedActivityRef = useRef(EMPTY);
|
|
493
|
+
if (rawChatHistory.length > 0) cachedChatRef.current = rawChatHistory;
|
|
494
|
+
if (rawActivityLog.length > 0) cachedActivityRef.current = rawActivityLog;
|
|
495
|
+
const chatHistory = rawChatHistory.length > 0 ? rawChatHistory : cachedChatRef.current;
|
|
496
|
+
const activityLog = rawActivityLog.length > 0 ? rawActivityLog : cachedActivityRef.current;
|
|
491
497
|
|
|
492
498
|
const storeInput = useGrooveStore((s) => s.chatInputs[agent.id] || '');
|
|
493
499
|
const setStoreInput = (val) => useGrooveStore.setState((s) => ({ chatInputs: { ...s.chatInputs, [agent.id]: val } }));
|
|
@@ -511,19 +511,18 @@ export function AgentFileTree({ agentId, onCollapse }) {
|
|
|
511
511
|
Agent Files
|
|
512
512
|
</div>
|
|
513
513
|
{touchedFiles.slice(0, 15).map((f) => {
|
|
514
|
-
const
|
|
515
|
-
const name = relPath.split('/').pop();
|
|
514
|
+
const name = f.path.split('/').pop();
|
|
516
515
|
const hasWrites = f.writes > 0;
|
|
517
516
|
return (
|
|
518
517
|
<button
|
|
519
518
|
key={f.path}
|
|
520
|
-
onClick={() => openFile(
|
|
521
|
-
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); handleContextMenu(e, { path:
|
|
522
|
-
onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); handleContextMenu(e, { path:
|
|
519
|
+
onClick={() => openFile(f.path)}
|
|
520
|
+
onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); handleContextMenu(e, { path: f.path, name, type: 'file' }); }}
|
|
521
|
+
onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); handleContextMenu(e, { path: f.path, name, type: 'file' }); }}
|
|
523
522
|
className={cn(
|
|
524
523
|
'w-full flex items-center gap-1.5 px-3 py-1 text-xs font-sans cursor-pointer',
|
|
525
524
|
'hover:bg-surface-4/50 transition-colors text-left',
|
|
526
|
-
|
|
525
|
+
editorActiveFile === f.path && 'bg-accent/8 text-accent',
|
|
527
526
|
)}
|
|
528
527
|
>
|
|
529
528
|
{hasWrites
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
-
import { useState, useRef } from 'react';
|
|
2
|
+
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { Badge } from '../ui/badge';
|
|
5
5
|
import { AgentFeed } from './agent-feed';
|
|
6
6
|
import { AgentConfig } from './agent-config';
|
|
7
7
|
import { AgentTelemetry } from './agent-telemetry';
|
|
8
8
|
import { AgentMdFiles } from './agent-mdfiles';
|
|
9
|
-
import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X } from 'lucide-react';
|
|
9
|
+
import { MessageSquare, Settings, Activity, FileText, Pencil, Check, X, TrendingDown } from 'lucide-react';
|
|
10
10
|
import { fmtNum, fmtUptime } from '../../lib/format';
|
|
11
11
|
import { cn } from '../../lib/cn';
|
|
12
12
|
import { roleColor } from '../../lib/status';
|
|
@@ -77,23 +77,91 @@ function InlineName({ agent }) {
|
|
|
77
77
|
);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function useRoutingSuggestion(agentId, isAlive) {
|
|
81
|
+
const [suggestion, setSuggestion] = useState(null);
|
|
82
|
+
const [dismissed, setDismissed] = useState(false);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!agentId || !isAlive || dismissed) { setSuggestion(null); return; }
|
|
86
|
+
let cancelled = false;
|
|
87
|
+
async function poll() {
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch(`/api/agents/${agentId}/routing/suggestion`);
|
|
90
|
+
if (cancelled) return;
|
|
91
|
+
if (res.status === 204 || !res.ok) { setSuggestion(null); return; }
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
setSuggestion(data);
|
|
94
|
+
} catch { setSuggestion(null); }
|
|
95
|
+
}
|
|
96
|
+
poll();
|
|
97
|
+
const id = setInterval(poll, 30000);
|
|
98
|
+
return () => { cancelled = true; clearInterval(id); };
|
|
99
|
+
}, [agentId, isAlive, dismissed]);
|
|
100
|
+
|
|
101
|
+
const dismiss = useCallback(() => setDismissed(true), []);
|
|
102
|
+
const reset = useCallback(() => setDismissed(false), []);
|
|
103
|
+
|
|
104
|
+
return { suggestion: dismissed ? null : suggestion, dismiss, reset };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function DownshiftPill({ suggestion, onAccept, onDismiss }) {
|
|
108
|
+
if (!suggestion) return null;
|
|
109
|
+
const { suggestedModel } = suggestion;
|
|
110
|
+
return (
|
|
111
|
+
<div className="flex items-center gap-1 px-1.5 py-0.5 rounded bg-success/10 border border-success/20 text-2xs font-mono animate-in fade-in slide-in-from-left-1 duration-200">
|
|
112
|
+
<TrendingDown size={10} className="text-success flex-shrink-0" />
|
|
113
|
+
<span className="text-success/90 truncate max-w-[80px]">{suggestedModel.name}</span>
|
|
114
|
+
<button
|
|
115
|
+
onClick={onAccept}
|
|
116
|
+
className="px-1 py-px rounded bg-success/20 text-success font-semibold hover:bg-success/30 transition-colors cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
Switch
|
|
119
|
+
</button>
|
|
120
|
+
<button
|
|
121
|
+
onClick={onDismiss}
|
|
122
|
+
className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"
|
|
123
|
+
>
|
|
124
|
+
<X size={8} />
|
|
125
|
+
</button>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
80
130
|
export function AgentPanel() {
|
|
81
131
|
const detailPanel = useGrooveStore((s) => s.detailPanel);
|
|
82
132
|
const agents = useGrooveStore((s) => s.agents);
|
|
83
133
|
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
134
|
+
const addToast = useGrooveStore((s) => s.addToast);
|
|
84
135
|
const [activeTab, setActiveTab] = useState('command');
|
|
136
|
+
const cachedAgentRef = useRef(null);
|
|
137
|
+
|
|
138
|
+
const agentId = detailPanel?.type === 'agent' ? detailPanel.agentId : null;
|
|
139
|
+
const liveAgent = agentId ? agents.find((a) => a.id === agentId) : null;
|
|
140
|
+
if (liveAgent) cachedAgentRef.current = liveAgent;
|
|
141
|
+
else if (cachedAgentRef.current && cachedAgentRef.current.id !== agentId) cachedAgentRef.current = null;
|
|
142
|
+
const agent = liveAgent || cachedAgentRef.current;
|
|
143
|
+
const isAlive = liveAgent?.status === 'running' || liveAgent?.status === 'starting';
|
|
144
|
+
const { suggestion, dismiss: dismissSuggestion } = useRoutingSuggestion(agentId, isAlive);
|
|
85
145
|
|
|
86
|
-
if (detailPanel?.type !== 'agent') return null;
|
|
87
|
-
const agent = agents.find((a) => a.id === detailPanel.agentId);
|
|
88
146
|
if (!agent) return null;
|
|
89
147
|
if (activeTeamId && agent.teamId && agent.teamId !== activeTeamId) return null;
|
|
90
148
|
|
|
91
|
-
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
92
149
|
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
93
150
|
const spawned = agent.spawnedAt || agent.createdAt;
|
|
94
151
|
const uptime = spawned ? Math.floor((Date.now() - new Date(spawned).getTime()) / 1000) : 0;
|
|
95
152
|
const colors = roleColor(agent.role);
|
|
96
153
|
|
|
154
|
+
async function acceptSuggestion() {
|
|
155
|
+
if (!suggestion) return;
|
|
156
|
+
try {
|
|
157
|
+
await api.patch(`/agents/${agent.id}`, { model: suggestion.suggestedModel.id });
|
|
158
|
+
addToast('success', `Model → ${suggestion.suggestedModel.name}`);
|
|
159
|
+
dismissSuggestion();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
addToast('error', 'Model switch failed', err.message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
97
165
|
return (
|
|
98
166
|
<div className="flex flex-col h-full">
|
|
99
167
|
{/* ── Header ─────────────────────────────────────────── */}
|
|
@@ -126,6 +194,12 @@ export function AgentPanel() {
|
|
|
126
194
|
)}
|
|
127
195
|
<span className="text-text-4">·</span>
|
|
128
196
|
<span>{fmtUptime(uptime)}</span>
|
|
197
|
+
{suggestion && (
|
|
198
|
+
<>
|
|
199
|
+
<span className="text-text-4">·</span>
|
|
200
|
+
<DownshiftPill suggestion={suggestion} onAccept={acceptSuggestion} onDismiss={dismissSuggestion} />
|
|
201
|
+
</>
|
|
202
|
+
)}
|
|
129
203
|
</div>
|
|
130
204
|
</div>
|
|
131
205
|
|
|
@@ -6,7 +6,6 @@ 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
9
|
import { SelectionMenu } from '../editor/selection-menu';
|
|
11
10
|
import { InlinePrompt } from '../editor/inline-prompt';
|
|
12
11
|
import { QuickSearch } from '../editor/quick-search';
|
|
@@ -15,7 +14,7 @@ import { roleColor } from '../../lib/status';
|
|
|
15
14
|
import { MediaViewer, isMediaFile } from '../editor/media-viewer';
|
|
16
15
|
import {
|
|
17
16
|
X, Code2, FileCode, GitCompareArrows,
|
|
18
|
-
ClipboardCheck, Users, PanelLeftOpen,
|
|
17
|
+
ClipboardCheck, Users, PanelLeftOpen, Search,
|
|
19
18
|
} from 'lucide-react';
|
|
20
19
|
|
|
21
20
|
const TREE_DEFAULT = 220;
|
|
@@ -61,7 +60,7 @@ function AgentRail({ agents, activeId, onSelect }) {
|
|
|
61
60
|
);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
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 }) {
|
|
65
64
|
const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
|
|
66
65
|
|
|
67
66
|
return (
|
|
@@ -142,19 +141,6 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
142
141
|
<ClipboardCheck size={12} />
|
|
143
142
|
</button>
|
|
144
143
|
</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>
|
|
158
144
|
<Tooltip content="Back to Team View" side="bottom">
|
|
159
145
|
<button
|
|
160
146
|
onClick={onBackToTeam}
|
|
@@ -177,10 +163,6 @@ export function WorkspaceMode() {
|
|
|
177
163
|
const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
|
|
178
164
|
const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
|
|
179
165
|
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
166
|
const setQuickSearchOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
|
|
185
167
|
|
|
186
168
|
const editorFiles = useGrooveStore((s) => s.editorFiles);
|
|
@@ -205,9 +187,6 @@ export function WorkspaceMode() {
|
|
|
205
187
|
const treeDragging = useRef(false);
|
|
206
188
|
const startX = useRef(0);
|
|
207
189
|
const startW = useRef(0);
|
|
208
|
-
const aiDragging = useRef(false);
|
|
209
|
-
const aiStartX = useRef(0);
|
|
210
|
-
const aiStartW = useRef(0);
|
|
211
190
|
|
|
212
191
|
useEffect(() => {
|
|
213
192
|
setDiffMode(false);
|
|
@@ -238,24 +217,6 @@ export function WorkspaceMode() {
|
|
|
238
217
|
document.addEventListener('mouseup', onUp);
|
|
239
218
|
}, [treeWidth]);
|
|
240
219
|
|
|
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
220
|
|
|
260
221
|
const handleEditorMouseUp = useCallback(() => {
|
|
261
222
|
const view = editorViewRef.current;
|
|
@@ -357,8 +318,6 @@ export function WorkspaceMode() {
|
|
|
357
318
|
onBackToTeam={() => setWorkspaceMode(false)}
|
|
358
319
|
onToggleReview={toggleReviewMode}
|
|
359
320
|
reviewMode={workspaceReviewMode}
|
|
360
|
-
onToggleAi={toggleAiPanel}
|
|
361
|
-
aiOpen={aiPanelOpen}
|
|
362
321
|
onCmdP={() => setQuickSearchOpen(true)}
|
|
363
322
|
/>
|
|
364
323
|
|
|
@@ -423,16 +382,6 @@ export function WorkspaceMode() {
|
|
|
423
382
|
)}
|
|
424
383
|
</div>
|
|
425
384
|
|
|
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
|
-
)}
|
|
436
385
|
</div>
|
|
437
386
|
|
|
438
387
|
{/* Quick Search modal */}
|
|
@@ -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 };
|