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.
- package/dist/tui/index.js +86 -24
- 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 {
|
|
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
|
-
|
|
33
|
-
const [
|
|
34
|
-
const [
|
|
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
|
-
|
|
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
|
-
|
|
141
|
+
setClaudePath(path);
|
|
133
142
|
handle = await tailTranscript(path);
|
|
134
143
|
if (cancelled) {
|
|
135
144
|
handle.stop();
|
|
136
145
|
return;
|
|
137
146
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
151
|
+
setClaudeLastTailAt(Date.now());
|
|
146
152
|
const tot = totalTokens(s.totals);
|
|
147
|
-
const delta = Math.max(0, tot -
|
|
153
|
+
const delta = Math.max(0, tot - claudeLastTotalRef.current);
|
|
148
154
|
if (delta > 0) {
|
|
149
|
-
|
|
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
|
-
|
|
156
|
-
|
|
161
|
+
claudeLastTotalRef.current = tot;
|
|
162
|
+
setClaudeStats({ ...s });
|
|
157
163
|
});
|
|
158
164
|
}
|
|
159
165
|
async function rescan() {
|
|
160
|
-
const active =
|
|
161
|
-
if (active && active.path !==
|
|
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
|
|
177
|
-
const
|
|
178
|
-
|
|
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.
|
|
4
|
-
"description": "Real-time token usage meter for Claude Code
|
|
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",
|