claude-code-hud 0.3.10 → 0.3.12

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.10",
3
+ "version": "0.3.12",
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
@@ -3,7 +3,7 @@
3
3
  * HUD Live — Ink TUI
4
4
  * Run: npm run hud (from hud-plugin root)
5
5
  */
6
- import React, { useState, useEffect, useCallback } from 'react';
6
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
7
7
  import { render, Box, Text, useStdout, useInput } from 'ink';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { dirname, join, basename } from 'path';
@@ -601,6 +601,13 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
601
601
 
602
602
  // ── Tab 3: GIT ─────────────────────────────────────────────────────────────
603
603
  function GitTab({ git, C, termWidth, branchMode, branchList, branchCursor }: any) {
604
+ if (!git.isRepo) return (
605
+ <Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1}>
606
+ <Text color={C.dimmer}>⚠ git repository not found in this directory</Text>
607
+ <Text color={C.dimmer}> cd into a git repo to see branch, diff, and commit info</Text>
608
+ </Box>
609
+ );
610
+
604
611
  const gitFiles = [
605
612
  ...(git.modified ?? []).map((f: string) => ({ status: 'MOD', path: f })),
606
613
  ...(git.added ?? []).map((f: string) => ({ status: 'ADD', path: f })),
@@ -746,7 +753,7 @@ function App() {
746
753
 
747
754
  const [usage, setUsage] = useState<any>(readTokenUsage(cwd));
748
755
  const [history, setHistory] = useState<any>(readTokenHistory(cwd));
749
- const [git, setGit] = useState<any>(readGitInfo(cwd));
756
+ const [git, setGit] = useState<any>({ isRepo: false, branch: 'loading…', modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 });
750
757
  const [project, setProject] = useState<ProjectInfo | null>(null);
751
758
  const [rateLimits, setRateLimits] = useState<any>(getUsageSync());
752
759
 
@@ -770,6 +777,9 @@ function App() {
770
777
  const [spinFrame, setSpinFrame] = useState(0);
771
778
  const SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
772
779
 
780
+ // q key debounce ref (require 2 presses within 600ms to quit)
781
+ const lastQRef = useRef(0);
782
+
773
783
  // Branch switcher state
774
784
  const [branchMode, setBranchMode] = useState(false);
775
785
  const [branchList, setBranchList] = useState<string[]>([]);
@@ -783,8 +793,8 @@ function App() {
783
793
  const refresh = useCallback(() => {
784
794
  setUsage(readTokenUsage(cwd));
785
795
  setHistory(readTokenHistory(cwd));
786
- setGit(readGitInfo(cwd));
787
796
  setUpdatedAt(Date.now());
797
+ readGitInfo(cwd).then(setGit).catch(() => {});
788
798
  getUsage().then(setRateLimits).catch(() => {});
789
799
  readSessionTimeline(cwd).then(entries => {
790
800
  setTimeline(entries);
@@ -802,6 +812,8 @@ function App() {
802
812
  .catch(() => { setLoading(false); });
803
813
  // Full deep scan in background → update silently
804
814
  scanProject(cwd, 8).then(p => { setProject(p); }).catch(() => {});
815
+ // Initial git load (async)
816
+ readGitInfo(cwd).then(setGit).catch(() => {});
805
817
  // Initial API usage fetch
806
818
  getUsage().then(setRateLimits).catch(() => {});
807
819
  // Initial timeline load
@@ -897,7 +909,12 @@ function App() {
897
909
  return;
898
910
  }
899
911
 
900
- if (input === 'q' || input === 'ㅂ') process.exit(0);
912
+ if (input === 'q' || input === 'ㅂ') {
913
+ const now = Date.now();
914
+ if (now - lastQRef.current < 600) { process.exit(0); }
915
+ lastQRef.current = now;
916
+ return;
917
+ }
901
918
 
902
919
  // Escape: close file viewer first, then quit
903
920
  if (key.escape) {