tokens-metric 0.4.3 → 0.4.4

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 +86 -24
  2. package/package.json +2 -2
package/dist/tui/index.js CHANGED
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react';
4
4
  import { render, Box, Text, useApp, useInput } from 'ink';
5
5
  import { createInterface } from 'node:readline';
6
6
  import { spawnSync } from 'node:child_process';
7
- import { findMostRecentActiveTranscript, listTranscripts } from '../core/parser.js';
7
+ import { findActiveTranscript, findActiveCodexTranscript, listTranscripts } from '../core/parser.js';
8
8
  import { tailTranscript } from '../core/tailer.js';
9
9
  import { detectAuth } from '../core/detect.js';
10
10
  import { categoryCostUSD, contextWindowSize, estimateCostUSD, fmtNumber, fmtUSD } from '../core/format.js';
@@ -29,11 +29,19 @@ const BAR_WIDTH = 20;
29
29
  function App() {
30
30
  const { exit } = useApp();
31
31
  const [auth] = useState(() => detectAuth());
32
- const [stats, setStats] = useState(null);
33
- const [transcriptPath, setTranscriptPath] = useState(null);
34
- const [series, setSeries] = useState(() => Array(SPARK_WIDTH).fill(0));
32
+ // Claude source
33
+ const [claudeStats, setClaudeStats] = useState(null);
34
+ const [claudePath, setClaudePath] = useState(null);
35
+ const [claudeSeries, setClaudeSeries] = useState(() => Array(SPARK_WIDTH).fill(0));
36
+ const [claudeLastTailAt, setClaudeLastTailAt] = useState(null);
37
+ const claudeLastTotalRef = useRef(0);
38
+ // Codex source
39
+ const [codexStats, setCodexStats] = useState(null);
40
+ const [codexPath, setCodexPath] = useState(null);
41
+ const [codexSeries, setCodexSeries] = useState(() => Array(SPARK_WIDTH).fill(0));
42
+ const [codexLastTailAt, setCodexLastTailAt] = useState(null);
43
+ const codexLastTotalRef = useRef(0);
35
44
  const [now, setNow] = useState(Date.now());
36
- const [lastTailAt, setLastTailAt] = useState(null);
37
45
  const [history, setHistory] = useState(null);
38
46
  const [updateAvailable, setUpdateAvailable] = useState(null);
39
47
  // focusedTab: where the cursor sits (arrow navigation)
@@ -41,7 +49,6 @@ function App() {
41
49
  const [focusedTab, setFocusedTab] = useState(1);
42
50
  const [openTab, setOpenTab] = useState(null);
43
51
  const startedAtRef = useRef(Date.now());
44
- const lastTotalRef = useRef(0);
45
52
  // useInput requires raw mode (interactive TTY). Skip it when stdin is piped
46
53
  // or otherwise non-interactive, so `node dist/tui/index.js | cat` still
47
54
  // renders instead of crashing.
@@ -120,45 +127,88 @@ function App() {
120
127
  useEffect(() => {
121
128
  const t = setInterval(() => {
122
129
  setNow(Date.now());
123
- setSeries((s) => [...s.slice(1), 0]);
130
+ setClaudeSeries((s) => [...s.slice(1), 0]);
131
+ setCodexSeries((s) => [...s.slice(1), 0]);
124
132
  }, 1000);
125
133
  return () => clearInterval(t);
126
134
  }, []);
135
+ // Claude tailing
127
136
  useEffect(() => {
128
137
  let handle = null;
129
138
  let cancelled = false;
130
139
  async function attach(path) {
131
140
  handle?.stop().catch(() => undefined);
132
- setTranscriptPath(path);
141
+ setClaudePath(path);
133
142
  handle = await tailTranscript(path);
134
143
  if (cancelled) {
135
144
  handle.stop();
136
145
  return;
137
146
  }
138
- // Seed initial state — tailTranscript already drained the file once
139
- // before returning, but its first notify() fires before any listener
140
- // is attached, so we'd otherwise wait for the next appended line.
141
- lastTotalRef.current = totalTokens(handle.stats.totals);
142
- setLastTailAt(Date.now());
143
- setStats({ ...handle.stats });
147
+ claudeLastTotalRef.current = totalTokens(handle.stats.totals);
148
+ setClaudeLastTailAt(Date.now());
149
+ setClaudeStats({ ...handle.stats });
144
150
  handle.onUpdate((s) => {
145
- setLastTailAt(Date.now());
151
+ setClaudeLastTailAt(Date.now());
146
152
  const tot = totalTokens(s.totals);
147
- const delta = Math.max(0, tot - lastTotalRef.current);
153
+ const delta = Math.max(0, tot - claudeLastTotalRef.current);
148
154
  if (delta > 0) {
149
- setSeries((arr) => {
155
+ setClaudeSeries((arr) => {
150
156
  const next = arr.slice();
151
157
  next[next.length - 1] = (next[next.length - 1] ?? 0) + delta;
152
158
  return next;
153
159
  });
154
160
  }
155
- lastTotalRef.current = tot;
156
- setStats({ ...s });
161
+ claudeLastTotalRef.current = tot;
162
+ setClaudeStats({ ...s });
157
163
  });
158
164
  }
159
165
  async function rescan() {
160
- const active = findMostRecentActiveTranscript();
161
- if (active && active.path !== transcriptPath)
166
+ const active = findActiveTranscript();
167
+ if (active && active.path !== claudePath)
168
+ await attach(active.path);
169
+ }
170
+ rescan();
171
+ const interval = setInterval(rescan, RESCAN_MS);
172
+ return () => {
173
+ cancelled = true;
174
+ clearInterval(interval);
175
+ handle?.stop().catch(() => undefined);
176
+ };
177
+ // eslint-disable-next-line react-hooks/exhaustive-deps
178
+ }, []);
179
+ // Codex tailing
180
+ useEffect(() => {
181
+ let handle = null;
182
+ let cancelled = false;
183
+ async function attach(path) {
184
+ handle?.stop().catch(() => undefined);
185
+ setCodexPath(path);
186
+ handle = await tailTranscript(path);
187
+ if (cancelled) {
188
+ handle.stop();
189
+ return;
190
+ }
191
+ codexLastTotalRef.current = totalTokens(handle.stats.totals);
192
+ setCodexLastTailAt(Date.now());
193
+ setCodexStats({ ...handle.stats });
194
+ handle.onUpdate((s) => {
195
+ setCodexLastTailAt(Date.now());
196
+ const tot = totalTokens(s.totals);
197
+ const delta = Math.max(0, tot - codexLastTotalRef.current);
198
+ if (delta > 0) {
199
+ setCodexSeries((arr) => {
200
+ const next = arr.slice();
201
+ next[next.length - 1] = (next[next.length - 1] ?? 0) + delta;
202
+ return next;
203
+ });
204
+ }
205
+ codexLastTotalRef.current = tot;
206
+ setCodexStats({ ...s });
207
+ });
208
+ }
209
+ async function rescan() {
210
+ const active = findActiveCodexTranscript();
211
+ if (active && active.path !== codexPath)
162
212
  await attach(active.path);
163
213
  }
164
214
  rescan();
@@ -173,9 +223,21 @@ function App() {
173
223
  const allTranscripts = listTranscripts();
174
224
  const transcripts = allTranscripts.slice(0, 5);
175
225
  const today = countToday(allTranscripts, now);
176
- const ratePerSec = series.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
177
- const todaySessions = getTodaySessions(now, transcriptPath);
178
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsx(Box, { marginTop: 1, children: _jsx(SessionStatusBar, { stats: stats, ratePerSec: ratePerSec, now: now, series: series }) }), _jsx(Box, { marginTop: 1, children: _jsx(TabBar, { focusedTab: focusedTab, openTab: openTab }) }), openTab !== null && (_jsxs(Box, { marginTop: 1, children: [openTab === 1 && (_jsx(BreakdownPanel, { stats: stats, series: series, ratePerSec: ratePerSec })), openTab === 2 && _jsx(HistoryPanel, { history: history }), openTab === 3 && _jsx(TodaySessionsPanel, { sessions: todaySessions, now: now }), openTab === 4 && (_jsx(TranscriptsPanel, { transcripts: transcripts, activePath: transcriptPath, 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"] }) })] }));
226
+ const claudeRate = claudeSeries.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
227
+ const codexRate = codexSeries.reduce((a, b) => a + b, 0) / SPARK_WIDTH;
228
+ const todaySessions = getTodaySessions(now, claudePath);
229
+ // BreakdownPanel follows the most recently active source
230
+ const claudeAt = claudeStats?.lastEventAt ?? 0;
231
+ const codexAt = codexStats?.lastEventAt ?? 0;
232
+ const primaryStats = codexAt > claudeAt ? codexStats : claudeStats;
233
+ const primarySeries = codexAt > claudeAt ? codexSeries : claudeSeries;
234
+ const primaryRate = codexAt > claudeAt ? codexRate : claudeRate;
235
+ const lastTailAt = claudeLastTailAt && codexLastTailAt
236
+ ? Math.max(claudeLastTailAt, codexLastTailAt)
237
+ : claudeLastTailAt ?? codexLastTailAt;
238
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Header, { auth: auth, sessionsToday: today.sessions, projectsToday: today.projects, lastTailAt: lastTailAt, startedAt: startedAtRef.current, now: now, updateAvailable: updateAvailable }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [claudeStats
239
+ ? _jsx(SessionStatusBar, { stats: claudeStats, ratePerSec: claudeRate, now: now, series: claudeSeries })
240
+ : !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"] }) })] }));
179
241
  }
180
242
  // ── Header ───────────────────────────────────────────────────────────────────
181
243
  function Header({ auth, sessionsToday, projectsToday, lastTailAt, startedAt, now, updateAvailable, }) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.3",
4
- "description": "Real-time token usage meter for Claude Code \u2014 statusline + Ink TUI.",
3
+ "version": "0.4.4",
4
+ "description": "Real-time token usage meter for Claude Code statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tokens-metric": "dist/tui/index.js",