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.
- package/README.md +154 -123
- package/package.json +1 -1
- package/tui/hud.tsx +152 -34
package/README.md
CHANGED
|
@@ -1,179 +1,210 @@
|
|
|
1
1
|
# claude-code-hud
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[한국어](#한국어) | [English](#english)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 한국어
|
|
8
|
+
|
|
9
|
+
Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE나 별도 탭 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
|
|
4
10
|
|
|
5
11
|
```
|
|
6
12
|
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
7
|
-
│ ◆
|
|
13
|
+
│ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
|
|
8
14
|
├──────────────────────────────────────────────────────────────────────────────┤
|
|
9
|
-
│ CONTEXT WINDOW
|
|
10
|
-
│
|
|
11
|
-
|
|
12
|
-
│ USAGE WINDOW
|
|
13
|
-
│
|
|
14
|
-
│
|
|
15
|
-
|
|
16
|
-
│ TOKENS (this session)
|
|
17
|
-
│
|
|
18
|
-
│
|
|
19
|
-
│
|
|
20
|
-
│
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
# tmux로 한 화면에서 split
|
|
49
|
+
cd ~/my-project
|
|
50
|
+
tmux split-window -h "npx claude-code-hud"
|
|
65
51
|
```
|
|
66
52
|
|
|
67
|
-
###
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
```bash
|
|
63
|
+
# Claude Code 플러그인
|
|
77
64
|
/plugin install letsgojh0810/hud-plugin
|
|
78
65
|
```
|
|
79
66
|
|
|
80
|
-
|
|
67
|
+
### 기능
|
|
81
68
|
|
|
82
|
-
|
|
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
|
-
|
|
75
|
+
**2 PROJECT 탭 — 인터랙티브 파일 브라우저**
|
|
76
|
+
- 디렉토리 트리 (펼치기/접기)
|
|
77
|
+
- Git 변경 파일 색상 표시 — 수정(노란색 M) / 추가(초록 A) / 삭제(빨강 D)
|
|
78
|
+
- 파일 선택 시 소스 코드 뷰어 (split 패널)
|
|
79
|
+
- 패키지 의존성 트리
|
|
80
|
+
- API 엔드포인트 감지
|
|
85
81
|
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
**3 GIT 탭**
|
|
92
|
+
- 현재 브랜치, ahead/behind 카운트
|
|
93
|
+
- 변경 파일 목록 (MOD / ADD / DEL) + 실제 +/- 라인 수
|
|
94
|
+
- 파일별 diff 시각화
|
|
95
|
+
- 최근 커밋 히스토리
|
|
96
|
+
- **브랜치 전환** — `b` 키로 로컬 브랜치 목록 표시, 선택해서 바로 checkout
|
|
103
97
|
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|---------|--------------|
|
|
117
|
-
| `j` / `↓` | Scroll down |
|
|
118
|
-
| `k` / `↑` | Scroll up |
|
|
111
|
+
> 한글 키보드 모드에서도 동작합니다 — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
|
|
119
112
|
|
|
120
|
-
###
|
|
113
|
+
### 요구사항
|
|
121
114
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
##
|
|
122
|
+
## English
|
|
135
123
|
|
|
136
|
-
-
|
|
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
|
-
|
|
132
|
+
### Usage
|
|
144
133
|
|
|
145
|
-
|
|
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
|
-
|
|
144
|
+
```bash
|
|
145
|
+
# tmux split pane
|
|
146
|
+
cd ~/my-project
|
|
147
|
+
tmux split-window -h "npx claude-code-hud"
|
|
148
|
+
```
|
|
152
149
|
|
|
153
|
-
|
|
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
|
-
|
|
156
|
+
# Global install
|
|
157
|
+
npm install -g claude-code-hud
|
|
158
|
+
claude-hud
|
|
161
159
|
|
|
162
|
-
|
|
160
|
+
# Claude Code plugin
|
|
161
|
+
/plugin install letsgojh0810/hud-plugin
|
|
162
|
+
```
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
### Features
|
|
165
165
|
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
208
|
+
---
|
|
178
209
|
|
|
179
210
|
MIT — [letsgojh0810](https://github.com/letsgojh0810)
|
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());
|
|
@@ -574,7 +646,10 @@ function App() {
|
|
|
574
646
|
// Initial API usage fetch
|
|
575
647
|
getUsage().then(setRateLimits).catch(() => {});
|
|
576
648
|
|
|
577
|
-
const onResize = () =>
|
|
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
|
-
|
|
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>◆
|
|
811
|
+
<Text color={C.brand} bold>◆ HUD</Text>
|
|
699
812
|
{TAB_NAMES.map((name, i) => (
|
|
700
|
-
<
|
|
701
|
-
|
|
702
|
-
|
|
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>
|