groove-dev 0.9.0 → 0.9.2

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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>GROOVE</title>
7
- <script type="module" crossorigin src="/assets/index-B5E3l1CX.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-iBqV1c12.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-CPzm9ZE9.css">
9
9
  </head>
10
10
  <body>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,117 +1,180 @@
1
- // GROOVE GUI — Agent Node Component (Unity/n8n inspired)
1
+ // GROOVE GUI — Agent Node Component
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React from 'react';
4
+ import React, { useRef, useEffect } from 'react';
5
5
  import { Handle, Position } from '@xyflow/react';
6
+ import { useGrooveStore } from '../stores/groove';
6
7
 
7
- const STATUS = {
8
- running: { color: '#4ae168', label: 'LIVE' },
9
- starting: { color: '#e5c07b', label: 'INIT' },
10
- stopped: { color: '#5c6370', label: 'STOP' },
11
- crashed: { color: '#e06c75', label: 'FAIL' },
12
- completed: { color: '#33afbc', label: 'DONE' },
13
- killed: { color: '#5c6370', label: 'KILL' },
8
+ const STATUS_COLORS = {
9
+ running: '#4ae168',
10
+ starting: '#e5c07b',
11
+ stopped: '#5c6370',
12
+ crashed: '#e06c75',
13
+ completed: '#33afbc',
14
+ killed: '#5c6370',
14
15
  };
15
16
 
16
- const ROLE_COLORS = {
17
- planner: '#c678dd',
18
- backend: '#33afbc',
19
- frontend: '#e5c07b',
20
- fullstack: '#4ae168',
21
- testing: '#61afef',
22
- devops: '#d19a66',
23
- docs: '#5c6370',
17
+ const ROLE_BADGES = {
18
+ planner: { bg: '#4ae16820', color: '#4ae168' },
19
+ backend: { bg: '#33afbc20', color: '#33afbc' },
20
+ frontend: { bg: '#e5c07b20', color: '#e5c07b' },
21
+ fullstack: { bg: '#4ae16820', color: '#4ae168' },
22
+ testing: { bg: '#61afef20', color: '#61afef' },
23
+ devops: { bg: '#d19a6620', color: '#d19a66' },
24
+ docs: { bg: '#5c637020', color: '#8b929e' },
24
25
  };
25
26
 
26
27
  export default function AgentNode({ data }) {
27
- const st = STATUS[data.status] || STATUS.stopped;
28
+ const statusColor = STATUS_COLORS[data.status] || STATUS_COLORS.stopped;
29
+ const badge = ROLE_BADGES[data.role] || ROLE_BADGES.docs;
28
30
  const alive = data.status === 'running' || data.status === 'starting';
29
31
  const sel = data.selected;
30
- const roleColor = ROLE_COLORS[data.role] || '#33afbc';
31
32
  const ctx = Math.round((data.contextUsage || 0) * 100);
32
33
 
33
34
  const tokens = data.tokensUsed > 0
34
35
  ? data.tokensUsed > 999 ? `${(data.tokensUsed / 1000).toFixed(1)}k` : `${data.tokensUsed}`
35
36
  : '0';
36
37
 
38
+ // Get scope summary for activity text
39
+ const activity = data.scope?.length > 0
40
+ ? data.scope[0].replace(/\/\*\*$/, '').replace(/^src\//, '')
41
+ : data.role;
42
+
37
43
  return (
38
44
  <div style={{
39
45
  background: '#282c34',
40
46
  border: sel ? '1px solid #33afbc' : '1px solid #3e4451',
41
- borderRadius: 8,
42
- width: 170,
47
+ borderRadius: 10,
48
+ width: 210,
49
+ padding: 0,
43
50
  cursor: 'pointer',
44
51
  fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, monospace",
45
52
  fontSize: 10,
46
53
  overflow: 'hidden',
47
- transition: 'border-color 0.2s',
48
54
  }}>
49
- {/* Target handle — circular port */}
50
55
  <Handle type="target" position={Position.Top} style={{
51
- background: '#282c34', border: `2px solid ${sel ? '#33afbc' : '#3e4451'}`,
56
+ background: '#282c34', border: '2px solid #3e4451',
52
57
  width: 8, height: 8, borderRadius: '50%', top: -4,
53
58
  }} />
54
59
 
55
- {/* Header */}
56
- <div style={{
57
- padding: '8px 10px 6px',
58
- display: 'flex', alignItems: 'center', justifyContent: 'space-between',
59
- }}>
60
- <span style={{
61
- color: '#e6e6e6', fontWeight: 700, fontSize: 11,
62
- overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1,
63
- }}>
64
- {data.name}
65
- </span>
66
- {/* Status dot */}
67
- <div style={{
68
- width: 6, height: 6, borderRadius: '50%', background: st.color, flexShrink: 0,
69
- ...(alive ? { animation: 'pulse 2s infinite' } : {}),
70
- }} />
71
- </div>
60
+ <div style={{ padding: '10px 12px 8px' }}>
61
+ {/* Name + status dot */}
62
+ <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 6 }}>
63
+ <div style={{
64
+ width: 7, height: 7, borderRadius: '50%', background: statusColor, flexShrink: 0,
65
+ ...(alive ? { animation: 'pulse 2s infinite' } : {}),
66
+ }} />
67
+ <span style={{ color: '#e6e6e6', fontWeight: 700, fontSize: 12, flex: 1 }}>
68
+ {data.name}
69
+ </span>
70
+ </div>
72
71
 
73
- {/* Role badge */}
74
- <div style={{ padding: '0 10px 6px' }}>
75
- <span style={{
76
- fontSize: 8, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1,
77
- color: roleColor, background: roleColor + '18', padding: '2px 6px', borderRadius: 3,
78
- }}>
79
- {data.role}
80
- </span>
81
- </div>
72
+ {/* Role badge */}
73
+ <div style={{ marginBottom: 10 }}>
74
+ <span style={{
75
+ fontSize: 9, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 1,
76
+ color: badge.color, background: badge.bg,
77
+ padding: '3px 8px', borderRadius: 4,
78
+ display: 'inline-block',
79
+ }}>
80
+ {data.role}
81
+ </span>
82
+ </div>
82
83
 
83
- {/* Metrics — minimal */}
84
- <div style={{
85
- padding: '4px 10px 6px',
86
- borderTop: '1px solid #2c313a',
87
- display: 'flex', justifyContent: 'space-between', alignItems: 'center',
88
- }}>
89
- <span style={{ color: '#8b929e', fontSize: 9 }}>
90
- {tokens} <span style={{ color: '#5c6370' }}>tok</span>
91
- </span>
92
- <span style={{ color: '#8b929e', fontSize: 9 }}>
93
- {ctx}% <span style={{ color: '#5c6370' }}>ctx</span>
94
- </span>
95
- </div>
84
+ {/* Metrics */}
85
+ <div style={{
86
+ display: 'flex', justifyContent: 'space-between',
87
+ color: '#5c6370', fontSize: 10, marginBottom: 4,
88
+ }}>
89
+ <span>{tokens} tok</span>
90
+ <span>{ctx}% ctx</span>
91
+ </div>
96
92
 
97
- {/* Activity bar for live agents */}
98
- {alive && (
93
+ {/* Activity text */}
99
94
  <div style={{
100
- height: 2, background: '#1a1e25', overflow: 'hidden',
95
+ color: '#3e4451', fontSize: 9,
96
+ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
101
97
  }}>
102
- <div style={{
103
- width: '200%', height: '100%',
104
- background: `linear-gradient(90deg, transparent 25%, ${st.color}44 35%, ${st.color} 50%, ${st.color}44 65%, transparent 75%)`,
105
- animation: 'neuralFlow 2s linear infinite',
106
- }} />
98
+ {activity}
107
99
  </div>
108
- )}
100
+ </div>
101
+
102
+ {/* Live heartbeat — real-time token line chart */}
103
+ {alive && <Heartbeat agentId={data.id} color={statusColor} />}
109
104
 
110
- {/* Source handle — circular port */}
111
105
  <Handle type="source" position={Position.Bottom} style={{
112
- background: '#282c34', border: `2px solid ${sel ? '#33afbc' : '#3e4451'}`,
106
+ background: '#282c34', border: '2px solid #3e4451',
113
107
  width: 8, height: 8, borderRadius: '50%', bottom: -4,
114
108
  }} />
115
109
  </div>
116
110
  );
117
111
  }
112
+
113
+ // ── HEARTBEAT — Mini real-time token line chart ──
114
+
115
+ function Heartbeat({ agentId, color }) {
116
+ const canvasRef = useRef();
117
+ const tokenTimeline = useGrooveStore((s) => s.tokenTimeline);
118
+
119
+ useEffect(() => {
120
+ const canvas = canvasRef.current;
121
+ if (!canvas) return;
122
+
123
+ const dpr = window.devicePixelRatio || 1;
124
+ const w = 210;
125
+ const h = 24;
126
+ canvas.width = w * dpr;
127
+ canvas.height = h * dpr;
128
+ const ctx = canvas.getContext('2d');
129
+ ctx.scale(dpr, dpr);
130
+ ctx.clearRect(0, 0, w, h);
131
+
132
+ const pts = tokenTimeline[agentId] || [];
133
+ if (pts.length < 2) {
134
+ // Flat baseline
135
+ ctx.strokeStyle = '#2c313a';
136
+ ctx.lineWidth = 0.5;
137
+ ctx.beginPath();
138
+ ctx.moveTo(0, h / 2);
139
+ ctx.lineTo(w, h / 2);
140
+ ctx.stroke();
141
+ return;
142
+ }
143
+
144
+ // Use last 40 data points
145
+ const data = pts.slice(-40);
146
+ const vals = data.map((p) => p.v);
147
+ const min = Math.min(...vals);
148
+ const max = Math.max(...vals);
149
+ const range = max - min || 1;
150
+
151
+ const padY = 4;
152
+ const usableH = h - padY * 2;
153
+ const step = w / (data.length - 1);
154
+
155
+ // Line
156
+ ctx.beginPath();
157
+ data.forEach((p, i) => {
158
+ const x = i * step;
159
+ const y = padY + usableH - ((p.v - min) / range) * usableH;
160
+ i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
161
+ });
162
+ ctx.strokeStyle = color;
163
+ ctx.lineWidth = 1;
164
+ ctx.stroke();
165
+
166
+ // End dot
167
+ const lastX = (data.length - 1) * step;
168
+ const lastY = padY + usableH - ((data[data.length - 1].v - min) / range) * usableH;
169
+ ctx.beginPath();
170
+ ctx.arc(lastX, lastY, 2, 0, Math.PI * 2);
171
+ ctx.fillStyle = color;
172
+ ctx.fill();
173
+ }, [tokenTimeline, agentId, color]);
174
+
175
+ return (
176
+ <div style={{ borderTop: '1px solid #2c313a', background: '#1e222a' }}>
177
+ <canvas ref={canvasRef} style={{ display: 'block', width: 210, height: 24 }} />
178
+ </div>
179
+ );
180
+ }
@@ -10,7 +10,7 @@ import AgentNode from '../components/AgentNode';
10
10
  const nodeTypes = { agent: AgentNode };
11
11
 
12
12
  const MAX_PER_ROW = 4;
13
- const NODE_X_SPACING = 220;
13
+ const NODE_X_SPACING = 250;
14
14
  const NODE_Y_SPACING = 140;
15
15
 
16
16
  function AgentTreeInner() {
@@ -67,13 +67,13 @@ function AgentTreeInner() {
67
67
  draggable: false,
68
68
  style: {
69
69
  background: '#282c34',
70
- color: '#33afbc',
71
- border: '1px solid #33afbc',
72
- borderRadius: 8,
73
- fontWeight: 800,
74
- fontSize: 10,
75
- letterSpacing: 4,
76
- padding: '8px 20px 7px',
70
+ color: '#e6e6e6',
71
+ border: '1px solid #3e4451',
72
+ borderRadius: 24,
73
+ fontWeight: 600,
74
+ fontSize: 11,
75
+ letterSpacing: 6,
76
+ padding: '10px 36px 9px',
77
77
  fontFamily: "'JetBrains Mono', 'SF Mono', Consolas, monospace",
78
78
  },
79
79
  };