@thiagos1lva/opencode-token-usage-chart 0.2.2 → 0.2.7

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.
@@ -0,0 +1,846 @@
1
+ import { effect as _$effect } from "opentui:runtime-module:%40opentui%2Fsolid";
2
+ import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
3
+ import { insert as _$insert } from "opentui:runtime-module:%40opentui%2Fsolid";
4
+ import { memo as _$memo } from "opentui:runtime-module:%40opentui%2Fsolid";
5
+ import { createTextNode as _$createTextNode } from "opentui:runtime-module:%40opentui%2Fsolid";
6
+ import { insertNode as _$insertNode } from "opentui:runtime-module:%40opentui%2Fsolid";
7
+ import { setProp as _$setProp } from "opentui:runtime-module:%40opentui%2Fsolid";
8
+ import { createElement as _$createElement } from "opentui:runtime-module:%40opentui%2Fsolid";
9
+ /** @jsxImportSource @opentui/solid */
10
+ import { useKeyboard, useTerminalDimensions } from "opentui:runtime-module:%40opentui%2Fsolid";
11
+ import { createEffect, createMemo, createSignal, For, Show, onCleanup } from "opentui:runtime-module:solid-js";
12
+ const id = "tui-token-usage";
13
+ const route = "token-usage";
14
+ const gran = ["15min", "30min", "hour", "day", "week", "month"];
15
+ const metr = ["tokens", "cost", "both"];
16
+ const GLOBAL_CALL_OPTIONS = {
17
+ headers: {
18
+ "x-opencode-directory": "",
19
+ "x-opencode-workspace": ""
20
+ }
21
+ };
22
+ const sessionAggregateCache = new Map();
23
+ const CACHE_VERSION = "v6";
24
+ function isFastMode(mode) {
25
+ return mode === "15min" || mode === "30min" || mode === "hour";
26
+ }
27
+ function messageLimit(mode) {
28
+ if (mode === "15min") return 400;
29
+ if (mode === "30min") return 500;
30
+ if (mode === "hour") return 800;
31
+ if (mode === "day") return 2000;
32
+ if (mode === "week") return 4000;
33
+ return 6000;
34
+ }
35
+ function sessionListLimit(mode) {
36
+ return isFastMode(mode) ? 5000 : 20000;
37
+ }
38
+ function count(input) {
39
+ if (input === "15min") return 48;
40
+ if (input === "30min") return 48;
41
+ if (input === "hour") return 24;
42
+ if (input === "day") return 30;
43
+ if (input === "week") return 20;
44
+ return 12;
45
+ }
46
+ function start(ts, mode) {
47
+ const d = new Date(ts);
48
+ if (mode === "15min") {
49
+ const minute = d.getMinutes();
50
+ d.setMinutes(minute - minute % 15, 0, 0);
51
+ return d.getTime();
52
+ }
53
+ if (mode === "30min") {
54
+ const minute = d.getMinutes();
55
+ d.setMinutes(minute - minute % 30, 0, 0);
56
+ return d.getTime();
57
+ }
58
+ if (mode === "hour") {
59
+ d.setMinutes(0, 0, 0);
60
+ return d.getTime();
61
+ }
62
+ if (mode === "day") {
63
+ d.setHours(0, 0, 0, 0);
64
+ return d.getTime();
65
+ }
66
+ if (mode === "week") {
67
+ d.setHours(0, 0, 0, 0);
68
+ const day = (d.getDay() + 6) % 7;
69
+ d.setDate(d.getDate() - day);
70
+ return d.getTime();
71
+ }
72
+ d.setHours(0, 0, 0, 0);
73
+ d.setDate(1);
74
+ return d.getTime();
75
+ }
76
+ function label(ts, mode) {
77
+ const d = new Date(ts);
78
+ if (mode === "15min" || mode === "30min") {
79
+ return d.toLocaleString(undefined, {
80
+ hour: "2-digit",
81
+ minute: "2-digit",
82
+ day: "2-digit",
83
+ month: "2-digit"
84
+ });
85
+ }
86
+ if (mode === "hour") return d.toLocaleString(undefined, {
87
+ hour: "2-digit",
88
+ day: "2-digit",
89
+ month: "2-digit"
90
+ });
91
+ if (mode === "day") return d.toLocaleDateString(undefined, {
92
+ day: "2-digit",
93
+ month: "2-digit"
94
+ });
95
+ if (mode === "week") {
96
+ const w = week(d);
97
+ return `W${w.number} ${w.year}`;
98
+ }
99
+ return d.toLocaleDateString(undefined, {
100
+ month: "short",
101
+ year: "2-digit"
102
+ });
103
+ }
104
+ function week(d) {
105
+ const x = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
106
+ x.setUTCDate(x.getUTCDate() + 4 - (x.getUTCDay() || 7));
107
+ const y = new Date(Date.UTC(x.getUTCFullYear(), 0, 1));
108
+ return {
109
+ number: Math.ceil(((x.getTime() - y.getTime()) / 86400000 + 1) / 7),
110
+ year: x.getUTCFullYear()
111
+ };
112
+ }
113
+ function add(ts, mode, amount) {
114
+ const d = new Date(ts);
115
+ if (mode === "15min") {
116
+ d.setMinutes(d.getMinutes() + amount * 15);
117
+ return d.getTime();
118
+ }
119
+ if (mode === "30min") {
120
+ d.setMinutes(d.getMinutes() + amount * 30);
121
+ return d.getTime();
122
+ }
123
+ if (mode === "hour") {
124
+ d.setHours(d.getHours() + amount);
125
+ return d.getTime();
126
+ }
127
+ if (mode === "day") {
128
+ d.setDate(d.getDate() + amount);
129
+ return d.getTime();
130
+ }
131
+ if (mode === "week") {
132
+ d.setDate(d.getDate() + amount * 7);
133
+ return d.getTime();
134
+ }
135
+ d.setMonth(d.getMonth() + amount);
136
+ return d.getTime();
137
+ }
138
+ function buildRows(mode, now = Date.now()) {
139
+ const n = count(mode);
140
+ const end = start(now, mode);
141
+ const first = add(end, mode, -(n - 1));
142
+ return Array.from({
143
+ length: n
144
+ }, (_, i) => {
145
+ const key = add(first, mode, i);
146
+ return {
147
+ key,
148
+ label: label(key, mode),
149
+ tokens: 0,
150
+ cost: 0
151
+ };
152
+ });
153
+ }
154
+ function tok(msg) {
155
+ return msg.input + msg.output + msg.reasoning + msg.cache.read + msg.cache.write;
156
+ }
157
+ function sessionStamp(session) {
158
+ if (!session || typeof session !== "object") return "";
159
+ const value = session;
160
+ const id = typeof value.id === "string" ? value.id : "";
161
+ const updated = typeof value.time?.updated === "number" ? value.time.updated : undefined;
162
+ const created = typeof value.time?.created === "number" ? value.time.created : undefined;
163
+ const version = typeof value.version === "string" ? value.version : "";
164
+ return `${id}:${updated ?? created ?? ""}:${version}`;
165
+ }
166
+ async function aggregateSession(client, sessionID, directory, stamp, mode, range, options) {
167
+ const key = `${directory ?? "default"}:${sessionID}:${mode}:${range.start}:${range.end}`;
168
+ const cached = sessionAggregateCache.get(key);
169
+ if (cached && cached.stamp === stamp) {
170
+ return {
171
+ ...cached,
172
+ stats: {
173
+ ...cached.stats,
174
+ cached: true
175
+ }
176
+ };
177
+ }
178
+ if (options.shouldStop?.()) {
179
+ return {
180
+ stamp,
181
+ bins: new Map(),
182
+ total: {
183
+ tokens: 0,
184
+ cost: 0
185
+ },
186
+ stats: {
187
+ messages: 0,
188
+ assistant: 0,
189
+ inRange: 0,
190
+ cached: false
191
+ }
192
+ };
193
+ }
194
+ let messageError;
195
+ const messages = await client.session.messages({
196
+ sessionID,
197
+ directory,
198
+ limit: messageLimit(mode)
199
+ }, GLOBAL_CALL_OPTIONS).then(x => x.data ?? []).catch(error => {
200
+ messageError = error instanceof Error ? error.message : String(error);
201
+ return [];
202
+ });
203
+ const bins = new Map();
204
+ let total = {
205
+ tokens: 0,
206
+ cost: 0
207
+ };
208
+ let assistantCount = 0;
209
+ let inRangeCount = 0;
210
+ for (let i = messages.length - 1; i >= 0; i--) {
211
+ if (options.shouldStop?.()) break;
212
+ const info = messages[i].info;
213
+ if (info.role !== "assistant") continue;
214
+ assistantCount++;
215
+ const created = info.time.created;
216
+ if (created >= range.end) continue;
217
+ if (created < range.start) continue;
218
+ inRangeCount++;
219
+ const bucket = start(created, mode);
220
+ const value = bins.get(bucket) ?? {
221
+ tokens: 0,
222
+ cost: 0
223
+ };
224
+ const tokens = tok(info.tokens);
225
+ value.tokens += tokens;
226
+ value.cost += info.cost;
227
+ bins.set(bucket, value);
228
+ total.tokens += tokens;
229
+ total.cost += info.cost;
230
+ }
231
+ const out = {
232
+ stamp,
233
+ bins,
234
+ total,
235
+ stats: {
236
+ messages: messages.length,
237
+ assistant: assistantCount,
238
+ inRange: inRangeCount,
239
+ cached: false,
240
+ error: messageError
241
+ }
242
+ };
243
+ sessionAggregateCache.set(key, out);
244
+ if (sessionAggregateCache.size > 500) {
245
+ const oldest = sessionAggregateCache.keys().next().value;
246
+ if (oldest) sessionAggregateCache.delete(oldest);
247
+ }
248
+ return out;
249
+ }
250
+ async function load(api, mode, scope, ref, options = {}) {
251
+ const debugLines = [];
252
+ const apiWithScopes = api;
253
+ const workspaceIDs = Array.from(new Set((apiWithScopes.state?.workspace?.list?.() ?? []).map(item => item?.id).filter(item => typeof item === "string" && item.length > 0)));
254
+ const allScopeRef = workspaceIDs.length > 0 ? `all:${workspaceIDs.sort().join(",")}` : "all";
255
+ const scopeRef = scope === "session" ? ref.sessionID ?? "none" : scope === "workspace" ? ref.workspaceID ?? "none" : "all";
256
+ const key = `token-usage-cache:${CACHE_VERSION}:${mode}:${scope}:${scope === "all" ? allScopeRef : scopeRef}`;
257
+ const hit = api.kv.get(key, undefined);
258
+ if (!options.force && hit && Date.now() - hit.time < 5 * 60 * 1000) {
259
+ return {
260
+ ...hit.data,
261
+ debug: {
262
+ lines: [...(hit.data.debug?.lines ?? []), `cache hit key=${key}`, `cache age ms=${Date.now() - hit.time}`]
263
+ }
264
+ };
265
+ }
266
+ const rows = buildRows(mode);
267
+ const idx = new Map(rows.map((item, i) => [item.key, i]));
268
+ const range = {
269
+ start: rows[0]?.key ?? 0,
270
+ end: add(rows[rows.length - 1]?.key ?? 0, mode, 1)
271
+ };
272
+ debugLines.push(`cache miss key=${key}`);
273
+ debugLines.push(`scope=${scope} mode=${mode}`);
274
+ debugLines.push(`workspace ids=${workspaceIDs.length}`);
275
+ debugLines.push(`window start=${new Date(range.start).toISOString()} end=${new Date(range.end).toISOString()}`);
276
+ const clientSources = [];
277
+ if (scope === "workspace") {
278
+ if (ref.workspaceID && apiWithScopes.scopedClient) {
279
+ clientSources.push({
280
+ key: `workspace:${ref.workspaceID}`,
281
+ client: apiWithScopes.scopedClient(ref.workspaceID)
282
+ });
283
+ } else {
284
+ clientSources.push({
285
+ key: "workspace:default",
286
+ client: api.client
287
+ });
288
+ }
289
+ } else if (scope === "all") {
290
+ clientSources.push({
291
+ key: "all:default",
292
+ client: api.client
293
+ });
294
+ if (apiWithScopes.scopedClient) {
295
+ workspaceIDs.forEach(workspaceID => {
296
+ clientSources.push({
297
+ key: `all:${workspaceID}`,
298
+ client: apiWithScopes.scopedClient?.(workspaceID) ?? api.client
299
+ });
300
+ });
301
+ }
302
+ } else {
303
+ clientSources.push({
304
+ key: "session",
305
+ client: api.client
306
+ });
307
+ }
308
+ debugLines.push(`client sources=${clientSources.map(item => item.key).join(",")}`);
309
+ let sessions = [];
310
+ if (scope === "session") {
311
+ if (!ref.sessionID) {
312
+ return {
313
+ rows,
314
+ total: {
315
+ tokens: 0,
316
+ cost: 0
317
+ },
318
+ debug: {
319
+ lines: [...debugLines, "missing sessionID for session scope"]
320
+ }
321
+ };
322
+ }
323
+ sessions = [{
324
+ id: ref.sessionID,
325
+ stamp: ref.sessionID,
326
+ client: api.client,
327
+ directory: undefined
328
+ }];
329
+ } else {
330
+ const dedup = new Map();
331
+ const globalList = await api.client.session.list({
332
+ limit: sessionListLimit(mode)
333
+ }, GLOBAL_CALL_OPTIONS).then(x => x.data ?? []).catch(() => []);
334
+ debugLines.push(`sessions from global override: ${globalList.length}`);
335
+ globalList.forEach(item => {
336
+ if (!item?.id) return;
337
+ if (dedup.has(item.id)) return;
338
+ dedup.set(item.id, {
339
+ id: item.id,
340
+ stamp: sessionStamp(item) || item.id,
341
+ client: api.client,
342
+ directory: undefined
343
+ });
344
+ });
345
+ const projects = await api.client.project.list(undefined, GLOBAL_CALL_OPTIONS).then(x => x.data ?? []).catch(() => []);
346
+ debugLines.push(`projects discovered: ${projects.length}`);
347
+ for (const project of projects) {
348
+ if (!project?.worktree) continue;
349
+ const list = await api.client.session.list({
350
+ directory: project.worktree,
351
+ limit: sessionListLimit(mode)
352
+ }, GLOBAL_CALL_OPTIONS).then(x => x.data ?? []).catch(() => []);
353
+ debugLines.push(`sessions from project ${project.worktree}: ${list.length}`);
354
+ list.forEach(item => {
355
+ if (!item?.id) return;
356
+ if (dedup.has(item.id)) return;
357
+ dedup.set(item.id, {
358
+ id: item.id,
359
+ stamp: sessionStamp(item) || item.id,
360
+ client: api.client,
361
+ directory: project.worktree
362
+ });
363
+ });
364
+ }
365
+ for (const source of clientSources) {
366
+ if (options.shouldStop?.()) break;
367
+ const list = await source.client.session.list({
368
+ limit: sessionListLimit(mode)
369
+ }, GLOBAL_CALL_OPTIONS).then(x => x.data ?? []).catch(() => []);
370
+ debugLines.push(`sessions from ${source.key}: ${list.length}`);
371
+ list.forEach(item => {
372
+ if (!item?.id) return;
373
+ if (dedup.has(item.id)) return;
374
+ dedup.set(item.id, {
375
+ id: item.id,
376
+ stamp: sessionStamp(item) || item.id,
377
+ client: source.client,
378
+ directory: undefined
379
+ });
380
+ });
381
+ }
382
+ sessions = Array.from(dedup.values());
383
+ }
384
+ debugLines.push(`dedup sessions=${sessions.length}`);
385
+ const size = isFastMode(mode) ? 6 : 10;
386
+ let total = {
387
+ tokens: 0,
388
+ cost: 0
389
+ };
390
+ let totalMessagesScanned = 0;
391
+ let totalAssistantMessages = 0;
392
+ let totalInRangeMessages = 0;
393
+ let cachedSessionCount = 0;
394
+ let sessionFetchErrors = 0;
395
+ for (let i = 0; i < sessions.length; i += size) {
396
+ if (options.shouldStop?.()) break;
397
+ const part = sessions.slice(i, i + size);
398
+ const packs = await Promise.all(part.map(session => aggregateSession(session.client, session.id, session.directory, session.stamp, mode, range, options).catch(() => ({
399
+ stamp: session.stamp,
400
+ bins: new Map(),
401
+ total: {
402
+ tokens: 0,
403
+ cost: 0
404
+ },
405
+ stats: {
406
+ messages: 0,
407
+ assistant: 0,
408
+ inRange: 0,
409
+ cached: false,
410
+ error: "aggregateSession failed"
411
+ }
412
+ }))));
413
+ packs.forEach(pack => {
414
+ pack.bins.forEach((value, bucket) => {
415
+ const rowIndex = idx.get(bucket);
416
+ if (rowIndex === undefined) return;
417
+ rows[rowIndex].tokens += value.tokens;
418
+ rows[rowIndex].cost += value.cost;
419
+ });
420
+ total.tokens += pack.total.tokens;
421
+ total.cost += pack.total.cost;
422
+ totalMessagesScanned += pack.stats.messages;
423
+ totalAssistantMessages += pack.stats.assistant;
424
+ totalInRangeMessages += pack.stats.inRange;
425
+ if (pack.stats.cached) cachedSessionCount++;
426
+ if (pack.stats.error) sessionFetchErrors++;
427
+ });
428
+ }
429
+ const rowsWithData = rows.reduce((count, row) => row.tokens > 0 || row.cost > 0 ? count + 1 : count, 0);
430
+ debugLines.push(`messages scanned=${totalMessagesScanned}`);
431
+ debugLines.push(`assistant messages=${totalAssistantMessages}`);
432
+ debugLines.push(`assistant in window=${totalInRangeMessages}`);
433
+ debugLines.push(`session cache hits=${cachedSessionCount}`);
434
+ debugLines.push(`session fetch errors=${sessionFetchErrors}`);
435
+ debugLines.push(`rows with data=${rowsWithData}/${rows.length}`);
436
+ debugLines.push(`total tokens=${Math.round(total.tokens)} total cost=${total.cost.toFixed(4)}`);
437
+ const out = {
438
+ rows,
439
+ total,
440
+ debug: {
441
+ lines: debugLines
442
+ }
443
+ };
444
+ api.kv.set(key, {
445
+ time: Date.now(),
446
+ data: out
447
+ });
448
+ return out;
449
+ }
450
+ function fmt(input) {
451
+ if (input >= 1000000) return `${(input / 1000000).toFixed(1)}M`;
452
+ if (input >= 1000) return `${(input / 1000).toFixed(1)}K`;
453
+ return `${Math.round(input)}`;
454
+ }
455
+ function bar(size) {
456
+ if (size <= 0) return "";
457
+ return "#".repeat(size);
458
+ }
459
+ function barWith(size, char) {
460
+ if (size <= 0) return "";
461
+ return char.repeat(size);
462
+ }
463
+ function next(all, cur, dir) {
464
+ const i = all.indexOf(cur);
465
+ if (i === -1) return all[0];
466
+ const len = all.length;
467
+ return all[(i + dir + len) % len];
468
+ }
469
+ function parseBackTarget(input) {
470
+ if (!input || typeof input !== "object") return {
471
+ name: "home"
472
+ };
473
+ const data = input;
474
+ if (!data.back || typeof data.back !== "object") return {
475
+ name: "home"
476
+ };
477
+ const back = data.back;
478
+ if (back.name === "session") {
479
+ const params = back.params;
480
+ if (params && typeof params.sessionID === "string" && params.sessionID.length > 0) {
481
+ return {
482
+ name: "session",
483
+ params: {
484
+ sessionID: params.sessionID
485
+ }
486
+ };
487
+ }
488
+ }
489
+ return {
490
+ name: "home"
491
+ };
492
+ }
493
+ function View(props) {
494
+ const dim = useTerminalDimensions();
495
+ const [mode, setMode] = createSignal("day");
496
+ const [kind, setKind] = createSignal("tokens");
497
+ const [scope, setScope] = createSignal(props.back.name === "session" ? "session" : "all");
498
+ const [debug, setDebug] = createSignal(false);
499
+ const [busy, setBusy] = createSignal(true);
500
+ const [err, setErr] = createSignal();
501
+ const [data, setData] = createSignal({
502
+ rows: [],
503
+ total: {
504
+ tokens: 0,
505
+ cost: 0
506
+ },
507
+ debug: {
508
+ lines: []
509
+ }
510
+ });
511
+ const [lastRefreshAt, setLastRefreshAt] = createSignal();
512
+ let requestID = 0;
513
+ let disposed = false;
514
+ onCleanup(() => {
515
+ disposed = true;
516
+ });
517
+ const workspaceID = () => {
518
+ const apiWithWorkspace = props.api;
519
+ return apiWithWorkspace.workspace?.current?.();
520
+ };
521
+ const scopeList = createMemo(() => {
522
+ const out = ["all"];
523
+ if (workspaceID()) out.push("workspace");
524
+ if (props.back.name === "session") out.push("session");
525
+ return out;
526
+ });
527
+ const pull = (force = false) => {
528
+ const id = ++requestID;
529
+ setBusy(true);
530
+ setErr(undefined);
531
+ load(props.api, mode(), scope(), {
532
+ sessionID: props.back.name === "session" ? props.back.params.sessionID : undefined,
533
+ workspaceID: workspaceID()
534
+ }, {
535
+ force,
536
+ shouldStop: () => disposed || id !== requestID || props.api.route.current.name !== route
537
+ }).then(value => {
538
+ if (disposed || id !== requestID) return;
539
+ setData(value);
540
+ setLastRefreshAt(Date.now());
541
+ }).catch(e => {
542
+ if (disposed || id !== requestID) return;
543
+ setErr(e instanceof Error ? e.message : String(e));
544
+ }).finally(() => {
545
+ if (disposed || id !== requestID) return;
546
+ setBusy(false);
547
+ });
548
+ };
549
+ const view = createMemo(() => {
550
+ const rows = data().rows;
551
+ const maxTokens = rows.reduce((acc, item) => Math.max(acc, item.tokens), 0);
552
+ const maxCost = rows.reduce((acc, item) => Math.max(acc, item.cost), 0);
553
+ const both = kind() === "both";
554
+ const width = both ? Math.max(8, Math.floor(dim().width * 0.18)) : Math.max(12, Math.floor(dim().width * 0.38));
555
+ return rows.map(item => {
556
+ const tokenSize = maxTokens <= 0 ? 0 : Math.max(1, Math.round(item.tokens / maxTokens * width));
557
+ const costSize = maxCost <= 0 ? 0 : Math.max(1, Math.round(item.cost / maxCost * width));
558
+ const size = kind() === "cost" ? costSize : kind() === "tokens" ? tokenSize : 0;
559
+ return {
560
+ ...item,
561
+ size,
562
+ tokenSize,
563
+ costSize
564
+ };
565
+ });
566
+ });
567
+ useKeyboard(evt => {
568
+ if (props.api.route.current.name !== route) return;
569
+ const key = (evt.name ?? "").toLowerCase();
570
+ if (evt.name === "escape") {
571
+ evt.preventDefault();
572
+ evt.stopPropagation();
573
+ if (props.back.name === "session") {
574
+ props.api.route.navigate("session", props.back.params);
575
+ } else {
576
+ props.api.route.navigate("home");
577
+ }
578
+ return;
579
+ }
580
+ if (key === "r" || key === "f5" || evt.ctrl && key === "r") {
581
+ evt.preventDefault();
582
+ evt.stopPropagation();
583
+ pull(true);
584
+ return;
585
+ }
586
+ if (evt.name === "d") {
587
+ evt.preventDefault();
588
+ evt.stopPropagation();
589
+ setDebug(value => !value);
590
+ return;
591
+ }
592
+ if (evt.name === "s") {
593
+ evt.preventDefault();
594
+ evt.stopPropagation();
595
+ setScope(x => next(scopeList(), x, 1));
596
+ return;
597
+ }
598
+ if (evt.name === "tab" || evt.name === "right" || evt.name === "l") {
599
+ evt.preventDefault();
600
+ evt.stopPropagation();
601
+ setMode(x => next(gran, x, 1));
602
+ return;
603
+ }
604
+ if (evt.name === "left" || evt.name === "h") {
605
+ evt.preventDefault();
606
+ evt.stopPropagation();
607
+ setMode(x => next(gran, x, -1));
608
+ return;
609
+ }
610
+ if (evt.name === "up" || evt.name === "k") {
611
+ evt.preventDefault();
612
+ evt.stopPropagation();
613
+ setKind(x => next(metr, x, 1));
614
+ return;
615
+ }
616
+ if (evt.name === "down" || evt.name === "j") {
617
+ evt.preventDefault();
618
+ evt.stopPropagation();
619
+ setKind(x => next(metr, x, -1));
620
+ return;
621
+ }
622
+ });
623
+ createEffect(() => {
624
+ mode();
625
+ scope();
626
+ pull();
627
+ });
628
+ createEffect(() => {
629
+ const all = scopeList();
630
+ const cur = scope();
631
+ if (!all.includes(cur)) {
632
+ setScope(all[0] ?? "all");
633
+ }
634
+ });
635
+ const money = new Intl.NumberFormat("en-US", {
636
+ style: "currency",
637
+ currency: "USD"
638
+ });
639
+ return (() => {
640
+ var _el$ = _$createElement("box"),
641
+ _el$2 = _$createElement("text"),
642
+ _el$3 = _$createElement("b"),
643
+ _el$5 = _$createElement("text"),
644
+ _el$6 = _$createTextNode(`window: `),
645
+ _el$7 = _$createTextNode(` | metric: `),
646
+ _el$8 = _$createTextNode(` | scope: `),
647
+ _el$9 = _$createTextNode(` | debug: `),
648
+ _el$0 = _$createTextNode(` | keys: tab/left/right window, up/down metric, s scope, r/ctrl+r/f5 refresh, d debug, esc back`),
649
+ _el$13 = _$createElement("box"),
650
+ _el$14 = _$createElement("text"),
651
+ _el$15 = _$createTextNode(`total tokens: `),
652
+ _el$16 = _$createElement("text"),
653
+ _el$17 = _$createTextNode(`total cost: `);
654
+ _$insertNode(_el$, _el$2);
655
+ _$insertNode(_el$, _el$5);
656
+ _$insertNode(_el$, _el$13);
657
+ _$setProp(_el$, "flexDirection", "column");
658
+ _$setProp(_el$, "paddingLeft", 2);
659
+ _$setProp(_el$, "paddingRight", 2);
660
+ _$setProp(_el$, "paddingTop", 1);
661
+ _$setProp(_el$, "paddingBottom", 1);
662
+ _$setProp(_el$, "gap", 1);
663
+ _$insertNode(_el$2, _el$3);
664
+ _$insertNode(_el$3, _$createTextNode(`Token Usage Chart`));
665
+ _$insertNode(_el$5, _el$6);
666
+ _$insertNode(_el$5, _el$7);
667
+ _$insertNode(_el$5, _el$8);
668
+ _$insertNode(_el$5, _el$9);
669
+ _$insertNode(_el$5, _el$0);
670
+ _$insert(_el$5, mode, _el$7);
671
+ _$insert(_el$5, kind, _el$8);
672
+ _$insert(_el$5, scope, _el$9);
673
+ _$insert(_el$5, () => debug() ? "on" : "off", _el$0);
674
+ _$insert(_el$, _$createComponent(Show, {
675
+ get when() {
676
+ return lastRefreshAt();
677
+ },
678
+ children: ts => (() => {
679
+ var _el$21 = _$createElement("text"),
680
+ _el$22 = _$createTextNode(`last refresh: `);
681
+ _$insertNode(_el$21, _el$22);
682
+ _$insert(_el$21, () => new Date(ts()).toLocaleTimeString(), null);
683
+ _$effect(_$p => _$setProp(_el$21, "fg", props.api.theme.current.textMuted, _$p));
684
+ return _el$21;
685
+ })()
686
+ }), _el$13);
687
+ _$insert(_el$, _$createComponent(Show, {
688
+ get when() {
689
+ return busy();
690
+ },
691
+ get children() {
692
+ var _el$1 = _$createElement("text");
693
+ _$insertNode(_el$1, _$createTextNode(`Loading usage...`));
694
+ _$effect(_$p => _$setProp(_el$1, "fg", props.api.theme.current.textMuted, _$p));
695
+ return _el$1;
696
+ }
697
+ }), _el$13);
698
+ _$insert(_el$, _$createComponent(Show, {
699
+ get when() {
700
+ return err();
701
+ },
702
+ children: item => (() => {
703
+ var _el$23 = _$createElement("text"),
704
+ _el$24 = _$createTextNode(`Error: `);
705
+ _$insertNode(_el$23, _el$24);
706
+ _$insert(_el$23, item, null);
707
+ _$effect(_$p => _$setProp(_el$23, "fg", props.api.theme.current.error, _$p));
708
+ return _el$23;
709
+ })()
710
+ }), _el$13);
711
+ _$insert(_el$, _$createComponent(Show, {
712
+ get when() {
713
+ return _$memo(() => !!(!busy() && !err()))() && view().length === 0;
714
+ },
715
+ get children() {
716
+ var _el$11 = _$createElement("text");
717
+ _$insertNode(_el$11, _$createTextNode(`No data found.`));
718
+ _$effect(_$p => _$setProp(_el$11, "fg", props.api.theme.current.textMuted, _$p));
719
+ return _el$11;
720
+ }
721
+ }), _el$13);
722
+ _$insert(_el$, _$createComponent(Show, {
723
+ get when() {
724
+ return _$memo(() => !!(!busy() && !err()))() && view().length > 0;
725
+ },
726
+ get children() {
727
+ return _$createComponent(For, {
728
+ get each() {
729
+ return view();
730
+ },
731
+ children: item => (() => {
732
+ var _el$25 = _$createElement("text");
733
+ _$setProp(_el$25, "wrapMode", "none");
734
+ _$insert(_el$25, _$createComponent(Show, {
735
+ get when() {
736
+ return kind() === "both";
737
+ },
738
+ get fallback() {
739
+ return `${item.label.padEnd(11)} ${bar(item.size)} ${kind() === "cost" ? money.format(item.cost) : fmt(item.tokens)}`;
740
+ },
741
+ get children() {
742
+ return [_$memo(() => item.label.padEnd(11)), " T:", _$memo(() => barWith(item.tokenSize, "#")), " ", _$memo(() => fmt(item.tokens)), " C:", _$memo(() => barWith(item.costSize, "=")), " ", _$memo(() => money.format(item.cost))];
743
+ }
744
+ }));
745
+ _$effect(_$p => _$setProp(_el$25, "fg", props.api.theme.current.textMuted, _$p));
746
+ return _el$25;
747
+ })()
748
+ });
749
+ }
750
+ }), _el$13);
751
+ _$insertNode(_el$13, _el$14);
752
+ _$insertNode(_el$13, _el$16);
753
+ _$setProp(_el$13, "flexDirection", "row");
754
+ _$setProp(_el$13, "gap", 3);
755
+ _$insertNode(_el$14, _el$15);
756
+ _$insert(_el$14, () => fmt(data().total.tokens), null);
757
+ _$insertNode(_el$16, _el$17);
758
+ _$insert(_el$16, () => money.format(data().total.cost), null);
759
+ _$insert(_el$, _$createComponent(Show, {
760
+ get when() {
761
+ return _$memo(() => !!debug())() && data().debug.lines.length > 0;
762
+ },
763
+ get children() {
764
+ var _el$18 = _$createElement("box"),
765
+ _el$19 = _$createElement("text");
766
+ _$insertNode(_el$18, _el$19);
767
+ _$setProp(_el$18, "flexDirection", "column");
768
+ _$setProp(_el$18, "marginTop", 1);
769
+ _$insertNode(_el$19, _$createTextNode(`Debug`));
770
+ _$insert(_el$18, _$createComponent(For, {
771
+ get each() {
772
+ return data().debug.lines;
773
+ },
774
+ children: line => (() => {
775
+ var _el$26 = _$createElement("text"),
776
+ _el$27 = _$createTextNode(`- `);
777
+ _$insertNode(_el$26, _el$27);
778
+ _$insert(_el$26, line, null);
779
+ _$effect(_$p => _$setProp(_el$26, "fg", props.api.theme.current.textMuted, _$p));
780
+ return _el$26;
781
+ })()
782
+ }), null);
783
+ _$effect(_$p => _$setProp(_el$19, "fg", props.api.theme.current.info, _$p));
784
+ return _el$18;
785
+ }
786
+ }), null);
787
+ _$effect(_p$ => {
788
+ var _v$ = props.api.theme.current.text,
789
+ _v$2 = props.api.theme.current.textMuted,
790
+ _v$3 = props.api.theme.current.textMuted,
791
+ _v$4 = props.api.theme.current.textMuted;
792
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$2, "fg", _v$, _p$.e));
793
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$5, "fg", _v$2, _p$.t));
794
+ _v$3 !== _p$.a && (_p$.a = _$setProp(_el$14, "fg", _v$3, _p$.a));
795
+ _v$4 !== _p$.o && (_p$.o = _$setProp(_el$16, "fg", _v$4, _p$.o));
796
+ return _p$;
797
+ }, {
798
+ e: undefined,
799
+ t: undefined,
800
+ a: undefined,
801
+ o: undefined
802
+ });
803
+ return _el$;
804
+ })();
805
+ }
806
+ const tui = async (api, options) => {
807
+ if (options?.enabled === false) return;
808
+ api.route.register([{
809
+ name: route,
810
+ render: ({
811
+ params
812
+ }) => _$createComponent(View, {
813
+ api: api,
814
+ get back() {
815
+ return parseBackTarget(params);
816
+ }
817
+ })
818
+ }]);
819
+ api.command.register(() => [{
820
+ title: "Token Usage Chart",
821
+ value: "token.usage.chart",
822
+ category: "Plugin",
823
+ slash: {
824
+ name: "token-chart"
825
+ },
826
+ onSelect: () => {
827
+ const current = api.route.current;
828
+ const back = current.name === "session" && current.params && typeof current.params.sessionID === "string" && current.params.sessionID.length > 0 ? {
829
+ name: "session",
830
+ params: {
831
+ sessionID: current.params.sessionID
832
+ }
833
+ } : {
834
+ name: "home"
835
+ };
836
+ api.route.navigate(route, {
837
+ back
838
+ });
839
+ }
840
+ }]);
841
+ };
842
+ const plugin = {
843
+ id,
844
+ tui
845
+ };
846
+ export default plugin;