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
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>Groove GUI</title>
8
- <script type="module" crossorigin src="/assets/index-CaKBNWcK.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-eCrVowF0.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
13
- <link rel="stylesheet" crossorigin href="/assets/index-CEFKgLGB.css">
13
+ <link rel="stylesheet" crossorigin href="/assets/index-DjORRpF0.css">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.22.27",
3
+ "version": "0.27.0",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -27,11 +27,8 @@
27
27
  "@fontsource-variable/jetbrains-mono": "^5.2.8",
28
28
  "@radix-ui/react-context-menu": "^2.2.16",
29
29
  "@radix-ui/react-dialog": "^1.1.15",
30
- "@radix-ui/react-dropdown-menu": "^2.1.16",
31
- "@radix-ui/react-popover": "^1.1.15",
32
30
  "@radix-ui/react-scroll-area": "^1.2.10",
33
31
  "@radix-ui/react-select": "^2.2.6",
34
- "@radix-ui/react-separator": "^1.1.8",
35
32
  "@radix-ui/react-tabs": "^1.1.13",
36
33
  "@radix-ui/react-tooltip": "^1.2.8",
37
34
  "@tailwindcss/vite": "^4.2.2",
@@ -1,6 +1,6 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useEffect } from 'react';
3
- import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip } from 'lucide-react';
3
+ import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip, Square } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
6
  import { Avatar } from '../ui/avatar';
@@ -236,28 +236,37 @@ export function AgentChat({ agent }) {
236
236
  placeholder={isAlive ? 'Instruct agent... (? to query)' : 'Continue conversation...'}
237
237
  rows={1}
238
238
  className={cn(
239
- 'flex-1 resize-none rounded-xl px-4 py-2.5 text-sm',
239
+ 'flex-1 resize-y rounded-xl px-4 py-2.5 text-sm',
240
240
  'bg-surface-0 border text-text-0 font-sans',
241
241
  'placeholder:text-text-4',
242
242
  'focus:outline-none focus:ring-1',
243
- 'max-h-[120px] min-h-[40px]',
243
+ 'min-h-[40px]',
244
244
  isQuery ? 'border-info/30 focus:ring-info/40' : 'border-border focus:ring-accent/40',
245
245
  )}
246
- style={{ height: Math.min(Math.max(40, input.split('\n').length * 22), 120) }}
247
246
  />
248
- <button
249
- onClick={handleSend}
250
- disabled={!input.trim() || sending}
251
- className={cn(
252
- 'w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer',
253
- 'disabled:opacity-20 disabled:cursor-not-allowed',
254
- input.trim()
255
- ? 'bg-accent text-surface-0 hover:bg-accent/90 shadow-lg shadow-accent/20'
256
- : 'bg-surface-4 text-text-4',
257
- )}
258
- >
259
- {sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
260
- </button>
247
+ {isThinking ? (
248
+ <button
249
+ onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
250
+ title="Stop agent"
251
+ className="w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer bg-danger/80 text-white hover:bg-danger shadow-lg shadow-danger/20"
252
+ >
253
+ <Square size={14} fill="currentColor" />
254
+ </button>
255
+ ) : (
256
+ <button
257
+ onClick={handleSend}
258
+ disabled={!input.trim() || sending}
259
+ className={cn(
260
+ 'w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer',
261
+ 'disabled:opacity-20 disabled:cursor-not-allowed',
262
+ input.trim()
263
+ ? 'bg-accent text-surface-0 hover:bg-accent/90 shadow-lg shadow-accent/20'
264
+ : 'bg-surface-4 text-text-4',
265
+ )}
266
+ >
267
+ {sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
268
+ </button>
269
+ )}
261
270
  </div>
262
271
  </div>
263
272
  </div>
@@ -5,7 +5,7 @@ import {
5
5
  Gauge, FolderSearch, Key, Check, Eye, EyeOff,
6
6
  AlertCircle, Layers, Activity,
7
7
  RotateCw, Skull, Copy, Trash2,
8
- Sparkles, Calendar,
8
+ Sparkles, Calendar, Plug,
9
9
  } from 'lucide-react';
10
10
  import { useGrooveStore } from '../../stores/groove';
11
11
  import { Badge } from '../ui/badge';
@@ -436,6 +436,27 @@ export function AgentConfig({ agent }) {
436
436
  />
437
437
  </ConfigSection>
438
438
 
439
+ {/* ── Integration Approvals ────────────────────────────── */}
440
+ {agent.integrations?.length > 0 && (
441
+ <ConfigSection label="Integration Approvals" icon={Plug} description="Manual = you approve dangerous actions. Auto = agent acts freely.">
442
+ <SegmentedControl
443
+ options={[
444
+ { value: 'manual', label: 'Manual' },
445
+ { value: 'auto', label: 'Auto' },
446
+ ]}
447
+ value={agent.integrationApproval || 'manual'}
448
+ onChange={async (val) => {
449
+ try {
450
+ await api.patch(`/agents/${agent.id}`, { integrationApproval: val });
451
+ addToast('success', `Integration approvals → ${val === 'auto' ? 'Auto' : 'Manual'}`);
452
+ } catch (err) {
453
+ addToast('error', 'Update failed', err.message);
454
+ }
455
+ }}
456
+ />
457
+ </ConfigSection>
458
+ )}
459
+
439
460
  {/* ── Model Routing ────────────────────────────────────── */}
440
461
  <ConfigSection label="Model Routing" icon={Activity} description="How Groove selects models for this agent's tasks.">
441
462
  <SegmentedControl
@@ -1,10 +1,10 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useState, useRef, useEffect, useMemo } from 'react';
2
+ import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
3
3
  import {
4
- Send, Loader2, MessageSquare, ArrowRight,
4
+ Send, Loader2, MessageSquare, ArrowRight, Square,
5
5
  FileEdit, Search, Terminal, CheckCircle2, AlertCircle,
6
6
  RotateCw, Zap, Wrench, Eye, Code2, Bug,
7
- ChevronDown, HelpCircle, Pencil, Paperclip,
7
+ ChevronDown, HelpCircle, Pencil, Paperclip, GripHorizontal,
8
8
  } from 'lucide-react';
9
9
  import { AnimatePresence, motion } from 'framer-motion';
10
10
  import { useGrooveStore } from '../../stores/groove';
@@ -480,10 +480,22 @@ export function AgentFeed({ agent }) {
480
480
  const setInput = setStoreInput;
481
481
  const [mode, setMode] = useState('instruct'); // instruct | query
482
482
  const [sending, setSending] = useState(false);
483
+ const [inputHeight, setInputHeight] = useState(36);
484
+ const dragRef = useRef(null);
483
485
  const scrollRef = useRef(null);
484
486
  const inputRef = useRef(null);
485
487
  const fileInputRef = useRef(null);
486
488
 
489
+ const onDragStart = useCallback((e) => {
490
+ e.preventDefault();
491
+ const startY = e.clientY;
492
+ const startH = inputHeight;
493
+ const onMove = (ev) => setInputHeight(Math.min(Math.max(36, startH - (ev.clientY - startY)), 280));
494
+ const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
495
+ window.addEventListener('mousemove', onMove);
496
+ window.addEventListener('mouseup', onUp);
497
+ }, [inputHeight]);
498
+
487
499
  const timeline = useMemo(() => {
488
500
  const items = [];
489
501
  const seen = new Set();
@@ -648,7 +660,17 @@ export function AgentFeed({ agent }) {
648
660
  </div>
649
661
 
650
662
  {/* Input area */}
651
- <div className="border-t border-border px-4 py-3 bg-surface-1/50 flex-shrink-0">
663
+ <div className="bg-surface-1/50 flex-shrink-0">
664
+ {/* Drag handle */}
665
+ <div
666
+ ref={dragRef}
667
+ onMouseDown={onDragStart}
668
+ className="flex items-center justify-center h-5 cursor-row-resize border-t border-border hover:bg-surface-3/50 transition-colors group"
669
+ >
670
+ <GripHorizontal size={12} className="text-text-4 group-hover:text-text-2 transition-colors" />
671
+ </div>
672
+
673
+ <div className="px-4 pb-3">
652
674
  {/* Mode pills */}
653
675
  <div className="flex items-center gap-1 mb-2">
654
676
  <button
@@ -713,25 +735,35 @@ export function AgentFeed({ agent }) {
713
735
  'bg-transparent text-text-0 font-sans',
714
736
  'placeholder:text-text-4',
715
737
  'focus:outline-none',
716
- 'max-h-[120px] min-h-[36px]',
717
738
  )}
718
- style={{ height: Math.min(Math.max(36, input.split('\n').length * 20), 120) }}
739
+ style={{ height: inputHeight }}
719
740
  />
720
- <button
721
- onClick={handleSend}
722
- disabled={!input.trim() || sending}
723
- className={cn(
724
- 'w-9 h-9 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0 mb-px',
725
- 'disabled:opacity-15 disabled:cursor-not-allowed',
726
- input.trim()
727
- ? mode === 'query'
728
- ? 'bg-info text-white hover:bg-info/85'
729
- : 'bg-accent text-white hover:bg-accent/85'
730
- : 'bg-transparent text-text-4',
731
- )}
732
- >
733
- {sending ? <Loader2 size={15} className="animate-spin" /> : <Send size={15} />}
734
- </button>
741
+ {isThinking ? (
742
+ <button
743
+ onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
744
+ title="Stop agent"
745
+ className="w-9 h-9 flex items-center justify-center rounded-lg border border-danger/30 bg-danger/12 text-danger hover:bg-danger/20 transition-all cursor-pointer flex-shrink-0 mb-px"
746
+ >
747
+ <Square size={13} fill="currentColor" />
748
+ </button>
749
+ ) : (
750
+ <button
751
+ onClick={handleSend}
752
+ disabled={!input.trim() || sending}
753
+ className={cn(
754
+ 'w-9 h-9 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0 mb-px',
755
+ 'disabled:opacity-15 disabled:cursor-not-allowed',
756
+ input.trim()
757
+ ? mode === 'query'
758
+ ? 'bg-info text-white hover:bg-info/85'
759
+ : 'bg-accent text-white hover:bg-accent/85'
760
+ : 'bg-transparent text-text-4',
761
+ )}
762
+ >
763
+ {sending ? <Loader2 size={15} className="animate-spin" /> : <Send size={15} />}
764
+ </button>
765
+ )}
766
+ </div>
735
767
  </div>
736
768
  </div>
737
769
  </div>
@@ -1,110 +1,48 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { memo, useState } from 'react';
2
+ import { memo, useState, useMemo, useRef, useEffect } from 'react';
3
3
  import { Handle, Position } from '@xyflow/react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
- import { cn } from '../../lib/cn';
6
5
  import { statusColor } from '../../lib/status';
7
- import { fmtNum } from '../../lib/format';
6
+ import { fmtNum, fmtDollar, fmtUptime } from '../../lib/format';
8
7
 
9
8
  const EMPTY = [];
9
+ const ERROR_RE = /error|crash|fail/i;
10
+ const BAR_BG = 'rgba(51, 175, 188, 0.15)';
11
+ const BAR_H = 'h-[2px]';
10
12
 
11
- // ── Clean up model ID → short display name ───────────────
12
- // "claude-haiku-4-5-20251001" → "Haiku 4.5"
13
- // "claude-opus-4-6" → "Opus 4.6"
14
- // "gemini-3.1-pro-preview" → "Gemini 3.1 Pro"
15
- // "o4-mini" → "o4-mini"
16
13
  function shortModel(id) {
17
14
  if (!id || id === 'auto') return 'auto';
18
- // Claude models: strip "claude-" prefix and date suffix, capitalize
19
15
  const claude = id.match(/^claude-(opus|sonnet|haiku)-(\d+)-(\d+)(?:-\d+)?$/);
20
16
  if (claude) {
21
17
  const name = claude[1][0].toUpperCase() + claude[1].slice(1);
22
18
  return `${name} ${claude[2]}.${claude[3]}`;
23
19
  }
24
- // Gemini: strip "-preview"
25
20
  if (id.startsWith('gemini-')) {
26
21
  return id.replace('gemini-', 'Gemini ').replace('-preview', '').replace('-flash-lite', ' Flash Lite').replace('-flash', ' Flash').replace('-pro', ' Pro');
27
22
  }
28
- // GPT: capitalize
29
23
  if (id.startsWith('gpt-')) return id.toUpperCase().replace('GPT-', 'GPT-');
30
24
  return id;
31
25
  }
32
26
 
33
- // ── Activity label ───────────────────────────────────────
34
- function activityLabel(text) {
35
- if (!text) return null;
36
- const t = text.toLowerCase();
37
- if (t.includes('read')) return 'READ';
38
- if (t.includes('edit') || t.includes('writ')) return 'WRITE';
39
- if (t.includes('search') || t.includes('grep') || t.includes('glob')) return 'SEARCH';
40
- if (t.includes('bash') || t.includes('exec') || t.includes('running')) return 'EXEC';
41
- if (t.includes('test')) return 'TEST';
42
- if (t.includes('error') || t.includes('fail')) return 'ERR';
43
- if (t.includes('complet') || t.includes('done')) return 'DONE';
44
- return 'WORK';
27
+ function burnRate(timeline) {
28
+ if (!timeline || timeline.length < 2) return null;
29
+ const samples = timeline.slice(-10);
30
+ const dt = (samples[samples.length - 1].t - samples[0].t) / 60000;
31
+ if (dt <= 0) return null;
32
+ const dv = samples[samples.length - 1].v - samples[0].v;
33
+ return dv / dt;
45
34
  }
46
35
 
47
- function timeShort(ts) {
48
- if (!ts) return '';
49
- const s = Math.floor((Date.now() - ts) / 1000);
50
- if (s < 60) return `${s}s`;
51
- if (s < 3600) return `${Math.floor(s / 60)}m`;
52
- return `${Math.floor(s / 3600)}h`;
36
+ function qualityColor(score) {
37
+ if (score >= 70) return 'var(--color-success)';
38
+ if (score >= 40) return 'var(--color-warning)';
39
+ return 'var(--color-danger)';
53
40
  }
54
41
 
55
- // ── Slide-out panel (appears to the right) ───────────────
56
- function NodePanel({ agent }) {
57
- const activityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
58
- const recent = activityLog.slice(-8);
59
-
60
- return (
61
- <div
62
- className="absolute left-full top-0 ml-2 z-50 pointer-events-none"
63
- style={{ width: 220, animation: 'tooltip-slide-in 0.15s ease-out' }}
64
- >
65
- <div
66
- className="overflow-hidden"
67
- style={{ background: '#181b21', border: '1px solid #262a32', borderRadius: 4 }}
68
- >
69
- {/* Prompt */}
70
- {agent.prompt && (
71
- <div className="px-2.5 py-2 border-b border-[#262a32]">
72
- <p className="text-[9px] font-sans text-[#8b929e] line-clamp-3 leading-snug">{agent.prompt}</p>
73
- </div>
74
- )}
75
-
76
- {/* Activity log */}
77
- {recent.length > 0 ? (
78
- <div>
79
- <div className="px-2.5 pt-1.5 pb-1">
80
- <span className="text-[8px] font-mono text-[#3a3f4b] uppercase tracking-widest">Activity</span>
81
- </div>
82
- {recent.map((entry, i) => {
83
- const label = activityLabel(entry.text);
84
- const display = entry.text?.length > 45 ? entry.text.slice(0, 45) + '...' : entry.text;
85
- return (
86
- <div key={i} className="px-2.5 py-[3px] flex items-start gap-1.5">
87
- <span className="text-[8px] font-mono text-[#333842] w-5 flex-shrink-0 text-right">{timeShort(entry.timestamp)}</span>
88
- {label && (
89
- <span className={cn(
90
- 'text-[7px] font-mono w-7 flex-shrink-0 text-center rounded-sm px-0.5 py-px',
91
- label === 'ERR' ? 'text-[#e06c75] bg-[#e06c75]/10' : 'text-[#505862] bg-[#505862]/10',
92
- )}>{label}</span>
93
- )}
94
- <span className="text-[9px] font-sans text-[#6e7681] truncate flex-1">{display}</span>
95
- </div>
96
- );
97
- })}
98
- <div className="h-1.5" />
99
- </div>
100
- ) : (
101
- <div className="px-2.5 py-3">
102
- <span className="text-[9px] font-mono text-[#333842]">Awaiting activity...</span>
103
- </div>
104
- )}
105
- </div>
106
- </div>
107
- );
42
+ function efficiencyColor(pct) {
43
+ if (pct >= 60) return 'var(--color-success)';
44
+ if (pct >= 30) return 'var(--color-warning)';
45
+ return 'var(--color-danger)';
108
46
  }
109
47
 
110
48
  // ── Status labels ────────────────────────────────────────
@@ -121,18 +59,41 @@ const AgentNode = memo(({ data, selected }) => {
121
59
  const sColor = statusColor(agent.status);
122
60
  const tokens = agent.tokensUsed || 0;
123
61
  const [hovered, setHovered] = useState(false);
62
+ const nodeRef = useRef(null);
63
+
64
+ useEffect(() => {
65
+ const rfNode = nodeRef.current?.closest('.react-flow__node');
66
+ if (rfNode) rfNode.style.zIndex = hovered ? '1000' : '';
67
+ }, [hovered]);
68
+
69
+ const activityLog = useGrooveStore((s) => s.activityLog[agent.id]) || EMPTY;
70
+ const tokenTimeline = useGrooveStore((s) => s.tokenTimeline[agent.id]) || EMPTY;
71
+ const rate = burnRate(tokenTimeline);
72
+ const errorCount = useMemo(() => activityLog.filter((e) => ERROR_RE.test(e.text)).length, [activityLog]);
73
+ const ctxColor = contextPct > 75 ? 'var(--color-danger)' : contextPct > 50 ? 'var(--color-warning)' : 'var(--color-success)';
74
+
75
+ const qScore = agent.qualityScore != null ? Math.round(agent.qualityScore) : null;
76
+ const qColor = qScore != null ? qualityColor(qScore) : null;
77
+
78
+ const effPct = agent.efficiency != null ? agent.efficiency : null;
79
+ const effColor = effPct != null ? efficiencyColor(effPct) : null;
80
+
81
+ const uptimeSec = agent.durationMs ? agent.durationMs / 1000
82
+ : agent.spawnedAt ? (Date.now() - new Date(agent.spawnedAt).getTime()) / 1000
83
+ : agent.createdAt ? (Date.now() - new Date(agent.createdAt).getTime()) / 1000
84
+ : 0;
124
85
 
125
86
  return (
126
87
  <div
127
- className="relative"
88
+ ref={nodeRef}
128
89
  onMouseEnter={() => setHovered(true)}
129
90
  onMouseLeave={() => setHovered(false)}
130
91
  >
131
92
  <div
132
- className="w-[220px] overflow-hidden transition-all duration-150"
93
+ className="w-[220px] overflow-hidden transition-all duration-200 ease-out"
133
94
  style={{
134
- background: '#1c1f26',
135
- border: `1px solid ${selected ? '#2e323a' : '#262a32'}`,
95
+ background: hovered ? '#141720' : '#1c1f26',
96
+ border: `1px solid ${hovered ? '#2e3640' : selected ? '#2e323a' : '#262a32'}`,
136
97
  borderRadius: 4,
137
98
  }}
138
99
  >
@@ -195,7 +156,7 @@ const AgentNode = memo(({ data, selected }) => {
195
156
  </div>
196
157
 
197
158
  {/* Context bar */}
198
- <div className="mt-1.5 h-[2px] rounded-sm overflow-hidden" style={{ background: 'rgba(51, 175, 188, 0.15)' }}>
159
+ <div className={`mt-1.5 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
199
160
  <div
200
161
  className="h-full rounded-sm transition-all duration-700"
201
162
  style={{
@@ -207,10 +168,91 @@ const AgentNode = memo(({ data, selected }) => {
207
168
  />
208
169
  </div>
209
170
  </div>
210
- </div>
211
171
 
212
- {/* ── Hover panel slides out right ────────────── */}
213
- {hovered && <NodePanel agent={agent} />}
172
+ {/* ── Expanded stats (morphs on hover) ────────── */}
173
+ <div
174
+ className="grid transition-[grid-template-rows] duration-200 ease-out"
175
+ style={{ gridTemplateRows: hovered ? '1fr' : '0fr' }}
176
+ >
177
+ <div className="overflow-hidden">
178
+ <div className="mx-3 border-t border-white/[0.04]" />
179
+
180
+ {/* Context Health */}
181
+ <div className="px-3 pt-1.5 pb-1">
182
+ <div className="flex items-center justify-between mb-1">
183
+ <span className="text-[9px] font-mono text-[#505862] uppercase tracking-wider">Context</span>
184
+ {(agent.rotations || 0) > 0 && (
185
+ <span className="text-[8px] font-mono text-[#606878] bg-white/[0.04] rounded px-1 py-px">{agent.rotations}x rot</span>
186
+ )}
187
+ </div>
188
+ <div className="flex items-center gap-2">
189
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
190
+ <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${Math.max(contextPct, 1)}%`, background: ctxColor }} />
191
+ </div>
192
+ <span className="text-[9px] font-mono font-medium" style={{ color: ctxColor }}>{contextPct}%</span>
193
+ </div>
194
+ </div>
195
+
196
+ {/* Quality */}
197
+ <div className="px-3 pt-1 pb-1">
198
+ <span className="text-[9px] font-mono text-[#505862] uppercase tracking-wider">Quality</span>
199
+ <div className="flex items-center gap-2 mt-1">
200
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
201
+ <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${qScore != null ? Math.max(qScore, 1) : 0}%`, background: qColor || '#505862' }} />
202
+ </div>
203
+ <span className="text-[9px] font-mono font-medium" style={{ color: qColor || '#505862' }}>{qScore != null ? qScore : '—'}</span>
204
+ </div>
205
+ </div>
206
+
207
+ {/* Efficiency (cache hit rate) */}
208
+ <div className="px-3 pt-1 pb-1">
209
+ <span className="text-[9px] font-mono text-[#505862] uppercase tracking-wider">Efficiency</span>
210
+ <div className="flex items-center gap-2 mt-1">
211
+ <div className={`flex-1 ${BAR_H} rounded-sm overflow-hidden`} style={{ background: BAR_BG }}>
212
+ <div className="h-full rounded-sm transition-all duration-500" style={{ width: `${effPct != null ? Math.max(effPct, 1) : 0}%`, background: effColor || '#505862' }} />
213
+ </div>
214
+ <span className="text-[9px] font-mono font-medium" style={{ color: effColor || '#505862' }}>{effPct != null ? `${effPct}%` : '—'}</span>
215
+ </div>
216
+ </div>
217
+
218
+ {/* Stats row */}
219
+ <div className="px-3 pt-1 pb-1">
220
+ <div className="grid grid-cols-3 gap-1">
221
+ <div>
222
+ <div className="text-[9px] font-mono font-medium text-[#bcc2cd]">{fmtDollar(agent.costUsd || 0)}</div>
223
+ <div className="text-[7px] font-mono text-[#505862]">cost</div>
224
+ </div>
225
+ <div>
226
+ <div className="text-[9px] font-mono font-medium text-[#bcc2cd]">{rate ? fmtNum(Math.round(rate)) : '—'}</div>
227
+ <div className="text-[7px] font-mono text-[#505862]">tok/m</div>
228
+ </div>
229
+ <div>
230
+ <div className="text-[9px] font-mono font-medium text-[#bcc2cd]">{agent.turns || 0}</div>
231
+ <div className="text-[7px] font-mono text-[#505862]">turns</div>
232
+ </div>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Session */}
237
+ <div className="px-3 pt-1 pb-2">
238
+ <div className="flex items-center gap-3">
239
+ <div className="flex items-center gap-1">
240
+ <span className="text-[9px] font-mono text-[#8b929e]">{fmtUptime(Math.max(0, Math.floor(uptimeSec)))}</span>
241
+ <span className="text-[7px] font-mono text-[#505862]">up</span>
242
+ </div>
243
+ <div className="flex items-center gap-1">
244
+ {errorCount > 0 ? (
245
+ <span className="text-[9px] font-mono text-[var(--color-danger)]">{errorCount}</span>
246
+ ) : (
247
+ <span className="text-[9px] font-mono text-[#505862]">0</span>
248
+ )}
249
+ <span className="text-[7px] font-mono text-[#505862]">err</span>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </div>
255
+ </div>
214
256
  </div>
215
257
  );
216
258
  });