@mbeato/contextscope 0.1.5 → 0.2.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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/context/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/items/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/items/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/page.js +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/sessions/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q3rzj9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_tsx_0fwe3kl._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/README.md +16 -6
- package/.next/standalone/app/page.tsx +290 -257
- package/.next/standalone/bin/cli.js +44 -13
- package/.next/standalone/bin/summary.js +558 -0
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/plugin/commands/usage.md +1 -1
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/0pb14~6.l8.f9.css +1 -0
- package/README.md +16 -6
- package/bin/cli.js +44 -13
- package/bin/summary.js +558 -0
- package/lib/model-prices.json +147 -0
- package/package.json +2 -1
- package/plugin/commands/usage.md +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0lbywin._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_0j0avc7._.js +0 -3
- package/.next/static/chunks/118uk9v3812u1.css +0 -1
- /package/.next/static/{I-W1iV4XdkTRyjis8Ros1 → JpNIY4MdsJTSd7Guhgw3Z}/_buildManifest.js +0 -0
- /package/.next/static/{I-W1iV4XdkTRyjis8Ros1 → JpNIY4MdsJTSd7Guhgw3Z}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{I-W1iV4XdkTRyjis8Ros1 → JpNIY4MdsJTSd7Guhgw3Z}/_ssgManifest.js +0 -0
|
@@ -6,16 +6,15 @@
|
|
|
6
6
|
* `text-[10px] uppercase tracking-widest text-zinc-500` label above a
|
|
7
7
|
* `text-3xl/text-2xl font-semibold tabular-nums tracking-tight` number,
|
|
8
8
|
* mirroring Stripe's "Gross volume / $7.8K" treatment on every metric.
|
|
9
|
-
* 2. Primary receipt card + 3 secondary receipt cards layout
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Cards are flat `bg-white dark:bg-zinc-900` with hairline borders;
|
|
17
|
-
* no shadow, no backdrop-blur, no glassmorphism (per DESIGN.md).
|
|
9
|
+
* 2. Primary receipt card + 3 secondary receipt cards layout.
|
|
10
|
+
* 3. Mode-switcher chip row pinned to the top of the primary card.
|
|
11
|
+
* Cards are flat `bg-white dark:bg-zinc-900` with hairline borders.
|
|
12
|
+
*
|
|
13
|
+
* Suspense boundaries: each receipt is its own async server component so
|
|
14
|
+
* fast sections (inventory ~300ms) paint immediately while slow sections
|
|
15
|
+
* (sessions/context ~7s on cold) fill in progressively.
|
|
18
16
|
*/
|
|
17
|
+
import { Suspense } from "react";
|
|
19
18
|
import Link from "next/link";
|
|
20
19
|
import { getInventory, summarize } from "@/lib/inventory";
|
|
21
20
|
import { getUsage, lookupUsage } from "@/lib/usage";
|
|
@@ -38,25 +37,225 @@ function shortNumber(n: number): string {
|
|
|
38
37
|
return String(n);
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
const [items, usage, sessions, contextFiles, hooks] = await Promise.all([
|
|
43
|
-
getInventory(),
|
|
44
|
-
getUsage(DAYS),
|
|
45
|
-
getSessions(DAYS),
|
|
46
|
-
getContextFiles(),
|
|
47
|
-
getHooks(),
|
|
48
|
-
]);
|
|
40
|
+
// ─── Skeletons ─────────────────────────────────────────────────────────────
|
|
49
41
|
|
|
42
|
+
function Pulse({ className = "" }: { className?: string }) {
|
|
43
|
+
return <div className={`animate-pulse bg-zinc-200/70 dark:bg-zinc-800/70 rounded ${className}`} />;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function PrimaryReceiptSkeleton() {
|
|
47
|
+
return (
|
|
48
|
+
<div className="w-full max-w-md rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
|
|
49
|
+
<div className="flex items-center justify-between px-4 pt-3 pb-2 border-b border-zinc-200 dark:border-zinc-800">
|
|
50
|
+
<Pulse className="h-2.5 w-20" />
|
|
51
|
+
<Pulse className="h-2.5 w-12" />
|
|
52
|
+
</div>
|
|
53
|
+
<div className="px-8 py-6 text-center space-y-2">
|
|
54
|
+
<Pulse className="h-2.5 w-24 mx-auto" />
|
|
55
|
+
<Pulse className="h-9 w-40 mx-auto" />
|
|
56
|
+
<Pulse className="h-2.5 w-44 mx-auto" />
|
|
57
|
+
</div>
|
|
58
|
+
<div className="grid grid-cols-4 border-t border-zinc-200 dark:border-zinc-800 divide-x divide-zinc-200 dark:divide-zinc-800">
|
|
59
|
+
{[0, 1, 2, 3].map((i) => (
|
|
60
|
+
<div key={i} className="px-3 py-2.5 space-y-1.5">
|
|
61
|
+
<Pulse className="h-2 w-12" />
|
|
62
|
+
<Pulse className="h-3.5 w-8" />
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function SecondaryReceiptSkeleton({ title }: { title: string }) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col h-full">
|
|
73
|
+
<div className="flex items-baseline justify-between">
|
|
74
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">{title}</div>
|
|
75
|
+
<Pulse className="h-2.5 w-12" />
|
|
76
|
+
</div>
|
|
77
|
+
<Pulse className="mt-1.5 h-7 w-32" />
|
|
78
|
+
<Pulse className="mt-2 h-2.5 w-20" />
|
|
79
|
+
<div className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 space-y-1.5 flex-1">
|
|
80
|
+
<Pulse className="h-3 w-full" />
|
|
81
|
+
<Pulse className="h-3 w-5/6" />
|
|
82
|
+
<Pulse className="h-3 w-4/6" />
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Receipts (one async server component per receipt) ─────────────────────
|
|
89
|
+
|
|
90
|
+
async function PrimaryReceipt() {
|
|
91
|
+
const items = await getInventory();
|
|
50
92
|
const inv = summarize(items);
|
|
93
|
+
return (
|
|
94
|
+
<div className="w-full max-w-md rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
|
|
95
|
+
<div className="flex items-center justify-between px-4 pt-3 pb-2 border-b border-zinc-200 dark:border-zinc-800">
|
|
96
|
+
<span className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
97
|
+
{DAYS}-day window
|
|
98
|
+
</span>
|
|
99
|
+
<Link
|
|
100
|
+
href="/items"
|
|
101
|
+
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
102
|
+
>
|
|
103
|
+
items →
|
|
104
|
+
</Link>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="px-8 py-6 text-center">
|
|
107
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
108
|
+
per-turn baseline
|
|
109
|
+
</div>
|
|
110
|
+
<div className="mt-2 text-4xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
111
|
+
{fmt.format(inv.totalPerTurnTokens)}
|
|
112
|
+
<span className="ml-1.5 text-sm text-zinc-500 font-normal">tok</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500 mt-1">
|
|
115
|
+
loaded into every system prompt
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="grid grid-cols-4 border-t border-zinc-200 dark:border-zinc-800 divide-x divide-zinc-200 dark:divide-zinc-800">
|
|
119
|
+
<MiniStat label="items" value={fmt.format(inv.totalItems)} />
|
|
120
|
+
<MiniStat label="skills" value={fmt.format(inv.byKind.skill.count)} />
|
|
121
|
+
<MiniStat label="agents" value={fmt.format(inv.byKind.agent.count)} />
|
|
122
|
+
<MiniStat label="commands" value={fmt.format(inv.byKind.command.count)} />
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function BurnReceipt() {
|
|
129
|
+
const sessions = await getSessions(DAYS);
|
|
51
130
|
const sess = summarizeSessions(sessions);
|
|
131
|
+
const maxBurn = Math.max(1, ...sess.dailyBurn.map((d) => d.tokens));
|
|
132
|
+
return (
|
|
133
|
+
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col h-full">
|
|
134
|
+
<div className="flex items-baseline justify-between">
|
|
135
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
136
|
+
{DAYS}-day burn
|
|
137
|
+
</div>
|
|
138
|
+
<Link
|
|
139
|
+
href="/sessions"
|
|
140
|
+
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
141
|
+
>
|
|
142
|
+
sessions →
|
|
143
|
+
</Link>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
146
|
+
{shortNumber(sess.totalTokens)}
|
|
147
|
+
<span className="ml-1 text-xs text-zinc-500 font-normal">tok</span>
|
|
148
|
+
<span className="ml-2 text-xs text-zinc-500 font-normal">·</span>
|
|
149
|
+
<span className="ml-2 text-base font-semibold tabular-nums text-zinc-900 dark:text-zinc-50">
|
|
150
|
+
{formatUsd(sess.totalCostUsd)}
|
|
151
|
+
</span>
|
|
152
|
+
<span className="ml-1 text-xs text-zinc-500 font-normal">api</span>
|
|
153
|
+
</div>
|
|
154
|
+
{sess.dailyBurn.length > 0 && (
|
|
155
|
+
<div className="mt-3">
|
|
156
|
+
<div className="flex items-end gap-px h-8">
|
|
157
|
+
{sess.dailyBurn.map((d) => {
|
|
158
|
+
const pct = Math.max(2, (d.tokens / maxBurn) * 100);
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
key={d.date}
|
|
162
|
+
className="flex-1 bg-zinc-300 dark:bg-zinc-700"
|
|
163
|
+
style={{ height: `${pct}%` }}
|
|
164
|
+
title={`${d.date}: ${shortNumber(d.tokens)} · ${formatUsd(d.cost)}`}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
<div className="flex items-center justify-between mt-1 text-[10px] uppercase tracking-widest text-zinc-500 tabular-nums">
|
|
170
|
+
<span>{sess.dailyBurn[0].date.slice(5)}</span>
|
|
171
|
+
<span>{sess.dailyBurn.length} active days</span>
|
|
172
|
+
<span>{sess.dailyBurn[sess.dailyBurn.length - 1].date.slice(5)}</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
<div className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 grid grid-cols-3 text-center">
|
|
177
|
+
<div>
|
|
178
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">sess</div>
|
|
179
|
+
<div className="text-xs font-semibold tabular-nums mt-0.5">{fmt.format(sess.count)}</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div>
|
|
182
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">median</div>
|
|
183
|
+
<div className="text-xs font-semibold tabular-nums mt-0.5">{shortNumber(sess.medianSessionTokens)}</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div>
|
|
186
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">p95</div>
|
|
187
|
+
<div className="text-xs font-semibold tabular-nums mt-0.5">{shortNumber(sess.p95SessionTokens)}</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
52
193
|
|
|
194
|
+
async function CandidatesReceipt() {
|
|
195
|
+
const [items, usage] = await Promise.all([getInventory(), getUsage(DAYS)]);
|
|
53
196
|
const annotated = items.map((it) => ({ ...it, ...lookupUsage(it, usage) }));
|
|
54
197
|
const candidates = annotated
|
|
55
198
|
.filter((a) => !a.disabled && a.invocations === 0 && a.source === "user")
|
|
56
199
|
.sort((a, b) => b.perTurnTokens - a.perTurnTokens)
|
|
57
200
|
.slice(0, 5);
|
|
58
201
|
const candidateSavings = candidates.reduce((acc, c) => acc + c.perTurnTokens, 0);
|
|
202
|
+
return (
|
|
203
|
+
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col h-full">
|
|
204
|
+
<div className="flex items-baseline justify-between">
|
|
205
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
206
|
+
disable candidates
|
|
207
|
+
</div>
|
|
208
|
+
<Link
|
|
209
|
+
href="/items"
|
|
210
|
+
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
211
|
+
>
|
|
212
|
+
all →
|
|
213
|
+
</Link>
|
|
214
|
+
</div>
|
|
215
|
+
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
216
|
+
{fmt.format(candidateSavings)}
|
|
217
|
+
<span className="ml-1 text-xs text-zinc-500 font-normal">tok/turn</span>
|
|
218
|
+
</div>
|
|
219
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
220
|
+
{candidates.length > 0 ? `top ${candidates.length} unused` : "none in window"}
|
|
221
|
+
</div>
|
|
222
|
+
<ul className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 text-xs space-y-1 flex-1">
|
|
223
|
+
{candidates.length === 0 ? (
|
|
224
|
+
<li className="text-zinc-500">every loaded user item invoked in {DAYS}d.</li>
|
|
225
|
+
) : (
|
|
226
|
+
candidates.map((c) => (
|
|
227
|
+
<li key={c.filePath} className="flex items-center gap-2">
|
|
228
|
+
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
229
|
+
{fmt.format(c.perTurnTokens)}
|
|
230
|
+
</span>
|
|
231
|
+
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">{c.name}</span>
|
|
232
|
+
<span className="text-[10px] uppercase tracking-widest text-zinc-500 w-12">
|
|
233
|
+
{c.kind}
|
|
234
|
+
</span>
|
|
235
|
+
<form
|
|
236
|
+
action={async () => {
|
|
237
|
+
"use server";
|
|
238
|
+
await toggleUserItem(c.filePath);
|
|
239
|
+
}}
|
|
240
|
+
>
|
|
241
|
+
<button
|
|
242
|
+
type="submit"
|
|
243
|
+
className="text-[10px] uppercase tracking-widest text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-zinc-700 hover:text-red-700 dark:hover:text-red-400 hover:border-red-300 dark:hover:border-red-800 hover:bg-red-50 dark:hover:bg-red-950/40 rounded px-1.5 py-0.5 transition-colors cursor-pointer"
|
|
244
|
+
title={`Disable ${c.name}`}
|
|
245
|
+
>
|
|
246
|
+
off
|
|
247
|
+
</button>
|
|
248
|
+
</form>
|
|
249
|
+
</li>
|
|
250
|
+
))
|
|
251
|
+
)}
|
|
252
|
+
</ul>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
59
256
|
|
|
257
|
+
async function ContextReceipt() {
|
|
258
|
+
const [contextFiles, hooks] = await Promise.all([getContextFiles(), getHooks()]);
|
|
60
259
|
const globalClaudeMd = contextFiles.find((f) => f.category === "claude-md-global");
|
|
61
260
|
const biggestMemoryMd = contextFiles
|
|
62
261
|
.filter((f) => f.category === "memory-md")
|
|
@@ -64,15 +263,72 @@ export default async function Cockpit() {
|
|
|
64
263
|
const sessionStartHookTokens = hooks
|
|
65
264
|
.filter((h) => h.event === "SessionStart" && h.status === "measured")
|
|
66
265
|
.reduce((acc, h) => acc + h.perTurnTokens, 0);
|
|
67
|
-
|
|
68
|
-
const maxBurn = Math.max(1, ...sess.dailyBurn.map((d) => d.tokens));
|
|
69
266
|
const contextTotal =
|
|
70
|
-
(globalClaudeMd?.tokens ?? 0) +
|
|
71
|
-
(biggestMemoryMd?.tokens ?? 0) +
|
|
72
|
-
sessionStartHookTokens;
|
|
267
|
+
(globalClaudeMd?.tokens ?? 0) + (biggestMemoryMd?.tokens ?? 0) + sessionStartHookTokens;
|
|
73
268
|
const contextRowCount =
|
|
74
269
|
(globalClaudeMd ? 1 : 0) + (biggestMemoryMd ? 1 : 0) + (sessionStartHookTokens > 0 ? 1 : 0);
|
|
270
|
+
return (
|
|
271
|
+
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col h-full">
|
|
272
|
+
<div className="flex items-baseline justify-between">
|
|
273
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
274
|
+
context overhead
|
|
275
|
+
</div>
|
|
276
|
+
<Link
|
|
277
|
+
href="/context"
|
|
278
|
+
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
279
|
+
>
|
|
280
|
+
context →
|
|
281
|
+
</Link>
|
|
282
|
+
</div>
|
|
283
|
+
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
284
|
+
{fmt.format(contextTotal)}
|
|
285
|
+
<span className="ml-1 text-xs text-zinc-500 font-normal">tok</span>
|
|
286
|
+
</div>
|
|
287
|
+
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
288
|
+
top {contextRowCount} sticky {contextRowCount === 1 ? "source" : "sources"}
|
|
289
|
+
</div>
|
|
290
|
+
<ul className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 text-xs space-y-1 flex-1">
|
|
291
|
+
{globalClaudeMd && (
|
|
292
|
+
<li className="flex items-center gap-2">
|
|
293
|
+
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
294
|
+
{fmt.format(globalClaudeMd.tokens)}
|
|
295
|
+
</span>
|
|
296
|
+
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
297
|
+
<code className="text-[11px]">~/.claude/CLAUDE.md</code>
|
|
298
|
+
</span>
|
|
299
|
+
<span className="text-[10px] uppercase tracking-widest text-zinc-500">every</span>
|
|
300
|
+
</li>
|
|
301
|
+
)}
|
|
302
|
+
{biggestMemoryMd && (
|
|
303
|
+
<li className="flex items-center gap-2">
|
|
304
|
+
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
305
|
+
{fmt.format(biggestMemoryMd.tokens)}
|
|
306
|
+
</span>
|
|
307
|
+
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
308
|
+
<code className="text-[11px]">{biggestMemoryMd.name}</code>
|
|
309
|
+
</span>
|
|
310
|
+
<span className="text-[10px] uppercase tracking-widest text-zinc-500">mem</span>
|
|
311
|
+
</li>
|
|
312
|
+
)}
|
|
313
|
+
{sessionStartHookTokens > 0 && (
|
|
314
|
+
<li className="flex items-center gap-2">
|
|
315
|
+
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
316
|
+
{fmt.format(sessionStartHookTokens)}
|
|
317
|
+
</span>
|
|
318
|
+
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
319
|
+
SessionStart hooks
|
|
320
|
+
</span>
|
|
321
|
+
<span className="text-[10px] uppercase tracking-widest text-zinc-500">sticky</span>
|
|
322
|
+
</li>
|
|
323
|
+
)}
|
|
324
|
+
</ul>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ─── Page ──────────────────────────────────────────────────────────────────
|
|
75
330
|
|
|
331
|
+
export default function Cockpit() {
|
|
76
332
|
return (
|
|
77
333
|
<main className="flex-1 font-mono bg-zinc-50 dark:bg-zinc-950">
|
|
78
334
|
<div className="max-w-6xl mx-auto px-6 py-10">
|
|
@@ -88,244 +344,22 @@ export default async function Cockpit() {
|
|
|
88
344
|
</code>
|
|
89
345
|
</header>
|
|
90
346
|
|
|
91
|
-
{/* PRIMARY RECEIPT — per-turn baseline */}
|
|
92
347
|
<section className="flex justify-center mb-6">
|
|
93
|
-
<
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
{DAYS}-day window
|
|
97
|
-
</span>
|
|
98
|
-
<Link
|
|
99
|
-
href="/items"
|
|
100
|
-
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
101
|
-
>
|
|
102
|
-
items →
|
|
103
|
-
</Link>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<div className="px-8 py-6 text-center">
|
|
107
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
108
|
-
per-turn baseline
|
|
109
|
-
</div>
|
|
110
|
-
<div className="mt-2 text-4xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
111
|
-
{fmt.format(inv.totalPerTurnTokens)}
|
|
112
|
-
<span className="ml-1.5 text-sm text-zinc-500 font-normal">tok</span>
|
|
113
|
-
</div>
|
|
114
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500 mt-1">
|
|
115
|
-
loaded into every system prompt
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
118
|
-
|
|
119
|
-
{/* Breakdown row — Stripe's tiny Payments / Customers numerals */}
|
|
120
|
-
<div className="grid grid-cols-4 border-t border-zinc-200 dark:border-zinc-800 divide-x divide-zinc-200 dark:divide-zinc-800">
|
|
121
|
-
<MiniStat label="items" value={fmt.format(inv.totalItems)} />
|
|
122
|
-
<MiniStat label="skills" value={fmt.format(inv.byKind.skill.count)} />
|
|
123
|
-
<MiniStat label="agents" value={fmt.format(inv.byKind.agent.count)} />
|
|
124
|
-
<MiniStat label="commands" value={fmt.format(inv.byKind.command.count)} />
|
|
125
|
-
</div>
|
|
126
|
-
</div>
|
|
348
|
+
<Suspense fallback={<PrimaryReceiptSkeleton />}>
|
|
349
|
+
<PrimaryReceipt />
|
|
350
|
+
</Suspense>
|
|
127
351
|
</section>
|
|
128
352
|
|
|
129
|
-
{/* SECONDARY RECEIPTS — 3-up horizontal row */}
|
|
130
353
|
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
>
|
|
141
|
-
sessions →
|
|
142
|
-
</Link>
|
|
143
|
-
</div>
|
|
144
|
-
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
145
|
-
{shortNumber(sess.totalTokens)}
|
|
146
|
-
<span className="ml-1 text-xs text-zinc-500 font-normal">tok</span>
|
|
147
|
-
<span className="ml-2 text-xs text-zinc-500 font-normal">·</span>
|
|
148
|
-
<span className="ml-2 text-base font-semibold tabular-nums text-zinc-900 dark:text-zinc-50">
|
|
149
|
-
{formatUsd(sess.totalCostUsd)}
|
|
150
|
-
</span>
|
|
151
|
-
<span className="ml-1 text-xs text-zinc-500 font-normal">api</span>
|
|
152
|
-
</div>
|
|
153
|
-
|
|
154
|
-
{sess.dailyBurn.length > 0 && (
|
|
155
|
-
<div className="mt-3">
|
|
156
|
-
<div className="flex items-end gap-px h-8">
|
|
157
|
-
{sess.dailyBurn.map((d) => {
|
|
158
|
-
const pct = Math.max(2, (d.tokens / maxBurn) * 100);
|
|
159
|
-
return (
|
|
160
|
-
<div
|
|
161
|
-
key={d.date}
|
|
162
|
-
className="flex-1 bg-zinc-300 dark:bg-zinc-700"
|
|
163
|
-
style={{ height: `${pct}%` }}
|
|
164
|
-
title={`${d.date}: ${shortNumber(d.tokens)} · ${formatUsd(d.cost)}`}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
})}
|
|
168
|
-
</div>
|
|
169
|
-
<div className="flex items-center justify-between mt-1 text-[10px] uppercase tracking-widest text-zinc-500 tabular-nums">
|
|
170
|
-
<span>{sess.dailyBurn[0].date.slice(5)}</span>
|
|
171
|
-
<span>{sess.dailyBurn.length} active days</span>
|
|
172
|
-
<span>{sess.dailyBurn[sess.dailyBurn.length - 1].date.slice(5)}</span>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
176
|
-
|
|
177
|
-
<div className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 grid grid-cols-3 text-center">
|
|
178
|
-
<div>
|
|
179
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
180
|
-
sess
|
|
181
|
-
</div>
|
|
182
|
-
<div className="text-xs font-semibold tabular-nums mt-0.5">
|
|
183
|
-
{fmt.format(sess.count)}
|
|
184
|
-
</div>
|
|
185
|
-
</div>
|
|
186
|
-
<div>
|
|
187
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
188
|
-
median
|
|
189
|
-
</div>
|
|
190
|
-
<div className="text-xs font-semibold tabular-nums mt-0.5">
|
|
191
|
-
{shortNumber(sess.medianSessionTokens)}
|
|
192
|
-
</div>
|
|
193
|
-
</div>
|
|
194
|
-
<div>
|
|
195
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
196
|
-
p95
|
|
197
|
-
</div>
|
|
198
|
-
<div className="text-xs font-semibold tabular-nums mt-0.5">
|
|
199
|
-
{shortNumber(sess.p95SessionTokens)}
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
{/* Disable candidates */}
|
|
206
|
-
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col">
|
|
207
|
-
<div className="flex items-baseline justify-between">
|
|
208
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
209
|
-
disable candidates
|
|
210
|
-
</div>
|
|
211
|
-
<Link
|
|
212
|
-
href="/items"
|
|
213
|
-
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
214
|
-
>
|
|
215
|
-
all →
|
|
216
|
-
</Link>
|
|
217
|
-
</div>
|
|
218
|
-
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
219
|
-
{fmt.format(candidateSavings)}
|
|
220
|
-
<span className="ml-1 text-xs text-zinc-500 font-normal">tok/turn</span>
|
|
221
|
-
</div>
|
|
222
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
223
|
-
{candidates.length > 0
|
|
224
|
-
? `top ${candidates.length} unused`
|
|
225
|
-
: "none in window"}
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
<ul className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 text-xs space-y-1 flex-1">
|
|
229
|
-
{candidates.length === 0 ? (
|
|
230
|
-
<li className="text-zinc-500">
|
|
231
|
-
every loaded user item invoked in {DAYS}d.
|
|
232
|
-
</li>
|
|
233
|
-
) : (
|
|
234
|
-
candidates.map((c) => (
|
|
235
|
-
<li key={c.filePath} className="flex items-center gap-2">
|
|
236
|
-
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
237
|
-
{fmt.format(c.perTurnTokens)}
|
|
238
|
-
</span>
|
|
239
|
-
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
240
|
-
{c.name}
|
|
241
|
-
</span>
|
|
242
|
-
<span className="text-[10px] uppercase tracking-widest text-zinc-500 w-12">
|
|
243
|
-
{c.kind}
|
|
244
|
-
</span>
|
|
245
|
-
<form
|
|
246
|
-
action={async () => {
|
|
247
|
-
"use server";
|
|
248
|
-
await toggleUserItem(c.filePath);
|
|
249
|
-
}}
|
|
250
|
-
>
|
|
251
|
-
<button
|
|
252
|
-
type="submit"
|
|
253
|
-
className="text-[10px] uppercase tracking-widest text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-zinc-700 hover:text-red-700 dark:hover:text-red-400 hover:border-red-300 dark:hover:border-red-800 hover:bg-red-50 dark:hover:bg-red-950/40 rounded px-1.5 py-0.5 transition-colors cursor-pointer"
|
|
254
|
-
title={`Disable ${c.name}`}
|
|
255
|
-
>
|
|
256
|
-
off
|
|
257
|
-
</button>
|
|
258
|
-
</form>
|
|
259
|
-
</li>
|
|
260
|
-
))
|
|
261
|
-
)}
|
|
262
|
-
</ul>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
{/* Context overhead */}
|
|
266
|
-
<div className="rounded-lg border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 px-4 py-4 flex flex-col">
|
|
267
|
-
<div className="flex items-baseline justify-between">
|
|
268
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
269
|
-
context overhead
|
|
270
|
-
</div>
|
|
271
|
-
<Link
|
|
272
|
-
href="/context"
|
|
273
|
-
className="text-[10px] uppercase tracking-widest text-zinc-500 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors"
|
|
274
|
-
>
|
|
275
|
-
context →
|
|
276
|
-
</Link>
|
|
277
|
-
</div>
|
|
278
|
-
<div className="mt-1.5 text-2xl font-semibold tabular-nums tracking-tight text-zinc-900 dark:text-zinc-50">
|
|
279
|
-
{fmt.format(contextTotal)}
|
|
280
|
-
<span className="ml-1 text-xs text-zinc-500 font-normal">tok</span>
|
|
281
|
-
</div>
|
|
282
|
-
<div className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
283
|
-
top {contextRowCount} sticky{" "}
|
|
284
|
-
{contextRowCount === 1 ? "source" : "sources"}
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
<ul className="mt-3 pt-3 border-t border-zinc-200 dark:border-zinc-800 text-xs space-y-1 flex-1">
|
|
288
|
-
{globalClaudeMd && (
|
|
289
|
-
<li className="flex items-center gap-2">
|
|
290
|
-
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
291
|
-
{fmt.format(globalClaudeMd.tokens)}
|
|
292
|
-
</span>
|
|
293
|
-
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
294
|
-
<code className="text-[11px]">~/.claude/CLAUDE.md</code>
|
|
295
|
-
</span>
|
|
296
|
-
<span className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
297
|
-
every
|
|
298
|
-
</span>
|
|
299
|
-
</li>
|
|
300
|
-
)}
|
|
301
|
-
{biggestMemoryMd && (
|
|
302
|
-
<li className="flex items-center gap-2">
|
|
303
|
-
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
304
|
-
{fmt.format(biggestMemoryMd.tokens)}
|
|
305
|
-
</span>
|
|
306
|
-
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
307
|
-
<code className="text-[11px]">{biggestMemoryMd.name}</code>
|
|
308
|
-
</span>
|
|
309
|
-
<span className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
310
|
-
mem
|
|
311
|
-
</span>
|
|
312
|
-
</li>
|
|
313
|
-
)}
|
|
314
|
-
{sessionStartHookTokens > 0 && (
|
|
315
|
-
<li className="flex items-center gap-2">
|
|
316
|
-
<span className="w-10 text-right tabular-nums text-zinc-500">
|
|
317
|
-
{fmt.format(sessionStartHookTokens)}
|
|
318
|
-
</span>
|
|
319
|
-
<span className="flex-1 truncate text-zinc-700 dark:text-zinc-300">
|
|
320
|
-
SessionStart hooks
|
|
321
|
-
</span>
|
|
322
|
-
<span className="text-[10px] uppercase tracking-widest text-zinc-500">
|
|
323
|
-
sticky
|
|
324
|
-
</span>
|
|
325
|
-
</li>
|
|
326
|
-
)}
|
|
327
|
-
</ul>
|
|
328
|
-
</div>
|
|
354
|
+
<Suspense fallback={<SecondaryReceiptSkeleton title={`${DAYS}-day burn`} />}>
|
|
355
|
+
<BurnReceipt />
|
|
356
|
+
</Suspense>
|
|
357
|
+
<Suspense fallback={<SecondaryReceiptSkeleton title="disable candidates" />}>
|
|
358
|
+
<CandidatesReceipt />
|
|
359
|
+
</Suspense>
|
|
360
|
+
<Suspense fallback={<SecondaryReceiptSkeleton title="context overhead" />}>
|
|
361
|
+
<ContextReceipt />
|
|
362
|
+
</Suspense>
|
|
329
363
|
</section>
|
|
330
364
|
|
|
331
365
|
<footer className="mt-8 pt-4 border-t border-zinc-200 dark:border-zinc-800 text-[10px] uppercase tracking-widest text-zinc-500 flex items-center justify-between">
|
|
@@ -343,4 +377,3 @@ export default async function Cockpit() {
|
|
|
343
377
|
</main>
|
|
344
378
|
);
|
|
345
379
|
}
|
|
346
|
-
|