claude-code-hud 0.3.1 → 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.
- package/README.md +46 -39
- package/bin/claude-hud +31 -0
- package/package.json +1 -1
- package/tui/hud.tsx +143 -28
package/README.md
CHANGED
|
@@ -10,20 +10,23 @@ Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE
|
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
13
|
-
│ ◆
|
|
13
|
+
│ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
|
|
14
14
|
├──────────────────────────────────────────────────────────────────────────────┤
|
|
15
|
-
│ CONTEXT WINDOW
|
|
16
|
-
│
|
|
17
|
-
|
|
18
|
-
│ USAGE WINDOW
|
|
19
|
-
│
|
|
20
|
-
│
|
|
21
|
-
|
|
22
|
-
│ TOKENS (this session)
|
|
23
|
-
│
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
│
|
|
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
|
-
-
|
|
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
|
|
80
|
-
▼ src/ 23f
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
▶ scripts/ 6f
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
-
|
|
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
|
-
|
|
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
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={
|
|
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
|
-
|
|
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=
|
|
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
|
-
{/*
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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>◆
|
|
811
|
+
<Text color={C.brand} bold>◆ HUD</Text>
|
|
702
812
|
{TAB_NAMES.map((name, i) => (
|
|
703
|
-
<
|
|
704
|
-
|
|
705
|
-
|
|
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>
|