claude-code-hud 0.3.12 → 0.3.14
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/package.json +1 -1
- package/scripts/lib/token-reader.mjs +8 -6
- package/tui/hud.tsx +55 -49
package/package.json
CHANGED
|
@@ -75,7 +75,7 @@ function findLatestSession(cwd) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/** Collect all JSONL lines for the given cwd (or all projects if no cwd) */
|
|
78
|
-
function readAllLines(cwd) {
|
|
78
|
+
async function readAllLines(cwd) {
|
|
79
79
|
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
80
80
|
if (!fs.existsSync(projectsDir)) return [];
|
|
81
81
|
const targetDir = cwd ? cwdToProjectDir(cwd) : null;
|
|
@@ -88,7 +88,8 @@ function readAllLines(cwd) {
|
|
|
88
88
|
const fullPath = path.join(projDir, file);
|
|
89
89
|
const fileMtime = fs.statSync(fullPath).mtimeMs;
|
|
90
90
|
try {
|
|
91
|
-
const
|
|
91
|
+
const raw = await fs.promises.readFile(fullPath, 'utf8');
|
|
92
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
92
93
|
for (const line of lines) {
|
|
93
94
|
try {
|
|
94
95
|
const obj = JSON.parse(line);
|
|
@@ -103,8 +104,8 @@ function readAllLines(cwd) {
|
|
|
103
104
|
return result;
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
export function readTokenHistory(cwd) {
|
|
107
|
-
const allLines = readAllLines(cwd);
|
|
107
|
+
export async function readTokenHistory(cwd) {
|
|
108
|
+
const allLines = await readAllLines(cwd);
|
|
108
109
|
const now = Date.now();
|
|
109
110
|
const h5 = now - 5 * 60 * 60 * 1000;
|
|
110
111
|
const wk = now - 7 * 24 * 60 * 60 * 1000;
|
|
@@ -155,7 +156,7 @@ export function readTokenHistory(cwd) {
|
|
|
155
156
|
return { last5h: acc5h, lastWeek: accWk, today: accToday, hourlyBuckets: buckets };
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
export function readTokenUsage(cwd) {
|
|
159
|
+
export async function readTokenUsage(cwd) {
|
|
159
160
|
const sessionFile = findLatestSession(cwd);
|
|
160
161
|
if (!sessionFile) {
|
|
161
162
|
return empty();
|
|
@@ -170,7 +171,8 @@ export function readTokenUsage(cwd) {
|
|
|
170
171
|
// For context window: use the LAST turn's snapshot (what's in context right now)
|
|
171
172
|
let lastUsage = null;
|
|
172
173
|
|
|
173
|
-
const
|
|
174
|
+
const raw = await fs.promises.readFile(sessionFile, 'utf8');
|
|
175
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
174
176
|
for (const line of lines) {
|
|
175
177
|
try {
|
|
176
178
|
const obj = JSON.parse(line);
|
package/tui/hud.tsx
CHANGED
|
@@ -9,7 +9,9 @@ import { fileURLToPath } from 'url';
|
|
|
9
9
|
import { dirname, join, basename } from 'path';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import os from 'os';
|
|
12
|
-
import {
|
|
12
|
+
import { exec as execCb } from 'child_process';
|
|
13
|
+
import { promisify } from 'util';
|
|
14
|
+
const execAsync = promisify(execCb);
|
|
13
15
|
|
|
14
16
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
15
17
|
const { readTokenUsage, readTokenHistory } = await import(join(__dir, '../scripts/lib/token-reader.mjs'));
|
|
@@ -197,11 +199,11 @@ function flattenTree(node: DirNode, depth: number, expanded: Record<string, bool
|
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
// ── Branch helper ───────────────────────────────────────────────────────────
|
|
200
|
-
function getBranches(cwd: string): string[] {
|
|
202
|
+
async function getBranches(cwd: string): Promise<string[]> {
|
|
201
203
|
try {
|
|
202
|
-
const
|
|
203
|
-
return
|
|
204
|
-
.map(b => b.replace(/^\*?\s+/, '').trim())
|
|
204
|
+
const { stdout } = await execAsync('git branch', { cwd });
|
|
205
|
+
return stdout.split('\n')
|
|
206
|
+
.map((b: string) => b.replace(/^\*?\s+/, '').trim())
|
|
205
207
|
.filter(Boolean);
|
|
206
208
|
} catch {
|
|
207
209
|
return [];
|
|
@@ -219,9 +221,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
|
219
221
|
if (!fs.existsSync(projectsDir)) return [];
|
|
220
222
|
|
|
221
223
|
const targetDirName = cwd.replace(/\//g, '-');
|
|
224
|
+
const allFiles: string[] = [];
|
|
222
225
|
|
|
223
|
-
let latestFile: string | null = null;
|
|
224
|
-
let latestMtime = 0;
|
|
225
226
|
try {
|
|
226
227
|
for (const projectHash of fs.readdirSync(projectsDir)) {
|
|
227
228
|
if (projectHash !== targetDirName) continue;
|
|
@@ -229,44 +230,43 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
|
229
230
|
if (!fs.statSync(sessionDir).isDirectory()) continue;
|
|
230
231
|
for (const file of fs.readdirSync(sessionDir)) {
|
|
231
232
|
if (!file.endsWith('.jsonl')) continue;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const mtime = fs.statSync(filePath).mtimeMs;
|
|
235
|
-
if (mtime > latestMtime) { latestMtime = mtime; latestFile = filePath; }
|
|
236
|
-
} catch {}
|
|
233
|
+
allFiles.push(join(sessionDir, file));
|
|
237
234
|
}
|
|
238
235
|
}
|
|
239
236
|
} catch {}
|
|
240
237
|
|
|
241
|
-
if (
|
|
238
|
+
if (allFiles.length === 0) return [];
|
|
242
239
|
|
|
243
|
-
const
|
|
244
|
-
const entries: TimelineEntry[] = [];
|
|
240
|
+
const entries: (TimelineEntry & { ts: number })[] = [];
|
|
245
241
|
|
|
246
|
-
for (const
|
|
242
|
+
for (const filePath of allFiles) {
|
|
247
243
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
244
|
+
const lines = (await fs.promises.readFile(filePath, 'utf-8')).split('\n').filter(Boolean);
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
try {
|
|
247
|
+
const obj = JSON.parse(line);
|
|
248
|
+
if (obj.type !== 'user') continue;
|
|
249
|
+
const content = obj.message?.content;
|
|
250
|
+
if (Array.isArray(content) && content.some((b: any) => b.type === 'tool_result')) continue;
|
|
251
|
+
const textBlock = Array.isArray(content)
|
|
252
|
+
? content.find((b: any) => b.type === 'text')
|
|
253
|
+
: null;
|
|
254
|
+
const text: string = textBlock?.text ?? (typeof content === 'string' ? content : '');
|
|
255
|
+
if (!text.trim()) continue;
|
|
256
|
+
|
|
257
|
+
const tsStr: string = obj.timestamp ?? '';
|
|
258
|
+
const tsNum = tsStr ? new Date(tsStr).getTime() : 0;
|
|
259
|
+
const time = tsStr ? new Date(tsStr).toTimeString().slice(0, 5) : '';
|
|
260
|
+
|
|
261
|
+
entries.push({ ts: tsNum, time, text: text.replace(/\n/g, ' ').slice(0, 80) });
|
|
262
|
+
} catch {}
|
|
263
263
|
}
|
|
264
|
-
|
|
265
|
-
entries.push({ time, text: text.replace(/\n/g, ' ').slice(0, 80) });
|
|
266
264
|
} catch {}
|
|
267
265
|
}
|
|
268
266
|
|
|
269
|
-
return
|
|
267
|
+
// Sort all sessions by time, return last 50
|
|
268
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
269
|
+
return entries.slice(-50).map(({ time, text }) => ({ time, text }));
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
// ── UI Components ──────────────────────────────────────────────────────────
|
|
@@ -751,8 +751,8 @@ function App() {
|
|
|
751
751
|
const cwd = process.env.CLAUDE_PROJECT_ROOT || process.cwd();
|
|
752
752
|
const C = makeTheme(accent);
|
|
753
753
|
|
|
754
|
-
const [usage, setUsage] = useState<any>(
|
|
755
|
-
const [history, setHistory] = useState<any>(
|
|
754
|
+
const [usage, setUsage] = useState<any>({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTokens: 0, contextWindow: 200000, model: 'claude-sonnet-4', cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } });
|
|
755
|
+
const [history, setHistory] = useState<any>({ last5h: null, lastWeek: null, today: null, hourlyBuckets: Array(12).fill(0) });
|
|
756
756
|
const [git, setGit] = useState<any>({ isRepo: false, branch: 'loading…', modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 });
|
|
757
757
|
const [project, setProject] = useState<ProjectInfo | null>(null);
|
|
758
758
|
const [rateLimits, setRateLimits] = useState<any>(getUsageSync());
|
|
@@ -791,8 +791,8 @@ function App() {
|
|
|
791
791
|
const [currentActivity, setCurrentActivity] = useState<string>('');
|
|
792
792
|
|
|
793
793
|
const refresh = useCallback(() => {
|
|
794
|
-
|
|
795
|
-
|
|
794
|
+
readTokenUsage(cwd).then(setUsage).catch(() => {});
|
|
795
|
+
readTokenHistory(cwd).then(setHistory).catch(() => {});
|
|
796
796
|
setUpdatedAt(Date.now());
|
|
797
797
|
readGitInfo(cwd).then(setGit).catch(() => {});
|
|
798
798
|
getUsage().then(setRateLimits).catch(() => {});
|
|
@@ -806,6 +806,9 @@ function App() {
|
|
|
806
806
|
}, [cwd]);
|
|
807
807
|
|
|
808
808
|
useEffect(() => {
|
|
809
|
+
// Initial token data loads (async)
|
|
810
|
+
readTokenUsage(cwd).then(setUsage).catch(() => {});
|
|
811
|
+
readTokenHistory(cwd).then(setHistory).catch(() => {});
|
|
809
812
|
// Scan project once
|
|
810
813
|
// Quick shallow scan first → show UI immediately
|
|
811
814
|
scanProject(cwd, 2).then(p => { setProject(p); setLoading(false); })
|
|
@@ -845,7 +848,7 @@ function App() {
|
|
|
845
848
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
846
849
|
watcher.on('change', () => {
|
|
847
850
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
848
|
-
debounceTimer = setTimeout(refresh,
|
|
851
|
+
debounceTimer = setTimeout(refresh, 2000);
|
|
849
852
|
});
|
|
850
853
|
});
|
|
851
854
|
}
|
|
@@ -883,11 +886,13 @@ function App() {
|
|
|
883
886
|
if (key.return) {
|
|
884
887
|
const selected = branchList[branchCursor];
|
|
885
888
|
if (selected && selected !== git.branch) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
889
|
+
execAsync(`git checkout ${selected}`, { cwd })
|
|
890
|
+
.then(() => {
|
|
891
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
892
|
+
readGitInfo(cwd).then(setGit).catch(() => {});
|
|
893
|
+
refresh();
|
|
894
|
+
})
|
|
895
|
+
.catch(() => {});
|
|
891
896
|
}
|
|
892
897
|
setBranchMode(false);
|
|
893
898
|
return;
|
|
@@ -901,11 +906,12 @@ function App() {
|
|
|
901
906
|
|
|
902
907
|
// b (or Korean ㅠ) = open branch switcher in GIT tab
|
|
903
908
|
if ((input === 'b' || input === 'ㅠ') && tab === 2) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
+
getBranches(cwd).then(branches => {
|
|
910
|
+
setBranchList(branches);
|
|
911
|
+
const idx = branches.findIndex((b: string) => b === git.branch);
|
|
912
|
+
setBranchCursor(idx >= 0 ? idx : 0);
|
|
913
|
+
setBranchMode(true);
|
|
914
|
+
});
|
|
909
915
|
return;
|
|
910
916
|
}
|
|
911
917
|
|