claude-code-hud 0.3.5 → 0.3.7

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 한국어
8
8
 
9
- Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE나 별도 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
9
+ Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조, 세션 히스토리를 IDE나 별도 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
10
10
 
11
11
  <img src="./demo.gif" width="700" alt="demo">
12
12
 
@@ -22,6 +22,8 @@ claude npx claude-code-hud
22
22
  (Claude Code 작업 중...) (HUD 실시간 표시)
23
23
  ```
24
24
 
25
+ <img src="./capture.png" width="700" alt="side-by-side terminals">
26
+
25
27
  HUD는 현재 디렉토리를 기준으로 토큰, git, 프로젝트 정보를 자동으로 인식합니다.
26
28
 
27
29
  ```bash
@@ -33,21 +35,22 @@ tmux split-window -h "npx claude-code-hud"
33
35
  ### 설치
34
36
 
35
37
  ```bash
36
- # 설치 없이 바로 실행
37
- npx claude-code-hud
38
+ # 방법1. 전역 설치 (권장)
39
+ > npm install -g claude-code-hud
40
+ > claude-hud
38
41
 
39
- # 전역 설치
40
- npm install -g claude-code-hud
41
- claude-hud
42
+ # 방법2. 설치 없이 바로 실행
43
+ > npx claude-code-hud
42
44
  ```
43
45
 
44
46
  ### 기능
45
47
 
46
48
  **1 TOKENS 탭**
47
49
  - 컨텍스트 윈도우 사용량 게이지 (OK / MID / WARN) — 사용량에 따라 헤더 색상 변경
48
- - Anthropic API 기반 5h / 주간 사용률 (실제 값, 추정치 아님) — `1h 23m` 형식으로 리셋까지 남은 시간 표시
50
+ - Anthropic API 기반 5h / 주간 사용률 — `1h 23m` 형식으로 리셋까지 남은 시간 표시
49
51
  - input / output / cache-read / cache-write 토큰 분류
50
52
  - 세션 output 통계 (total / avg / peak)
53
+ - `now` — 현재 진행 중인 작업 (마지막 사용자 메시지) 한 줄 표시
51
54
 
52
55
  **2 PROJECT 탭 — 인터랙티브 파일 브라우저**
53
56
  - 디렉토리 트리 (펼치기/접기)
@@ -72,16 +75,20 @@ claude-hud
72
75
  - 최근 커밋 히스토리
73
76
  - **브랜치 전환** — `b` 키로 로컬 브랜치 목록 표시, 선택해서 바로 checkout
74
77
 
78
+ **4 TIMELINE 탭**
79
+ - 현재 세션에서 사용자가 입력한 메시지 히스토리
80
+ - 시간 + 내용 표시, 10개씩 j/k 스크롤
81
+
75
82
  ### 키보드 단축키
76
83
 
77
84
  | 키 | 동작 |
78
85
  |----|------|
79
- | `1` `2` `3` | 탭 전환 |
86
+ | `1` `2` `3` `4` | 탭 전환 |
80
87
  | `j` / `k` | 스크롤 / 트리 이동 |
81
88
  | `→` / `Enter` | 디렉토리 펼치기 / 파일 열기 |
82
89
  | `←` / `Esc` | 접기 / 소스 뷰어 닫기 |
83
90
  | `b` | 브랜치 전환 (GIT 탭) |
84
- | `d` | 다크 / 라이트 모드 전환 |
91
+ | `d` | 액센트 색상 변경 (blue red → amber → green → pink) |
85
92
  | `r` | 수동 새로고침 |
86
93
  | `q` | 종료 |
87
94
 
@@ -115,7 +122,7 @@ Claude Code를 한 번 실행하면 `~/.claude/.credentials.json`에 credentials
115
122
 
116
123
  ## English
117
124
 
118
- A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status, and interactive project file browser. No IDE, no extra tabs. Just a second terminal window.
125
+ A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status, interactive project file browser, and session history. No IDE, no extra tabs. Just a second terminal window.
119
126
 
120
127
  <img src="./demo.gif" width="700" alt="demo">
121
128
 
@@ -131,6 +138,8 @@ claude npx claude-code-hud
131
138
  (working with Claude Code) (HUD live display)
132
139
  ```
133
140
 
141
+ <img src="./capture.png" width="700" alt="side-by-side terminals">
142
+
134
143
  ```bash
135
144
  # tmux split pane
136
145
  cd ~/my-project
@@ -140,13 +149,12 @@ tmux split-window -h "npx claude-code-hud"
140
149
  ### Installation
141
150
 
142
151
  ```bash
143
- # No install run directly
144
- npx claude-code-hud
145
-
146
- # Global install
147
- npm install -g claude-code-hud
148
- claude-hud
152
+ # Option 1. Global install (recommended)
153
+ > npm install -g claude-code-hud
154
+ > claude-hud
149
155
 
156
+ # Option 2. Run directly without install
157
+ > npx claude-code-hud
150
158
  ```
151
159
 
152
160
  ### Features
@@ -156,6 +164,7 @@ claude-hud
156
164
  - Real 5h / weekly usage from Anthropic OAuth API — not estimates. Reset time shown as `1h 23m`
157
165
  - Input / output / cache-read / cache-write breakdown
158
166
  - Session output stats: total / avg / peak per hour
167
+ - `now` line — last user message (current task at a glance)
159
168
 
160
169
  **2 PROJECT tab — interactive file browser**
161
170
  - Navigable directory tree with expand/collapse
@@ -164,6 +173,15 @@ claude-hud
164
173
  - Package dependency tree from `package.json`
165
174
  - API endpoint detection (GET / POST / PUT / DELETE / PATCH)
166
175
 
176
+ ```
177
+ ▸ TREE │ ▸ SOURCE src/index.ts
178
+ ▼ src/ 23f │ 1 import React from 'react'
179
+ ▼ components/ 8f │ 2 import { render } from 'ink'
180
+ Header.tsx M │ 3
181
+ ▶ hooks/ 4f │ 4 render(<App />)
182
+ ▶ scripts/ 6f │ … [j/k] scroll [esc] close
183
+ ```
184
+
167
185
  **3 GIT tab**
168
186
  - Branch status, ahead/behind remote
169
187
  - Changed file list (MOD / ADD / DEL) with real `+N -N` diff counts
@@ -171,16 +189,20 @@ claude-hud
171
189
  - Recent commit history
172
190
  - **Branch switcher** — press `b` to list local branches and checkout instantly
173
191
 
192
+ **4 TIMELINE tab**
193
+ - User message history from the current Claude Code session
194
+ - Timestamped entries, 10 per page, j/k to scroll
195
+
174
196
  ### Keyboard Shortcuts
175
197
 
176
198
  | Key | Action |
177
199
  |-----|--------|
178
- | `1` `2` `3` | Switch tabs |
200
+ | `1` `2` `3` `4` | Switch tabs |
179
201
  | `j` / `k` | Scroll / move tree cursor |
180
202
  | `→` / `Enter` | Expand dir / open file |
181
203
  | `←` / `Esc` | Collapse / close source viewer |
182
204
  | `b` | Branch switcher (GIT tab) |
183
- | `d` | Toggle dark / light mode |
205
+ | `d` | Cycle accent color (blue red → amber → green → pink) |
184
206
  | `r` | Manual refresh |
185
207
  | `q` | Quit |
186
208
 
@@ -206,6 +228,7 @@ Run `claude` once to authenticate — credentials are saved to `~/.claude/.crede
206
228
  ### How it works
207
229
 
208
230
  - **Token data**: Watches `~/.claude/projects/*/sessions/*.jsonl` with chokidar — updates instantly on each Claude response
231
+ - **Timeline**: Reads the same JSONL files, filters `type === "user"` entries for message history
209
232
  - **Usage window**: Calls `api.anthropic.com/api/oauth/usage` using local Claude credentials — cached 5 min
210
233
  - **Git**: Polls every 3 seconds
211
234
  - **Project scan**: One-time fast-glob scan on startup, `r` to rescan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hud",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,15 +34,23 @@ function getContextWindow(model) {
34
34
  return 200000;
35
35
  }
36
36
 
37
- /** Find the most recently modified .jsonl session file */
38
- function findLatestSession() {
37
+ /** Convert cwd to the Claude project directory name (/ replaced with -) */
38
+ function cwdToProjectDir(cwd) {
39
+ return cwd.replace(/\//g, '-');
40
+ }
41
+
42
+ /** Find the most recently modified .jsonl session file for the given cwd */
43
+ function findLatestSession(cwd) {
39
44
  const projectsDir = path.join(os.homedir(), '.claude', 'projects');
40
45
  if (!fs.existsSync(projectsDir)) return null;
41
46
 
47
+ const targetDir = cwd ? cwdToProjectDir(cwd) : null;
48
+
42
49
  let latest = null;
43
50
  let latestMtime = 0;
44
51
 
45
- const projects = fs.readdirSync(projectsDir);
52
+ const projects = fs.readdirSync(projectsDir)
53
+ .filter(p => !targetDir || p === targetDir);
46
54
  for (const proj of projects) {
47
55
  const projDir = path.join(projectsDir, proj);
48
56
 
@@ -66,12 +74,13 @@ function findLatestSession() {
66
74
  return latest;
67
75
  }
68
76
 
69
- /** Collect all JSONL lines across all sessions with their timestamps */
70
- function readAllLines() {
77
+ /** Collect all JSONL lines for the given cwd (or all projects if no cwd) */
78
+ function readAllLines(cwd) {
71
79
  const projectsDir = path.join(os.homedir(), '.claude', 'projects');
72
80
  if (!fs.existsSync(projectsDir)) return [];
81
+ const targetDir = cwd ? cwdToProjectDir(cwd) : null;
73
82
  const result = [];
74
- for (const proj of fs.readdirSync(projectsDir)) {
83
+ for (const proj of fs.readdirSync(projectsDir).filter(p => !targetDir || p === targetDir)) {
75
84
  const projDir = path.join(projectsDir, proj);
76
85
  let files = [];
77
86
  try { files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl')); } catch { continue; }
@@ -94,15 +103,17 @@ function readAllLines() {
94
103
  return result;
95
104
  }
96
105
 
97
- export function readTokenHistory() {
98
- const allLines = readAllLines();
106
+ export function readTokenHistory(cwd) {
107
+ const allLines = readAllLines(cwd);
99
108
  const now = Date.now();
100
109
  const h5 = now - 5 * 60 * 60 * 1000;
101
110
  const wk = now - 7 * 24 * 60 * 60 * 1000;
102
111
  const h12 = now - 12 * 60 * 60 * 1000;
112
+ const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0);
113
+ const todayStart = todayMidnight.getTime();
103
114
 
104
115
  const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } });
105
- const acc5h = empty(), accWk = empty();
116
+ const acc5h = empty(), accWk = empty(), accToday = empty();
106
117
 
107
118
  // 12 hourly buckets (index 0 = oldest, 11 = most recent)
108
119
  const buckets = Array(12).fill(0);
@@ -126,8 +137,9 @@ export function readTokenHistory() {
126
137
  acc.cost.cacheWrite += (cw / M) * pricing.cacheWrite;
127
138
  };
128
139
 
129
- if (ts >= wk) { addTo(accWk); }
130
- if (ts >= h5) { addTo(acc5h); }
140
+ if (ts >= wk) { addTo(accWk); }
141
+ if (ts >= h5) { addTo(acc5h); }
142
+ if (ts >= todayStart) { addTo(accToday); }
131
143
 
132
144
  if (ts >= h12) {
133
145
  const hoursAgo = (now - ts) / (60 * 60 * 1000);
@@ -136,15 +148,15 @@ export function readTokenHistory() {
136
148
  }
137
149
  }
138
150
 
139
- [acc5h, accWk].forEach(acc => {
151
+ [acc5h, accWk, accToday].forEach(acc => {
140
152
  acc.cost.total = acc.cost.input + acc.cost.output + acc.cost.cacheRead + acc.cost.cacheWrite;
141
153
  });
142
154
 
143
- return { last5h: acc5h, lastWeek: accWk, hourlyBuckets: buckets };
155
+ return { last5h: acc5h, lastWeek: accWk, today: accToday, hourlyBuckets: buckets };
144
156
  }
145
157
 
146
- export function readTokenUsage() {
147
- const sessionFile = findLatestSession();
158
+ export function readTokenUsage(cwd) {
159
+ const sessionFile = findLatestSession(cwd);
148
160
  if (!sessionFile) {
149
161
  return empty();
150
162
  }
package/tui/hud.tsx CHANGED
@@ -218,10 +218,13 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
218
218
  const projectsDir = join(os.homedir(), '.claude', 'projects');
219
219
  if (!fs.existsSync(projectsDir)) return [];
220
220
 
221
+ const targetDirName = cwd.replace(/\//g, '-');
222
+
221
223
  let latestFile: string | null = null;
222
224
  let latestMtime = 0;
223
225
  try {
224
226
  for (const projectHash of fs.readdirSync(projectsDir)) {
227
+ if (projectHash !== targetDirName) continue;
225
228
  const sessionDir = join(projectsDir, projectHash);
226
229
  if (!fs.statSync(sessionDir).isDirectory()) continue;
227
230
  for (const file of fs.readdirSync(sessionDir)) {
@@ -245,6 +248,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
245
248
  const obj = JSON.parse(line);
246
249
  if (obj.type !== 'user') continue;
247
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;
248
253
  const textBlock = Array.isArray(content)
249
254
  ? content.find((b: any) => b.type === 'text')
250
255
  : null;
@@ -398,6 +403,20 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
398
403
  );
399
404
  })()}
400
405
 
406
+ {/* Today summary + sparkline */}
407
+ <Section title="TODAY" C={C}>
408
+ <Box>
409
+ <Text color={C.dimmer}>in </Text>
410
+ <Text color={C.brand} bold>{fmtNum(history.today?.inputTokens ?? 0)}</Text>
411
+ <Text color={C.dimmer}> out </Text>
412
+ <Text color={C.purple} bold>{fmtNum(history.today?.outputTokens ?? 0)}</Text>
413
+ <Text color={C.dimmer}> cache </Text>
414
+ <Text color={C.cyan} bold>{fmtNum((history.today?.cacheReadTokens ?? 0) + (history.today?.cacheWriteTokens ?? 0))}</Text>
415
+ <Text color={C.dimmer}> </Text>
416
+ <Text color={costColor(history.today?.cost?.total ?? 0, C)} bold>{fmtCost(history.today?.cost?.total ?? 0)}</Text>
417
+ </Box>
418
+ </Section>
419
+
401
420
  {/* Current activity */}
402
421
  {currentActivity && (
403
422
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
@@ -410,7 +429,7 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
410
429
  }
411
430
 
412
431
  // ── Tab 2: PROJECT ─────────────────────────────────────────────────────────
413
- function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, git, C }: any) {
432
+ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, contentH, git, C }: any) {
414
433
  if (!info) return (
415
434
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
416
435
  <Text color={C.dimmer}>scanning project…</Text>
@@ -436,16 +455,26 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
436
455
  return result;
437
456
  }
438
457
 
439
- const flatNodes: FlatNode[] = info.dirTree ? flatNodes_inner(info.dirTree, 0) : [];
458
+ const ch = contentH ?? 30;
459
+ const hasFile = !!selectedFile;
460
+
461
+ // Budget: summary=3, tree border+header+marginTop=4, packages≈14 when shown
462
+ const showPackages = !hasFile && ch >= 28;
463
+ const packagesBudget = showPackages ? 14 : 0;
464
+ const maxTreeRows = Math.max(4, ch - 7 - packagesBudget);
465
+ const treePanelH = Math.max(4, ch - 3 - packagesBudget);
466
+ const VISIBLE_LINES = Math.max(4, ch - 8);
467
+
468
+ // Flatten nodes
469
+ const allFlatNodes: FlatNode[] = info.dirTree ? flatNodes_inner(info.dirTree, 0) : [];
470
+ const flatNodes = allFlatNodes.slice(0, maxTreeRows);
440
471
  const safeCursor = Math.min(treeCursor, Math.max(0, flatNodes.length - 1));
441
472
 
442
473
  const totalEndpoints = Object.values(info.endpoints as Record<string, number>).reduce((a: number, b: number) => a + b, 0);
443
474
 
444
475
  // Split layout when file is open
445
- const hasFile = !!selectedFile;
446
476
  const TREE_W = hasFile ? Math.max(28, Math.floor(termWidth * 0.36)) : termWidth - 2;
447
477
  const SOURCE_W = hasFile ? termWidth - TREE_W - 5 : 0;
448
- const VISIBLE_LINES = 22;
449
478
 
450
479
  // Git changed file sets
451
480
  const gitModified = new Set<string>([...(git?.modified ?? []), ...(git?.added ?? []), ...(git?.deleted ?? [])]);
@@ -460,9 +489,9 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
460
489
  };
461
490
 
462
491
  return (
463
- <Box flexDirection="column">
492
+ <Box flexDirection="column" height={ch}>
464
493
  {/* Summary bar */}
465
- <Box borderStyle="single" borderColor={C.border} paddingX={1}>
494
+ <Box borderStyle="single" borderColor={C.border} paddingX={1} height={3}>
466
495
  <Text color={C.text} bold>{info.totalFiles} files</Text>
467
496
  <Text color={C.dim}> │ </Text>
468
497
  <Text color={C.text} bold>{info.packages.filter((p: any) => p.depth === 0).length} pkgs</Text>
@@ -474,10 +503,10 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
474
503
  </Box>
475
504
 
476
505
  {/* Main area: tree + optional source */}
477
- <Box flexDirection="row">
506
+ <Box flexDirection="row" height={treePanelH}>
478
507
 
479
508
  {/* ── Tree panel ── */}
480
- <Box flexDirection="column" borderStyle="single" borderColor={hasFile ? C.brand : C.border} paddingX={1} width={TREE_W}>
509
+ <Box flexDirection="column" borderStyle="single" borderColor={hasFile ? C.brand : C.border} paddingX={1} width={TREE_W} height={treePanelH}>
481
510
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>TREE</Text></Text>
482
511
  <Box marginTop={1} flexDirection="column">
483
512
  {flatNodes.length === 0 && <Text color={C.dimmer}> (empty)</Text>}
@@ -493,7 +522,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
493
522
  <Box key={`d_${fn.node.path}_${idx}`}>
494
523
  <Text color={C.dimmer}>{indent}</Text>
495
524
  <Text color={isSelected ? C.brand : C.dimmer}>{expIcon}</Text>
496
- <Text color={nameColor} bold={isSelected}>{fn.node.name}/</Text>
525
+ <Text color={nameColor} bold={isSelected} wrap="truncate">{fn.node.name}/</Text>
497
526
  <Text color={C.dimmer}> {fn.node.totalFiles}f</Text>
498
527
  </Box>
499
528
  );
@@ -510,7 +539,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
510
539
  <Box key={`f_${fn.filePath}_${idx}`}>
511
540
  <Text color={C.dimmer}>{indent}</Text>
512
541
  <Text color={isSelected ? C.brand : C.dimmer}>{isOpen ? '▶ ' : ' '}</Text>
513
- <Text color={fileColor} bold={isSelected || isOpen}>{fn.fileName}</Text>
542
+ <Text color={fileColor} bold={isSelected || isOpen} wrap="truncate">{fn.fileName}</Text>
514
543
  {gitBadge ? <Text color={gitColor!} bold>{gitBadge}</Text> : null}
515
544
  </Box>
516
545
  );
@@ -521,7 +550,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
521
550
 
522
551
  {/* ── Source viewer panel ── */}
523
552
  {hasFile && (
524
- <Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} width={SOURCE_W}>
553
+ <Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} width={SOURCE_W} height={treePanelH}>
525
554
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>SOURCE <Text color={C.dim}>{selectedFile}</Text></Text></Text>
526
555
  <Box marginTop={1} flexDirection="column">
527
556
  {(fileLines as string[]).slice(fileScroll, fileScroll + VISIBLE_LINES).map((line, i) => {
@@ -533,7 +562,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
533
562
  <Text color={C.dimmer}>{lineNo}</Text>
534
563
  </Box>
535
564
  <Text color={C.dimmer}> </Text>
536
- <Text color={C.text}>{truncated}</Text>
565
+ <Text color={C.text} wrap="truncate">{truncated}</Text>
537
566
  </Box>
538
567
  );
539
568
  })}
@@ -546,8 +575,8 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
546
575
  </Box>
547
576
 
548
577
  {/* Packages (hidden when file open to save space) */}
549
- {!hasFile && (
550
- <Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1}>
578
+ {showPackages && (
579
+ <Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1} height={packagesBudget}>
551
580
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>PACKAGES</Text></Text>
552
581
  <Box marginTop={1} flexDirection="column">
553
582
  {info.packages.slice(0, 10).map((p: any, i: number) => {
@@ -715,8 +744,8 @@ function App() {
715
744
  const cwd = process.env.CLAUDE_PROJECT_ROOT || process.cwd();
716
745
  const C = makeTheme(accent);
717
746
 
718
- const [usage, setUsage] = useState<any>(readTokenUsage());
719
- const [history, setHistory] = useState<any>(readTokenHistory());
747
+ const [usage, setUsage] = useState<any>(readTokenUsage(cwd));
748
+ const [history, setHistory] = useState<any>(readTokenHistory(cwd));
720
749
  const [git, setGit] = useState<any>(readGitInfo(cwd));
721
750
  const [project, setProject] = useState<ProjectInfo | null>(null);
722
751
  const [rateLimits, setRateLimits] = useState<any>(getUsageSync());
@@ -730,6 +759,17 @@ function App() {
730
759
  const [fileLines, setFileLines] = useState<string[]>([]);
731
760
  const [fileScroll, setFileScroll] = useState(0);
732
761
 
762
+ // Help overlay state
763
+ const [showHelp, setShowHelp] = useState(false);
764
+
765
+ // Token warning blink state
766
+ const [blinkOn, setBlinkOn] = useState(true);
767
+
768
+ // Loading spinner state
769
+ const [loading, setLoading] = useState(true);
770
+ const [spinFrame, setSpinFrame] = useState(0);
771
+ const SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
772
+
733
773
  // Branch switcher state
734
774
  const [branchMode, setBranchMode] = useState(false);
735
775
  const [branchList, setBranchList] = useState<string[]>([]);
@@ -741,8 +781,8 @@ function App() {
741
781
  const [currentActivity, setCurrentActivity] = useState<string>('');
742
782
 
743
783
  const refresh = useCallback(() => {
744
- setUsage(readTokenUsage());
745
- setHistory(readTokenHistory());
784
+ setUsage(readTokenUsage(cwd));
785
+ setHistory(readTokenHistory(cwd));
746
786
  setGit(readGitInfo(cwd));
747
787
  setUpdatedAt(Date.now());
748
788
  getUsage().then(setRateLimits).catch(() => {});
@@ -757,7 +797,7 @@ function App() {
757
797
 
758
798
  useEffect(() => {
759
799
  // Scan project once
760
- scanProject(cwd).then(setProject).catch(() => {});
800
+ scanProject(cwd).then(p => { setProject(p); setLoading(false); }).catch(() => { setLoading(false); });
761
801
  // Initial API usage fetch
762
802
  getUsage().then(setRateLimits).catch(() => {});
763
803
  // Initial timeline load
@@ -786,11 +826,15 @@ function App() {
786
826
  depth: 2, persistent: true, ignoreInitial: true,
787
827
  ignored: (p: string) => !p.endsWith('.jsonl'),
788
828
  });
789
- watcher.on('change', refresh);
829
+ let debounceTimer: ReturnType<typeof setTimeout> | null = null;
830
+ watcher.on('change', () => {
831
+ if (debounceTimer) clearTimeout(debounceTimer);
832
+ debounceTimer = setTimeout(refresh, 800);
833
+ });
790
834
  });
791
835
  }
792
836
 
793
- const tickInterval = setInterval(() => setTick(t => t + 1), 1000);
837
+ const tickInterval = setInterval(() => setTick(t => t + 1), 5000);
794
838
 
795
839
  return () => {
796
840
  stdout?.off('resize', onResize);
@@ -800,7 +844,16 @@ function App() {
800
844
  };
801
845
  }, []);
802
846
 
847
+ useEffect(() => {
848
+ if (!loading) return;
849
+ const id = setInterval(() => setSpinFrame(f => f + 1), 80);
850
+ return () => clearInterval(id);
851
+ }, [loading]);
852
+
803
853
  useInput((input, key) => {
854
+ if (input === '?') { setShowHelp(h => !h); return; }
855
+ if (key.escape && showHelp) { setShowHelp(false); return; }
856
+
804
857
  // Branch switcher intercepts input when active
805
858
  if (branchMode) {
806
859
  if (input === 'j' || key.downArrow) {
@@ -932,11 +985,28 @@ function App() {
932
985
  const uptime = fmtSince(SESSION_START - Date.now() + (Date.now() - SESSION_START)); // forces tick dep
933
986
  void tick;
934
987
 
988
+ const ctxPct = usage.contextWindow > 0 ? usage.totalTokens / usage.contextWindow : 0;
989
+
990
+ useEffect(() => {
991
+ if (ctxPct <= 0.85) { setBlinkOn(true); return; }
992
+ const id = setInterval(() => setBlinkOn(b => !b), 600);
993
+ return () => clearInterval(id);
994
+ }, [ctxPct > 0.85]);
995
+
996
+ if (termWidth < 60 || termHeight < 15) {
997
+ return (
998
+ <Box width={termWidth} height={termHeight} alignItems="center" justifyContent="center" flexDirection="column">
999
+ <Text color={C.yellow} bold>⚠ terminal too small</Text>
1000
+ <Text color={C.dimmer}>{termWidth}×{termHeight} — min 60×15</Text>
1001
+ </Box>
1002
+ );
1003
+ }
1004
+
935
1005
  return (
936
- <Box flexDirection="column">
1006
+ <Box flexDirection="column" height={termHeight}>
937
1007
 
938
1008
  {/* ── Header / Tab bar ── */}
939
- <Box borderStyle="single" borderColor={usage.contextWindow > 0 && usage.totalTokens / usage.contextWindow > 0.85 ? C.red : usage.contextWindow > 0 && usage.totalTokens / usage.contextWindow > 0.65 ? C.yellow : C.brand} paddingX={1} justifyContent="space-between">
1009
+ <Box height={3} borderStyle="single" borderColor={ctxPct > 0.85 ? (blinkOn ? C.red : C.border) : ctxPct > 0.65 ? C.yellow : C.brand} paddingX={1} justifyContent="space-between">
940
1010
  <Box>
941
1011
  <Text color={C.brand} bold>◆ HUD</Text>
942
1012
  {TAB_NAMES.map((name, i) => (
@@ -961,31 +1031,53 @@ function App() {
961
1031
  const contentH = Math.max(4, termHeight - 7);
962
1032
  return (
963
1033
  <Box flexDirection="column" height={contentH} overflow="hidden">
964
- <Box flexDirection="column" marginTop={-scrollY}>
965
- {tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} currentActivity={currentActivity} C={C} />}
966
- {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} git={git} C={C} />}
967
- {tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
968
- {tab === 3 && <TimelineTab timeline={timeline} timelineScroll={timelineScroll} C={C} />}
969
- </Box>
1034
+ {showHelp ? (
1035
+ <Box flexDirection="column" borderStyle="round" borderColor={C.brand} paddingX={2} paddingY={1}>
1036
+ <Text color={C.brand} bold> Keyboard Shortcuts</Text>
1037
+ <Text> </Text>
1038
+ <Text><Text color={C.dim}> 1 / 2 / 3 / 4 </Text><Text color={C.text}>switch tabs</Text></Text>
1039
+ <Text><Text color={C.dim}> j / k </Text><Text color={C.text}>scroll / tree move</Text></Text>
1040
+ <Text><Text color={C.dim}> → / Enter </Text><Text color={C.text}>expand dir / open file</Text></Text>
1041
+ <Text><Text color={C.dim}> ← / Esc </Text><Text color={C.text}>collapse / close</Text></Text>
1042
+ <Text><Text color={C.dim}> b </Text><Text color={C.text}>branch switcher (GIT tab)</Text></Text>
1043
+ <Text><Text color={C.dim}> d </Text><Text color={C.text}>cycle accent color</Text></Text>
1044
+ <Text><Text color={C.dim}> r </Text><Text color={C.text}>refresh</Text></Text>
1045
+ <Text><Text color={C.dim}> q / Esc </Text><Text color={C.text}>quit</Text></Text>
1046
+ <Text><Text color={C.dim}> ? </Text><Text color={C.text}>toggle this help</Text></Text>
1047
+ <Text> </Text>
1048
+ <Text color={C.dimmer}> Korean: ㅓ/ㅏ (j/k) ㅇ (d) ㄱ (r) ㅂ (q) ㅠ (b)</Text>
1049
+ </Box>
1050
+ ) : loading ? (
1051
+ <Box height={contentH} alignItems="center" justifyContent="center">
1052
+ <Text color={C.brand} bold>{SPIN[spinFrame % SPIN.length]} scanning project…</Text>
1053
+ </Box>
1054
+ ) : (
1055
+ <Box flexDirection="column" height={contentH} marginTop={-scrollY}>
1056
+ {tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} currentActivity={currentActivity} C={C} />}
1057
+ {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} contentH={contentH - 1} git={git} C={C} />}
1058
+ {tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
1059
+ {tab === 3 && <TimelineTab timeline={timeline} timelineScroll={timelineScroll} C={C} />}
1060
+ </Box>
1061
+ )}
970
1062
  </Box>
971
1063
  );
972
1064
  })()}
973
1065
 
974
1066
  {/* ── Footer row 1: keys ── */}
975
- <Box justifyContent="space-between" paddingX={1}>
1067
+ <Box height={1} justifyContent="space-between" paddingX={1}>
976
1068
  <Box>
977
1069
  <Text color={C.green}>● </Text>
978
1070
  <Text color={C.dimmer}>[1/2/3/4] tabs </Text>
979
1071
  <Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
980
- <Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc/←] close [j/k] scroll ' : '[enter] open [→←] expand ') : ''}</Text>
1072
+ <Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc] close ' : '[↵/→←] open ') : ''}</Text>
981
1073
  {tab === 2 && !branchMode && <Text color={C.brand}>[b] branch </Text>}
982
- <Text color={C.dimmer}>[r] refresh [d] color [q] quit</Text>
1074
+ <Text color={C.dimmer}>[r] refresh [d] color [?] help [q] quit</Text>
983
1075
  </Box>
984
1076
  <Text color={C.dimmer}>↻ {since}</Text>
985
1077
  </Box>
986
1078
 
987
1079
  {/* ── Footer row 2: current dir ── */}
988
- <Box paddingX={1} borderStyle="single" borderColor={C.brand}>
1080
+ <Box height={3} paddingX={1} borderStyle="single" borderColor={C.brand}>
989
1081
  <Text color={C.brand} bold>◆ </Text>
990
1082
  <Text color={C.text} bold>~/{basename(cwd)}</Text>
991
1083
  </Box>