tokens-metric 0.4.7 → 0.4.9
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/dist/tui/index.js +59 -9
- package/package.json +1 -1
package/dist/tui/index.js
CHANGED
|
@@ -49,6 +49,7 @@ function App() {
|
|
|
49
49
|
// openTab: which panel is expanded (null = all collapsed)
|
|
50
50
|
const [focusedTab, setFocusedTab] = useState(1);
|
|
51
51
|
const [openTab, setOpenTab] = useState(null);
|
|
52
|
+
const [sessionCursor, setSessionCursor] = useState(0);
|
|
52
53
|
const startedAtRef = useRef(Date.now());
|
|
53
54
|
// useInput requires raw mode (interactive TTY). Skip it when stdin is piped
|
|
54
55
|
// or otherwise non-interactive, so `node dist/tui/index.js | cat` still
|
|
@@ -63,7 +64,18 @@ function App() {
|
|
|
63
64
|
setOpenTab(null);
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
//
|
|
67
|
+
// ↑↓ navigate sessions when Sessions tab is open
|
|
68
|
+
if (openTab === 3) {
|
|
69
|
+
if (key.upArrow) {
|
|
70
|
+
setSessionCursor((c) => Math.max(0, c - 1));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (key.downArrow) {
|
|
74
|
+
setSessionCursor((c) => c + 1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Arrow keys move the tab cursor
|
|
67
79
|
if (key.leftArrow) {
|
|
68
80
|
setFocusedTab((t) => (t > 1 ? (t - 1) : t));
|
|
69
81
|
return;
|
|
@@ -238,7 +250,7 @@ function App() {
|
|
|
238
250
|
: claudeLastTailAt ?? codexLastTailAt;
|
|
239
251
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, codexDetected: codexDetected, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [claudeStats
|
|
240
252
|
? _jsx(SessionStatusBar, { stats: claudeStats, ratePerSec: claudeRate, now: now, series: claudeSeries })
|
|
241
|
-
: !codexStats && _jsx(SessionStatusBar, { stats: null, ratePerSec: 0, now: now, series: claudeSeries }), codexStats && (_jsx(Box, { marginTop: claudeStats ? 1 : 0, children: _jsx(SessionStatusBar, { stats: codexStats, ratePerSec: codexRate, now: now, series: codexSeries }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: primaryStats, series: primarySeries, ratePerSec: primaryRate })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: claudePath, now: now }))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "magenta", children: "q" }), " quit \u00B7", ' ', _jsx(Text, { color: "magenta", children: "\u2190\u2192" }), " move \u00B7", ' ', _jsx(Text, { color: "magenta", children: "enter" }), " open/close \u00B7", ' ', _jsx(Text, { color: "magenta", children: "1\u20134" }), " jump \u00B7 pricing is"
|
|
253
|
+
: !codexStats && _jsx(SessionStatusBar, { stats: null, ratePerSec: 0, now: now, series: claudeSeries }), codexStats && (_jsx(Box, { marginTop: claudeStats ? 1 : 0, children: _jsx(SessionStatusBar, { stats: codexStats, ratePerSec: codexRate, now: now, series: codexSeries }) }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: primaryStats, series: primarySeries, ratePerSec: primaryRate })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now, cursor: Math.min(sessionCursor, Math.max(0, todaySessions.length - 1)) }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: claudePath, now: now }))] })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "magenta", children: "q" }), " quit \u00B7", ' ', _jsx(Text, { color: "magenta", children: "\u2190\u2192" }), " move \u00B7", ' ', _jsx(Text, { color: "magenta", children: "enter" }), " open/close \u00B7", ' ', _jsx(Text, { color: "magenta", children: "1\u20134" }), " jump", openTab === 3 && (_jsxs(Text, { dimColor: true, children: [' · ', _jsx(Text, { color: "magenta", children: "\u2191\u2193" }), " select session"] })), _jsx(Text, { dimColor: true, children: " \u00B7 pricing is " }), _jsx(Text, { italic: true, dimColor: true, children: "API-equivalent" }), _jsx(Text, { dimColor: true, children: ", not your real bill on a subscription" })] }) })] }));
|
|
242
254
|
}
|
|
243
255
|
// ── Header ───────────────────────────────────────────────────────────────────
|
|
244
256
|
function Header({ auth, codexDetected, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
|
|
@@ -327,7 +339,7 @@ function BarRow({ label, value, max, total, color, cost, }) {
|
|
|
327
339
|
}
|
|
328
340
|
// ── History panel ────────────────────────────────────────────────────────────
|
|
329
341
|
function HistoryPanel({ history }) {
|
|
330
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ['Usage history', _jsx(Text, { bold: false, color: "blue", children: " \u00B7 refreshes every 60s" })] }), !history ? (_jsx(Text, { dimColor: true, children: "Scanning\u2026" })) : history.scannedFiles === 0 ? (_jsx(Text, { dimColor: true, children: "No transcripts found." })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(DualBarChart, { days: history.last7Days, now: history.generatedAt }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(HistoryRow, { label: "", today: "Today", d7: "7d", d30: "30d", dim: true }), _jsx(HistoryRow, { label: "Tokens ", today: fmtNumber(bucketTokens(history.today)), d7: fmtNumber(bucketTokens(history.d7)), d30: fmtNumber(bucketTokens(history.d30)) }), _jsx(HistoryRow, { label: "Cost~ ", today: fmtCost(bucketCostUSD(history.today)), d7: fmtCost(bucketCostUSD(history.d7)), d30: fmtCost(bucketCostUSD(history.d30)) }), _jsx(HistoryRow, { label: "Sessions", today: String(history.today.sessions.size), d7: String(history.d7.sessions.size), d30: String(history.d30.sessions.size) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [`scanned ${history.scannedFiles} transcripts`, history.oldestMtimeMs !== null &&
|
|
342
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ['Usage history', _jsx(Text, { bold: false, color: "blue", children: " \u00B7 refreshes every 60s" })] }), !history ? (_jsx(Text, { dimColor: true, children: "Scanning\u2026" })) : history.scannedFiles === 0 ? (_jsx(Text, { dimColor: true, children: "No transcripts found." })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(DualBarChart, { days: history.last7Days, now: history.generatedAt }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HistoryRow, { label: "", today: "Today", d7: "7d", d30: "30d", dim: true }), _jsx(HistoryRow, { label: "Tokens ", today: fmtNumber(bucketTokens(history.today)), d7: fmtNumber(bucketTokens(history.d7)), d30: fmtNumber(bucketTokens(history.d30)) }), _jsx(HistoryRow, { label: "Cost~ ", today: fmtCost(bucketCostUSD(history.today)), d7: fmtCost(bucketCostUSD(history.d7)), d30: fmtCost(bucketCostUSD(history.d30)) }), _jsx(HistoryRow, { label: "Sessions", today: String(history.today.sessions.size), d7: String(history.d7.sessions.size), d30: String(history.d30.sessions.size) })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [`scanned ${history.scannedFiles} transcripts`, history.oldestMtimeMs !== null &&
|
|
331
343
|
` · data since ${fmtDate(history.oldestMtimeMs)} (${daysAgo(history.oldestMtimeMs, history.generatedAt)} days)`] }) })] }))] }));
|
|
332
344
|
}
|
|
333
345
|
// ── Dual bar chart ────────────────────────────────────────────────────────────
|
|
@@ -349,7 +361,11 @@ function DualBarChart({ days, now }) {
|
|
|
349
361
|
const maxTokens = Math.max(1, ...days.flatMap((d) => [claudeTokens(d.byModel), codexTokens(d.byModel)]));
|
|
350
362
|
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
351
363
|
const todayStart = (() => { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime(); })();
|
|
352
|
-
|
|
364
|
+
const fmtDay = (ms) => {
|
|
365
|
+
const d = new Date(ms);
|
|
366
|
+
return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')}`;
|
|
367
|
+
};
|
|
368
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["last 7 days \u00B7 ", _jsx(Text, { color: "cyan", children: "\u2588" }), " claude ", _jsx(Text, { color: "magenta", children: "\u2588" }), " codex"] }), days.map((day) => {
|
|
353
369
|
const ct = claudeTokens(day.byModel);
|
|
354
370
|
const cxt = codexTokens(day.byModel);
|
|
355
371
|
const cc = claudeCost(day.byModel);
|
|
@@ -357,9 +373,16 @@ function DualBarChart({ days, now }) {
|
|
|
357
373
|
.filter(([m]) => m === 'codex')
|
|
358
374
|
.reduce((s, [m, u]) => s + (estimateCostUSD(m, u) ?? 0), 0);
|
|
359
375
|
const isToday = day.dayStart === todayStart;
|
|
360
|
-
const
|
|
376
|
+
const dayName = isToday ? 'today' : DAY_LABELS[new Date(day.dayStart).getDay()];
|
|
377
|
+
const label = `${dayName.padEnd(5)} ${fmtDay(day.dayStart)}`;
|
|
361
378
|
const hasData = ct > 0 || cxt > 0;
|
|
362
|
-
|
|
379
|
+
if (!hasData) {
|
|
380
|
+
return (_jsxs(Text, { dimColor: true, children: [label, " \u2500"] }, day.dayStart));
|
|
381
|
+
}
|
|
382
|
+
const labelPad = ' '.repeat(label.length + 2);
|
|
383
|
+
return (_jsxs(Box, { flexDirection: "column", children: [ct > 0 && (_jsxs(Text, { children: [_jsxs(Text, { bold: isToday, color: isToday ? 'white' : undefined, children: [label, " "] }), _jsx(Text, { dimColor: true, children: "claude " }), _jsx(Text, { color: "cyan", children: solidBar(ct / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: fmtNumber(ct).padStart(7) }), cc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cc)}` })] })), cxt > 0 && (_jsxs(Text, { children: [ct > 0
|
|
384
|
+
? _jsx(Text, { children: labelPad })
|
|
385
|
+
: _jsxs(Text, { bold: isToday, color: isToday ? 'white' : undefined, children: [label, " "] }), _jsx(Text, { dimColor: true, children: "codex " }), _jsx(Text, { color: "magenta", children: solidBar(cxt / maxTokens, CHART_BAR_WIDTH) }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: fmtNumber(cxt).padStart(7) }), cxc > 0 && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cxc)}` })] }))] }, day.dayStart));
|
|
363
386
|
})] }));
|
|
364
387
|
}
|
|
365
388
|
function solidBar(ratio, width) {
|
|
@@ -388,8 +411,9 @@ function fmtTopModel(b) {
|
|
|
388
411
|
return m ? shortModel(m) : '—';
|
|
389
412
|
}
|
|
390
413
|
// ── Today's sessions panel ───────────────────────────────────────────────────
|
|
391
|
-
function TodaySessionsPanel({ sessions, now }) {
|
|
392
|
-
return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ["Today's sessions", _jsx(Text, { bold: false, color: "blue", children: ` · ${sessions.length} ${plural(sessions.length, 'session', 'sessions')} today` })] }), sessions.map((s) => {
|
|
414
|
+
function TodaySessionsPanel({ sessions, now, cursor, }) {
|
|
415
|
+
return (_jsxs(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1, flexDirection: "column", width: "100%", children: [_jsxs(Text, { bold: true, color: "blue", children: ["Today's sessions", _jsx(Text, { bold: false, color: "blue", children: ` · ${sessions.length} ${plural(sessions.length, 'session', 'sessions')} today` })] }), sessions.map((s, idx) => {
|
|
416
|
+
const isSelected = idx === cursor;
|
|
393
417
|
const tokens = Object.values(s.byModel).reduce((acc, u) => acc + totalTokens(u), 0);
|
|
394
418
|
const cost = Object.entries(s.byModel).reduce((acc, [m, u]) => {
|
|
395
419
|
const c = estimateCostUSD(m, u);
|
|
@@ -403,9 +427,35 @@ function TodaySessionsPanel({ sessions, now }) {
|
|
|
403
427
|
const duration = s.startedAt !== null && s.endedAt !== null
|
|
404
428
|
? fmtDuration(s.endedAt - s.startedAt)
|
|
405
429
|
: null;
|
|
406
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: s.isActive ? 'green' : 'gray', children: s.isActive ? '▶ ' : ' ' }), _jsx(Text, { bold: s.isActive, children: s.startedAt ? fmtTime(s.startedAt) : '??:??' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { wrap: "truncate-middle", dimColor: !s.isActive, children: OPTS.reveal ? (s.cwd ?? '—') : displayCwd(s.cwd) }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: topModel ? shortModel(topModel) : '—' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { children: fmtNumber(tokens) }), cost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cost)}` }), duration && _jsx(Text, { dimColor: true, children: ` ${duration}` }), s.isActive && _jsx(Text, { color: "green", children: " active" })] }, s.sessionId));
|
|
430
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { color: isSelected ? 'cyan' : s.isActive ? 'green' : 'gray', children: isSelected ? '› ' : s.isActive ? '▶ ' : ' ' }), _jsx(Text, { bold: isSelected || s.isActive, color: isSelected ? 'cyan' : undefined, children: s.startedAt ? fmtTime(s.startedAt) : '??:??' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { wrap: "truncate-middle", dimColor: !isSelected && !s.isActive, color: isSelected ? 'cyan' : undefined, children: OPTS.reveal ? (s.cwd ?? '—') : displayCwd(s.cwd) }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: topModel ? shortModel(topModel) : '—' }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { bold: isSelected, children: fmtNumber(tokens) }), cost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cost)}` }), duration && _jsx(Text, { dimColor: true, children: ` ${duration}` }), s.isActive && _jsx(Text, { color: "green", children: " active" })] }), isSelected && _jsx(SessionDetail, { session: s })] }, s.sessionId));
|
|
407
431
|
})] }));
|
|
408
432
|
}
|
|
433
|
+
function SessionDetail({ session: s }) {
|
|
434
|
+
const DETAIL_BAR = 16;
|
|
435
|
+
// Aggregate all models into one Usage for the bar chart
|
|
436
|
+
const totals = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
|
|
437
|
+
for (const u of Object.values(s.byModel)) {
|
|
438
|
+
totals.input_tokens += u.input_tokens;
|
|
439
|
+
totals.output_tokens += u.output_tokens;
|
|
440
|
+
totals.cache_creation_input_tokens += u.cache_creation_input_tokens;
|
|
441
|
+
totals.cache_read_input_tokens += u.cache_read_input_tokens;
|
|
442
|
+
}
|
|
443
|
+
const total = totalTokens(totals);
|
|
444
|
+
const max = Math.max(1, totals.input_tokens, totals.output_tokens, totals.cache_creation_input_tokens, totals.cache_read_input_tokens);
|
|
445
|
+
const allModels = Object.keys(s.byModel);
|
|
446
|
+
const primaryModel = allModels.reduce((best, m) => best === null || totalTokens(s.byModel[m]) > totalTokens(s.byModel[best]) ? m : best, null) ?? '';
|
|
447
|
+
const totalCost = Object.entries(s.byModel).reduce((acc, [m, u]) => {
|
|
448
|
+
const c = estimateCostUSD(m, u);
|
|
449
|
+
if (c === null)
|
|
450
|
+
return acc;
|
|
451
|
+
return (acc ?? 0) + c;
|
|
452
|
+
}, null);
|
|
453
|
+
return (_jsx(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Input " }), _jsx(Text, { color: "cyan", children: bar(totals.input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'input', totals.input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Output " }), _jsx(Text, { color: "green", children: bar(totals.output_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.output_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.output_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'output', totals.output_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }), totals.cache_creation_input_tokens > 0 && (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "C.write " }), _jsx(Text, { color: "yellow", children: bar(totals.cache_creation_input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.cache_creation_input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.cache_creation_input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'cacheWrite', totals.cache_creation_input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] })), totals.cache_read_input_tokens > 0 && (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "C.read " }), _jsx(Text, { color: "magenta", children: bar(totals.cache_read_input_tokens / max, DETAIL_BAR) }), _jsxs(Text, { children: [" ", fmtNumber(totals.cache_read_input_tokens).padStart(7)] }), _jsx(Text, { dimColor: true, children: ` ${total > 0 ? ((totals.cache_read_input_tokens / total) * 100).toFixed(1).padStart(5) : ' 0.0'}%` }), (() => { const c = categoryCostUSD(primaryModel, 'cacheRead', totals.cache_read_input_tokens); return c !== null ? _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(c)}` }) : null; })()] }))] }), _jsx(Text, { dimColor: true, children: '─'.repeat(DETAIL_BAR + 32) }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: 'Total ' }), _jsx(Text, { bold: true, children: fmtNumber(total).padStart(DETAIL_BAR + 2 + 7) }), totalCost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(totalCost)}` })] }), allModels.length > 1 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "by model" }), allModels.map((m) => {
|
|
454
|
+
const mu = s.byModel[m];
|
|
455
|
+
const mc = estimateCostUSD(m, mu);
|
|
456
|
+
return (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: [" ", shortModel(m)] }), _jsx(Text, { dimColor: true, children: " \u03A3 " }), _jsx(Text, { children: fmtNumber(totalTokens(mu)) }), mc !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(mc)}` })] }, m));
|
|
457
|
+
})] }))] }) }));
|
|
458
|
+
}
|
|
409
459
|
function fmtDuration(ms) {
|
|
410
460
|
const totalSec = Math.floor(ms / 1000);
|
|
411
461
|
const h = Math.floor(totalSec / 3600);
|