groove-dev 0.26.38 → 0.27.0
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/CHANGELOG.md +59 -0
- package/CLAUDE.md +24 -19
- package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
- package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
- package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
- package/node_modules/@groove-dev/daemon/src/api.js +346 -22
- package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
- package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
- package/node_modules/@groove-dev/daemon/src/index.js +28 -4
- package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
- package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
- package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
- package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
- package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
- package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +141 -9
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
- package/node_modules/@groove-dev/daemon/src/router.js +43 -0
- package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
- package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
- package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
- package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
- package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
- package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
- package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -4
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
- package/package.json +2 -8
- package/packages/cli/bin/groove.js +2 -0
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/nuke.js +16 -4
- package/packages/cli/src/commands/stop.js +17 -2
- package/packages/daemon/integrations-registry.json +681 -75
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/adaptive.js +23 -25
- package/packages/daemon/src/api.js +346 -22
- package/packages/daemon/src/classifier.js +53 -6
- package/packages/daemon/src/firstrun.js +14 -1
- package/packages/daemon/src/gateways/manager.js +2 -2
- package/packages/daemon/src/index.js +28 -4
- package/packages/daemon/src/integrations.js +215 -14
- package/packages/daemon/src/introducer.js +84 -11
- package/packages/daemon/src/journalist.js +43 -1
- package/packages/daemon/src/lockmanager.js +60 -0
- package/packages/daemon/src/mcp-manager.js +270 -0
- package/packages/daemon/src/memory.js +370 -0
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +141 -9
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/rotator.js +334 -31
- package/packages/daemon/src/router.js +43 -0
- package/packages/daemon/src/tokentracker.js +70 -18
- package/packages/daemon/src/validate.js +5 -13
- package/packages/daemon/templates/groove-slides.cjs +306 -0
- package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
- package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -4
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
- package/packages/gui/src/components/agents/agent-config.jsx +22 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
- package/packages/gui/src/components/agents/agent-node.jsx +132 -90
- package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
- package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
- package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
- package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
- package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
- package/packages/gui/src/components/layout/app-shell.jsx +24 -19
- package/packages/gui/src/components/layout/command-palette.jsx +2 -2
- package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
- package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
- package/packages/gui/src/lib/format.js +0 -6
- package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
- package/packages/gui/src/stores/groove.js +59 -9
- package/packages/gui/src/views/agents.jsx +84 -10
- package/packages/gui/src/views/dashboard.jsx +24 -21
- package/packages/gui/src/views/marketplace.jsx +153 -85
- package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
- package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
- package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
- package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
- package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
- package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
- package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
- package/node_modules/@radix-ui/react-popover/README.md +0 -3
- package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
- package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
- package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
- package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-popover/package.json +0 -82
- package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
- package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
- package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
- package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
- package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
- package/node_modules/@radix-ui/react-separator/package.json +0 -69
- package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
- package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
- package/packages/gui/dist/groove-logo-short.png +0 -0
- package/packages/gui/dist/groove-logo.png +0 -0
- package/packages/gui/public/groove-logo-short.png +0 -0
- package/packages/gui/public/groove-logo.png +0 -0
- package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
- package/packages/gui/src/lib/hooks/use-media-query.js +0 -18
|
@@ -8,6 +8,8 @@ export function useDashboard() {
|
|
|
8
8
|
const agents = useGrooveStore((s) => s.agents);
|
|
9
9
|
|
|
10
10
|
const [data, setData] = useState(null);
|
|
11
|
+
const [teamBurn, setTeamBurn] = useState([]);
|
|
12
|
+
const [memory, setMemory] = useState({ constraints: [], discoveries: [], roles: [], specializations: null });
|
|
11
13
|
const [loading, setLoading] = useState(true);
|
|
12
14
|
const [kpiHistory, setKpiHistory] = useState({
|
|
13
15
|
tokens: [], cost: [], saved: [], efficiency: [],
|
|
@@ -21,9 +23,23 @@ export function useDashboard() {
|
|
|
21
23
|
|
|
22
24
|
async function fetch() {
|
|
23
25
|
try {
|
|
24
|
-
const d = await
|
|
26
|
+
const [d, teams, constraints, discoveries, chainRoles, specs] = await Promise.all([
|
|
27
|
+
api.get('/dashboard'),
|
|
28
|
+
api.get('/tokens/by-team').catch(() => ({ teams: [] })),
|
|
29
|
+
api.get('/memory/constraints').catch(() => ({ constraints: [] })),
|
|
30
|
+
api.get('/memory/discoveries?limit=20').catch(() => ({ discoveries: [] })),
|
|
31
|
+
api.get('/memory/handoff-chain').catch(() => ({ roles: [] })),
|
|
32
|
+
api.get('/memory/specializations').catch(() => ({ perAgent: {}, perProjectRole: {} })),
|
|
33
|
+
]);
|
|
25
34
|
if (!alive) return;
|
|
26
35
|
setData(d);
|
|
36
|
+
setTeamBurn(teams?.teams || []);
|
|
37
|
+
setMemory({
|
|
38
|
+
constraints: constraints?.constraints || [],
|
|
39
|
+
discoveries: discoveries?.discoveries || [],
|
|
40
|
+
roles: chainRoles?.roles || [],
|
|
41
|
+
specializations: specs || { perAgent: {}, perProjectRole: {} },
|
|
42
|
+
});
|
|
27
43
|
setLoading(false);
|
|
28
44
|
lastFetch.current = Date.now();
|
|
29
45
|
|
|
@@ -32,15 +48,16 @@ export function useDashboard() {
|
|
|
32
48
|
const now = Date.now();
|
|
33
49
|
const add = (arr, val) => [...arr.slice(-59), { t: now, v: val || 0 }];
|
|
34
50
|
const totalUsed = d.tokens?.totalTokens || 0;
|
|
35
|
-
const totalSaved = d.tokens?.savings?.total || 0;
|
|
36
|
-
const hypothetical = totalUsed + totalSaved;
|
|
37
51
|
const input = d.tokens?.totalInputTokens || 0;
|
|
38
52
|
const output = d.tokens?.totalOutputTokens || 0;
|
|
53
|
+
const breakdown = d.agents?.breakdown || [];
|
|
54
|
+
const withQ = breakdown.filter((a) => a.quality?.score != null);
|
|
55
|
+
const avgQ = withQ.length > 0 ? withQ.reduce((s, a) => s + a.quality.score, 0) / withQ.length : 0;
|
|
39
56
|
return {
|
|
40
57
|
tokens: add(prev.tokens, totalUsed),
|
|
41
58
|
cost: add(prev.cost, d.tokens?.totalCostUsd),
|
|
42
|
-
saved: add(prev.saved,
|
|
43
|
-
efficiency: add(prev.efficiency,
|
|
59
|
+
saved: add(prev.saved, avgQ),
|
|
60
|
+
efficiency: add(prev.efficiency, d.rotation?.totalRotations || 0),
|
|
44
61
|
cache: add(prev.cache, d.tokens?.cacheHitRate),
|
|
45
62
|
inputOutput: add(prev.inputOutput, output > 0 ? input / output : 0),
|
|
46
63
|
agents: add(prev.agents, d.agents?.running || 0),
|
|
@@ -69,5 +86,6 @@ export function useDashboard() {
|
|
|
69
86
|
data, loading, agents, connected, kpiHistory,
|
|
70
87
|
lastFetch: lastFetch.current,
|
|
71
88
|
agentBreakdown, routing, rotation, adaptive, journalist, rotating,
|
|
89
|
+
teamBurn, memory,
|
|
72
90
|
};
|
|
73
91
|
}
|
|
@@ -44,6 +44,7 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
44
44
|
// ── Navigation ────────────────────────────────────────────
|
|
45
45
|
activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
|
|
46
46
|
detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
|
|
47
|
+
teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
|
|
47
48
|
commandPaletteOpen: false,
|
|
48
49
|
|
|
49
50
|
// ── Layout persistence ────────────────────────────────────
|
|
@@ -57,8 +58,6 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
57
58
|
chatHistory: loadJSON('groove:chatHistory'),
|
|
58
59
|
chatInputs: {}, // Per-agent draft input text — persists across tab switches
|
|
59
60
|
tokenTimeline: {},
|
|
60
|
-
dashTelemetry: {},
|
|
61
|
-
ccChartTimeline: [],
|
|
62
61
|
|
|
63
62
|
// ── Approvals ─────────────────────────────────────────────
|
|
64
63
|
pendingApprovals: [],
|
|
@@ -264,7 +263,9 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
264
263
|
if (chatHistory[msg.oldAgentId]?.length) chatHistory[msg.newAgentId] = [...chatHistory[msg.oldAgentId]];
|
|
265
264
|
if (tokenTimeline[msg.oldAgentId]?.length) tokenTimeline[msg.newAgentId] = [...tokenTimeline[msg.oldAgentId]];
|
|
266
265
|
if (activityLog[msg.oldAgentId]?.length) activityLog[msg.newAgentId] = [...activityLog[msg.oldAgentId]];
|
|
267
|
-
|
|
266
|
+
const newPanel = { type: 'agent', agentId: msg.newAgentId };
|
|
267
|
+
const tid = get().activeTeamId;
|
|
268
|
+
return { chatHistory, tokenTimeline, activityLog, detailPanel: newPanel, teamDetailPanels: { ...s.teamDetailPanels, [tid]: newPanel } };
|
|
268
269
|
});
|
|
269
270
|
}
|
|
270
271
|
break;
|
|
@@ -339,8 +340,17 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
339
340
|
async fetchTeams() {
|
|
340
341
|
try {
|
|
341
342
|
const data = await api.get('/teams');
|
|
342
|
-
|
|
343
|
+
let teams = data.teams || [];
|
|
343
344
|
const defaultTeamId = data.defaultTeamId;
|
|
345
|
+
try {
|
|
346
|
+
const saved = JSON.parse(localStorage.getItem('groove:teamOrder') || '[]');
|
|
347
|
+
if (saved.length) {
|
|
348
|
+
const byId = Object.fromEntries(teams.map((t) => [t.id, t]));
|
|
349
|
+
const ordered = saved.filter((id) => byId[id]).map((id) => byId[id]);
|
|
350
|
+
const remaining = teams.filter((t) => !saved.includes(t.id));
|
|
351
|
+
teams = [...ordered, ...remaining];
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
344
354
|
const { activeTeamId } = get();
|
|
345
355
|
const ids = teams.map((t) => t.id);
|
|
346
356
|
const resolved = ids.includes(activeTeamId) ? activeTeamId : defaultTeamId;
|
|
@@ -350,7 +360,11 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
350
360
|
},
|
|
351
361
|
|
|
352
362
|
switchTeam(id) {
|
|
353
|
-
|
|
363
|
+
const { activeTeamId, detailPanel, teamDetailPanels } = get();
|
|
364
|
+
const updated = { ...teamDetailPanels };
|
|
365
|
+
if (activeTeamId) updated[activeTeamId] = detailPanel;
|
|
366
|
+
const restored = updated[id] || null;
|
|
367
|
+
set({ activeTeamId: id, detailPanel: restored, teamDetailPanels: updated });
|
|
354
368
|
localStorage.setItem('groove:activeTeamId', id);
|
|
355
369
|
},
|
|
356
370
|
|
|
@@ -382,6 +396,14 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
382
396
|
}
|
|
383
397
|
},
|
|
384
398
|
|
|
399
|
+
reorderTeams(fromIndex, toIndex) {
|
|
400
|
+
const teams = [...get().teams];
|
|
401
|
+
const [moved] = teams.splice(fromIndex, 1);
|
|
402
|
+
teams.splice(toIndex, 0, moved);
|
|
403
|
+
set({ teams });
|
|
404
|
+
try { localStorage.setItem('groove:teamOrder', JSON.stringify(teams.map((t) => t.id))); } catch {}
|
|
405
|
+
},
|
|
406
|
+
|
|
385
407
|
async renameTeam(id, name) {
|
|
386
408
|
try {
|
|
387
409
|
const team = await api.patch(`/teams/${id}`, { name });
|
|
@@ -392,10 +414,23 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
392
414
|
throw err;
|
|
393
415
|
}
|
|
394
416
|
},
|
|
395
|
-
openDetail(descriptor) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
417
|
+
openDetail(descriptor) {
|
|
418
|
+
const tid = get().activeTeamId;
|
|
419
|
+
set((s) => ({ detailPanel: descriptor, teamDetailPanels: { ...s.teamDetailPanels, [tid]: descriptor } }));
|
|
420
|
+
},
|
|
421
|
+
closeDetail() {
|
|
422
|
+
const tid = get().activeTeamId;
|
|
423
|
+
set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
|
|
424
|
+
},
|
|
425
|
+
selectAgent(id) {
|
|
426
|
+
const tid = get().activeTeamId;
|
|
427
|
+
const panel = { type: 'agent', agentId: id };
|
|
428
|
+
set((s) => ({ detailPanel: panel, teamDetailPanels: { ...s.teamDetailPanels, [tid]: panel } }));
|
|
429
|
+
},
|
|
430
|
+
clearSelection() {
|
|
431
|
+
const tid = get().activeTeamId;
|
|
432
|
+
set((s) => ({ detailPanel: null, teamDetailPanels: { ...s.teamDetailPanels, [tid]: null } }));
|
|
433
|
+
},
|
|
399
434
|
toggleCommandPalette() { set((s) => ({ commandPaletteOpen: !s.commandPaletteOpen })); },
|
|
400
435
|
|
|
401
436
|
setDetailPanelWidth(w) {
|
|
@@ -656,6 +691,21 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
656
691
|
// Track which agents are thinking (sent a message, waiting for response)
|
|
657
692
|
thinkingAgents: new Set(),
|
|
658
693
|
|
|
694
|
+
async stopAgent(id) {
|
|
695
|
+
try {
|
|
696
|
+
await api.post(`/agents/${id}/stop`);
|
|
697
|
+
// Clear thinking indicator
|
|
698
|
+
set((s) => {
|
|
699
|
+
const next = new Set(s.thinkingAgents);
|
|
700
|
+
next.delete(id);
|
|
701
|
+
return { thinkingAgents: next };
|
|
702
|
+
});
|
|
703
|
+
get().addToast('info', 'Stopped agent');
|
|
704
|
+
} catch (err) {
|
|
705
|
+
get().addToast('error', 'Stop failed', err.message);
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
|
|
659
709
|
async instructAgent(id, message) {
|
|
660
710
|
const agent = get().agents.find((a) => a.id === id);
|
|
661
711
|
const isAlive = agent && (agent.status === 'running' || agent.status === 'starting');
|
|
@@ -28,9 +28,33 @@ function savePositions(positions) {
|
|
|
28
28
|
try { localStorage.setItem('groove:nodePositions', JSON.stringify(positions)); } catch {}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function loadTeamViewports() {
|
|
32
|
+
try { return JSON.parse(localStorage.getItem('groove:teamViewports') || '{}'); } catch { return {}; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function saveTeamViewport(teamId, viewport) {
|
|
36
|
+
try {
|
|
37
|
+
const all = loadTeamViewports();
|
|
38
|
+
all[teamId] = viewport;
|
|
39
|
+
localStorage.setItem('groove:teamViewports', JSON.stringify(all));
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
/* ── Team Tab Bar (IDE-style) ──────────────────────────────── */
|
|
32
44
|
|
|
33
|
-
function
|
|
45
|
+
function teamStatus(agents, teamId) {
|
|
46
|
+
const ta = agents.filter((a) => a.teamId === teamId);
|
|
47
|
+
if (ta.length === 0) return 'idle';
|
|
48
|
+
const running = ta.some((a) => a.status === 'running' || a.status === 'starting');
|
|
49
|
+
if (running) return 'working';
|
|
50
|
+
const allDone = ta.every((a) => a.status === 'completed');
|
|
51
|
+
if (allDone) return 'completed';
|
|
52
|
+
const anyCrashed = ta.some((a) => a.status === 'crashed');
|
|
53
|
+
if (anyCrashed) return 'crashed';
|
|
54
|
+
return 'idle';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function TeamTabBar() {
|
|
34
58
|
const teams = useGrooveStore((s) => s.teams);
|
|
35
59
|
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
36
60
|
const agents = useGrooveStore((s) => s.agents);
|
|
@@ -39,11 +63,15 @@ function TeamTabBar() {
|
|
|
39
63
|
const deleteTeam = useGrooveStore((s) => s.deleteTeam);
|
|
40
64
|
const renameTeam = useGrooveStore((s) => s.renameTeam);
|
|
41
65
|
|
|
66
|
+
const reorderTeams = useGrooveStore((s) => s.reorderTeams);
|
|
67
|
+
|
|
42
68
|
const [creating, setCreating] = useState(false);
|
|
43
69
|
const [newName, setNewName] = useState('');
|
|
44
70
|
const [renamingId, setRenamingId] = useState(null);
|
|
45
71
|
const [renameValue, setRenameValue] = useState('');
|
|
46
72
|
const submitting = useRef(false);
|
|
73
|
+
const [dragId, setDragId] = useState(null);
|
|
74
|
+
const [dragOverId, setDragOverId] = useState(null);
|
|
47
75
|
|
|
48
76
|
function handleCreate() {
|
|
49
77
|
const name = newName.trim();
|
|
@@ -77,18 +105,48 @@ function TeamTabBar() {
|
|
|
77
105
|
return (
|
|
78
106
|
<div
|
|
79
107
|
key={team.id}
|
|
108
|
+
draggable={!isRenaming}
|
|
109
|
+
onDragStart={(e) => { setDragId(team.id); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ''); }}
|
|
110
|
+
onDragEnd={() => { setDragId(null); setDragOverId(null); }}
|
|
111
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (dragId && dragId !== team.id) setDragOverId(team.id); }}
|
|
112
|
+
onDragLeave={() => { if (dragOverId === team.id) setDragOverId(null); }}
|
|
113
|
+
onDrop={(e) => {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
if (!dragId || dragId === team.id) return;
|
|
116
|
+
const from = teams.findIndex((t) => t.id === dragId);
|
|
117
|
+
const to = teams.findIndex((t) => t.id === team.id);
|
|
118
|
+
if (from !== -1 && to !== -1) reorderTeams(from, to);
|
|
119
|
+
setDragId(null);
|
|
120
|
+
setDragOverId(null);
|
|
121
|
+
}}
|
|
80
122
|
onClick={() => !isRenaming && switchTeam(team.id)}
|
|
81
123
|
onDoubleClick={() => startRename(team)}
|
|
82
124
|
className={cn(
|
|
83
125
|
'group relative flex items-center gap-2 px-4 h-9 text-xs font-sans cursor-pointer select-none transition-colors',
|
|
84
126
|
isActive
|
|
85
|
-
? '
|
|
127
|
+
? 'text-text-0 font-semibold border-x border-x-border bg-[#242830]'
|
|
86
128
|
: 'text-text-3 hover:text-text-1 hover:bg-surface-3/50',
|
|
129
|
+
dragId === team.id && 'opacity-40',
|
|
130
|
+
dragOverId === team.id && dragId !== team.id && 'border-l-2 !border-l-accent',
|
|
87
131
|
)}
|
|
88
132
|
>
|
|
89
133
|
{/* Thin accent line at top */}
|
|
90
134
|
{isActive && <div className="absolute top-0 left-0 right-0 h-px bg-accent" style={{ height: '0.5px' }} />}
|
|
91
|
-
|
|
135
|
+
{(() => {
|
|
136
|
+
const status = teamStatus(agents, team.id);
|
|
137
|
+
const iconColor = status === 'working' ? 'text-green-400'
|
|
138
|
+
: status === 'completed' ? 'text-green-400'
|
|
139
|
+
: status === 'crashed' ? 'text-red-400'
|
|
140
|
+
: isActive ? 'text-accent' : 'text-text-4';
|
|
141
|
+
return (
|
|
142
|
+
<span className="relative flex-shrink-0">
|
|
143
|
+
<Users size={13} className={cn(iconColor, status === 'working' && 'animate-pulse')} />
|
|
144
|
+
{status === 'working' && (
|
|
145
|
+
<span className="absolute -top-0.5 -right-0.5 w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
|
|
146
|
+
)}
|
|
147
|
+
</span>
|
|
148
|
+
);
|
|
149
|
+
})()}
|
|
92
150
|
|
|
93
151
|
{isRenaming ? (
|
|
94
152
|
<input
|
|
@@ -138,7 +196,7 @@ function TeamTabBar() {
|
|
|
138
196
|
|
|
139
197
|
{/* Bottom edge hides the parent border for active tab */}
|
|
140
198
|
{isActive && (
|
|
141
|
-
<div className="absolute bottom-[-1px] left-0 right-0 h-px bg-
|
|
199
|
+
<div className="absolute bottom-[-1px] left-0 right-0 h-px bg-[#242830]" />
|
|
142
200
|
)}
|
|
143
201
|
</div>
|
|
144
202
|
);
|
|
@@ -193,8 +251,9 @@ function AgentTreeInner() {
|
|
|
193
251
|
[allAgents, activeTeamId],
|
|
194
252
|
);
|
|
195
253
|
|
|
196
|
-
const { fitView } = useReactFlow();
|
|
254
|
+
const { fitView, setViewport } = useReactFlow();
|
|
197
255
|
const [prevCount, setPrevCount] = useState(0);
|
|
256
|
+
const prevTeamIdRef = useRef(activeTeamId);
|
|
198
257
|
|
|
199
258
|
// Build nodes — positions are stable, data updates flow to node components
|
|
200
259
|
const targetNodes = useMemo(() => {
|
|
@@ -346,11 +405,23 @@ function AgentTreeInner() {
|
|
|
346
405
|
});
|
|
347
406
|
}, [targetEdges, nodes, setEdges]);
|
|
348
407
|
|
|
349
|
-
// Only fitView when agents are added — debounced so team launches (multiple spawns)
|
|
350
|
-
// don't cause repeated zoom/pan jitter
|
|
351
408
|
const agentIdStr = agents.map((a) => a.id).join(',');
|
|
352
409
|
const fitTimer = useRef(null);
|
|
353
410
|
useEffect(() => {
|
|
411
|
+
// Team switch — restore saved viewport instead of fitting
|
|
412
|
+
if (prevTeamIdRef.current !== activeTeamId) {
|
|
413
|
+
prevTeamIdRef.current = activeTeamId;
|
|
414
|
+
prevAgentIds.current = new Set(agents.map((a) => a.id));
|
|
415
|
+
setPrevCount(agents.length);
|
|
416
|
+
const saved = loadTeamViewports()[activeTeamId];
|
|
417
|
+
if (saved) {
|
|
418
|
+
setViewport(saved, { duration: 200 });
|
|
419
|
+
} else if (agents.length > 0) {
|
|
420
|
+
fitView({ padding: 0.3, maxZoom: 1.2, duration: 200 });
|
|
421
|
+
}
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
354
425
|
const currentIds = new Set(agents.map((a) => a.id));
|
|
355
426
|
const isNewAgent = agents.length > 0 && [...currentIds].some((id) => !prevAgentIds.current.has(id));
|
|
356
427
|
prevAgentIds.current = currentIds;
|
|
@@ -358,12 +429,15 @@ function AgentTreeInner() {
|
|
|
358
429
|
if (prevCount === 0 && agents.length > 0) {
|
|
359
430
|
fitView({ padding: 0.3, maxZoom: 1.2, duration: 0 });
|
|
360
431
|
} else if (isNewAgent) {
|
|
361
|
-
// Debounce: wait 500ms for batch spawns to settle before fitting
|
|
362
432
|
clearTimeout(fitTimer.current);
|
|
363
433
|
fitTimer.current = setTimeout(() => fitView({ padding: 0.3, maxZoom: 1.2, duration: 300 }), 500);
|
|
364
434
|
}
|
|
365
435
|
setPrevCount(agents.length);
|
|
366
|
-
}, [agentIdStr, prevCount, fitView]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
436
|
+
}, [agentIdStr, prevCount, fitView, activeTeamId, setViewport]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
437
|
+
|
|
438
|
+
const onMoveEnd = useCallback((_e, viewport) => {
|
|
439
|
+
saveTeamViewport(activeTeamId, viewport);
|
|
440
|
+
}, [activeTeamId]);
|
|
367
441
|
|
|
368
442
|
const onNodeClick = useCallback((_e, node) => {
|
|
369
443
|
if (node.id === ROOT_ID) return;
|
|
@@ -419,6 +493,7 @@ function AgentTreeInner() {
|
|
|
419
493
|
onPaneClick={onPaneClick}
|
|
420
494
|
onNodeDrag={onNodeDrag}
|
|
421
495
|
onNodeDragStop={onNodeDragStop}
|
|
496
|
+
onMoveEnd={onMoveEnd}
|
|
422
497
|
defaultViewport={{ x: 0, y: 0, zoom: 1.2 }}
|
|
423
498
|
proOptions={{ hideAttribution: true }}
|
|
424
499
|
minZoom={0.2}
|
|
@@ -651,7 +726,6 @@ export default function AgentsView() {
|
|
|
651
726
|
|
|
652
727
|
return (
|
|
653
728
|
<div className="flex flex-col h-full relative">
|
|
654
|
-
<TeamTabBar />
|
|
655
729
|
<div className="flex-1 min-h-0">
|
|
656
730
|
{isLoading ? (
|
|
657
731
|
<div className={cn(
|
|
@@ -8,6 +8,7 @@ import { TokenChart } from '../components/dashboard/token-chart';
|
|
|
8
8
|
import { CacheRing } from '../components/dashboard/cache-ring';
|
|
9
9
|
import { RoutingChart } from '../components/dashboard/routing-chart';
|
|
10
10
|
import { IntelPanel } from '../components/dashboard/intel-panel';
|
|
11
|
+
import { TeamBurnPanel } from '../components/dashboard/team-burn-panel';
|
|
11
12
|
import { ActivityFeed } from '../components/dashboard/activity-feed';
|
|
12
13
|
import { Skeleton } from '../components/ui/skeleton';
|
|
13
14
|
import { HEX } from '../lib/theme-hex';
|
|
@@ -34,7 +35,7 @@ function DashboardSkeleton() {
|
|
|
34
35
|
export default function DashboardView() {
|
|
35
36
|
const {
|
|
36
37
|
data, loading, agents, connected, kpiHistory, lastFetch,
|
|
37
|
-
agentBreakdown, routing, rotation, adaptive, journalist, rotating,
|
|
38
|
+
agentBreakdown, routing, rotation, adaptive, journalist, rotating, teamBurn, memory,
|
|
38
39
|
} = useDashboard();
|
|
39
40
|
|
|
40
41
|
const teams = useGrooveStore((s) => s.teams);
|
|
@@ -72,30 +73,34 @@ export default function DashboardView() {
|
|
|
72
73
|
totalTurns: rawTokens.totalTurns || 0,
|
|
73
74
|
agentCount: rawTokens.agentCount || 0,
|
|
74
75
|
savings: rawTokens.savings || {},
|
|
76
|
+
internalOverhead: rawTokens.internalOverhead || { tokens: 0, costUsd: 0, components: {} },
|
|
75
77
|
};
|
|
76
78
|
|
|
77
|
-
const totalHypothetical = tokens.totalTokens + (tokens.savings.total || 0);
|
|
78
|
-
const efficiency = totalHypothetical > 0 ? ((tokens.savings.total || 0) / totalHypothetical) * 100 : 0;
|
|
79
79
|
const ioRatio = tokens.totalOutputTokens > 0 ? (tokens.totalInputTokens / tokens.totalOutputTokens).toFixed(1) : '—';
|
|
80
|
+
const totalRotations = rotation?.totalRotations || 0;
|
|
81
|
+
|
|
82
|
+
const agentsWithQ = (agentBreakdown || []).filter((a) => a.quality?.score != null);
|
|
83
|
+
const avgQuality = agentsWithQ.length > 0
|
|
84
|
+
? Math.round(agentsWithQ.reduce((s, a) => s + a.quality.score, 0) / agentsWithQ.length)
|
|
85
|
+
: null;
|
|
80
86
|
|
|
81
87
|
const timeline = data.timeline || {};
|
|
82
88
|
const snapshots = timeline.snapshots || [];
|
|
83
89
|
const events = timeline.events || data.events || [];
|
|
84
90
|
|
|
85
91
|
const kpis = [
|
|
86
|
-
{ label: 'Tokens Used', value: fmtNum(tokens.totalTokens), sparkData: kpiHistory.tokens, color: HEX.accent },
|
|
87
|
-
{ label: 'Total Cost', value: fmtDollar(tokens.totalCostUsd), sparkData: kpiHistory.cost, color: HEX.warning },
|
|
88
|
-
{ label: '
|
|
89
|
-
{ label: '
|
|
90
|
-
{ label: '
|
|
91
|
-
{ label: 'I/O Ratio', value: `${ioRatio}:1`, sparkData: kpiHistory.inputOutput, color: HEX.orange },
|
|
92
|
-
{ label: 'Agents', value: `${runningCount}/${agents.length}`, sparkData: kpiHistory.agents, color: HEX.accent },
|
|
93
|
-
{ label: 'Turns', value: fmtNum(tokens.totalTurns), sparkData: kpiHistory.turns, color: HEX.text2 },
|
|
92
|
+
{ label: 'Tokens Used', value: fmtNum(tokens.totalTokens), sparkData: kpiHistory.tokens, color: HEX.accent, hint: 'Total tokens consumed across all agents — input, output, and cache tokens combined.' },
|
|
93
|
+
{ label: 'Total Cost', value: fmtDollar(tokens.totalCostUsd), sparkData: kpiHistory.cost, color: HEX.warning, hint: 'Actual cost reported by providers. Claude Code reports real billing; other providers use estimated rates.' },
|
|
94
|
+
{ label: 'Quality', value: avgQuality != null ? `${avgQuality}` : '—', sparkData: kpiHistory.saved, color: avgQuality >= 70 ? HEX.success : avgQuality >= 40 ? HEX.warning : HEX.danger, hint: 'Average session quality score (0-100) across running agents. Based on error rate, repetitions, file churn, and tool success. Below 40 triggers auto-rotation.' },
|
|
95
|
+
{ label: 'Cache Rate', value: fmtPct(tokens.cacheHitRate * 100), sparkData: kpiHistory.cache, color: HEX.info, hint: 'Percentage of input tokens served from prompt cache. Higher = faster responses and lower cost. Managed by your AI provider.' },
|
|
96
|
+
{ label: 'Rotations', value: `${totalRotations}`, sparkData: kpiHistory.efficiency, color: HEX.purple, hint: 'Total context rotations — includes quality-based (auto), context threshold, natural compaction (provider-managed), and manual rotations.' },
|
|
97
|
+
{ label: 'I/O Ratio', value: `${ioRatio}:1`, sparkData: kpiHistory.inputOutput, color: HEX.orange, hint: 'Ratio of input to output tokens. High ratios mean agents are reading more than writing — common for analysis tasks.' },
|
|
98
|
+
{ label: 'Agents', value: `${runningCount}/${agents.length}`, sparkData: kpiHistory.agents, color: HEX.accent, hint: 'Running agents out of total spawned this session (including completed and crashed).' },
|
|
99
|
+
{ label: 'Turns', value: fmtNum(tokens.totalTurns), sparkData: kpiHistory.turns, color: HEX.text2, hint: 'Total conversation turns across all agents. Each turn is one request-response cycle with the AI provider.' },
|
|
94
100
|
];
|
|
95
101
|
|
|
96
102
|
return (
|
|
97
103
|
<div className="flex flex-col h-full">
|
|
98
|
-
{/* Header */}
|
|
99
104
|
<DashboardHeader
|
|
100
105
|
connected={connected}
|
|
101
106
|
runningCount={runningCount}
|
|
@@ -105,22 +110,18 @@ export default function DashboardView() {
|
|
|
105
110
|
activeTeam={data.activeTeam}
|
|
106
111
|
/>
|
|
107
112
|
|
|
108
|
-
{/* KPI Strip */}
|
|
109
113
|
<KpiStrip kpis={kpis} />
|
|
110
114
|
|
|
111
|
-
{/* Main grid */}
|
|
112
115
|
<div className="flex-1 min-h-0 grid" style={{
|
|
113
116
|
gridTemplateRows: 'minmax(0, 1fr) minmax(0, 1fr)',
|
|
114
117
|
gridTemplateColumns: '3fr 1.5fr 1.5fr',
|
|
115
118
|
background: '#282c34',
|
|
116
119
|
gap: '1px',
|
|
117
120
|
}}>
|
|
118
|
-
{/* R3C1: Token Flow Chart — self-sizing via absolute inset-0 */}
|
|
119
121
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 relative">
|
|
120
122
|
<TokenChart data={snapshots} />
|
|
121
123
|
</div>
|
|
122
124
|
|
|
123
|
-
{/* R3C2: Cache Ring */}
|
|
124
125
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
|
|
125
126
|
<div className="px-3 pt-2.5 pb-1">
|
|
126
127
|
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Cache Performance</span>
|
|
@@ -132,7 +133,6 @@ export default function DashboardView() {
|
|
|
132
133
|
/>
|
|
133
134
|
</div>
|
|
134
135
|
|
|
135
|
-
{/* R3C3: Routing Chart */}
|
|
136
136
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-l border-border">
|
|
137
137
|
<div className="px-3 pt-2.5 pb-1">
|
|
138
138
|
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Model Routing</span>
|
|
@@ -140,7 +140,6 @@ export default function DashboardView() {
|
|
|
140
140
|
<RoutingChart routing={routing} agentBreakdown={agentBreakdown} />
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
|
-
{/* R4C1: Agent Fleet */}
|
|
144
143
|
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-border">
|
|
145
144
|
<div className="px-3 pt-2.5 pb-1 flex-shrink-0">
|
|
146
145
|
<span className="text-2xs font-mono text-text-3 uppercase tracking-widest">Agent Fleet</span>
|
|
@@ -148,18 +147,22 @@ export default function DashboardView() {
|
|
|
148
147
|
<FleetPanel agentBreakdown={agentBreakdown} rotating={rotating} teams={teams} />
|
|
149
148
|
</div>
|
|
150
149
|
|
|
151
|
-
|
|
152
|
-
<div className="col-span-2 min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
|
|
150
|
+
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
|
|
153
151
|
<IntelPanel
|
|
154
152
|
tokens={tokens}
|
|
155
153
|
rotation={rotation}
|
|
156
154
|
adaptive={adaptive}
|
|
157
155
|
journalist={journalist}
|
|
156
|
+
agentBreakdown={agentBreakdown}
|
|
157
|
+
memory={memory}
|
|
158
158
|
/>
|
|
159
159
|
</div>
|
|
160
|
+
|
|
161
|
+
<div className="min-w-0 min-h-0 overflow-hidden bg-surface-1 flex flex-col border-t border-l border-border">
|
|
162
|
+
<TeamBurnPanel teams={teamBurn} />
|
|
163
|
+
</div>
|
|
160
164
|
</div>
|
|
161
165
|
|
|
162
|
-
{/* Activity feed */}
|
|
163
166
|
<div className="flex-shrink-0 bg-surface-1 border-t border-border">
|
|
164
167
|
<ActivityFeed events={events} />
|
|
165
168
|
</div>
|