agentlytics 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -62
- package/cache.js +221 -5
- package/editors/base.js +1 -1
- package/editors/codebuff.js +338 -0
- package/editors/copilot.js +3 -3
- package/editors/gsd.js +366 -0
- package/editors/index.js +10 -5
- package/editors/windsurf.js +64 -37
- package/index.js +32 -12
- package/package.json +6 -6
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/assets/index-SOQVJIDS.js +73 -0
- package/public/index.html +16 -0
- package/relay-client.js +10 -8
- package/server.js +104 -2
- package/share-image.js +9 -7
- package/ui/src/App.jsx +5 -2
- package/ui/src/components/ChatSidebar.jsx +31 -2
- package/ui/src/components/EditorIcon.jsx +60 -11
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/lib/api.js +43 -0
- package/ui/src/lib/constants.js +10 -8
- package/ui/src/pages/Artifacts.jsx +0 -12
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/Settings.jsx +1 -1
- package/ui/src/pages/Subscriptions.jsx +3 -3
- package/deno.json +0 -9
- package/mod.ts +0 -1020
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useMemo } from 'react'
|
|
2
|
+
import { Activity, ChevronDown, ChevronUp, Clock } from 'lucide-react'
|
|
3
|
+
import { formatNumber } from '../lib/constants'
|
|
4
|
+
|
|
5
|
+
const CHART_H = 56
|
|
6
|
+
const POINT_STEP = 52
|
|
7
|
+
const PAD_X = 20
|
|
8
|
+
const PAD_Y = 6
|
|
9
|
+
const BUCKET_MINUTES = 5
|
|
10
|
+
const BUCKET_MSG_COUNT = 8
|
|
11
|
+
|
|
12
|
+
const AVG_TOKEN_SPEED = 80
|
|
13
|
+
|
|
14
|
+
function formatDuration(ms) {
|
|
15
|
+
if (ms < 60000) return `${Math.round(ms / 1000)}s`
|
|
16
|
+
const mins = Math.floor(ms / 60000)
|
|
17
|
+
if (mins < 60) return `${mins}m`
|
|
18
|
+
const hrs = Math.floor(mins / 60)
|
|
19
|
+
const rem = mins % 60
|
|
20
|
+
return rem > 0 ? `${hrs}h ${rem}m` : `${hrs}h`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lerp(a, b, t) {
|
|
24
|
+
return a + (b - a) * Math.max(0, Math.min(1, t))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tokenColor(ratio) {
|
|
28
|
+
const r = Math.round(lerp(52, 239, ratio))
|
|
29
|
+
const g = Math.round(lerp(211, 68, ratio))
|
|
30
|
+
const b = Math.round(lerp(153, 68, ratio))
|
|
31
|
+
return `rgb(${r},${g},${b})`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildTimeBuckets(messages, createdAt, lastUpdatedAt) {
|
|
35
|
+
const start = new Date(createdAt).getTime()
|
|
36
|
+
const end = new Date(lastUpdatedAt).getTime()
|
|
37
|
+
const duration = Math.max(end - start, 1)
|
|
38
|
+
const bucketMs = BUCKET_MINUTES * 60 * 1000
|
|
39
|
+
const bucketCount = Math.max(1, Math.ceil(duration / bucketMs))
|
|
40
|
+
|
|
41
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
|
42
|
+
label: `${i * BUCKET_MINUTES}m`,
|
|
43
|
+
tokens: 0,
|
|
44
|
+
messageIndices: [],
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
messages.forEach((msg, idx) => {
|
|
48
|
+
const totalTokens = (msg.inputTokens || 0) + (msg.outputTokens || 0)
|
|
49
|
+
const msgTime = start + (idx / Math.max(messages.length - 1, 1)) * duration
|
|
50
|
+
const bucketIdx = Math.min(Math.floor((msgTime - start) / bucketMs), bucketCount - 1)
|
|
51
|
+
buckets[bucketIdx].tokens += totalTokens
|
|
52
|
+
buckets[bucketIdx].messageIndices.push(idx)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return buckets
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildSeqBuckets(messages) {
|
|
59
|
+
const bucketCount = Math.max(1, Math.ceil(messages.length / BUCKET_MSG_COUNT))
|
|
60
|
+
|
|
61
|
+
const buckets = Array.from({ length: bucketCount }, () => ({
|
|
62
|
+
label: '',
|
|
63
|
+
tokens: 0,
|
|
64
|
+
messageIndices: [],
|
|
65
|
+
}))
|
|
66
|
+
|
|
67
|
+
messages.forEach((msg, idx) => {
|
|
68
|
+
const chars = typeof msg.content === 'string' ? msg.content.length : 0
|
|
69
|
+
const totalTokens = Math.round(chars / 4)
|
|
70
|
+
const bucketIdx = Math.min(Math.floor(idx / BUCKET_MSG_COUNT), bucketCount - 1)
|
|
71
|
+
buckets[bucketIdx].tokens += totalTokens
|
|
72
|
+
buckets[bucketIdx].messageIndices.push(idx)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return buckets
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default function TokenTimeline({ messages, createdAt, lastUpdatedAt, onScrollToMessage }) {
|
|
79
|
+
const [open, setOpen] = useState(true)
|
|
80
|
+
const [hoveredIdx, setHoveredIdx] = useState(null)
|
|
81
|
+
const scrollContainerRef = useRef(null)
|
|
82
|
+
|
|
83
|
+
const hasRealTokens = useMemo(() => {
|
|
84
|
+
if (!messages) return false
|
|
85
|
+
return messages.some(m => (m.inputTokens || 0) + (m.outputTokens || 0) > 0)
|
|
86
|
+
}, [messages])
|
|
87
|
+
|
|
88
|
+
const buckets = useMemo(() => {
|
|
89
|
+
if (!messages || messages.length === 0) return []
|
|
90
|
+
if (hasRealTokens) return buildTimeBuckets(messages, createdAt, lastUpdatedAt)
|
|
91
|
+
return buildSeqBuckets(messages)
|
|
92
|
+
}, [messages, createdAt, lastUpdatedAt, hasRealTokens])
|
|
93
|
+
|
|
94
|
+
const maxTokens = useMemo(() => Math.max(...buckets.map(b => b.tokens), 1), [buckets])
|
|
95
|
+
|
|
96
|
+
const totalTokens = useMemo(() => buckets.reduce((s, b) => s + b.tokens, 0), [buckets])
|
|
97
|
+
|
|
98
|
+
const durationLabel = useMemo(() => {
|
|
99
|
+
if (hasRealTokens && createdAt && lastUpdatedAt) {
|
|
100
|
+
const ms = new Date(lastUpdatedAt).getTime() - new Date(createdAt).getTime()
|
|
101
|
+
if (ms > 0) return formatDuration(ms)
|
|
102
|
+
}
|
|
103
|
+
if (totalTokens > 0) {
|
|
104
|
+
const estSeconds = totalTokens / AVG_TOKEN_SPEED
|
|
105
|
+
return '~' + formatDuration(estSeconds * 1000)
|
|
106
|
+
}
|
|
107
|
+
return null
|
|
108
|
+
}, [hasRealTokens, createdAt, lastUpdatedAt, totalTokens])
|
|
109
|
+
|
|
110
|
+
const handlePointClick = useCallback((bucket) => {
|
|
111
|
+
if (bucket.messageIndices.length > 0 && onScrollToMessage) {
|
|
112
|
+
onScrollToMessage(bucket.messageIndices[0])
|
|
113
|
+
}
|
|
114
|
+
}, [onScrollToMessage])
|
|
115
|
+
|
|
116
|
+
if (!messages || messages.length === 0 || totalTokens === 0) return null
|
|
117
|
+
|
|
118
|
+
const svgW = PAD_X * 2 + Math.max((buckets.length - 1) * POINT_STEP, 0)
|
|
119
|
+
const svgH = CHART_H + PAD_Y * 2 + (hasRealTokens ? 16 : 4)
|
|
120
|
+
|
|
121
|
+
const points = buckets.map((b, i) => {
|
|
122
|
+
const x = PAD_X + i * POINT_STEP
|
|
123
|
+
const ratio = b.tokens / maxTokens
|
|
124
|
+
const y = PAD_Y + CHART_H - ratio * CHART_H
|
|
125
|
+
return { x, y, ratio, bucket: b }
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x} ${p.y}`).join(' ')
|
|
129
|
+
|
|
130
|
+
const areaPath = points.length > 0
|
|
131
|
+
? `${linePath} L ${points[points.length - 1].x} ${PAD_Y + CHART_H} L ${points[0].x} ${PAD_Y + CHART_H} Z`
|
|
132
|
+
: ''
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="shrink-0" style={{ borderBottom: '1px solid var(--c-border)' }}>
|
|
136
|
+
{/* Toggle button */}
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => setOpen(o => !o)}
|
|
139
|
+
className="flex items-center gap-2 w-full px-4 py-1.5 text-[11px] transition hover:bg-[var(--c-bg3)]"
|
|
140
|
+
style={{ color: 'var(--c-text2)' }}
|
|
141
|
+
>
|
|
142
|
+
<Activity size={12} style={{ color: 'var(--c-accent)' }} />
|
|
143
|
+
<span className="font-medium">Token Timeline</span>
|
|
144
|
+
<span style={{ color: 'var(--c-text3)' }}>
|
|
145
|
+
({formatNumber(totalTokens)} tokens{!hasRealTokens ? ', est.' : ''})
|
|
146
|
+
</span>
|
|
147
|
+
{durationLabel && (
|
|
148
|
+
<span className="inline-flex items-center gap-1" style={{ color: 'var(--c-text3)' }}>
|
|
149
|
+
<Clock size={10} /> {durationLabel}
|
|
150
|
+
</span>
|
|
151
|
+
)}
|
|
152
|
+
<span className="ml-auto">
|
|
153
|
+
{open ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
|
|
154
|
+
</span>
|
|
155
|
+
</button>
|
|
156
|
+
|
|
157
|
+
{/* Collapsible chart area */}
|
|
158
|
+
{open && (
|
|
159
|
+
<div className="px-2 pb-2 pt-0">
|
|
160
|
+
<div
|
|
161
|
+
ref={scrollContainerRef}
|
|
162
|
+
className="overflow-x-auto scrollbar-thin"
|
|
163
|
+
style={{ scrollBehavior: 'smooth' }}
|
|
164
|
+
>
|
|
165
|
+
<svg
|
|
166
|
+
width={svgW}
|
|
167
|
+
height={svgH}
|
|
168
|
+
style={{ display: 'block', minWidth: svgW }}
|
|
169
|
+
>
|
|
170
|
+
<defs>
|
|
171
|
+
<linearGradient id="tokenAreaGrad" x1="0" y1="0" x2="0" y2="1">
|
|
172
|
+
<stop offset="0%" stopColor="var(--c-accent)" stopOpacity="0.15" />
|
|
173
|
+
<stop offset="100%" stopColor="var(--c-accent)" stopOpacity="0.02" />
|
|
174
|
+
</linearGradient>
|
|
175
|
+
</defs>
|
|
176
|
+
|
|
177
|
+
{/* Baseline */}
|
|
178
|
+
<line
|
|
179
|
+
x1={PAD_X} y1={PAD_Y + CHART_H}
|
|
180
|
+
x2={PAD_X + (buckets.length - 1) * POINT_STEP} y2={PAD_Y + CHART_H}
|
|
181
|
+
stroke="var(--c-border)" strokeWidth="1"
|
|
182
|
+
/>
|
|
183
|
+
|
|
184
|
+
{/* Area fill */}
|
|
185
|
+
{areaPath && <path d={areaPath} fill="url(#tokenAreaGrad)" />}
|
|
186
|
+
|
|
187
|
+
{/* Line */}
|
|
188
|
+
{points.length > 1 && (
|
|
189
|
+
<path d={linePath} fill="none" stroke="var(--c-accent)" strokeWidth="1.5" strokeLinejoin="round" />
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Data points + labels */}
|
|
193
|
+
{points.map((p, i) => {
|
|
194
|
+
const isHovered = hoveredIdx === i
|
|
195
|
+
const color = tokenColor(p.ratio)
|
|
196
|
+
return (
|
|
197
|
+
<g
|
|
198
|
+
key={i}
|
|
199
|
+
style={{ cursor: p.bucket.tokens > 0 ? 'pointer' : 'default' }}
|
|
200
|
+
onMouseEnter={() => setHoveredIdx(i)}
|
|
201
|
+
onMouseLeave={() => setHoveredIdx(null)}
|
|
202
|
+
onClick={() => handlePointClick(p.bucket)}
|
|
203
|
+
>
|
|
204
|
+
<circle cx={p.x} cy={p.y} r={12} fill="transparent" />
|
|
205
|
+
|
|
206
|
+
<circle
|
|
207
|
+
cx={p.x} cy={p.y}
|
|
208
|
+
r={isHovered ? 5 : 3}
|
|
209
|
+
fill={p.bucket.tokens > 0 ? color : 'var(--c-border)'}
|
|
210
|
+
stroke={isHovered ? 'var(--c-white)' : 'none'}
|
|
211
|
+
strokeWidth={1.5}
|
|
212
|
+
style={{ transition: 'r 0.15s' }}
|
|
213
|
+
/>
|
|
214
|
+
|
|
215
|
+
{isHovered && (
|
|
216
|
+
<line
|
|
217
|
+
x1={p.x} y1={p.y} x2={p.x} y2={PAD_Y + CHART_H}
|
|
218
|
+
stroke="var(--c-text3)" strokeWidth="0.5" strokeDasharray="2,2"
|
|
219
|
+
/>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{isHovered && p.bucket.tokens > 0 && (
|
|
223
|
+
<g>
|
|
224
|
+
<rect
|
|
225
|
+
x={p.x - 32} y={p.y - 22}
|
|
226
|
+
width={64} height={16} rx={3}
|
|
227
|
+
fill="var(--c-bg)" stroke="var(--c-border)" strokeWidth="0.5"
|
|
228
|
+
/>
|
|
229
|
+
<text
|
|
230
|
+
x={p.x} y={p.y - 11}
|
|
231
|
+
textAnchor="middle" fontSize="9" fontFamily="monospace"
|
|
232
|
+
fill="var(--c-white)"
|
|
233
|
+
>
|
|
234
|
+
{formatNumber(p.bucket.tokens)} tok
|
|
235
|
+
</text>
|
|
236
|
+
</g>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{/* X label — only for time-based buckets */}
|
|
240
|
+
{hasRealTokens && p.bucket.label && (
|
|
241
|
+
<text
|
|
242
|
+
x={p.x} y={PAD_Y + CHART_H + 12}
|
|
243
|
+
textAnchor="middle" fontSize="9" fontFamily="monospace"
|
|
244
|
+
fill="var(--c-text3)"
|
|
245
|
+
>
|
|
246
|
+
{p.bucket.label}
|
|
247
|
+
</text>
|
|
248
|
+
)}
|
|
249
|
+
</g>
|
|
250
|
+
)
|
|
251
|
+
})}
|
|
252
|
+
</svg>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
)
|
|
258
|
+
}
|
package/ui/src/lib/api.js
CHANGED
|
@@ -234,6 +234,49 @@ export async function fetchMCPs() {
|
|
|
234
234
|
return res.json();
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// ── GSD API ──
|
|
238
|
+
|
|
239
|
+
export async function fetchGSDProjects() {
|
|
240
|
+
const res = await fetch(`${BASE}/api/gsd/projects`);
|
|
241
|
+
return res.json();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function fetchGSDPhases(folder) {
|
|
245
|
+
const q = new URLSearchParams({ folder });
|
|
246
|
+
const res = await fetch(`${BASE}/api/gsd/phases?${q}`);
|
|
247
|
+
return res.json();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export async function fetchGSDPlan(folder, phase) {
|
|
251
|
+
const q = new URLSearchParams({ folder, phase });
|
|
252
|
+
const res = await fetch(`${BASE}/api/gsd/plan?${q}`);
|
|
253
|
+
return res.json();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function fetchGSDOverview() {
|
|
257
|
+
const res = await fetch(`${BASE}/api/gsd/overview`);
|
|
258
|
+
return res.json();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function fetchGSDConfig(folder) {
|
|
262
|
+
const q = new URLSearchParams({ folder });
|
|
263
|
+
const res = await fetch(`${BASE}/api/gsd/config?${q}`);
|
|
264
|
+
return res.json();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function fetchGSDPhaseTokens(folder) {
|
|
268
|
+
const q = new URLSearchParams({ folder });
|
|
269
|
+
const res = await fetch(`${BASE}/api/gsd/phase-tokens?${q}`);
|
|
270
|
+
return res.json();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function fetchGSDFile(folder, type, phaseDir) {
|
|
274
|
+
const q = new URLSearchParams({ folder, type });
|
|
275
|
+
if (phaseDir) q.set('phase', phaseDir);
|
|
276
|
+
const res = await fetch(`${BASE}/api/gsd/file?${q}`);
|
|
277
|
+
return res.json();
|
|
278
|
+
}
|
|
279
|
+
|
|
237
280
|
// ── Relay API ──
|
|
238
281
|
|
|
239
282
|
export async function fetchMode() {
|
package/ui/src/lib/constants.js
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
export const EDITOR_COLORS = {
|
|
2
2
|
'cursor': '#f59e0b',
|
|
3
|
-
'
|
|
4
|
-
'
|
|
3
|
+
'devin': '#2563eb',
|
|
4
|
+
'devin-next': '#7c3aed',
|
|
5
5
|
'antigravity': '#a78bfa',
|
|
6
6
|
'claude-code': '#f97316',
|
|
7
7
|
'claude': '#f97316',
|
|
8
8
|
'vscode': '#3b82f6',
|
|
9
9
|
'vscode-insiders': '#60a5fa',
|
|
10
10
|
'zed': '#10b981',
|
|
11
|
-
'opencode': '#
|
|
12
|
-
'codex': '#
|
|
11
|
+
'opencode': '#656363',
|
|
12
|
+
'codex': '#3941FF',
|
|
13
13
|
'gemini-cli': '#4285f4',
|
|
14
14
|
'copilot-cli': '#8957e5',
|
|
15
15
|
'cursor-agent': '#f59e0b',
|
|
16
16
|
'commandcode': '#e11d48',
|
|
17
17
|
'goose': '#333333',
|
|
18
|
-
'kiro': '#
|
|
18
|
+
'kiro': '#9046FF',
|
|
19
|
+
'codebuff': '#44ff00',
|
|
19
20
|
};
|
|
20
21
|
|
|
21
22
|
export const EDITOR_LABELS = {
|
|
22
23
|
'cursor': 'Cursor',
|
|
23
|
-
'
|
|
24
|
-
'
|
|
24
|
+
'devin': 'Devin',
|
|
25
|
+
'devin-next': 'Devin Next',
|
|
25
26
|
'antigravity': 'Antigravity',
|
|
26
27
|
'claude-code': 'Claude Code',
|
|
27
28
|
'claude': 'Claude Code',
|
|
@@ -31,11 +32,12 @@ export const EDITOR_LABELS = {
|
|
|
31
32
|
'opencode': 'OpenCode',
|
|
32
33
|
'codex': 'Codex',
|
|
33
34
|
'gemini-cli': 'Gemini CLI',
|
|
34
|
-
'copilot-cli': 'Copilot
|
|
35
|
+
'copilot-cli': 'GitHub Copilot',
|
|
35
36
|
'cursor-agent': 'Cursor Agent',
|
|
36
37
|
'commandcode': 'Command Code',
|
|
37
38
|
'goose': 'Goose',
|
|
38
39
|
'kiro': 'Kiro',
|
|
40
|
+
'codebuff': 'Codebuff',
|
|
39
41
|
};
|
|
40
42
|
|
|
41
43
|
export function editorColor(src) {
|
|
@@ -10,18 +10,6 @@ import PageHeader from '../components/PageHeader'
|
|
|
10
10
|
|
|
11
11
|
const MONO = 'JetBrains Mono, monospace'
|
|
12
12
|
|
|
13
|
-
const EDITOR_ICONS = {
|
|
14
|
-
'claude-code': '🟠',
|
|
15
|
-
'cursor': '🟡',
|
|
16
|
-
'windsurf': '🔵',
|
|
17
|
-
'kiro': '🟠',
|
|
18
|
-
'copilot-cli': '🟣',
|
|
19
|
-
'codex': '🟢',
|
|
20
|
-
'gemini-cli': '🔵',
|
|
21
|
-
'goose': '⚫',
|
|
22
|
-
'_general': '📄',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
13
|
function formatSize(bytes) {
|
|
26
14
|
if (bytes < 1024) return bytes + ' B'
|
|
27
15
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|