claude-code-hud 0.3.6 → 0.3.8

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.6",
3
+ "version": "0.3.8",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
@@ -109,9 +109,11 @@ export function readTokenHistory(cwd) {
109
109
  const h5 = now - 5 * 60 * 60 * 1000;
110
110
  const wk = now - 7 * 24 * 60 * 60 * 1000;
111
111
  const h12 = now - 12 * 60 * 60 * 1000;
112
+ const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0);
113
+ const todayStart = todayMidnight.getTime();
112
114
 
113
115
  const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } });
114
- const acc5h = empty(), accWk = empty();
116
+ const acc5h = empty(), accWk = empty(), accToday = empty();
115
117
 
116
118
  // 12 hourly buckets (index 0 = oldest, 11 = most recent)
117
119
  const buckets = Array(12).fill(0);
@@ -135,8 +137,9 @@ export function readTokenHistory(cwd) {
135
137
  acc.cost.cacheWrite += (cw / M) * pricing.cacheWrite;
136
138
  };
137
139
 
138
- if (ts >= wk) { addTo(accWk); }
139
- if (ts >= h5) { addTo(acc5h); }
140
+ if (ts >= wk) { addTo(accWk); }
141
+ if (ts >= h5) { addTo(acc5h); }
142
+ if (ts >= todayStart) { addTo(accToday); }
140
143
 
141
144
  if (ts >= h12) {
142
145
  const hoursAgo = (now - ts) / (60 * 60 * 1000);
@@ -145,11 +148,11 @@ export function readTokenHistory(cwd) {
145
148
  }
146
149
  }
147
150
 
148
- [acc5h, accWk].forEach(acc => {
151
+ [acc5h, accWk, accToday].forEach(acc => {
149
152
  acc.cost.total = acc.cost.input + acc.cost.output + acc.cost.cacheRead + acc.cost.cacheWrite;
150
153
  });
151
154
 
152
- return { last5h: acc5h, lastWeek: accWk, hourlyBuckets: buckets };
155
+ return { last5h: acc5h, lastWeek: accWk, today: accToday, hourlyBuckets: buckets };
153
156
  }
154
157
 
155
158
  export function readTokenUsage(cwd) {
package/tui/hud.tsx CHANGED
@@ -248,6 +248,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
248
248
  const obj = JSON.parse(line);
249
249
  if (obj.type !== 'user') continue;
250
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;
251
253
  const textBlock = Array.isArray(content)
252
254
  ? content.find((b: any) => b.type === 'text')
253
255
  : null;
@@ -401,6 +403,20 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
401
403
  );
402
404
  })()}
403
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
+
404
420
  {/* Current activity */}
405
421
  {currentActivity && (
406
422
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
@@ -413,7 +429,7 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
413
429
  }
414
430
 
415
431
  // ── Tab 2: PROJECT ─────────────────────────────────────────────────────────
416
- 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) {
417
433
  if (!info) return (
418
434
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
419
435
  <Text color={C.dimmer}>scanning project…</Text>
@@ -439,16 +455,26 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
439
455
  return result;
440
456
  }
441
457
 
442
- 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);
443
471
  const safeCursor = Math.min(treeCursor, Math.max(0, flatNodes.length - 1));
444
472
 
445
473
  const totalEndpoints = Object.values(info.endpoints as Record<string, number>).reduce((a: number, b: number) => a + b, 0);
446
474
 
447
475
  // Split layout when file is open
448
- const hasFile = !!selectedFile;
449
476
  const TREE_W = hasFile ? Math.max(28, Math.floor(termWidth * 0.36)) : termWidth - 2;
450
477
  const SOURCE_W = hasFile ? termWidth - TREE_W - 5 : 0;
451
- const VISIBLE_LINES = 22;
452
478
 
453
479
  // Git changed file sets
454
480
  const gitModified = new Set<string>([...(git?.modified ?? []), ...(git?.added ?? []), ...(git?.deleted ?? [])]);
@@ -463,9 +489,9 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
463
489
  };
464
490
 
465
491
  return (
466
- <Box flexDirection="column">
492
+ <Box flexDirection="column" height={ch}>
467
493
  {/* Summary bar */}
468
- <Box borderStyle="single" borderColor={C.border} paddingX={1}>
494
+ <Box borderStyle="single" borderColor={C.border} paddingX={1} height={3}>
469
495
  <Text color={C.text} bold>{info.totalFiles} files</Text>
470
496
  <Text color={C.dim}> │ </Text>
471
497
  <Text color={C.text} bold>{info.packages.filter((p: any) => p.depth === 0).length} pkgs</Text>
@@ -477,10 +503,10 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
477
503
  </Box>
478
504
 
479
505
  {/* Main area: tree + optional source */}
480
- <Box flexDirection="row">
506
+ <Box flexDirection="row" height={treePanelH}>
481
507
 
482
508
  {/* ── Tree panel ── */}
483
- <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}>
484
510
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>TREE</Text></Text>
485
511
  <Box marginTop={1} flexDirection="column">
486
512
  {flatNodes.length === 0 && <Text color={C.dimmer}> (empty)</Text>}
@@ -496,7 +522,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
496
522
  <Box key={`d_${fn.node.path}_${idx}`}>
497
523
  <Text color={C.dimmer}>{indent}</Text>
498
524
  <Text color={isSelected ? C.brand : C.dimmer}>{expIcon}</Text>
499
- <Text color={nameColor} bold={isSelected}>{fn.node.name}/</Text>
525
+ <Text color={nameColor} bold={isSelected} wrap="truncate">{fn.node.name}/</Text>
500
526
  <Text color={C.dimmer}> {fn.node.totalFiles}f</Text>
501
527
  </Box>
502
528
  );
@@ -513,7 +539,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
513
539
  <Box key={`f_${fn.filePath}_${idx}`}>
514
540
  <Text color={C.dimmer}>{indent}</Text>
515
541
  <Text color={isSelected ? C.brand : C.dimmer}>{isOpen ? '▶ ' : ' '}</Text>
516
- <Text color={fileColor} bold={isSelected || isOpen}>{fn.fileName}</Text>
542
+ <Text color={fileColor} bold={isSelected || isOpen} wrap="truncate">{fn.fileName}</Text>
517
543
  {gitBadge ? <Text color={gitColor!} bold>{gitBadge}</Text> : null}
518
544
  </Box>
519
545
  );
@@ -524,7 +550,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
524
550
 
525
551
  {/* ── Source viewer panel ── */}
526
552
  {hasFile && (
527
- <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}>
528
554
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>SOURCE <Text color={C.dim}>{selectedFile}</Text></Text></Text>
529
555
  <Box marginTop={1} flexDirection="column">
530
556
  {(fileLines as string[]).slice(fileScroll, fileScroll + VISIBLE_LINES).map((line, i) => {
@@ -536,7 +562,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
536
562
  <Text color={C.dimmer}>{lineNo}</Text>
537
563
  </Box>
538
564
  <Text color={C.dimmer}> </Text>
539
- <Text color={C.text}>{truncated}</Text>
565
+ <Text color={C.text} wrap="truncate">{truncated}</Text>
540
566
  </Box>
541
567
  );
542
568
  })}
@@ -549,8 +575,8 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
549
575
  </Box>
550
576
 
551
577
  {/* Packages (hidden when file open to save space) */}
552
- {!hasFile && (
553
- <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}>
554
580
  <Text color={C.dimmer} bold>▸ <Text color={C.text}>PACKAGES</Text></Text>
555
581
  <Box marginTop={1} flexDirection="column">
556
582
  {info.packages.slice(0, 10).map((p: any, i: number) => {
@@ -733,6 +759,17 @@ function App() {
733
759
  const [fileLines, setFileLines] = useState<string[]>([]);
734
760
  const [fileScroll, setFileScroll] = useState(0);
735
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
+
736
773
  // Branch switcher state
737
774
  const [branchMode, setBranchMode] = useState(false);
738
775
  const [branchList, setBranchList] = useState<string[]>([]);
@@ -760,7 +797,7 @@ function App() {
760
797
 
761
798
  useEffect(() => {
762
799
  // Scan project once
763
- scanProject(cwd).then(setProject).catch(() => {});
800
+ scanProject(cwd).then(p => { setProject(p); setLoading(false); }).catch(() => { setLoading(false); });
764
801
  // Initial API usage fetch
765
802
  getUsage().then(setRateLimits).catch(() => {});
766
803
  // Initial timeline load
@@ -789,11 +826,15 @@ function App() {
789
826
  depth: 2, persistent: true, ignoreInitial: true,
790
827
  ignored: (p: string) => !p.endsWith('.jsonl'),
791
828
  });
792
- 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
+ });
793
834
  });
794
835
  }
795
836
 
796
- const tickInterval = setInterval(() => setTick(t => t + 1), 1000);
837
+ const tickInterval = setInterval(() => setTick(t => t + 1), 5000);
797
838
 
798
839
  return () => {
799
840
  stdout?.off('resize', onResize);
@@ -803,7 +844,16 @@ function App() {
803
844
  };
804
845
  }, []);
805
846
 
847
+ useEffect(() => {
848
+ if (!loading) return;
849
+ const id = setInterval(() => setSpinFrame(f => f + 1), 80);
850
+ return () => clearInterval(id);
851
+ }, [loading]);
852
+
806
853
  useInput((input, key) => {
854
+ if (input === '?') { setShowHelp(h => !h); return; }
855
+ if (key.escape && showHelp) { setShowHelp(false); return; }
856
+
807
857
  // Branch switcher intercepts input when active
808
858
  if (branchMode) {
809
859
  if (input === 'j' || key.downArrow) {
@@ -935,11 +985,28 @@ function App() {
935
985
  const uptime = fmtSince(SESSION_START - Date.now() + (Date.now() - SESSION_START)); // forces tick dep
936
986
  void tick;
937
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
+
938
1005
  return (
939
- <Box flexDirection="column">
1006
+ <Box flexDirection="column" height={termHeight}>
940
1007
 
941
1008
  {/* ── Header / Tab bar ── */}
942
- <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">
943
1010
  <Box>
944
1011
  <Text color={C.brand} bold>◆ HUD</Text>
945
1012
  {TAB_NAMES.map((name, i) => (
@@ -964,31 +1031,53 @@ function App() {
964
1031
  const contentH = Math.max(4, termHeight - 7);
965
1032
  return (
966
1033
  <Box flexDirection="column" height={contentH} overflow="hidden">
967
- <Box flexDirection="column" marginTop={-scrollY}>
968
- {tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} currentActivity={currentActivity} C={C} />}
969
- {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} git={git} C={C} />}
970
- {tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
971
- {tab === 3 && <TimelineTab timeline={timeline} timelineScroll={timelineScroll} C={C} />}
972
- </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
+ )}
973
1062
  </Box>
974
1063
  );
975
1064
  })()}
976
1065
 
977
1066
  {/* ── Footer row 1: keys ── */}
978
- <Box justifyContent="space-between" paddingX={1}>
1067
+ <Box height={1} justifyContent="space-between" paddingX={1}>
979
1068
  <Box>
980
1069
  <Text color={C.green}>● </Text>
981
1070
  <Text color={C.dimmer}>[1/2/3/4] tabs </Text>
982
1071
  <Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
983
- <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>
984
1073
  {tab === 2 && !branchMode && <Text color={C.brand}>[b] branch </Text>}
985
- <Text color={C.dimmer}>[r] refresh [d] color [q] quit</Text>
1074
+ <Text color={C.dimmer}>[r] refresh [d] color [?] help [q] quit</Text>
986
1075
  </Box>
987
1076
  <Text color={C.dimmer}>↻ {since}</Text>
988
1077
  </Box>
989
1078
 
990
1079
  {/* ── Footer row 2: current dir ── */}
991
- <Box paddingX={1} borderStyle="single" borderColor={C.brand}>
1080
+ <Box height={3} paddingX={1} borderStyle="single" borderColor={C.brand}>
992
1081
  <Text color={C.brand} bold>◆ </Text>
993
1082
  <Text color={C.text} bold>~/{basename(cwd)}</Text>
994
1083
  </Box>