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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hud",
3
- "version": "0.3.12",
3
+ "version": "0.3.14",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 lines = fs.readFileSync(fullPath, 'utf8').split('\n').filter(Boolean);
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 lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
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 { execSync } from 'child_process';
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 out = execSync('git branch', { cwd, encoding: 'utf8' });
203
- return out.split('\n')
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
- const filePath = join(sessionDir, file);
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 (!latestFile) return [];
238
+ if (allFiles.length === 0) return [];
242
239
 
243
- const lines = fs.readFileSync(latestFile, 'utf-8').split('\n').filter(Boolean);
244
- const entries: TimelineEntry[] = [];
240
+ const entries: (TimelineEntry & { ts: number })[] = [];
245
241
 
246
- for (const line of lines) {
242
+ for (const filePath of allFiles) {
247
243
  try {
248
- const obj = JSON.parse(line);
249
- if (obj.type !== 'user') continue;
250
- const content = obj.message?.content;
251
- // Skip tool_result messages (not direct user prompts)
252
- if (Array.isArray(content) && content.some((b: any) => b.type === 'tool_result')) continue;
253
- const textBlock = Array.isArray(content)
254
- ? content.find((b: any) => b.type === 'text')
255
- : null;
256
- const text: string = textBlock?.text ?? (typeof content === 'string' ? content : '');
257
- if (!text.trim()) continue;
258
-
259
- const ts: string = obj.timestamp ?? '';
260
- let time = '';
261
- if (ts) {
262
- try { time = new Date(ts).toTimeString().slice(0, 5); } catch {}
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 entries.slice(-30);
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>(readTokenUsage(cwd));
755
- const [history, setHistory] = useState<any>(readTokenHistory(cwd));
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
- setUsage(readTokenUsage(cwd));
795
- setHistory(readTokenHistory(cwd));
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, 800);
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
- try {
887
- execSync(`git checkout ${selected}`, { cwd });
888
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
889
- refresh();
890
- } catch {}
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
- const branches = getBranches(cwd);
905
- setBranchList(branches);
906
- const idx = branches.findIndex(b => b === git.branch);
907
- setBranchCursor(idx >= 0 ? idx : 0);
908
- setBranchMode(true);
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