claude-code-hud 0.3.0 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +154 -123
  2. package/package.json +1 -1
  3. package/tui/hud.tsx +152 -34
package/README.md CHANGED
@@ -1,179 +1,210 @@
1
1
  # claude-code-hud
2
2
 
3
- 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.
3
+ [한국어](#한국어) | [English](#english)
4
+
5
+ ---
6
+
7
+ ## 한국어
8
+
9
+ Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE나 별도 탭 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
4
10
 
5
11
  ```
6
12
  ┌──────────────────────────────────────────────────────────────────────────────┐
7
- │ ◆ HUD [1 TOKENS] 2 PROJECT 3 GIT sonnet-4-6 · up 4m │
13
+ │ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
8
14
  ├──────────────────────────────────────────────────────────────────────────────┤
9
- │ CONTEXT WINDOW
10
- ████████████████████░░░░░░░░░░░░░░░░░░░░░░░ 46% 92K / 200K OK
11
- ├──────────────────────────────────────────────────────────────────────────────┤
12
- │ USAGE WINDOW (Anthropic API)
13
- 5h ████████░░░░░░░░░░░░░░░░░░░░ 28.0% resets in 3h
14
- wk ███░░░░░░░░░░░░░░░░░░░░░░░░░ 9.0% resets in 148h
15
- ├──────────────────────────────────────────────────────────────────────────────┤
16
- │ TOKENS (this session)
17
- input ░░░░░░░░░░░░░░░░░░░░░░░░ 4.8K 0%
18
- output ░░░░░░░░░░░░░░░░░░░░░░░░ 188.5K 0%
19
- cache-read ████████████████████████ 51.5M 100%
20
- 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 │
21
30
  └──────────────────────────────────────────────────────────────────────────────┘
22
31
  ```
23
32
 
24
- ---
25
-
26
- ## Features
33
+ ### 사용법
27
34
 
28
- ### 1 TOKENS tab
29
- - Context window usage gauge with percentage (OK / MID / WARN)
30
- - **5h and weekly usage** from Anthropic OAuth API — real percentages, not estimates
31
- - Input / output / cache-read / cache-write token breakdown with bars
32
- - Output tokens sparkline (▁▂▃▄▅▆▇█) over the last 12 hours
33
- - Model name and session uptime
34
-
35
- ### 2 PROJECT tab — interactive file browser
36
- - Directory tree with `▶`/`▼` expand/collapse
37
- - **Source file viewer** — select any file and read its contents in a split panel
38
- - File count per directory, extension-based color coding
39
- - Package dependency tree from `package.json`
40
- - API endpoint detection (GET / POST / PUT / DELETE / PATCH)
35
+ 터미널 개를 열고 같은 프로젝트 디렉토리에서 실행하면 됩니다.
41
36
 
42
37
  ```
43
- TREE │ SOURCE src/index.ts
44
- ▼ src/ 23f │ 1 import React from 'react'
45
- components/ 8f │ 2 import { render } from 'ink'
46
- Header.tsx ◀ open │ 3
47
- hooks/ 4f │ 4 render(<App />)
48
- ▶ scripts/ 6f │ … [j/k] scroll [esc] close
38
+ 터미널 A 터미널 B
39
+ ───────────────────────────── ─────────────────────────────
40
+ cd ~/my-project cd ~/my-project
41
+ claude npx claude-code-hud
42
+ (Claude Code 작업 중...) (HUD 실시간 표시)
49
43
  ```
50
44
 
51
- ### 3 GIT tab
52
- - Current branch, ahead/behind remote counts
53
- - Changed file list (MOD / ADD / DEL)
54
- - Per-file diff visualization with real `+N -N` line counts
55
- - Recent commit history with hash, message, and relative time
56
-
57
- ---
58
-
59
- ## Installation
60
-
61
- ### Option 1 — npx (no install required)
45
+ HUD는 현재 디렉토리를 기준으로 토큰, git, 프로젝트 정보를 자동으로 인식합니다.
62
46
 
63
47
  ```bash
64
- npx claude-code-hud
48
+ # tmux로 한 화면에서 split
49
+ cd ~/my-project
50
+ tmux split-window -h "npx claude-code-hud"
65
51
  ```
66
52
 
67
- ### Option 2 — npm global install
53
+ ### 설치
68
54
 
69
55
  ```bash
56
+ # 설치 없이 바로 실행
57
+ npx claude-code-hud
58
+
59
+ # 전역 설치
70
60
  npm install -g claude-code-hud
71
61
  claude-hud
72
- ```
73
62
 
74
- ### Option 3 — Claude Code Plugin
75
-
76
- ```bash
63
+ # Claude Code 플러그인
77
64
  /plugin install letsgojh0810/hud-plugin
78
65
  ```
79
66
 
80
- ---
67
+ ### 기능
81
68
 
82
- ## Usage
69
+ **1 TOKENS 탭**
70
+ - 컨텍스트 윈도우 사용량 게이지 (OK / MID / WARN) — 사용량에 따라 헤더 색상 변경
71
+ - Anthropic API 기반 5h / 주간 사용률 (실제 값, 추정치 아님) — `1h 23m` 형식으로 리셋까지 남은 시간 표시
72
+ - input / output / cache-read / cache-write 토큰 분류
73
+ - 세션 output 통계 (total / avg / peak)
83
74
 
84
- Run in a **separate terminal window** or **tmux split pane** while Claude Code is active:
75
+ **2 PROJECT 인터랙티브 파일 브라우저**
76
+ - 디렉토리 트리 (펼치기/접기)
77
+ - Git 변경 파일 색상 표시 — 수정(노란색 M) / 추가(초록 A) / 삭제(빨강 D)
78
+ - 파일 선택 시 소스 코드 뷰어 (split 패널)
79
+ - 패키지 의존성 트리
80
+ - API 엔드포인트 감지
85
81
 
86
- ```bash
87
- # Separate terminal — run from your project directory
88
- cd ~/my-project
89
- npx claude-code-hud
90
-
91
- # tmux split pane
92
- tmux split-window -h "cd ~/my-project && npx claude-code-hud"
93
-
94
- # Specify project root explicitly
95
- CLAUDE_PROJECT_ROOT=/path/to/project npx claude-code-hud
82
+ ```
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
96
89
  ```
97
90
 
98
- ---
99
-
100
- ## Keyboard Shortcuts
101
-
102
- ### Global
91
+ **3 GIT 탭**
92
+ - 현재 브랜치, ahead/behind 카운트
93
+ - 변경 파일 목록 (MOD / ADD / DEL) + 실제 +/- 라인 수
94
+ - 파일별 diff 시각화
95
+ - 최근 커밋 히스토리
96
+ - **브랜치 전환** — `b` 키로 로컬 브랜치 목록 표시, 선택해서 바로 checkout
103
97
 
104
- | Key | Action |
105
- |---------|--------------------------|
106
- | `1` | Switch to TOKENS tab |
107
- | `2` | Switch to PROJECT tab |
108
- | `3` | Switch to GIT tab |
109
- | `d` | Toggle dark / light mode |
110
- | `r` | Manual refresh |
111
- | `q` | Quit |
98
+ ### 키보드 단축키
112
99
 
113
- ### TOKENS / GIT tab
100
+ | | 동작 |
101
+ |----|------|
102
+ | `1` `2` `3` | 탭 전환 |
103
+ | `j` / `k` | 스크롤 / 트리 이동 |
104
+ | `→` / `Enter` | 디렉토리 펼치기 / 파일 열기 |
105
+ | `←` / `Esc` | 접기 / 소스 뷰어 닫기 |
106
+ | `b` | 브랜치 전환 (GIT 탭) |
107
+ | `d` | 다크 / 라이트 모드 전환 |
108
+ | `r` | 수동 새로고침 |
109
+ | `q` | 종료 |
114
110
 
115
- | Key | Action |
116
- |---------|--------------|
117
- | `j` / `↓` | Scroll down |
118
- | `k` / `↑` | Scroll up |
111
+ > 한글 키보드 모드에서도 동작합니다 — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
119
112
 
120
- ### PROJECT tab — file browser
113
+ ### 요구사항
121
114
 
122
- | Key | Action |
123
- |--------------|-------------------------------|
124
- | `j` / `↓` | Move cursor down |
125
- | `k` / `↑` | Move cursor up |
126
- | `→` / `Enter`| Expand directory |
127
- | `←` | Collapse directory / close viewer |
128
- | `Enter` on file | Open source viewer |
129
- | `Esc` | Close source viewer |
130
- | `j` / `k` | Scroll source (when open) |
115
+ - Node.js 18+
116
+ - Claude Code 설치 및 로그인 (토큰 데이터 수집)
117
+ - Claude Pro / Max 플랜 권장 (5h / 주간 사용률 표시)
118
+ - Git (GIT 사용 시)
131
119
 
132
120
  ---
133
121
 
134
- ## Requirements
122
+ ## English
135
123
 
136
- - **Node.js 18+**
137
- - **Claude Code** installed and authenticated (for token data)
138
- - **Claude Pro or Max plan** recommended — enables real 5h/weekly usage % from Anthropic API
139
- - Git (optional, for GIT tab)
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.
140
125
 
141
- ---
126
+ ```
127
+ ┌──────────────────────────────────────────────────────────────────────────────┐
128
+ │ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
129
+ └──────────────────────────────────────────────────────────────────────────────┘
130
+ ```
142
131
 
143
- ## Environment Variables
132
+ ### Usage
144
133
 
145
- | Variable | Default | Description |
146
- |-----------------------|-----------------|------------------------------------------|
147
- | `CLAUDE_PROJECT_ROOT` | `process.cwd()` | Project root directory to monitor |
134
+ Open two terminals in the same project directory.
148
135
 
149
- ---
136
+ ```
137
+ Terminal A Terminal B
138
+ ───────────────────────────── ─────────────────────────────
139
+ cd ~/my-project cd ~/my-project
140
+ claude npx claude-code-hud
141
+ (working with Claude Code) (HUD live display)
142
+ ```
150
143
 
151
- ## How it works
144
+ ```bash
145
+ # tmux split pane
146
+ cd ~/my-project
147
+ tmux split-window -h "npx claude-code-hud"
148
+ ```
152
149
 
153
- - **Token data**: Watches `~/.claude/projects/*/sessions/*.jsonl` with chokidar — updates instantly when Claude responds
154
- - **Usage window**: Calls `api.anthropic.com/api/oauth/usage` with your local Claude credentials (same as Claude Code uses) — cached 5 min
155
- - **Git status**: Polls git every 3 seconds
156
- - **Project scan**: One-time fast-glob scan on startup, `r` to rescan
150
+ ### Installation
157
151
 
158
- ---
152
+ ```bash
153
+ # No install — run directly
154
+ npx claude-code-hud
159
155
 
160
- ## Color Theme
156
+ # Global install
157
+ npm install -g claude-code-hud
158
+ claude-hud
161
159
 
162
- Toss Blue (`#3182F6`) based palette. Full dark and light mode — toggle with `d`.
160
+ # Claude Code plugin
161
+ /plugin install letsgojh0810/hud-plugin
162
+ ```
163
163
 
164
- ---
164
+ ### Features
165
165
 
166
- ## Development
166
+ **1 TOKENS tab**
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`
169
+ - Input / output / cache-read / cache-write breakdown
170
+ - Session output stats: total / avg / peak per hour
167
171
 
168
- ```bash
169
- git clone https://github.com/letsgojh0810/hud-plugin.git
170
- cd hud-plugin
171
- npm install
172
- npm run hud
173
- ```
172
+ **2 PROJECT tab — interactive file browser**
173
+ - Navigable directory tree with expand/collapse
174
+ - Git-changed files highlighted — modified (yellow M) / added (green A) / deleted (red D)
175
+ - Source file viewer in a split panel
176
+ - Package dependency tree from `package.json`
177
+ - API endpoint detection (GET / POST / PUT / DELETE / PATCH)
174
178
 
175
- ---
179
+ **3 GIT tab**
180
+ - Branch status, ahead/behind remote
181
+ - Changed file list (MOD / ADD / DEL) with real `+N -N` diff counts
182
+ - Per-file diff visualization
183
+ - Recent commit history
184
+ - **Branch switcher** — press `b` to list local branches and checkout instantly
185
+
186
+ ### Keyboard Shortcuts
187
+
188
+ | Key | Action |
189
+ |-----|--------|
190
+ | `1` `2` `3` | Switch tabs |
191
+ | `j` / `k` | Scroll / move tree cursor |
192
+ | `→` / `Enter` | Expand dir / open file |
193
+ | `←` / `Esc` | Collapse / close source viewer |
194
+ | `b` | Branch switcher (GIT tab) |
195
+ | `d` | Toggle dark / light mode |
196
+ | `r` | Manual refresh |
197
+ | `q` | Quit |
198
+
199
+ > Korean keyboard layout supported — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
200
+
201
+ ### How it works
202
+
203
+ - **Token data**: Watches `~/.claude/projects/*/sessions/*.jsonl` with chokidar — updates instantly on each Claude response
204
+ - **Usage window**: Calls `api.anthropic.com/api/oauth/usage` using local Claude credentials — cached 5 min
205
+ - **Git**: Polls every 3 seconds
206
+ - **Project scan**: One-time fast-glob scan on startup, `r` to rescan
176
207
 
177
- ## License
208
+ ---
178
209
 
179
210
  MIT — [letsgojh0810](https://github.com/letsgojh0810)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-hud",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Terminal HUD for Claude Code — real-time token usage, git status, project monitor",
5
5
  "type": "module",
6
6
  "bin": {
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());
@@ -574,7 +646,10 @@ function App() {
574
646
  // Initial API usage fetch
575
647
  getUsage().then(setRateLimits).catch(() => {});
576
648
 
577
- const onResize = () => setTermWidth(stdout?.columns ?? 80);
649
+ const onResize = () => {
650
+ process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
651
+ setTermWidth(stdout?.columns ?? 80);
652
+ };
578
653
  stdout?.on('resize', onResize);
579
654
 
580
655
  const poll = setInterval(refresh, 3000);
@@ -602,7 +677,45 @@ function App() {
602
677
  }, []);
603
678
 
604
679
  useInput((input, key) => {
605
- if (input === 'q') process.exit(0);
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
+
718
+ if (input === 'q' || input === 'ㅂ') process.exit(0);
606
719
 
607
720
  // Escape: close file viewer first, then quit
608
721
  if (key.escape) {
@@ -613,17 +726,17 @@ function App() {
613
726
  if (input === '1') { setTab(0); setScrollY(0); }
614
727
  if (input === '2') { setTab(1); setScrollY(0); }
615
728
  if (input === '3') { setTab(2); setScrollY(0); }
616
- if (input === 'd') setDark(d => !d);
729
+ if (input === 'd' || input === 'ㅇ') setDark(d => !d);
617
730
 
618
731
  // r = manual refresh
619
- if (input === 'r') {
732
+ if (input === 'r' || input === 'ㄱ') {
620
733
  refresh();
621
734
  setProject(null);
622
735
  setSelectedFile(null); setFileLines([]); setFileScroll(0);
623
736
  scanProject(cwd).then(p => { setProject(p); setTreeCursor(0); }).catch(() => {});
624
737
  }
625
738
 
626
- if (input === 'j' || key.downArrow) {
739
+ if (input === 'j' || input === 'ㅓ' || key.downArrow) {
627
740
  if (tab === 1 && selectedFile) {
628
741
  setFileScroll(s => Math.min(s + 1, Math.max(0, fileLines.length - 5)));
629
742
  } else if (tab === 1) {
@@ -633,7 +746,7 @@ function App() {
633
746
  setScrollY(s => Math.min(s + 1, 20));
634
747
  }
635
748
  }
636
- if (input === 'k' || key.upArrow) {
749
+ if (input === 'k' || input === 'ㅏ' || key.upArrow) {
637
750
  if (tab === 1 && selectedFile) {
638
751
  setFileScroll(s => Math.max(s - 1, 0));
639
752
  } else if (tab === 1) {
@@ -693,13 +806,17 @@ function App() {
693
806
  <Box flexDirection="column">
694
807
 
695
808
  {/* ── Header / Tab bar ── */}
696
- <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">
697
810
  <Box>
698
- <Text color={C.brand} bold>◆ HUD </Text>
811
+ <Text color={C.brand} bold>◆ HUD</Text>
699
812
  {TAB_NAMES.map((name, i) => (
700
- <Text key={i} color={tab === i ? C.text : C.dimmer} bold={tab === i}>
701
- {tab === i ? `[${i + 1} ${name}]` : ` ${i + 1} ${name} `}
702
- </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>
703
820
  ))}
704
821
  </Box>
705
822
  <Box>
@@ -711,8 +828,8 @@ function App() {
711
828
  {/* ── Content (with scroll offset) ── */}
712
829
  <Box flexDirection="column" marginTop={-scrollY}>
713
830
  {tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} C={C} />}
714
- {tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} C={C} />}
715
- {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} />}
716
833
  </Box>
717
834
 
718
835
  {/* ── Footer ── */}
@@ -722,6 +839,7 @@ function App() {
722
839
  <Text color={C.dimmer}>[1/2/3] tabs </Text>
723
840
  <Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
724
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>}
725
843
  <Text color={C.dimmer}>[r] refresh [d] theme [q] quit</Text>
726
844
  </Box>
727
845
  <Text color={C.dimmer}>↻ {since}</Text>