groove-dev 0.24.5 → 0.24.6

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.
@@ -1,11 +1,11 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
- import { useRef, useEffect, useState, useCallback, memo } from 'react';
2
+ import { useRef, useEffect, useState, useCallback, useMemo, memo } from 'react';
3
3
  import { HEX, hexAlpha } from '../../lib/theme-hex';
4
- import { fmtNum, fmtDollar, fmtPct } from '../../lib/format';
4
+ import { fmtNum, fmtPct } from '../../lib/format';
5
5
 
6
6
  /**
7
- * Modern token flow chart — edge-to-edge gradient fill, floating labels,
8
- * hover tooltip for detail. Self-sizing via internal ResizeObserver.
7
+ * Modern burn-rate chart — shows tokens/interval (velocity) + running agent count
8
+ * instead of monotonically climbing cumulative values. Self-sizing.
9
9
  */
10
10
  const TokenChart = memo(function TokenChart({ data }) {
11
11
  const containerRef = useRef(null);
@@ -14,11 +14,27 @@ const TokenChart = memo(function TokenChart({ data }) {
14
14
  const [hover, setHover] = useState(null);
15
15
 
16
16
  const { width, height } = size;
17
- // Minimal padding — just enough for floating labels to breathe
18
17
  const pad = { top: 28, right: 12, bottom: 8, left: 12 };
19
18
  const w = Math.max(width - pad.left - pad.right, 0);
20
19
  const h = Math.max(height - pad.top - pad.bottom, 0);
21
20
 
21
+ // Derive burn rate + cache rate from consecutive snapshots
22
+ const chartData = useMemo(() => {
23
+ if (!data || data.length < 2) return [];
24
+ return data.slice(1).map((d, i) => {
25
+ const prev = data[i];
26
+ const dt = (d.t - prev.t) / 60000; // minutes
27
+ const dTokens = Math.max((d.tokens || 0) - (prev.tokens || 0), 0);
28
+ return {
29
+ burnRate: dt > 0 ? Math.round(dTokens / dt) : 0, // tokens/min
30
+ cacheHitRate: d.cacheHitRate || 0,
31
+ running: d.running || 0,
32
+ agents: d.agents || 0,
33
+ t: d.t,
34
+ };
35
+ });
36
+ }, [data]);
37
+
22
38
  // Self-size
23
39
  useEffect(() => {
24
40
  const el = containerRef.current;
@@ -33,20 +49,20 @@ const TokenChart = memo(function TokenChart({ data }) {
33
49
 
34
50
  const onMouseMove = useCallback((e) => {
35
51
  const canvas = canvasRef.current;
36
- if (!canvas || !data?.length || w <= 0) return;
52
+ if (!canvas || !chartData.length || w <= 0) return;
37
53
  const rect = canvas.getBoundingClientRect();
38
54
  const x = e.clientX - rect.left - pad.left;
39
55
  if (x < 0 || x > w) { setHover(null); return; }
40
- const index = Math.round((x / w) * (data.length - 1));
41
- setHover({ x: pad.left + (index / (data.length - 1)) * w, index });
42
- }, [data, w, pad.left]);
56
+ const index = Math.round((x / w) * (chartData.length - 1));
57
+ setHover({ x: pad.left + (index / Math.max(chartData.length - 1, 1)) * w, index });
58
+ }, [chartData, w, pad.left]);
43
59
 
44
60
  const onMouseLeave = useCallback(() => setHover(null), []);
45
61
 
46
62
  // Draw
47
63
  useEffect(() => {
48
64
  const canvas = canvasRef.current;
49
- if (!canvas || !data?.length || width <= 0 || height <= 0 || w <= 0 || h <= 0) return;
65
+ if (!canvas || !chartData.length || width <= 0 || height <= 0 || w <= 0 || h <= 0) return;
50
66
  const ctx = canvas.getContext('2d');
51
67
  const dpr = window.devicePixelRatio || 1;
52
68
 
@@ -55,21 +71,31 @@ const TokenChart = memo(function TokenChart({ data }) {
55
71
  ctx.scale(dpr, dpr);
56
72
  ctx.clearRect(0, 0, width, height);
57
73
 
58
- const tokens = data.map((d) => d.tokens || 0);
59
- const costs = data.map((d) => d.costUsd || 0);
60
- const caches = data.map((d) => d.cacheHitRate ?? null);
61
- const maxT = Math.max(...tokens, 1);
62
- const maxC = Math.max(...costs, 0.01);
63
- const hasCacheData = caches.some((c) => c !== null && c > 0);
74
+ const burns = chartData.map((d) => d.burnRate);
75
+ const caches = chartData.map((d) => d.cacheHitRate);
76
+ const running = chartData.map((d) => d.running);
77
+ const maxBurn = Math.max(...burns, 100);
78
+ const maxRunning = Math.max(...running, 1);
64
79
 
65
- const xAt = (i) => pad.left + (i / Math.max(data.length - 1, 1)) * w;
66
- const yToken = (v) => pad.top + h - (v / maxT) * h;
67
- const yCost = (v) => pad.top + h - (v / maxC) * h;
80
+ const xAt = (i) => pad.left + (i / Math.max(chartData.length - 1, 1)) * w;
81
+ const yBurn = (v) => pad.top + h - (v / maxBurn) * h;
68
82
  const yCache = (v) => pad.top + h - (v * h);
69
83
 
70
- // ── Subtle horizontal guidelines (3 lines, dotted) ──────
84
+ // ── Running agents bars (background, subtle) ────────────
85
+ const barW = Math.max(w / chartData.length - 1, 2);
86
+ for (let i = 0; i < chartData.length; i++) {
87
+ const r = running[i];
88
+ if (r <= 0) continue;
89
+ const barH = (r / maxRunning) * h * 0.3; // max 30% height
90
+ const x = xAt(i) - barW / 2;
91
+ const y = pad.top + h - barH;
92
+ ctx.fillStyle = hexAlpha(HEX.surface5, 0.5);
93
+ ctx.fillRect(x, y, barW, barH);
94
+ }
95
+
96
+ // ── Subtle horizontal guidelines ────────────────────────
71
97
  ctx.setLineDash([2, 4]);
72
- ctx.strokeStyle = hexAlpha(HEX.text4, 0.25);
98
+ ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
73
99
  ctx.lineWidth = 1;
74
100
  for (let i = 1; i <= 3; i++) {
75
101
  const y = pad.top + (h / 4) * i;
@@ -80,77 +106,58 @@ const TokenChart = memo(function TokenChart({ data }) {
80
106
  }
81
107
  ctx.setLineDash([]);
82
108
 
83
- // ── Floating Y-axis labels (inside chart, top-left) ─────
109
+ // ── Floating Y labels ───────────────────────────────────
84
110
  ctx.font = "9px 'JetBrains Mono Variable', monospace";
85
111
  ctx.textAlign = 'left';
86
- ctx.fillStyle = hexAlpha(HEX.text3, 0.6);
87
- // Top label (max)
88
- ctx.fillText(fmtNum(maxT), pad.left + 4, pad.top + 10);
89
- // Mid label
90
- ctx.fillText(fmtNum(maxT / 2), pad.left + 4, pad.top + h / 2 + 4);
112
+ ctx.fillStyle = hexAlpha(HEX.text3, 0.5);
113
+ ctx.fillText(`${fmtNum(maxBurn)}/m`, pad.left + 4, pad.top + 10);
114
+ ctx.fillText(`${fmtNum(Math.round(maxBurn / 2))}/m`, pad.left + 4, pad.top + h / 2 + 4);
91
115
 
92
- // ── Token area fill (edge-to-edge gradient) ─────────────
116
+ // ── Burn rate area fill ─────────────────────────────────
93
117
  ctx.beginPath();
94
118
  ctx.moveTo(pad.left, pad.top + h);
95
- for (let i = 0; i < data.length; i++) {
96
- ctx.lineTo(xAt(i), yToken(tokens[i]));
119
+ for (let i = 0; i < chartData.length; i++) {
120
+ ctx.lineTo(xAt(i), yBurn(burns[i]));
97
121
  }
98
- ctx.lineTo(xAt(data.length - 1), pad.top + h);
122
+ ctx.lineTo(xAt(chartData.length - 1), pad.top + h);
99
123
  ctx.closePath();
100
124
  const grad = ctx.createLinearGradient(0, pad.top, 0, pad.top + h);
101
- grad.addColorStop(0, hexAlpha(HEX.accent, 0.18));
125
+ grad.addColorStop(0, hexAlpha(HEX.accent, 0.2));
102
126
  grad.addColorStop(0.7, hexAlpha(HEX.accent, 0.04));
103
127
  grad.addColorStop(1, hexAlpha(HEX.accent, 0));
104
128
  ctx.fillStyle = grad;
105
129
  ctx.fill();
106
130
 
107
- // ── Token line ──────────────────────────────────────────
131
+ // ── Burn rate line ──────────────────────────────────────
108
132
  ctx.beginPath();
109
133
  ctx.strokeStyle = HEX.accent;
110
134
  ctx.lineWidth = 1.5;
111
135
  ctx.lineJoin = 'round';
112
- for (let i = 0; i < data.length; i++) {
113
- const x = xAt(i);
114
- const y = yToken(tokens[i]);
115
- i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
116
- }
117
- ctx.stroke();
118
-
119
- // ── Cost line (dashed, subtle) ──────────────────────────
120
- ctx.beginPath();
121
- ctx.strokeStyle = hexAlpha(HEX.warning, 0.6);
122
- ctx.lineWidth = 1;
123
- ctx.lineJoin = 'round';
124
- ctx.setLineDash([5, 4]);
125
- for (let i = 0; i < data.length; i++) {
136
+ for (let i = 0; i < chartData.length; i++) {
126
137
  const x = xAt(i);
127
- const y = yCost(costs[i]);
138
+ const y = yBurn(burns[i]);
128
139
  i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
129
140
  }
130
141
  ctx.stroke();
131
- ctx.setLineDash([]);
132
142
 
133
- // ── Cache hit rate line (dotted, subtle) ────────────────
143
+ // ── Cache hit rate line ─────────────────────────────────
144
+ const hasCacheData = caches.some((c) => c > 0);
134
145
  if (hasCacheData) {
135
146
  ctx.beginPath();
136
147
  ctx.strokeStyle = hexAlpha(HEX.info, 0.45);
137
148
  ctx.lineWidth = 1;
138
149
  ctx.lineJoin = 'round';
139
150
  ctx.setLineDash([2, 3]);
140
- let started = false;
141
- for (let i = 0; i < data.length; i++) {
142
- const c = caches[i];
143
- if (c === null || c === undefined) continue;
151
+ for (let i = 0; i < chartData.length; i++) {
144
152
  const x = xAt(i);
145
- const y = yCache(c);
146
- if (!started) { ctx.moveTo(x, y); started = true; }
147
- else ctx.lineTo(x, y);
153
+ const y = yCache(caches[i]);
154
+ i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
148
155
  }
149
156
  ctx.stroke();
150
157
  ctx.setLineDash([]);
151
158
  }
152
159
 
153
- // ── Inline legend (top-right, pill-style) ───────────────
160
+ // ── Inline legend (top-right) ───────────────────────────
154
161
  ctx.font = "9px 'Inter Variable', sans-serif";
155
162
  ctx.textAlign = 'right';
156
163
  let rx = width - pad.right - 4;
@@ -158,99 +165,72 @@ const TokenChart = memo(function TokenChart({ data }) {
158
165
 
159
166
  if (hasCacheData) {
160
167
  ctx.fillStyle = hexAlpha(HEX.info, 0.5);
161
- ctx.fillText('Cache', rx, ly);
162
- rx -= ctx.measureText('Cache').width + 4;
163
- ctx.beginPath();
164
- ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2);
165
- ctx.fillStyle = hexAlpha(HEX.info, 0.5);
166
- ctx.fill();
167
- rx -= 12;
168
+ ctx.fillText('Cache %', rx, ly);
169
+ rx -= ctx.measureText('Cache %').width + 4;
170
+ ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
171
+ rx -= 14;
168
172
  }
169
173
 
170
- ctx.fillStyle = hexAlpha(HEX.warning, 0.7);
171
- ctx.fillText('Cost', rx, ly);
172
- rx -= ctx.measureText('Cost').width + 4;
173
- ctx.beginPath();
174
- ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2);
175
- ctx.fillStyle = hexAlpha(HEX.warning, 0.7);
176
- ctx.fill();
177
- rx -= 12;
174
+ ctx.fillStyle = hexAlpha(HEX.surface5, 0.7);
175
+ ctx.fillText('Agents', rx, ly);
176
+ rx -= ctx.measureText('Agents').width + 4;
177
+ ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
178
+ rx -= 14;
178
179
 
179
180
  ctx.fillStyle = HEX.accent;
180
- ctx.fillText('Tokens', rx, ly);
181
- rx -= ctx.measureText('Tokens').width + 4;
182
- ctx.beginPath();
183
- ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2);
184
- ctx.fillStyle = HEX.accent;
185
- ctx.fill();
181
+ ctx.fillText('Burn Rate', rx, ly);
182
+ rx -= ctx.measureText('Burn Rate').width + 4;
183
+ ctx.beginPath(); ctx.arc(rx, ly - 3, 2.5, 0, Math.PI * 2); ctx.fill();
186
184
 
187
- // ── Hover crosshair + tooltip ───────────────────────────
188
- if (hover && hover.index >= 0 && hover.index < data.length) {
185
+ // ── Hover ───────────────────────────────────────────────
186
+ if (hover && hover.index >= 0 && hover.index < chartData.length) {
189
187
  const hx = hover.x;
188
+ const d = chartData[hover.index];
190
189
 
191
- // Vertical line
190
+ // Crosshair
192
191
  ctx.beginPath();
193
192
  ctx.moveTo(hx, pad.top);
194
193
  ctx.lineTo(hx, pad.top + h);
195
194
  ctx.strokeStyle = hexAlpha(HEX.text1, 0.15);
196
195
  ctx.lineWidth = 1;
197
- ctx.setLineDash([]);
198
196
  ctx.stroke();
199
197
 
200
- // Dot on token line
201
- const d = data[hover.index];
202
- const dotY = yToken(d.tokens || 0);
203
- ctx.beginPath();
204
- ctx.arc(hx, dotY, 3, 0, Math.PI * 2);
205
- ctx.fillStyle = HEX.accent;
206
- ctx.fill();
198
+ // Dot
199
+ const dotY = yBurn(d.burnRate);
200
+ ctx.beginPath(); ctx.arc(hx, dotY, 3, 0, Math.PI * 2);
201
+ ctx.fillStyle = HEX.accent; ctx.fill();
207
202
 
208
203
  // Tooltip
209
204
  const lines = [
210
- { label: 'Tokens', value: fmtNum(d.tokens || 0), color: HEX.accent },
211
- { label: 'Cost', value: fmtDollar(d.costUsd || 0), color: HEX.warning },
205
+ { label: 'Burn', value: `${fmtNum(d.burnRate)}/m`, color: HEX.accent },
206
+ { label: 'Cache', value: fmtPct(d.cacheHitRate * 100), color: HEX.info },
207
+ { label: 'Agents', value: `${d.running}/${d.agents}`, color: HEX.text2 },
212
208
  ];
213
- if (d.cacheHitRate != null) {
214
- lines.push({ label: 'Cache', value: fmtPct(d.cacheHitRate * 100), color: HEX.info });
215
- }
216
-
217
- const tooltipW = 100;
209
+ const tooltipW = 104;
218
210
  const tooltipH = lines.length * 16 + 12;
219
211
  let tx = hx + 12;
220
212
  if (tx + tooltipW > width - 8) tx = hx - tooltipW - 12;
221
213
  const ty = Math.max(pad.top, dotY - tooltipH / 2);
222
214
 
223
- // Background
224
215
  ctx.fillStyle = hexAlpha(HEX.surface0, 0.92);
225
- ctx.beginPath();
226
- ctx.roundRect(tx, ty, tooltipW, tooltipH, 4);
227
- ctx.fill();
216
+ ctx.beginPath(); ctx.roundRect(tx, ty, tooltipW, tooltipH, 4); ctx.fill();
228
217
  ctx.strokeStyle = hexAlpha(HEX.text4, 0.2);
229
- ctx.lineWidth = 1;
230
- ctx.stroke();
218
+ ctx.lineWidth = 1; ctx.stroke();
231
219
 
232
- // Rows
233
220
  ctx.textAlign = 'left';
234
221
  lines.forEach((line, i) => {
235
222
  const rowY = ty + 14 + i * 16;
236
- // Dot
237
- ctx.beginPath();
238
- ctx.arc(tx + 8, rowY - 3, 2, 0, Math.PI * 2);
239
- ctx.fillStyle = line.color;
240
- ctx.fill();
241
- // Label
223
+ ctx.beginPath(); ctx.arc(tx + 8, rowY - 3, 2, 0, Math.PI * 2);
224
+ ctx.fillStyle = line.color; ctx.fill();
242
225
  ctx.font = "8px 'Inter Variable', sans-serif";
243
- ctx.fillStyle = HEX.text3;
244
- ctx.fillText(line.label, tx + 14, rowY);
245
- // Value
226
+ ctx.fillStyle = HEX.text3; ctx.fillText(line.label, tx + 14, rowY);
246
227
  ctx.font = "9px 'JetBrains Mono Variable', monospace";
247
- ctx.fillStyle = HEX.text0;
248
- ctx.textAlign = 'right';
228
+ ctx.fillStyle = HEX.text0; ctx.textAlign = 'right';
249
229
  ctx.fillText(line.value, tx + tooltipW - 8, rowY);
250
230
  ctx.textAlign = 'left';
251
231
  });
252
232
  }
253
- }, [data, width, height, hover, w, h, pad]);
233
+ }, [chartData, width, height, hover, w, h, pad]);
254
234
 
255
235
  return (
256
236
  <div ref={containerRef} className="absolute inset-0">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.24.5",
3
+ "version": "0.24.6",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1718,6 +1718,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
1718
1718
  outputTokens: tokenData.outputTokens || 0,
1719
1719
  cacheHitRate: agentCacheTotal > 0 ? Math.round(((tokenData.cacheReadTokens || 0) / agentCacheTotal) * 1000) / 1000 : 0,
1720
1720
  contextUsage: a.contextUsage || 0,
1721
+ rotationThreshold: daemon.adaptive.getThreshold(a.provider, a.role),
1721
1722
  durationMs: a.durationMs || tokenData.totalDurationMs || 0,
1722
1723
  turns: a.turns || tokenData.totalTurns || 0,
1723
1724
  modelDistribution: tokenData.modelDistribution || {},
@@ -1773,6 +1774,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
1773
1774
  journalist: {
1774
1775
  ...journalistStatus,
1775
1776
  lastSummary: lastSynthesis?.summary || '',
1777
+ projectMap: lastSynthesis?.projectMap || '',
1778
+ decisions: lastSynthesis?.decisions || '',
1776
1779
  recentHistory: journalistHistory,
1777
1780
  },
1778
1781
  timeline: timelineData,
@@ -317,6 +317,9 @@ For normal file edits within your scope, proceed without review.
317
317
  model: output.model,
318
318
  estimatedCostUsd: output.estimatedCostUsd,
319
319
  });
320
+ // Feed router cost log for tier tracking
321
+ const tier = this.daemon.classifier.classify(agent.id);
322
+ this.daemon.router.recordUsage(agent.id, output.model || current.model, output.tokensUsed, tier);
320
323
  }
321
324
  }
322
325
  // Record session result data (cost, duration, turns)