claude-code-hud 0.3.1 → 0.3.3

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
@@ -10,20 +10,23 @@ Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE
10
10
 
11
11
  ```
12
12
  ┌──────────────────────────────────────────────────────────────────────────────┐
13
- │ ◆ HUD [1 TOKENS] 2 PROJECT 3 GIT sonnet-4-6 · up 4m
13
+ │ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m
14
14
  ├──────────────────────────────────────────────────────────────────────────────┤
15
- │ CONTEXT WINDOW
16
- ████████████████████░░░░░░░░░░░░░░░░░░░░░░░ 46% 92K / 200K OK
17
- ├──────────────────────────────────────────────────────────────────────────────┤
18
- │ USAGE WINDOW (Anthropic API)
19
- 5h ████████░░░░░░░░░░░░░░░░░░░░ 28.0% resets in 3h
20
- wk ███░░░░░░░░░░░░░░░░░░░░░░░░░ 9.0% resets in 148h
21
- ├──────────────────────────────────────────────────────────────────────────────┤
22
- │ TOKENS (this session)
23
- input ░░░░░░░░░░░░░░░░░░░░░░░░ 4.8K 0%
24
- output ░░░░░░░░░░░░░░░░░░░░░░░░ 188.5K 0%
25
- cache-read ████████████████████████ 51.5M 100%
26
- cache-write ██░░░░░░░░░░░░░░░░░░░░░░ 3.8M 7%
15
+ CONTEXT WINDOW
16
+ ████████████████████░░░░░░░░░░░░░░░░░░░░░░░ 46% 92K / 200K OK
17
+ │ │
18
+ USAGE WINDOW
19
+ 5h ████████░░░░░░░░░░░░░░░░░░░░ 28.0% resets in 3h 12m
20
+ wk ███░░░░░░░░░░░░░░░░░░░░░░░░░ 9.0% resets in 6h 48m
21
+ │ │
22
+ TOKENS (this session)
23
+ input ░░░░░░░░░░░░░░░░░░░░░░░░ 4.8K 0%
24
+ output ░░░░░░░░░░░░░░░░░░░░░░░░ 188.5K 0%
25
+ cache-read ████████████████████████ 51.5M 100%
26
+ cache-write ██░░░░░░░░░░░░░░░░░░░░░░ 3.8M 7%
27
+ │ │
28
+ │ ▸ OUTPUT TOKENS / HR │
29
+ │ total 2.1M │ avg 48.2K/hr │ peak 312K/hr │
27
30
  └──────────────────────────────────────────────────────────────────────────────┘
28
31
  ```
29
32
 
@@ -64,31 +67,33 @@ claude-hud
64
67
  ### 기능
65
68
 
66
69
  **1 TOKENS 탭**
67
- - 컨텍스트 윈도우 사용량 게이지 (OK / MID / WARN)
68
- - Anthropic API 기반 5h / 주간 사용률 (실제 값, 추정치 아님)
70
+ - 컨텍스트 윈도우 사용량 게이지 (OK / MID / WARN) — 사용량에 따라 헤더 색상 변경
71
+ - Anthropic API 기반 5h / 주간 사용률 (실제 값, 추정치 아님) — `1h 23m` 형식으로 리셋까지 남은 시간 표시
69
72
  - input / output / cache-read / cache-write 토큰 분류
70
- - 최근 12시간 output 토큰 sparkline
73
+ - 세션 output 통계 (total / avg / peak)
71
74
 
72
75
  **2 PROJECT 탭 — 인터랙티브 파일 브라우저**
73
76
  - 디렉토리 트리 (펼치기/접기)
77
+ - Git 변경 파일 색상 표시 — 수정(노란색 M) / 추가(초록 A) / 삭제(빨강 D)
74
78
  - 파일 선택 시 소스 코드 뷰어 (split 패널)
75
79
  - 패키지 의존성 트리
76
80
  - API 엔드포인트 감지
77
81
 
78
82
  ```
79
- TREE │ SOURCE src/index.ts
80
- ▼ src/ 23f 1 import React from 'react'
81
- ▼ components/ 8f 2 import { render } from 'ink'
82
- Header.tsx ◀ open 3
83
- ▶ hooks/ 4f 4 render(<App />)
84
- ▶ scripts/ 6f … [j/k] scroll [esc] close
83
+ TREE SOURCE src/index.ts
84
+ ▼ src/ 23f 1 import React from 'react'
85
+ ▼ components/ 8f 2 import { render } from 'ink'
86
+ Header.tsx M 3
87
+ ▶ hooks/ 4f 4 render(<App />)
88
+ ▶ scripts/ 6f … [j/k] scroll [esc] close
85
89
  ```
86
90
 
87
91
  **3 GIT 탭**
88
92
  - 현재 브랜치, ahead/behind 카운트
89
- - 변경 파일 목록 (MOD / ADD / DEL)
90
- - 파일별 diff 시각화 (+/- 바)
93
+ - 변경 파일 목록 (MOD / ADD / DEL) + 실제 +/- 라인 수
94
+ - 파일별 diff 시각화
91
95
  - 최근 커밋 히스토리
96
+ - **브랜치 전환** — `b` 키로 로컬 브랜치 목록 표시, 선택해서 바로 checkout
92
97
 
93
98
  ### 키보드 단축키
94
99
 
@@ -98,10 +103,13 @@ TREE │ SOURCE src/index.ts
98
103
  | `j` / `k` | 스크롤 / 트리 이동 |
99
104
  | `→` / `Enter` | 디렉토리 펼치기 / 파일 열기 |
100
105
  | `←` / `Esc` | 접기 / 소스 뷰어 닫기 |
106
+ | `b` | 브랜치 전환 (GIT 탭) |
101
107
  | `d` | 다크 / 라이트 모드 전환 |
102
108
  | `r` | 수동 새로고침 |
103
109
  | `q` | 종료 |
104
110
 
111
+ > 한글 키보드 모드에서도 동작합니다 — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
112
+
105
113
  ### 요구사항
106
114
 
107
115
  - Node.js 18+
@@ -113,11 +121,13 @@ TREE │ SOURCE src/index.ts
113
121
 
114
122
  ## English
115
123
 
116
- When working with Claude Code in the terminal, I kept running into the same friction: how many tokens are left? what's the git status? what does this file structure look like? answering any of these meant switching to another app or opening more terminal tabs.
124
+ 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.
117
125
 
118
- So I built this. Two terminals. One for Claude Code, one for the HUD. That's it.
119
-
120
- A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status, and interactive project file browser in a separate terminal window or tmux pane.
126
+ ```
127
+ ┌──────────────────────────────────────────────────────────────────────────────┐
128
+ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m
129
+ └──────────────────────────────────────────────────────────────────────────────┘
130
+ ```
121
131
 
122
132
  ### Usage
123
133
 
@@ -131,8 +141,6 @@ claude npx claude-code-hud
131
141
  (working with Claude Code) (HUD live display)
132
142
  ```
133
143
 
134
- The HUD automatically detects your current directory and shows token, git, and project info for that project.
135
-
136
144
  ```bash
137
145
  # tmux split pane
138
146
  cd ~/my-project
@@ -156,13 +164,14 @@ claude-hud
156
164
  ### Features
157
165
 
158
166
  **1 TOKENS tab**
159
- - Context window usage gauge (OK / MID / WARN)
160
- - Real 5h / weekly usage from Anthropic OAuth API — not estimates
167
+ - Context window gauge (OK / MID / WARN) — header border changes color with usage
168
+ - Real 5h / weekly usage from Anthropic OAuth API — not estimates. Reset time shown as `1h 23m`
161
169
  - Input / output / cache-read / cache-write breakdown
162
- - Output tokens sparkline over the last 12 hours
170
+ - Session output stats: total / avg / peak per hour
163
171
 
164
172
  **2 PROJECT tab — interactive file browser**
165
173
  - Navigable directory tree with expand/collapse
174
+ - Git-changed files highlighted — modified (yellow M) / added (green A) / deleted (red D)
166
175
  - Source file viewer in a split panel
167
176
  - Package dependency tree from `package.json`
168
177
  - API endpoint detection (GET / POST / PUT / DELETE / PATCH)
@@ -170,7 +179,9 @@ claude-hud
170
179
  **3 GIT tab**
171
180
  - Branch status, ahead/behind remote
172
181
  - Changed file list (MOD / ADD / DEL) with real `+N -N` diff counts
182
+ - Per-file diff visualization
173
183
  - Recent commit history
184
+ - **Branch switcher** — press `b` to list local branches and checkout instantly
174
185
 
175
186
  ### Keyboard Shortcuts
176
187
 
@@ -180,16 +191,12 @@ claude-hud
180
191
  | `j` / `k` | Scroll / move tree cursor |
181
192
  | `→` / `Enter` | Expand dir / open file |
182
193
  | `←` / `Esc` | Collapse / close source viewer |
194
+ | `b` | Branch switcher (GIT tab) |
183
195
  | `d` | Toggle dark / light mode |
184
196
  | `r` | Manual refresh |
185
197
  | `q` | Quit |
186
198
 
187
- ### Requirements
188
-
189
- - Node.js 18+
190
- - Claude Code installed and authenticated
191
- - Claude Pro or Max plan recommended (for real 5h / weekly usage %)
192
- - Git (optional, for GIT tab)
199
+ > Korean keyboard layout supported — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
193
200
 
194
201
  ### How it works
195
202
 
package/bin/claude-hud ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * claude-code-hud entry point
4
+ * Launches the Ink TUI for Claude Code token/git monitoring
5
+ */
6
+ import { spawn } from 'child_process';
7
+ import { existsSync } from 'fs';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __dir = dirname(fileURLToPath(import.meta.url));
12
+ const hudFile = join(__dir, '..', 'tui', 'hud.tsx');
13
+
14
+ // Use local tsx if available, otherwise try PATH
15
+ const localTsx = join(__dir, '..', 'node_modules', '.bin', 'tsx');
16
+ const tsxBin = existsSync(localTsx) ? localTsx : 'tsx';
17
+
18
+ const proc = spawn(tsxBin, [hudFile], {
19
+ stdio: 'inherit',
20
+ env: { ...process.env, CLAUDE_PROJECT_ROOT: process.env.CLAUDE_PROJECT_ROOT || process.cwd() },
21
+ });
22
+
23
+ proc.on('exit', (code) => process.exit(code ?? 0));
24
+ proc.on('error', (err) => {
25
+ if (err.code === 'ENOENT') {
26
+ console.error('tsx not found. Run: npm install -g tsx');
27
+ } else {
28
+ console.error('Failed to start HUD:', err.message);
29
+ }
30
+ process.exit(1);
31
+ });
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "claude-code-hud",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-hud": "./bin/claude-hud.js"
7
+ "claude-hud": "./bin/claude-hud.js",
8
+ "claude-code-hud": "./bin/claude-hud.js"
8
9
  },
9
10
  "keywords": [
10
11
  "claude",
package/tui/hud.tsx CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'url';
9
9
  import { dirname, join } from 'path';
10
10
  import fs from 'fs';
11
11
  import os from 'os';
12
+ import { execSync } from 'child_process';
12
13
 
13
14
  const __dir = dirname(fileURLToPath(import.meta.url));
14
15
  const { readTokenUsage, readTokenHistory } = await import(join(__dir, '../scripts/lib/token-reader.mjs'));
@@ -184,6 +185,18 @@ function flattenTree(node: DirNode, depth: number, expanded: Record<string, bool
184
185
  return result;
185
186
  }
186
187
 
188
+ // ── Branch helper ───────────────────────────────────────────────────────────
189
+ function getBranches(cwd: string): string[] {
190
+ try {
191
+ const out = execSync('git branch', { cwd, encoding: 'utf8' });
192
+ return out.split('\n')
193
+ .map(b => b.replace(/^\*?\s+/, '').trim())
194
+ .filter(Boolean);
195
+ } catch {
196
+ return [];
197
+ }
198
+ }
199
+
187
200
  // ── UI Components ──────────────────────────────────────────────────────────
188
201
  function Bar({ ratio, width, color, C }: { ratio: number; width: number; color: string; C: typeof DARK }) {
189
202
  const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
@@ -198,8 +211,8 @@ function Bar({ ratio, width, color, C }: { ratio: number; width: number; color:
198
211
  function Section({ title, children, C, accent }: { title: string; children: React.ReactNode; C: typeof DARK; accent?: string }) {
199
212
  return (
200
213
  <Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1} marginBottom={0}>
201
- <Text color={accent ?? C.dim} bold>{title}</Text>
202
- <Box flexDirection="column">{children}</Box>
214
+ <Text color={C.dimmer} bold>▸ <Text color={C.text}>{title}</Text></Text>
215
+ <Box flexDirection="column" marginTop={1}>{children}</Box>
203
216
  </Box>
204
217
  );
205
218
  }
@@ -247,12 +260,14 @@ function TokensTab({ usage, history, rateLimits, termWidth, C }: any) {
247
260
  const mins = Math.round((d.getTime() - Date.now()) / 60000);
248
261
  if (mins <= 0) return ' resets soon';
249
262
  if (mins < 60) return ` resets in ${mins}m`;
250
- return ` resets in ${Math.round(mins / 60)}h`;
263
+ const h = Math.floor(mins / 60);
264
+ const m = mins % 60;
265
+ return m > 0 ? ` resets in ${h}h ${m}m` : ` resets in ${h}h`;
251
266
  };
252
267
 
253
268
  return (
254
- <Section title={hasApi ? "USAGE WINDOW (Anthropic API)" : "USAGE WINDOW (from JSONL)"} C={C} accent={hasApi ? C.green : C.dim}>
255
- <Box>
269
+ <Section title="USAGE WINDOW" C={C}>
270
+ <Box marginBottom={1}>
256
271
  <Text color={C.dim}>5h </Text>
257
272
  <Bar ratio={(pct5h ?? 0) / 100} width={WIN_BAR} color={color5h} C={C} />
258
273
  <Text color={color5h} bold> {pct5h != null ? pct5h.toFixed(1) : '--'}%</Text>
@@ -285,7 +300,7 @@ function TokensTab({ usage, history, rateLimits, termWidth, C }: any) {
285
300
  ].map(({ label, tokens, color }) => {
286
301
  const pct = maxTok > 0 ? Math.round(tokens / maxTok * 100) : 0;
287
302
  return (
288
- <Box key={label}>
303
+ <Box key={label} marginBottom={1}>
289
304
  <Box width={14}><Text color={C.dim}>{label}</Text></Box>
290
305
  <Box width={BAR_W}><Bar ratio={maxTok > 0 ? tokens / maxTok : 0} width={BAR_W} color={color} C={C} /></Box>
291
306
  <Box width={9} justifyContent="flex-end"><Text color={C.text}> {fmtNum(tokens)}</Text></Box>
@@ -295,20 +310,32 @@ function TokensTab({ usage, history, rateLimits, termWidth, C }: any) {
295
310
  })}
296
311
  </Section>
297
312
 
298
- {/* Sparkline */}
299
- <Section title="OUTPUT TOKENS / HR" C={C}>
300
- <Text color={C.brand}>{spark}</Text>
301
- <Box justifyContent="space-between">
302
- <Text color={C.dimmer}>12h ago</Text>
303
- <Text color={C.dimmer}>now</Text>
304
- </Box>
305
- </Section>
313
+ {/* Output stats */}
314
+ {(() => {
315
+ const buckets = history.hourlyBuckets as number[];
316
+ const total = buckets.reduce((a: number, b: number) => a + b, 0);
317
+ const nonZero = buckets.filter((b: number) => b > 0);
318
+ const avg = nonZero.length > 0 ? Math.round(total / nonZero.length) : 0;
319
+ const peak = Math.max(...buckets, 0);
320
+ return (
321
+ <Section title="OUTPUT TOKENS / HR" C={C}>
322
+ <Box>
323
+ <Text color={C.dimmer}>total </Text>
324
+ <Text color={C.brand} bold>{fmtNum(total)}</Text>
325
+ <Text color={C.dimmer}> │ avg </Text>
326
+ <Text color={C.text}>{fmtNum(avg)}/hr</Text>
327
+ <Text color={C.dimmer}> │ peak </Text>
328
+ <Text color={C.text}>{fmtNum(peak)}/hr</Text>
329
+ </Box>
330
+ </Section>
331
+ );
332
+ })()}
306
333
  </Box>
307
334
  );
308
335
  }
309
336
 
310
337
  // ── Tab 2: PROJECT ─────────────────────────────────────────────────────────
311
- function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, C }: any) {
338
+ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, git, C }: any) {
312
339
  if (!info) return (
313
340
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
314
341
  <Text color={C.dimmer}>scanning project…</Text>
@@ -345,6 +372,11 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
345
372
  const SOURCE_W = hasFile ? termWidth - TREE_W - 5 : 0;
346
373
  const VISIBLE_LINES = 22;
347
374
 
375
+ // Git changed file sets
376
+ const gitModified = new Set<string>([...(git?.modified ?? []), ...(git?.added ?? []), ...(git?.deleted ?? [])]);
377
+ const gitAdded = new Set<string>(git?.added ?? []);
378
+ const gitDeleted = new Set<string>(git?.deleted ?? []);
379
+
348
380
  const EXT_COLOR: Record<string, string> = {
349
381
  '.ts': C.brand, '.tsx': C.brand, '.js': C.cyan, '.jsx': C.cyan,
350
382
  '.py': C.yellow, '.go': C.cyan, '.java': C.yellow, '.rs': C.red,
@@ -371,7 +403,8 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
371
403
 
372
404
  {/* ── Tree panel ── */}
373
405
  <Box flexDirection="column" borderStyle="single" borderColor={hasFile ? C.brand : C.border} paddingX={1} width={TREE_W}>
374
- <Text color={C.dim} bold>TREE</Text>
406
+ <Text color={C.dimmer} bold>▸ <Text color={C.text}>TREE</Text></Text>
407
+ <Box marginTop={1} flexDirection="column">
375
408
  {flatNodes.length === 0 && <Text color={C.dimmer}> (empty)</Text>}
376
409
  {flatNodes.map((fn, idx) => {
377
410
  const isSelected = idx === safeCursor;
@@ -391,23 +424,31 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
391
424
  );
392
425
  } else {
393
426
  const ext = fn.fileName.includes('.') ? '.' + fn.fileName.split('.').pop()! : '';
394
- const fileColor = isSelected ? C.brand : (EXT_COLOR[ext] ?? C.text);
395
427
  const isOpen = selectedFile === fn.filePath;
428
+ const isGitAdded = gitAdded.has(fn.filePath);
429
+ const isGitDeleted = gitDeleted.has(fn.filePath);
430
+ const isGitMod = !isGitAdded && !isGitDeleted && gitModified.has(fn.filePath);
431
+ const gitColor = isGitAdded ? C.green : isGitDeleted ? C.red : isGitMod ? C.yellow : null;
432
+ const gitBadge = isGitAdded ? ' A' : isGitDeleted ? ' D' : isGitMod ? ' M' : '';
433
+ const fileColor = isSelected ? C.brand : gitColor ?? (EXT_COLOR[ext] ?? C.text);
396
434
  return (
397
435
  <Box key={`f_${fn.filePath}_${idx}`}>
398
436
  <Text color={C.dimmer}>{indent}</Text>
399
437
  <Text color={isSelected ? C.brand : C.dimmer}>{isOpen ? '▶ ' : ' '}</Text>
400
438
  <Text color={fileColor} bold={isSelected || isOpen}>{fn.fileName}</Text>
439
+ {gitBadge ? <Text color={gitColor!} bold>{gitBadge}</Text> : null}
401
440
  </Box>
402
441
  );
403
442
  }
404
443
  })}
444
+ </Box>
405
445
  </Box>
406
446
 
407
447
  {/* ── Source viewer panel ── */}
408
448
  {hasFile && (
409
449
  <Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} width={SOURCE_W}>
410
- <Text color={C.brand} bold>SOURCE <Text color={C.dim}>{selectedFile}</Text></Text>
450
+ <Text color={C.dimmer} bold>▸ <Text color={C.text}>SOURCE <Text color={C.dim}>{selectedFile}</Text></Text></Text>
451
+ <Box marginTop={1} flexDirection="column">
411
452
  {(fileLines as string[]).slice(fileScroll, fileScroll + VISIBLE_LINES).map((line, i) => {
412
453
  const lineNo = fileScroll + i + 1;
413
454
  const truncated = line.length > SOURCE_W - 6 ? line.slice(0, SOURCE_W - 7) + '…' : line;
@@ -424,6 +465,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
424
465
  {(fileLines as string[]).length > VISIBLE_LINES && (
425
466
  <Text color={C.dimmer}> ↕ {fileScroll + 1}–{Math.min(fileScroll + VISIBLE_LINES, fileLines.length)} / {fileLines.length} lines [j/k] scroll [esc] close</Text>
426
467
  )}
468
+ </Box>
427
469
  </Box>
428
470
  )}
429
471
  </Box>
@@ -431,7 +473,8 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
431
473
  {/* Packages (hidden when file open to save space) */}
432
474
  {!hasFile && (
433
475
  <Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1}>
434
- <Text color={C.dim} bold>PACKAGES</Text>
476
+ <Text color={C.dimmer} bold>▸ <Text color={C.text}>PACKAGES</Text></Text>
477
+ <Box marginTop={1} flexDirection="column">
435
478
  {info.packages.slice(0, 10).map((p: any, i: number) => {
436
479
  const isRoot = p.depth === 0;
437
480
  const nextIsRoot = i + 1 < info.packages.length && info.packages[i + 1].depth === 0;
@@ -445,6 +488,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
445
488
  </Box>
446
489
  );
447
490
  })}
491
+ </Box>
448
492
  </Box>
449
493
  )}
450
494
  </Box>
@@ -452,7 +496,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
452
496
  }
453
497
 
454
498
  // ── Tab 3: GIT ─────────────────────────────────────────────────────────────
455
- function GitTab({ git, C, termWidth }: any) {
499
+ function GitTab({ git, C, termWidth, branchMode, branchList, branchCursor }: any) {
456
500
  const gitFiles = [
457
501
  ...(git.modified ?? []).map((f: string) => ({ status: 'MOD', path: f })),
458
502
  ...(git.added ?? []).map((f: string) => ({ status: 'ADD', path: f })),
@@ -463,6 +507,29 @@ function GitTab({ git, C, termWidth }: any) {
463
507
 
464
508
  return (
465
509
  <Box flexDirection="column">
510
+ {/* Branch switcher overlay */}
511
+ {branchMode && (
512
+ <Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} marginBottom={1}>
513
+ <Text color={C.dimmer} bold>▸ <Text color={C.text}>SWITCH BRANCH</Text></Text>
514
+ <Box flexDirection="column" marginTop={1}>
515
+ {branchList.map((b: string, i: number) => {
516
+ const isSelected = i === branchCursor;
517
+ const isCurrent = b === git.branch;
518
+ return (
519
+ <Box key={i}>
520
+ <Text color={isSelected ? C.brand : C.dimmer}>{isSelected ? '▶ ' : ' '}</Text>
521
+ <Text color={isSelected ? C.text : C.dim} bold={isSelected}>{b}</Text>
522
+ {isCurrent && <Text color={C.green}> ◎ current</Text>}
523
+ </Box>
524
+ );
525
+ })}
526
+ </Box>
527
+ <Box marginTop={1}>
528
+ <Text color={C.dimmer}>[j/k] navigate [enter] switch [esc] cancel</Text>
529
+ </Box>
530
+ </Box>
531
+ )}
532
+
466
533
  {/* Branch */}
467
534
  <Box borderStyle="single" borderColor={C.border} paddingX={1}>
468
535
  <Text color={C.dim} bold>GIT </Text>
@@ -504,7 +571,7 @@ function GitTab({ git, C, termWidth }: any) {
504
571
  : f.status === 'DEL' ? barTotal : f.status === 'MOD' ? Math.round(barTotal * 0.3) : 0;
505
572
  const name = f.path.length > 22 ? '…' + f.path.slice(-21) : f.path;
506
573
  return (
507
- <Box key={i}>
574
+ <Box key={i} marginBottom={1}>
508
575
  <Box width={24}><Text color={C.dimmer}>{name}</Text></Box>
509
576
  <Text color={C.green}>{'▐'.repeat(addLen)}</Text>
510
577
  <Text color={C.red}>{'▌'.repeat(delLen)}</Text>
@@ -560,6 +627,11 @@ function App() {
560
627
  const [fileLines, setFileLines] = useState<string[]>([]);
561
628
  const [fileScroll, setFileScroll] = useState(0);
562
629
 
630
+ // Branch switcher state
631
+ const [branchMode, setBranchMode] = useState(false);
632
+ const [branchList, setBranchList] = useState<string[]>([]);
633
+ const [branchCursor, setBranchCursor] = useState(0);
634
+
563
635
  const refresh = useCallback(() => {
564
636
  setUsage(readTokenUsage());
565
637
  setHistory(readTokenHistory());
@@ -605,6 +677,44 @@ function App() {
605
677
  }, []);
606
678
 
607
679
  useInput((input, key) => {
680
+ // Branch switcher intercepts input when active
681
+ if (branchMode) {
682
+ if (input === 'j' || key.downArrow) {
683
+ setBranchCursor(c => Math.min(c + 1, branchList.length - 1));
684
+ return;
685
+ }
686
+ if (input === 'k' || key.upArrow) {
687
+ setBranchCursor(c => Math.max(c - 1, 0));
688
+ return;
689
+ }
690
+ if (key.return) {
691
+ const selected = branchList[branchCursor];
692
+ if (selected && selected !== git.branch) {
693
+ try {
694
+ execSync(`git checkout ${selected}`, { cwd });
695
+ refresh();
696
+ } catch {}
697
+ }
698
+ setBranchMode(false);
699
+ return;
700
+ }
701
+ if (key.escape || input === 'q' || input === 'ㅂ') {
702
+ setBranchMode(false);
703
+ return;
704
+ }
705
+ return;
706
+ }
707
+
708
+ // b (or Korean ㅠ) = open branch switcher in GIT tab
709
+ if ((input === 'b' || input === 'ㅠ') && tab === 2) {
710
+ const branches = getBranches(cwd);
711
+ setBranchList(branches);
712
+ const idx = branches.findIndex(b => b === git.branch);
713
+ setBranchCursor(idx >= 0 ? idx : 0);
714
+ setBranchMode(true);
715
+ return;
716
+ }
717
+
608
718
  if (input === 'q' || input === 'ㅂ') process.exit(0);
609
719
 
610
720
  // Escape: close file viewer first, then quit
@@ -696,13 +806,17 @@ function App() {
696
806
  <Box flexDirection="column">
697
807
 
698
808
  {/* ── Header / Tab bar ── */}
699
- <Box borderStyle="single" borderColor={C.brand} paddingX={1} justifyContent="space-between">
809
+ <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">
700
810
  <Box>
701
- <Text color={C.brand} bold>◆ HUD </Text>
811
+ <Text color={C.brand} bold>◆ HUD</Text>
702
812
  {TAB_NAMES.map((name, i) => (
703
- <Text key={i} color={tab === i ? C.text : C.dimmer} bold={tab === i}>
704
- {tab === i ? `[${i + 1} ${name}]` : ` ${i + 1} ${name} `}
705
- </Text>
813
+ <React.Fragment key={i}>
814
+ <Text color={C.border}> │ </Text>
815
+ <Text color={tab === i ? C.brand : C.dimmer} bold={tab === i}>
816
+ {tab === i ? '◉ ' : '○ '}
817
+ </Text>
818
+ <Text color={tab === i ? C.text : C.dimmer} bold={tab === i}>{name}</Text>
819
+ </React.Fragment>
706
820
  ))}
707
821
  </Box>
708
822
  <Box>
@@ -714,8 +828,8 @@ function App() {
714
828
  {/* ── Content (with scroll offset) ── */}
715
829
  <Box flexDirection="column" marginTop={-scrollY}>
716
830
  {tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} C={C} />}
717
- {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} C={C} />}
718
- {tab === 2 && <GitTab git={git} termWidth={termWidth} C={C} />}
831
+ {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} git={git} C={C} />}
832
+ {tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
719
833
  </Box>
720
834
 
721
835
  {/* ── Footer ── */}
@@ -725,6 +839,7 @@ function App() {
725
839
  <Text color={C.dimmer}>[1/2/3] tabs </Text>
726
840
  <Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
727
841
  <Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc/←] close [j/k] scroll ' : '[enter] open [→←] expand ') : ''}</Text>
842
+ {tab === 2 && !branchMode && <Text color={C.brand}>[b] branch </Text>}
728
843
  <Text color={C.dimmer}>[r] refresh [d] theme [q] quit</Text>
729
844
  </Box>
730
845
  <Text color={C.dimmer}>↻ {since}</Text>