tokens-metric 0.4.8 → 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.
Files changed (2) hide show
  1. package/dist/tui/index.js +44 -5
  2. 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
- // Arrow keys move the cursor
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", ' ', _jsx(Text, { italic: true, children: "API-equivalent" }), ", not your real bill on a subscription"] }) })] }));
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, }) {
@@ -399,8 +411,9 @@ function fmtTopModel(b) {
399
411
  return m ? shortModel(m) : '—';
400
412
  }
401
413
  // ── Today's sessions panel ───────────────────────────────────────────────────
402
- function TodaySessionsPanel({ sessions, now }) {
403
- 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;
404
417
  const tokens = Object.values(s.byModel).reduce((acc, u) => acc + totalTokens(u), 0);
405
418
  const cost = Object.entries(s.byModel).reduce((acc, [m, u]) => {
406
419
  const c = estimateCostUSD(m, u);
@@ -414,9 +427,35 @@ function TodaySessionsPanel({ sessions, now }) {
414
427
  const duration = s.startedAt !== null && s.endedAt !== null
415
428
  ? fmtDuration(s.endedAt - s.startedAt)
416
429
  : null;
417
- 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));
418
431
  })] }));
419
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
+ }
420
459
  function fmtDuration(ms) {
421
460
  const totalSec = Math.floor(ms / 1000);
422
461
  const h = Math.floor(totalSec / 3600);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {