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.
Files changed (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CLAUDE.md +24 -19
  3. package/node_modules/@groove-dev/cli/bin/groove.js +2 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/nuke.js +16 -4
  6. package/node_modules/@groove-dev/cli/src/commands/stop.js +17 -2
  7. package/node_modules/@groove-dev/daemon/integrations-registry.json +681 -75
  8. package/node_modules/@groove-dev/daemon/package.json +1 -1
  9. package/node_modules/@groove-dev/daemon/src/adaptive.js +23 -25
  10. package/node_modules/@groove-dev/daemon/src/api.js +346 -22
  11. package/node_modules/@groove-dev/daemon/src/classifier.js +53 -6
  12. package/node_modules/@groove-dev/daemon/src/firstrun.js +14 -1
  13. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
  14. package/node_modules/@groove-dev/daemon/src/index.js +28 -4
  15. package/node_modules/@groove-dev/daemon/src/integrations.js +215 -14
  16. package/node_modules/@groove-dev/daemon/src/introducer.js +84 -11
  17. package/node_modules/@groove-dev/daemon/src/journalist.js +43 -1
  18. package/node_modules/@groove-dev/daemon/src/lockmanager.js +60 -0
  19. package/node_modules/@groove-dev/daemon/src/mcp-manager.js +270 -0
  20. package/node_modules/@groove-dev/daemon/src/memory.js +370 -0
  21. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  22. package/node_modules/@groove-dev/daemon/src/process.js +141 -9
  23. package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
  24. package/node_modules/@groove-dev/daemon/src/rotator.js +334 -31
  25. package/node_modules/@groove-dev/daemon/src/router.js +43 -0
  26. package/node_modules/@groove-dev/daemon/src/tokentracker.js +70 -18
  27. package/node_modules/@groove-dev/daemon/src/validate.js +5 -13
  28. package/node_modules/@groove-dev/daemon/templates/groove-slides.cjs +306 -0
  29. package/node_modules/@groove-dev/daemon/test/classifier.test.js +3 -5
  30. package/node_modules/@groove-dev/daemon/test/lockmanager.test.js +64 -0
  31. package/node_modules/@groove-dev/daemon/test/memory.test.js +252 -0
  32. package/node_modules/@groove-dev/daemon/test/rotator.test.js +108 -0
  33. package/node_modules/@groove-dev/daemon/test/router.test.js +64 -0
  34. package/node_modules/@groove-dev/daemon/test/slides-engine.test.js +230 -0
  35. package/node_modules/@groove-dev/daemon/test/tokentracker.test.js +78 -0
  36. package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +1 -0
  37. package/node_modules/@groove-dev/gui/dist/assets/index-eCrVowF0.js +652 -0
  38. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  39. package/node_modules/@groove-dev/gui/package.json +1 -4
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -17
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +22 -1
  42. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +53 -21
  43. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +132 -90
  44. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +212 -1
  45. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +6 -2
  46. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +495 -174
  47. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +12 -2
  48. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  49. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +3 -3
  50. package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +24 -19
  51. package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +2 -2
  52. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  53. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  54. package/node_modules/@groove-dev/gui/src/lib/format.js +0 -6
  55. package/node_modules/@groove-dev/gui/src/lib/hooks/use-dashboard.js +23 -5
  56. package/node_modules/@groove-dev/gui/src/stores/groove.js +59 -9
  57. package/node_modules/@groove-dev/gui/src/views/agents.jsx +84 -10
  58. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +24 -21
  59. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +153 -85
  60. package/package.json +2 -8
  61. package/packages/cli/bin/groove.js +2 -0
  62. package/packages/cli/package.json +1 -1
  63. package/packages/cli/src/commands/nuke.js +16 -4
  64. package/packages/cli/src/commands/stop.js +17 -2
  65. package/packages/daemon/integrations-registry.json +681 -75
  66. package/packages/daemon/package.json +1 -1
  67. package/packages/daemon/src/adaptive.js +23 -25
  68. package/packages/daemon/src/api.js +346 -22
  69. package/packages/daemon/src/classifier.js +53 -6
  70. package/packages/daemon/src/firstrun.js +14 -1
  71. package/packages/daemon/src/gateways/manager.js +2 -2
  72. package/packages/daemon/src/index.js +28 -4
  73. package/packages/daemon/src/integrations.js +215 -14
  74. package/packages/daemon/src/introducer.js +84 -11
  75. package/packages/daemon/src/journalist.js +43 -1
  76. package/packages/daemon/src/lockmanager.js +60 -0
  77. package/packages/daemon/src/mcp-manager.js +270 -0
  78. package/packages/daemon/src/memory.js +370 -0
  79. package/packages/daemon/src/pm.js +1 -1
  80. package/packages/daemon/src/process.js +141 -9
  81. package/packages/daemon/src/registry.js +1 -1
  82. package/packages/daemon/src/rotator.js +334 -31
  83. package/packages/daemon/src/router.js +43 -0
  84. package/packages/daemon/src/tokentracker.js +70 -18
  85. package/packages/daemon/src/validate.js +5 -13
  86. package/packages/daemon/templates/groove-slides.cjs +306 -0
  87. package/packages/gui/dist/assets/index-DjORRpF0.css +1 -0
  88. package/packages/gui/dist/assets/index-eCrVowF0.js +652 -0
  89. package/packages/gui/dist/index.html +2 -2
  90. package/packages/gui/package.json +1 -4
  91. package/packages/gui/src/components/agents/agent-chat.jsx +26 -17
  92. package/packages/gui/src/components/agents/agent-config.jsx +22 -1
  93. package/packages/gui/src/components/agents/agent-feed.jsx +53 -21
  94. package/packages/gui/src/components/agents/agent-node.jsx +132 -90
  95. package/packages/gui/src/components/agents/spawn-wizard.jsx +212 -1
  96. package/packages/gui/src/components/dashboard/cache-ring.jsx +6 -2
  97. package/packages/gui/src/components/dashboard/intel-panel.jsx +495 -174
  98. package/packages/gui/src/components/dashboard/kpi-card.jsx +12 -2
  99. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +55 -0
  100. package/packages/gui/src/components/layout/activity-bar.jsx +3 -3
  101. package/packages/gui/src/components/layout/app-shell.jsx +24 -19
  102. package/packages/gui/src/components/layout/command-palette.jsx +2 -2
  103. package/packages/gui/src/components/marketplace/integration-wizard.jsx +391 -61
  104. package/packages/gui/src/components/marketplace/marketplace-card.jsx +29 -7
  105. package/packages/gui/src/lib/format.js +0 -6
  106. package/packages/gui/src/lib/hooks/use-dashboard.js +23 -5
  107. package/packages/gui/src/stores/groove.js +59 -9
  108. package/packages/gui/src/views/agents.jsx +84 -10
  109. package/packages/gui/src/views/dashboard.jsx +24 -21
  110. package/packages/gui/src/views/marketplace.jsx +153 -85
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CEFKgLGB.css +0 -1
  112. package/node_modules/@groove-dev/gui/dist/assets/index-CaKBNWcK.js +0 -638
  113. package/node_modules/@groove-dev/gui/dist/groove-logo-short.png +0 -0
  114. package/node_modules/@groove-dev/gui/dist/groove-logo.png +0 -0
  115. package/node_modules/@groove-dev/gui/public/groove-logo-short.png +0 -0
  116. package/node_modules/@groove-dev/gui/public/groove-logo.png +0 -0
  117. package/node_modules/@groove-dev/gui/src/components/ui/dropdown-menu.jsx +0 -60
  118. package/node_modules/@groove-dev/gui/src/lib/hooks/use-media-query.js +0 -18
  119. package/node_modules/@radix-ui/react-dropdown-menu/LICENSE +0 -21
  120. package/node_modules/@radix-ui/react-dropdown-menu/README.md +0 -3
  121. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.mts +0 -97
  122. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.d.ts +0 -97
  123. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js +0 -337
  124. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.js.map +0 -7
  125. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs +0 -305
  126. package/node_modules/@radix-ui/react-dropdown-menu/dist/index.mjs.map +0 -7
  127. package/node_modules/@radix-ui/react-dropdown-menu/package.json +0 -75
  128. package/node_modules/@radix-ui/react-popover/LICENSE +0 -21
  129. package/node_modules/@radix-ui/react-popover/README.md +0 -3
  130. package/node_modules/@radix-ui/react-popover/dist/index.d.mts +0 -85
  131. package/node_modules/@radix-ui/react-popover/dist/index.d.ts +0 -85
  132. package/node_modules/@radix-ui/react-popover/dist/index.js +0 -352
  133. package/node_modules/@radix-ui/react-popover/dist/index.js.map +0 -7
  134. package/node_modules/@radix-ui/react-popover/dist/index.mjs +0 -320
  135. package/node_modules/@radix-ui/react-popover/dist/index.mjs.map +0 -7
  136. package/node_modules/@radix-ui/react-popover/package.json +0 -82
  137. package/node_modules/@radix-ui/react-separator/LICENSE +0 -21
  138. package/node_modules/@radix-ui/react-separator/README.md +0 -3
  139. package/node_modules/@radix-ui/react-separator/dist/index.d.mts +0 -21
  140. package/node_modules/@radix-ui/react-separator/dist/index.d.ts +0 -21
  141. package/node_modules/@radix-ui/react-separator/dist/index.js +0 -65
  142. package/node_modules/@radix-ui/react-separator/dist/index.js.map +0 -7
  143. package/node_modules/@radix-ui/react-separator/dist/index.mjs +0 -32
  144. package/node_modules/@radix-ui/react-separator/dist/index.mjs.map +0 -7
  145. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/LICENSE +0 -21
  146. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/README.md +0 -3
  147. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.mts +0 -52
  148. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.d.ts +0 -52
  149. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js +0 -80
  150. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.js.map +0 -7
  151. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs +0 -47
  152. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/dist/index.mjs.map +0 -7
  153. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive/package.json +0 -69
  154. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/LICENSE +0 -21
  155. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/README.md +0 -3
  156. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.mts +0 -22
  157. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.d.ts +0 -22
  158. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js +0 -152
  159. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.js.map +0 -7
  160. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs +0 -119
  161. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/dist/index.mjs.map +0 -7
  162. package/node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot/package.json +0 -64
  163. package/node_modules/@radix-ui/react-separator/package.json +0 -69
  164. package/packages/gui/dist/assets/index-CEFKgLGB.css +0 -1
  165. package/packages/gui/dist/assets/index-CaKBNWcK.js +0 -638
  166. package/packages/gui/dist/groove-logo-short.png +0 -0
  167. package/packages/gui/dist/groove-logo.png +0 -0
  168. package/packages/gui/public/groove-logo-short.png +0 -0
  169. package/packages/gui/public/groove-logo.png +0 -0
  170. package/packages/gui/src/components/ui/dropdown-menu.jsx +0 -60
  171. 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 api.get('/dashboard');
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, totalSaved),
43
- efficiency: add(prev.efficiency, hypothetical > 0 ? (totalSaved / hypothetical) * 100 : 0),
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
- return { chatHistory, tokenTimeline, activityLog, detailPanel: { type: 'agent', agentId: msg.newAgentId } };
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
- const teams = data.teams || [];
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
- set({ activeTeamId: id, detailPanel: null });
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) { set({ detailPanel: descriptor }); },
396
- closeDetail() { set({ detailPanel: null }); },
397
- selectAgent(id) { set({ detailPanel: { type: 'agent', agentId: id } }); },
398
- clearSelection() { set({ detailPanel: null }); },
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 TeamTabBar() {
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
- ? 'bg-surface-0 text-text-0 font-semibold border-x border-x-border'
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
- <Users size={13} className={isActive ? 'text-accent' : 'text-text-4'} />
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-surface-0" />
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: 'Tokens Saved', value: fmtNum(tokens.savings.total || 0), sparkData: kpiHistory.saved, color: HEX.success },
89
- { label: 'Efficiency', value: fmtPct(efficiency), sparkData: kpiHistory.efficiency, color: HEX.purple },
90
- { label: 'Cache Rate', value: fmtPct(tokens.cacheHitRate * 100), sparkData: kpiHistory.cache, color: HEX.info },
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
- {/* R4C2-3: Intel Panel (spans 2 cols) */}
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>