claude-code-hud 0.3.11 → 0.3.13

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.11",
3
+ "version": "0.3.13",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,39 +1,44 @@
1
1
  /**
2
- * Git status via child_process. No external deps.
2
+ * Git status via child_process exec (async — non-blocking).
3
3
  */
4
- import { execSync } from 'child_process';
4
+ import { exec } from 'child_process';
5
+ import { promisify } from 'util';
5
6
 
6
- function run(cmd, cwd) {
7
+ const execAsync = promisify(exec);
8
+
9
+ async function run(cmd, cwd) {
7
10
  try {
8
- return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
11
+ const { stdout } = await execAsync(cmd, { cwd, timeout: 3000 });
12
+ return stdout.trim();
9
13
  } catch {
10
14
  return '';
11
15
  }
12
16
  }
13
17
 
14
- export function readGitInfo(cwd = process.cwd()) {
15
- const branch = run('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown';
18
+ export async function readGitInfo(cwd = process.cwd()) {
19
+ const branch = await run('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown';
16
20
  if (branch === 'unknown' || branch === 'HEAD') {
17
21
  return { isRepo: false, branch: 'unknown', ahead: 0, behind: 0, modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 };
18
22
  }
19
23
 
20
- // ahead/behind
21
- const aheadBehind = run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo "0\t0"', cwd);
24
+ const [aheadBehind, statusOut, logOut, numstatOut] = await Promise.all([
25
+ run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo "0\t0"', cwd),
26
+ run('git status --porcelain', cwd),
27
+ run('git log --oneline -5 --format="%h|%s|%cr"', cwd),
28
+ run('git diff --numstat HEAD 2>/dev/null', cwd),
29
+ ]);
30
+
22
31
  const [behind = 0, ahead = 0] = aheadBehind.split('\t').map(Number);
23
32
 
24
- // status
25
- const statusOut = run('git status --porcelain', cwd);
26
33
  const modified = [], added = [], deleted = [];
27
34
  for (const line of statusOut.split('\n').filter(Boolean)) {
28
35
  const st = line.slice(0, 2).trim();
29
36
  const file = line.slice(2).trimStart();
30
37
  if (st === 'M' || st === 'MM' || st === 'AM') modified.push(file);
31
- else if (st === 'A' || st === '??' ) added.push(file);
38
+ else if (st === 'A' || st === '??') added.push(file);
32
39
  else if (st === 'D') deleted.push(file);
33
40
  }
34
41
 
35
- // recent commits
36
- const logOut = run('git log --oneline -5 --format="%h|%s|%cr"', cwd);
37
42
  const recentCommits = logOut.split('\n').filter(Boolean).map(l => {
38
43
  const [hash, ...rest] = l.split('|');
39
44
  const time = rest.pop();
@@ -41,8 +46,6 @@ export function readGitInfo(cwd = process.cwd()) {
41
46
  return { hash, msg, time };
42
47
  });
43
48
 
44
- // diff stats: actual +/- line counts per file
45
- const numstatOut = run('git diff --numstat HEAD 2>/dev/null', cwd);
46
49
  const diffStats = {};
47
50
  for (const line of numstatOut.split('\n').filter(Boolean)) {
48
51
  const [addStr, delStr, ...fileParts] = line.split('\t');
package/tui/hud.tsx CHANGED
@@ -219,9 +219,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
219
219
  if (!fs.existsSync(projectsDir)) return [];
220
220
 
221
221
  const targetDirName = cwd.replace(/\//g, '-');
222
+ const allFiles: string[] = [];
222
223
 
223
- let latestFile: string | null = null;
224
- let latestMtime = 0;
225
224
  try {
226
225
  for (const projectHash of fs.readdirSync(projectsDir)) {
227
226
  if (projectHash !== targetDirName) continue;
@@ -229,44 +228,43 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
229
228
  if (!fs.statSync(sessionDir).isDirectory()) continue;
230
229
  for (const file of fs.readdirSync(sessionDir)) {
231
230
  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 {}
231
+ allFiles.push(join(sessionDir, file));
237
232
  }
238
233
  }
239
234
  } catch {}
240
235
 
241
- if (!latestFile) return [];
236
+ if (allFiles.length === 0) return [];
242
237
 
243
- const lines = fs.readFileSync(latestFile, 'utf-8').split('\n').filter(Boolean);
244
- const entries: TimelineEntry[] = [];
238
+ const entries: (TimelineEntry & { ts: number })[] = [];
245
239
 
246
- for (const line of lines) {
240
+ for (const filePath of allFiles) {
247
241
  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 {}
242
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
243
+ for (const line of lines) {
244
+ try {
245
+ const obj = JSON.parse(line);
246
+ if (obj.type !== 'user') continue;
247
+ const content = obj.message?.content;
248
+ if (Array.isArray(content) && content.some((b: any) => b.type === 'tool_result')) continue;
249
+ const textBlock = Array.isArray(content)
250
+ ? content.find((b: any) => b.type === 'text')
251
+ : null;
252
+ const text: string = textBlock?.text ?? (typeof content === 'string' ? content : '');
253
+ if (!text.trim()) continue;
254
+
255
+ const tsStr: string = obj.timestamp ?? '';
256
+ const tsNum = tsStr ? new Date(tsStr).getTime() : 0;
257
+ const time = tsStr ? new Date(tsStr).toTimeString().slice(0, 5) : '';
258
+
259
+ entries.push({ ts: tsNum, time, text: text.replace(/\n/g, ' ').slice(0, 80) });
260
+ } catch {}
263
261
  }
264
-
265
- entries.push({ time, text: text.replace(/\n/g, ' ').slice(0, 80) });
266
262
  } catch {}
267
263
  }
268
264
 
269
- return entries.slice(-30);
265
+ // Sort all sessions by time, return last 50
266
+ entries.sort((a, b) => a.ts - b.ts);
267
+ return entries.slice(-50).map(({ time, text }) => ({ time, text }));
270
268
  }
271
269
 
272
270
  // ── UI Components ──────────────────────────────────────────────────────────
@@ -753,7 +751,7 @@ function App() {
753
751
 
754
752
  const [usage, setUsage] = useState<any>(readTokenUsage(cwd));
755
753
  const [history, setHistory] = useState<any>(readTokenHistory(cwd));
756
- const [git, setGit] = useState<any>(readGitInfo(cwd));
754
+ const [git, setGit] = useState<any>({ isRepo: false, branch: 'loading…', modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 });
757
755
  const [project, setProject] = useState<ProjectInfo | null>(null);
758
756
  const [rateLimits, setRateLimits] = useState<any>(getUsageSync());
759
757
 
@@ -793,8 +791,8 @@ function App() {
793
791
  const refresh = useCallback(() => {
794
792
  setUsage(readTokenUsage(cwd));
795
793
  setHistory(readTokenHistory(cwd));
796
- setGit(readGitInfo(cwd));
797
794
  setUpdatedAt(Date.now());
795
+ readGitInfo(cwd).then(setGit).catch(() => {});
798
796
  getUsage().then(setRateLimits).catch(() => {});
799
797
  readSessionTimeline(cwd).then(entries => {
800
798
  setTimeline(entries);
@@ -812,6 +810,8 @@ function App() {
812
810
  .catch(() => { setLoading(false); });
813
811
  // Full deep scan in background → update silently
814
812
  scanProject(cwd, 8).then(p => { setProject(p); }).catch(() => {});
813
+ // Initial git load (async)
814
+ readGitInfo(cwd).then(setGit).catch(() => {});
815
815
  // Initial API usage fetch
816
816
  getUsage().then(setRateLimits).catch(() => {});
817
817
  // Initial timeline load
@@ -843,7 +843,7 @@ function App() {
843
843
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
844
844
  watcher.on('change', () => {
845
845
  if (debounceTimer) clearTimeout(debounceTimer);
846
- debounceTimer = setTimeout(refresh, 800);
846
+ debounceTimer = setTimeout(refresh, 2000);
847
847
  });
848
848
  });
849
849
  }