groove-dev 0.27.161 → 0.27.164
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/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +1 -0
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +15 -0
- package/node_modules/@groove-dev/daemon/src/process.js +227 -105
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +2 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +1 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +4 -8
- package/node_modules/@groove-dev/gui/dist/assets/index-BJVNpGIp.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CkCFf4Fl.js +1025 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +20 -12
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +2 -19
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-agent-row.jsx +176 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +135 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-pane.jsx +105 -0
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +216 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -1
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +4 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +71 -1
- package/node_modules/@groove-dev/gui/src/views/fleet.jsx +15 -0
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/gateways/manager.js +1 -0
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/model-lab.js +15 -0
- package/packages/daemon/src/process.js +227 -105
- package/packages/daemon/src/routes/teams.js +2 -0
- package/packages/daemon/src/scheduler.js +1 -0
- package/packages/daemon/src/tunnel-manager.js +4 -8
- package/packages/gui/dist/assets/index-BJVNpGIp.css +1 -0
- package/packages/gui/dist/assets/index-CkCFf4Fl.js +1025 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/App.jsx +2 -0
- package/packages/gui/src/components/agents/diff-viewer.jsx +20 -12
- package/packages/gui/src/components/agents/spawn-wizard.jsx +5 -2
- package/packages/gui/src/components/agents/workspace-mode.jsx +2 -19
- package/packages/gui/src/components/fleet/fleet-agent-row.jsx +176 -0
- package/packages/gui/src/components/fleet/fleet-content.jsx +135 -0
- package/packages/gui/src/components/fleet/fleet-pane.jsx +105 -0
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +216 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -1
- package/packages/gui/src/stores/slices/agents-slice.js +4 -0
- package/packages/gui/src/stores/slices/ui-slice.js +71 -1
- package/packages/gui/src/views/fleet.jsx +15 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DpRdb7o1.js +0 -1020
- package/node_modules/@groove-dev/gui/dist/assets/index-Dzofq3wS.css +0 -1
- package/packages/gui/dist/assets/index-DpRdb7o1.js +0 -1020
- package/packages/gui/dist/assets/index-Dzofq3wS.css +0 -1
|
@@ -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-CkCFf4Fl.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/codemirror-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-BJVNpGIp.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/packages/gui/src/App.jsx
CHANGED
|
@@ -24,6 +24,7 @@ import ModelLabView from './views/model-lab';
|
|
|
24
24
|
import NetworkView from './views/network';
|
|
25
25
|
import ChatView from './views/chat';
|
|
26
26
|
import MemoryView from './views/memory';
|
|
27
|
+
import FleetView from './views/fleet';
|
|
27
28
|
|
|
28
29
|
// Agent components
|
|
29
30
|
import { AgentPanel } from './components/agents/agent-panel';
|
|
@@ -77,6 +78,7 @@ function ViewRouter() {
|
|
|
77
78
|
case 'federation': content = <FederationView />; break;
|
|
78
79
|
case 'settings': content = <SettingsView />; break;
|
|
79
80
|
case 'chat': content = <ChatView />; break;
|
|
81
|
+
case 'fleet': content = <FleetView />; break;
|
|
80
82
|
case 'memory': content = <MemoryView />; break;
|
|
81
83
|
case 'network': content = networkUnlocked ? <NetworkView /> : <AgentsView />; break;
|
|
82
84
|
default: content = <AgentsView />;
|
|
@@ -168,21 +168,21 @@ function SideBySideView({ pairs }) {
|
|
|
168
168
|
|
|
169
169
|
export function DiffViewer({ filePath, gitDiffData, originalContent, modifiedContent }) {
|
|
170
170
|
const file = useGrooveStore((s) => s.editorFiles[filePath]);
|
|
171
|
-
const snapshot = useGrooveStore((s) => s.workspaceSnapshots[filePath]);
|
|
172
171
|
const [viewMode, setViewMode] = useState('side-by-side');
|
|
173
|
-
const [
|
|
172
|
+
const [headContent, setHeadContent] = useState(undefined);
|
|
174
173
|
|
|
175
174
|
useEffect(() => {
|
|
176
|
-
if (gitDiffData?.original !== undefined)
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
}, [filePath, gitDiffData,
|
|
175
|
+
if (originalContent !== undefined || gitDiffData?.original !== undefined) return;
|
|
176
|
+
let cancelled = false;
|
|
177
|
+
setHeadContent(undefined);
|
|
178
|
+
api.get(`/files/git-show?path=${encodeURIComponent(filePath)}`).then((data) => {
|
|
179
|
+
if (!cancelled) setHeadContent(data?.content ?? '');
|
|
180
|
+
}).catch(() => { if (!cancelled) setHeadContent(''); });
|
|
181
|
+
return () => { cancelled = true; };
|
|
182
|
+
}, [filePath, gitDiffData, originalContent]);
|
|
184
183
|
|
|
185
|
-
const
|
|
184
|
+
const loading = originalContent === undefined && gitDiffData?.original === undefined && headContent === undefined;
|
|
185
|
+
const original = originalContent ?? gitDiffData?.original ?? headContent ?? '';
|
|
186
186
|
const modified = modifiedContent ?? file?.content ?? '';
|
|
187
187
|
|
|
188
188
|
const diffLines = useMemo(() => computeDiff(original, modified), [original, modified]);
|
|
@@ -197,6 +197,14 @@ export function DiffViewer({ filePath, gitDiffData, originalContent, modifiedCon
|
|
|
197
197
|
return { adds, dels };
|
|
198
198
|
}, [diffLines]);
|
|
199
199
|
|
|
200
|
+
if (loading) {
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
|
|
203
|
+
Loading diff…
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
200
208
|
if (!original && !modified) {
|
|
201
209
|
return (
|
|
202
210
|
<div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
|
|
@@ -205,7 +213,7 @@ export function DiffViewer({ filePath, gitDiffData, originalContent, modifiedCon
|
|
|
205
213
|
);
|
|
206
214
|
}
|
|
207
215
|
|
|
208
|
-
if (original === modified
|
|
216
|
+
if (original === modified) {
|
|
209
217
|
return (
|
|
210
218
|
<div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
|
|
211
219
|
No changes detected
|
|
@@ -94,7 +94,8 @@ export function SpawnWizard() {
|
|
|
94
94
|
const availableModels = selectedProvider?.models || [];
|
|
95
95
|
const installedProviders = providers.filter((p) => p.authType === 'api-key' ? (p.installed && p.hasKey) : p.installed);
|
|
96
96
|
const isFromModelLab = !!(detailPanel?.presetProvider || detailPanel?.presetModel);
|
|
97
|
-
const
|
|
97
|
+
const presetTeamId = detailPanel?.presetTeamId || null;
|
|
98
|
+
const showTeamSelector = !presetTeamId && (isFromModelLab || !activeTeamId);
|
|
98
99
|
|
|
99
100
|
useEffect(() => {
|
|
100
101
|
if (open) {
|
|
@@ -185,7 +186,9 @@ export function SpawnWizard() {
|
|
|
185
186
|
setSpawning(true);
|
|
186
187
|
try {
|
|
187
188
|
let teamId;
|
|
188
|
-
if (
|
|
189
|
+
if (presetTeamId) {
|
|
190
|
+
teamId = presetTeamId;
|
|
191
|
+
} else if (!showTeamSelector) {
|
|
189
192
|
teamId = activeTeamId;
|
|
190
193
|
} else if (teamMode === 'new') {
|
|
191
194
|
const teamName = newTeamName.trim() || selectedRole || 'New Team';
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
3
|
import { useGrooveStore } from '../../stores/groove';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
|
-
import { api } from '../../lib/api';
|
|
6
5
|
import { AgentFileTree } from './agent-file-tree';
|
|
7
6
|
import { DiffViewer } from './diff-viewer';
|
|
8
7
|
import { CodeReview } from './code-review';
|
|
@@ -61,9 +60,7 @@ function AgentRail({ agents, activeId, onSelect }) {
|
|
|
61
60
|
);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff,
|
|
65
|
-
const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
|
|
66
|
-
|
|
63
|
+
function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, onBackToTeam, onToggleReview, reviewMode, onCmdP }) {
|
|
67
64
|
return (
|
|
68
65
|
<div className="flex items-stretch h-8 bg-surface-2 border-b border-border flex-shrink-0">
|
|
69
66
|
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
|
|
@@ -106,7 +103,7 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
|
|
|
106
103
|
<Search size={12} />
|
|
107
104
|
</button>
|
|
108
105
|
</Tooltip>
|
|
109
|
-
{
|
|
106
|
+
{activeFile && (
|
|
110
107
|
<>
|
|
111
108
|
<button
|
|
112
109
|
onClick={() => onToggleDiff(false)}
|
|
@@ -162,7 +159,6 @@ export function WorkspaceMode() {
|
|
|
162
159
|
const setWorkspaceAgent = useGrooveStore((s) => s.setWorkspaceAgent);
|
|
163
160
|
const workspaceReviewMode = useGrooveStore((s) => s.workspaceReviewMode);
|
|
164
161
|
const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
|
|
165
|
-
const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
|
|
166
162
|
const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
|
|
167
163
|
const setQuickSearchOpen = useGrooveStore((s) => s.setEditorQuickSearchOpen);
|
|
168
164
|
|
|
@@ -189,18 +185,6 @@ export function WorkspaceMode() {
|
|
|
189
185
|
const startX = useRef(0);
|
|
190
186
|
const startW = useRef(0);
|
|
191
187
|
|
|
192
|
-
// Fetch git HEAD content as diff baseline when no snapshot exists
|
|
193
|
-
useEffect(() => {
|
|
194
|
-
if (!editorActiveFile || workspaceSnapshots[editorActiveFile]) return;
|
|
195
|
-
let cancelled = false;
|
|
196
|
-
api.get(`/files/git-show?path=${encodeURIComponent(editorActiveFile)}`).then((data) => {
|
|
197
|
-
if (cancelled) return;
|
|
198
|
-
const captureSnapshot = useGrooveStore.getState().captureSnapshot;
|
|
199
|
-
captureSnapshot(editorActiveFile, data.content ?? '');
|
|
200
|
-
}).catch(() => {});
|
|
201
|
-
return () => { cancelled = true; };
|
|
202
|
-
}, [editorActiveFile, workspaceSnapshots]);
|
|
203
|
-
|
|
204
188
|
// Set the selected agent in the store so AI features use it
|
|
205
189
|
useEffect(() => {
|
|
206
190
|
if (agent?.id) {
|
|
@@ -323,7 +307,6 @@ export function WorkspaceMode() {
|
|
|
323
307
|
onClose={closeFile}
|
|
324
308
|
diffMode={diffMode}
|
|
325
309
|
onToggleDiff={setDiffMode}
|
|
326
|
-
workspaceSnapshots={workspaceSnapshots}
|
|
327
310
|
onBackToTeam={() => setWorkspaceMode(false)}
|
|
328
311
|
onToggleReview={toggleReviewMode}
|
|
329
312
|
reviewMode={workspaceReviewMode}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { api } from '../../lib/api';
|
|
7
|
+
|
|
8
|
+
const STATUS_DOT = {
|
|
9
|
+
running: 'bg-accent',
|
|
10
|
+
starting: 'bg-text-3',
|
|
11
|
+
completed: 'bg-info',
|
|
12
|
+
crashed: 'bg-danger',
|
|
13
|
+
stopped: 'bg-text-4',
|
|
14
|
+
killed: 'bg-text-4',
|
|
15
|
+
rotating: 'bg-accent',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function ctxBarColor(pct) {
|
|
19
|
+
if (pct >= 75) return 'bg-danger';
|
|
20
|
+
if (pct >= 50) return 'bg-warning';
|
|
21
|
+
return 'bg-accent';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function FleetAgentRow({ agent }) {
|
|
25
|
+
const selected = useGrooveStore((s) => s.fleetSelectedAgents);
|
|
26
|
+
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
27
|
+
const fleetMarkRead = useGrooveStore((s) => s.fleetMarkRead);
|
|
28
|
+
const killAgent = useGrooveStore((s) => s.killAgent);
|
|
29
|
+
const unreadTs = useGrooveStore((s) => s.fleetUnreadMap[agent.id]);
|
|
30
|
+
const chatHistory = useGrooveStore((s) => s.chatHistory[agent.id]);
|
|
31
|
+
|
|
32
|
+
const [confirmKill, setConfirmKill] = useState(false);
|
|
33
|
+
const [editing, setEditing] = useState(false);
|
|
34
|
+
const [editName, setEditName] = useState('');
|
|
35
|
+
const inputRef = useRef(null);
|
|
36
|
+
|
|
37
|
+
const isSelected = selected[0] === agent.id || selected[1] === agent.id;
|
|
38
|
+
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
39
|
+
|
|
40
|
+
const lastMsg = chatHistory?.[chatHistory.length - 1];
|
|
41
|
+
const hasUnread = lastMsg && (!unreadTs || lastMsg.timestamp > unreadTs) && lastMsg.from === 'agent';
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (editing && inputRef.current) {
|
|
45
|
+
inputRef.current.focus();
|
|
46
|
+
inputRef.current.select();
|
|
47
|
+
}
|
|
48
|
+
}, [editing]);
|
|
49
|
+
|
|
50
|
+
function handleClick(e) {
|
|
51
|
+
if (editing) return;
|
|
52
|
+
if (e.metaKey || e.ctrlKey) {
|
|
53
|
+
fleetSelectAgent(agent.id, 1);
|
|
54
|
+
} else {
|
|
55
|
+
fleetSelectAgent(agent.id, 0);
|
|
56
|
+
}
|
|
57
|
+
fleetMarkRead(agent.id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleDoubleClick(e) {
|
|
61
|
+
e.stopPropagation();
|
|
62
|
+
setEditing(true);
|
|
63
|
+
setEditName(agent.name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function commitRename() {
|
|
67
|
+
setEditing(false);
|
|
68
|
+
const trimmed = editName.trim().replace(/\s+/g, '-');
|
|
69
|
+
if (!trimmed || trimmed === agent.name) return;
|
|
70
|
+
try {
|
|
71
|
+
await api.patch(`/agents/${agent.id}`, { name: trimmed });
|
|
72
|
+
} catch { /* toast handles */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handleKeyDown(e) {
|
|
76
|
+
if (e.key === 'Enter') {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
commitRename();
|
|
79
|
+
} else if (e.key === 'Escape') {
|
|
80
|
+
setEditing(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleRemove(e) {
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
87
|
+
if (confirmKill) {
|
|
88
|
+
killAgent(agent.id, !isAlive);
|
|
89
|
+
setConfirmKill(false);
|
|
90
|
+
} else {
|
|
91
|
+
setConfirmKill(true);
|
|
92
|
+
setTimeout(() => setConfirmKill(false), 3000);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handleDragStart = useCallback((e) => {
|
|
97
|
+
if (editing) { e.preventDefault(); return; }
|
|
98
|
+
e.dataTransfer.setData('application/x-fleet-agent', agent.id);
|
|
99
|
+
e.dataTransfer.effectAllowed = 'link';
|
|
100
|
+
}, [agent.id, editing]);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
onClick={handleClick}
|
|
105
|
+
draggable={!editing}
|
|
106
|
+
onDragStart={handleDragStart}
|
|
107
|
+
className={cn(
|
|
108
|
+
'w-full flex items-center gap-2 px-2 py-1.5 transition-colors cursor-pointer group rounded-md',
|
|
109
|
+
isSelected ? 'text-text-0' : 'hover:bg-surface-2',
|
|
110
|
+
confirmKill && 'bg-danger/10',
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
{/* Status dot */}
|
|
114
|
+
<span className={cn('w-2 h-2 rounded-full flex-shrink-0', STATUS_DOT[agent.status] || 'bg-text-4')} />
|
|
115
|
+
|
|
116
|
+
{/* Name + role */}
|
|
117
|
+
<div className="flex-1 min-w-0 text-left">
|
|
118
|
+
{editing ? (
|
|
119
|
+
<input
|
|
120
|
+
ref={inputRef}
|
|
121
|
+
type="text"
|
|
122
|
+
value={editName}
|
|
123
|
+
onChange={(e) => setEditName(e.target.value)}
|
|
124
|
+
onBlur={commitRename}
|
|
125
|
+
onKeyDown={handleKeyDown}
|
|
126
|
+
className="w-full text-xs font-sans text-text-0 bg-surface-3 border border-border-subtle rounded px-1 py-0 leading-tight outline-none focus:border-accent"
|
|
127
|
+
/>
|
|
128
|
+
) : (
|
|
129
|
+
<>
|
|
130
|
+
<div
|
|
131
|
+
onDoubleClick={handleDoubleClick}
|
|
132
|
+
className={cn(
|
|
133
|
+
'text-xs font-sans truncate leading-tight',
|
|
134
|
+
confirmKill ? 'text-danger' : isSelected ? 'text-accent' : 'text-text-0',
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{confirmKill ? 'Click again to remove' : agent.name}
|
|
138
|
+
</div>
|
|
139
|
+
{!confirmKill && (
|
|
140
|
+
<div className="text-xs text-text-3 font-sans truncate leading-tight">{agent.role}</div>
|
|
141
|
+
)}
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Context gauge (hidden on hover to make room for X) */}
|
|
147
|
+
{ctxPct > 0 && !confirmKill && !editing && (
|
|
148
|
+
<div className="group-hover:hidden w-8 h-1 rounded-sm bg-surface-4 overflow-hidden flex-shrink-0">
|
|
149
|
+
<div
|
|
150
|
+
className={cn('h-full rounded-sm transition-all', ctxBarColor(ctxPct))}
|
|
151
|
+
style={{ width: `${ctxPct}%` }}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Unread dot (hidden on hover) */}
|
|
157
|
+
{hasUnread && !confirmKill && !editing && (
|
|
158
|
+
<span className="group-hover:hidden w-1.5 h-1.5 rounded-full bg-accent flex-shrink-0" />
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Remove button (visible on hover) */}
|
|
162
|
+
{!editing && (
|
|
163
|
+
<button
|
|
164
|
+
onClick={handleRemove}
|
|
165
|
+
className={cn(
|
|
166
|
+
'hidden group-hover:flex items-center justify-center p-0.5 rounded transition-colors cursor-pointer flex-shrink-0',
|
|
167
|
+
confirmKill ? 'flex text-danger' : 'text-text-4 hover:text-danger',
|
|
168
|
+
)}
|
|
169
|
+
title={agent.status === 'running' || agent.status === 'starting' ? 'Kill agent' : 'Remove agent'}
|
|
170
|
+
>
|
|
171
|
+
<X size={14} />
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useRef, useCallback, useState } from 'react';
|
|
3
|
+
import { LayoutList } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { FleetPane } from './fleet-pane';
|
|
6
|
+
|
|
7
|
+
export function FleetContent() {
|
|
8
|
+
const selected = useGrooveStore((s) => s.fleetSelectedAgents);
|
|
9
|
+
const splitMode = useGrooveStore((s) => s.fleetSplitMode);
|
|
10
|
+
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
11
|
+
|
|
12
|
+
const [dropTarget, setDropTarget] = useState(null);
|
|
13
|
+
|
|
14
|
+
const dragging = useRef(false);
|
|
15
|
+
const dividerRef = useRef(null);
|
|
16
|
+
const leftRef = useRef(null);
|
|
17
|
+
const rightRef = useRef(null);
|
|
18
|
+
|
|
19
|
+
const onDividerDown = useCallback((e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
dragging.current = true;
|
|
22
|
+
const container = dividerRef.current?.parentElement;
|
|
23
|
+
if (!container) return;
|
|
24
|
+
|
|
25
|
+
function onMove(ev) {
|
|
26
|
+
if (!dragging.current || !container || !leftRef.current || !rightRef.current) return;
|
|
27
|
+
const rect = container.getBoundingClientRect();
|
|
28
|
+
const pct = ((ev.clientX - rect.left) / rect.width) * 100;
|
|
29
|
+
const clamped = Math.max(25, Math.min(75, pct));
|
|
30
|
+
leftRef.current.style.flex = `0 0 ${clamped}%`;
|
|
31
|
+
rightRef.current.style.flex = `0 0 ${100 - clamped}%`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function onUp() {
|
|
35
|
+
dragging.current = false;
|
|
36
|
+
document.removeEventListener('mousemove', onMove);
|
|
37
|
+
document.removeEventListener('mouseup', onUp);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
document.addEventListener('mousemove', onMove);
|
|
41
|
+
document.addEventListener('mouseup', onUp);
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
function handleDragOver(e, side) {
|
|
45
|
+
const agentId = e.dataTransfer.types.includes('application/x-fleet-agent');
|
|
46
|
+
if (!agentId) return;
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.dataTransfer.dropEffect = 'link';
|
|
49
|
+
setDropTarget(side);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function handleDrop(e, pane) {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
setDropTarget(null);
|
|
55
|
+
const agentId = e.dataTransfer.getData('application/x-fleet-agent');
|
|
56
|
+
if (!agentId) return;
|
|
57
|
+
fleetSelectAgent(agentId, pane);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function handleDragLeave() {
|
|
61
|
+
setDropTarget(null);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!selected[0] && !selected[1]) {
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3"
|
|
68
|
+
onDragOver={(e) => handleDragOver(e, 'left')}
|
|
69
|
+
onDragLeave={handleDragLeave}
|
|
70
|
+
onDrop={(e) => handleDrop(e, 0)}
|
|
71
|
+
>
|
|
72
|
+
<LayoutList size={32} strokeWidth={1} className="text-text-4" />
|
|
73
|
+
<p className="text-sm font-sans">Select an agent or drag one here</p>
|
|
74
|
+
<p className="text-xs font-sans text-text-4">Cmd+Click or drag to open side-by-side</p>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!splitMode || !selected[1]) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex-1 flex min-w-0 min-h-0">
|
|
82
|
+
<div className="flex-1 min-w-0 min-h-0">
|
|
83
|
+
<FleetPane agentId={selected[0]} paneIndex={0} />
|
|
84
|
+
</div>
|
|
85
|
+
{/* Drop zone for second pane */}
|
|
86
|
+
<div
|
|
87
|
+
className={`w-1 flex-shrink-0 transition-all ${dropTarget === 'right' ? 'w-1 bg-accent/40' : ''}`}
|
|
88
|
+
onDragOver={(e) => handleDragOver(e, 'right')}
|
|
89
|
+
onDragLeave={handleDragLeave}
|
|
90
|
+
onDrop={(e) => handleDrop(e, 1)}
|
|
91
|
+
/>
|
|
92
|
+
{dropTarget === 'right' && (
|
|
93
|
+
<div
|
|
94
|
+
className="w-48 flex-shrink-0 flex items-center justify-center border-2 border-dashed border-accent/30 bg-accent/5 rounded-lg m-1 transition-all"
|
|
95
|
+
onDragOver={(e) => handleDragOver(e, 'right')}
|
|
96
|
+
onDragLeave={handleDragLeave}
|
|
97
|
+
onDrop={(e) => handleDrop(e, 1)}
|
|
98
|
+
>
|
|
99
|
+
<p className="text-xs text-accent font-sans">Drop to open</p>
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="flex-1 flex min-w-0 min-h-0">
|
|
108
|
+
<div
|
|
109
|
+
ref={leftRef}
|
|
110
|
+
className={`min-w-0 min-h-0 ${dropTarget === 'left' ? 'ring-2 ring-inset ring-accent/30' : ''}`}
|
|
111
|
+
style={{ flex: '0 0 50%' }}
|
|
112
|
+
onDragOver={(e) => handleDragOver(e, 'left')}
|
|
113
|
+
onDragLeave={handleDragLeave}
|
|
114
|
+
onDrop={(e) => handleDrop(e, 0)}
|
|
115
|
+
>
|
|
116
|
+
<FleetPane agentId={selected[0]} paneIndex={0} />
|
|
117
|
+
</div>
|
|
118
|
+
<div
|
|
119
|
+
ref={dividerRef}
|
|
120
|
+
className="w-1 bg-border hover:bg-accent/30 cursor-col-resize transition-colors flex-shrink-0"
|
|
121
|
+
onMouseDown={onDividerDown}
|
|
122
|
+
/>
|
|
123
|
+
<div
|
|
124
|
+
ref={rightRef}
|
|
125
|
+
className={`min-w-0 min-h-0 ${dropTarget === 'right' ? 'ring-2 ring-inset ring-accent/30' : ''}`}
|
|
126
|
+
style={{ flex: '0 0 calc(50% - 4px)' }}
|
|
127
|
+
onDragOver={(e) => handleDragOver(e, 'right')}
|
|
128
|
+
onDragLeave={handleDragLeave}
|
|
129
|
+
onDrop={(e) => handleDrop(e, 1)}
|
|
130
|
+
>
|
|
131
|
+
<FleetPane agentId={selected[1]} paneIndex={1} />
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { X } from 'lucide-react';
|
|
4
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
5
|
+
import { cn } from '../../lib/cn';
|
|
6
|
+
import { Badge } from '../ui/badge';
|
|
7
|
+
import { AgentFeed } from '../agents/agent-feed';
|
|
8
|
+
import { fmtNum } from '../../lib/format';
|
|
9
|
+
|
|
10
|
+
const STATUS_VARIANT = {
|
|
11
|
+
running: 'success',
|
|
12
|
+
starting: 'warning',
|
|
13
|
+
stopped: 'default',
|
|
14
|
+
crashed: 'danger',
|
|
15
|
+
completed: 'accent',
|
|
16
|
+
killed: 'default',
|
|
17
|
+
rotating: 'purple',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const STATUS_LABEL = {
|
|
21
|
+
running: 'Running',
|
|
22
|
+
starting: 'Starting',
|
|
23
|
+
stopped: 'Stopped',
|
|
24
|
+
crashed: 'Crashed',
|
|
25
|
+
completed: 'Done',
|
|
26
|
+
killed: 'Killed',
|
|
27
|
+
rotating: 'Rotating',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function FleetPane({ agentId, paneIndex }) {
|
|
31
|
+
const liveAgent = useGrooveStore((s) => s.agents.find((a) => a.id === agentId));
|
|
32
|
+
const fleetSelectAgent = useGrooveStore((s) => s.fleetSelectAgent);
|
|
33
|
+
const fleetMarkRead = useGrooveStore((s) => s.fleetMarkRead);
|
|
34
|
+
|
|
35
|
+
const lastAgentRef = useRef(liveAgent);
|
|
36
|
+
const [gone, setGone] = useState(false);
|
|
37
|
+
const goneTimer = useRef(null);
|
|
38
|
+
|
|
39
|
+
if (liveAgent) {
|
|
40
|
+
lastAgentRef.current = liveAgent;
|
|
41
|
+
if (gone) setGone(false);
|
|
42
|
+
if (goneTimer.current) { clearTimeout(goneTimer.current); goneTimer.current = null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!liveAgent && agentId && !gone) {
|
|
47
|
+
goneTimer.current = setTimeout(() => setGone(true), 2000);
|
|
48
|
+
return () => { if (goneTimer.current) clearTimeout(goneTimer.current); };
|
|
49
|
+
}
|
|
50
|
+
}, [liveAgent, agentId, gone]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (agentId) fleetMarkRead(agentId);
|
|
54
|
+
}, [agentId, fleetMarkRead]);
|
|
55
|
+
|
|
56
|
+
const agent = liveAgent || lastAgentRef.current;
|
|
57
|
+
|
|
58
|
+
if (!agent) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="h-full flex items-center justify-center text-text-4">
|
|
61
|
+
<p className="text-xs font-sans">Select an agent</p>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex flex-col h-full min-h-0">
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="h-10 bg-surface-1 border-b border-border px-3 flex items-center gap-2 flex-shrink-0">
|
|
72
|
+
<span className="text-sm font-medium text-text-0 font-sans truncate">{agent.name}</span>
|
|
73
|
+
<Badge variant="default">{agent.role}</Badge>
|
|
74
|
+
{agent.provider && (
|
|
75
|
+
<span className="text-xs text-text-3 font-mono truncate">
|
|
76
|
+
{agent.provider}{agent.model ? `:${agent.model}` : ''}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
<Badge variant={STATUS_VARIANT[agent.status] || 'default'} dot={agent.status === 'running' ? 'pulse' : undefined}>
|
|
80
|
+
{STATUS_LABEL[agent.status] || agent.status}
|
|
81
|
+
</Badge>
|
|
82
|
+
<div className="flex-1" />
|
|
83
|
+
<span className="text-xs text-text-3 font-mono">{fmtNum(agent.tokensUsed)}</span>
|
|
84
|
+
<span className={cn(
|
|
85
|
+
'text-xs font-mono',
|
|
86
|
+
ctxPct >= 75 ? 'text-danger' : ctxPct >= 50 ? 'text-warning' : 'text-text-3',
|
|
87
|
+
)}>
|
|
88
|
+
{ctxPct}%
|
|
89
|
+
</span>
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => fleetSelectAgent(null, paneIndex)}
|
|
92
|
+
className="p-1 rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
|
|
93
|
+
title="Close pane"
|
|
94
|
+
>
|
|
95
|
+
<X size={14} />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Agent feed */}
|
|
100
|
+
<div className="flex-1 min-h-0">
|
|
101
|
+
<AgentFeed agent={agent} />
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|