@mbeato/contextscope 0.1.5 → 0.1.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.
- 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_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/app/page.tsx +290 -257
- package/.next/standalone/bin/cli.js +22 -6
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/.next/static/chunks/0pb14~6.l8.f9.css +1 -0
- package/bin/cli.js +22 -6
- package/package.json +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 → uI28k7hBjH7vKGE3jdIaM}/_buildManifest.js +0 -0
- /package/.next/static/{I-W1iV4XdkTRyjis8Ros1 → uI28k7hBjH7vKGE3jdIaM}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{I-W1iV4XdkTRyjis8Ros1 → uI28k7hBjH7vKGE3jdIaM}/_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
|
-
|
|
@@ -148,27 +148,43 @@ async function main() {
|
|
|
148
148
|
stdio: "inherit",
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
process.stdout.write(`contextscope running on ${url}\n`);
|
|
152
|
+
|
|
153
|
+
// Wait for server to respond, warm the cache by hitting `/` (which triggers
|
|
154
|
+
// the heavy transcript scans), then open the browser. Shifts the cold wait
|
|
155
|
+
// from "blank skeleton in browser" to "scanning... in terminal" so the page
|
|
156
|
+
// renders almost instantly when the browser opens.
|
|
153
157
|
if (!noOpen) {
|
|
154
158
|
const { default: open } = await import("open");
|
|
155
159
|
(async () => {
|
|
160
|
+
// Probe readiness with a path that doesn't trigger the scans.
|
|
161
|
+
let ready = false;
|
|
156
162
|
for (let i = 0; i < 50; i++) {
|
|
157
163
|
try {
|
|
158
|
-
const res = await fetch(url
|
|
164
|
+
const res = await fetch(`${url}/_not-found`, { method: "HEAD" });
|
|
159
165
|
if (res.status < 500) {
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
ready = true;
|
|
167
|
+
break;
|
|
162
168
|
}
|
|
163
169
|
} catch {
|
|
164
170
|
// not yet ready
|
|
165
171
|
}
|
|
166
172
|
await new Promise((r) => setTimeout(r, 200));
|
|
167
173
|
}
|
|
174
|
+
if (!ready) return;
|
|
175
|
+
process.stdout.write(` scanning ~/.claude/projects ...`);
|
|
176
|
+
const start = Date.now();
|
|
177
|
+
try {
|
|
178
|
+
await fetch(url);
|
|
179
|
+
} catch {
|
|
180
|
+
// best-effort warm-up; open browser anyway
|
|
181
|
+
}
|
|
182
|
+
process.stdout.write(` ${((Date.now() - start) / 1000).toFixed(1)}s\n`);
|
|
183
|
+
await open(url).catch(() => {});
|
|
168
184
|
})();
|
|
169
185
|
}
|
|
170
186
|
|
|
171
|
-
process.stdout.write(`
|
|
187
|
+
process.stdout.write(` (Ctrl+C to stop)\n`);
|
|
172
188
|
|
|
173
189
|
const shutdown = (code = 0) => {
|
|
174
190
|
if (!child.killed) child.kill("SIGTERM");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mbeato/contextscope",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Local dashboard auditing Claude Code's per-turn token context (skills, agents, commands, CLAUDE.md, MEMORY.md, hooks, MCP) with toggle-based disable and session analytics.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|