claude-code-hud 0.3.6 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -18
- package/package.json +1 -1
- package/scripts/lib/token-reader.mjs +8 -5
- package/tui/hud.tsx +118 -29
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 한국어
|
|
8
8
|
|
|
9
|
-
Claude Code로 작업할 때 토큰 사용량, git 상태, 파일
|
|
9
|
+
Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조, 세션 히스토리를 IDE나 별도 툴 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
|
|
10
10
|
|
|
11
11
|
<img src="./demo.gif" width="700" alt="demo">
|
|
12
12
|
|
|
@@ -22,6 +22,8 @@ claude npx claude-code-hud
|
|
|
22
22
|
(Claude Code 작업 중...) (HUD 실시간 표시)
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
<img src="./capture.png" width="700" alt="side-by-side terminals">
|
|
26
|
+
|
|
25
27
|
HUD는 현재 디렉토리를 기준으로 토큰, git, 프로젝트 정보를 자동으로 인식합니다.
|
|
26
28
|
|
|
27
29
|
```bash
|
|
@@ -33,21 +35,22 @@ tmux split-window -h "npx claude-code-hud"
|
|
|
33
35
|
### 설치
|
|
34
36
|
|
|
35
37
|
```bash
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
+
# 방법1. 전역 설치 (권장)
|
|
39
|
+
> npm install -g claude-code-hud
|
|
40
|
+
> claude-hud
|
|
38
41
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
claude-hud
|
|
42
|
+
# 방법2. 설치 없이 바로 실행
|
|
43
|
+
> npx claude-code-hud
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
### 기능
|
|
45
47
|
|
|
46
48
|
**1 TOKENS 탭**
|
|
47
49
|
- 컨텍스트 윈도우 사용량 게이지 (OK / MID / WARN) — 사용량에 따라 헤더 색상 변경
|
|
48
|
-
- Anthropic API 기반 5h / 주간 사용률
|
|
50
|
+
- Anthropic API 기반 5h / 주간 사용률 — `1h 23m` 형식으로 리셋까지 남은 시간 표시
|
|
49
51
|
- input / output / cache-read / cache-write 토큰 분류
|
|
50
52
|
- 세션 output 통계 (total / avg / peak)
|
|
53
|
+
- `now` — 현재 진행 중인 작업 (마지막 사용자 메시지) 한 줄 표시
|
|
51
54
|
|
|
52
55
|
**2 PROJECT 탭 — 인터랙티브 파일 브라우저**
|
|
53
56
|
- 디렉토리 트리 (펼치기/접기)
|
|
@@ -72,16 +75,20 @@ claude-hud
|
|
|
72
75
|
- 최근 커밋 히스토리
|
|
73
76
|
- **브랜치 전환** — `b` 키로 로컬 브랜치 목록 표시, 선택해서 바로 checkout
|
|
74
77
|
|
|
78
|
+
**4 TIMELINE 탭**
|
|
79
|
+
- 현재 세션에서 사용자가 입력한 메시지 히스토리
|
|
80
|
+
- 시간 + 내용 표시, 10개씩 j/k 스크롤
|
|
81
|
+
|
|
75
82
|
### 키보드 단축키
|
|
76
83
|
|
|
77
84
|
| 키 | 동작 |
|
|
78
85
|
|----|------|
|
|
79
|
-
| `1` `2` `3` | 탭 전환 |
|
|
86
|
+
| `1` `2` `3` `4` | 탭 전환 |
|
|
80
87
|
| `j` / `k` | 스크롤 / 트리 이동 |
|
|
81
88
|
| `→` / `Enter` | 디렉토리 펼치기 / 파일 열기 |
|
|
82
89
|
| `←` / `Esc` | 접기 / 소스 뷰어 닫기 |
|
|
83
90
|
| `b` | 브랜치 전환 (GIT 탭) |
|
|
84
|
-
| `d` |
|
|
91
|
+
| `d` | 액센트 색상 변경 (blue → red → amber → green → pink) |
|
|
85
92
|
| `r` | 수동 새로고침 |
|
|
86
93
|
| `q` | 종료 |
|
|
87
94
|
|
|
@@ -115,7 +122,7 @@ Claude Code를 한 번 실행하면 `~/.claude/.credentials.json`에 credentials
|
|
|
115
122
|
|
|
116
123
|
## English
|
|
117
124
|
|
|
118
|
-
A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status,
|
|
125
|
+
A Terminal HUD (Heads-Up Display) for Claude Code — real-time token usage, git status, interactive project file browser, and session history. No IDE, no extra tabs. Just a second terminal window.
|
|
119
126
|
|
|
120
127
|
<img src="./demo.gif" width="700" alt="demo">
|
|
121
128
|
|
|
@@ -131,6 +138,8 @@ claude npx claude-code-hud
|
|
|
131
138
|
(working with Claude Code) (HUD live display)
|
|
132
139
|
```
|
|
133
140
|
|
|
141
|
+
<img src="./capture.png" width="700" alt="side-by-side terminals">
|
|
142
|
+
|
|
134
143
|
```bash
|
|
135
144
|
# tmux split pane
|
|
136
145
|
cd ~/my-project
|
|
@@ -140,13 +149,12 @@ tmux split-window -h "npx claude-code-hud"
|
|
|
140
149
|
### Installation
|
|
141
150
|
|
|
142
151
|
```bash
|
|
143
|
-
#
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# Global install
|
|
147
|
-
npm install -g claude-code-hud
|
|
148
|
-
claude-hud
|
|
152
|
+
# Option 1. Global install (recommended)
|
|
153
|
+
> npm install -g claude-code-hud
|
|
154
|
+
> claude-hud
|
|
149
155
|
|
|
156
|
+
# Option 2. Run directly without install
|
|
157
|
+
> npx claude-code-hud
|
|
150
158
|
```
|
|
151
159
|
|
|
152
160
|
### Features
|
|
@@ -156,6 +164,7 @@ claude-hud
|
|
|
156
164
|
- Real 5h / weekly usage from Anthropic OAuth API — not estimates. Reset time shown as `1h 23m`
|
|
157
165
|
- Input / output / cache-read / cache-write breakdown
|
|
158
166
|
- Session output stats: total / avg / peak per hour
|
|
167
|
+
- `now` line — last user message (current task at a glance)
|
|
159
168
|
|
|
160
169
|
**2 PROJECT tab — interactive file browser**
|
|
161
170
|
- Navigable directory tree with expand/collapse
|
|
@@ -164,6 +173,15 @@ claude-hud
|
|
|
164
173
|
- Package dependency tree from `package.json`
|
|
165
174
|
- API endpoint detection (GET / POST / PUT / DELETE / PATCH)
|
|
166
175
|
|
|
176
|
+
```
|
|
177
|
+
▸ TREE │ ▸ SOURCE src/index.ts
|
|
178
|
+
▼ src/ 23f │ 1 import React from 'react'
|
|
179
|
+
▼ components/ 8f │ 2 import { render } from 'ink'
|
|
180
|
+
Header.tsx M │ 3
|
|
181
|
+
▶ hooks/ 4f │ 4 render(<App />)
|
|
182
|
+
▶ scripts/ 6f │ … [j/k] scroll [esc] close
|
|
183
|
+
```
|
|
184
|
+
|
|
167
185
|
**3 GIT tab**
|
|
168
186
|
- Branch status, ahead/behind remote
|
|
169
187
|
- Changed file list (MOD / ADD / DEL) with real `+N -N` diff counts
|
|
@@ -171,16 +189,20 @@ claude-hud
|
|
|
171
189
|
- Recent commit history
|
|
172
190
|
- **Branch switcher** — press `b` to list local branches and checkout instantly
|
|
173
191
|
|
|
192
|
+
**4 TIMELINE tab**
|
|
193
|
+
- User message history from the current Claude Code session
|
|
194
|
+
- Timestamped entries, 10 per page, j/k to scroll
|
|
195
|
+
|
|
174
196
|
### Keyboard Shortcuts
|
|
175
197
|
|
|
176
198
|
| Key | Action |
|
|
177
199
|
|-----|--------|
|
|
178
|
-
| `1` `2` `3` | Switch tabs |
|
|
200
|
+
| `1` `2` `3` `4` | Switch tabs |
|
|
179
201
|
| `j` / `k` | Scroll / move tree cursor |
|
|
180
202
|
| `→` / `Enter` | Expand dir / open file |
|
|
181
203
|
| `←` / `Esc` | Collapse / close source viewer |
|
|
182
204
|
| `b` | Branch switcher (GIT tab) |
|
|
183
|
-
| `d` |
|
|
205
|
+
| `d` | Cycle accent color (blue → red → amber → green → pink) |
|
|
184
206
|
| `r` | Manual refresh |
|
|
185
207
|
| `q` | Quit |
|
|
186
208
|
|
|
@@ -206,6 +228,7 @@ Run `claude` once to authenticate — credentials are saved to `~/.claude/.crede
|
|
|
206
228
|
### How it works
|
|
207
229
|
|
|
208
230
|
- **Token data**: Watches `~/.claude/projects/*/sessions/*.jsonl` with chokidar — updates instantly on each Claude response
|
|
231
|
+
- **Timeline**: Reads the same JSONL files, filters `type === "user"` entries for message history
|
|
209
232
|
- **Usage window**: Calls `api.anthropic.com/api/oauth/usage` using local Claude credentials — cached 5 min
|
|
210
233
|
- **Git**: Polls every 3 seconds
|
|
211
234
|
- **Project scan**: One-time fast-glob scan on startup, `r` to rescan
|
package/package.json
CHANGED
|
@@ -109,9 +109,11 @@ export function readTokenHistory(cwd) {
|
|
|
109
109
|
const h5 = now - 5 * 60 * 60 * 1000;
|
|
110
110
|
const wk = now - 7 * 24 * 60 * 60 * 1000;
|
|
111
111
|
const h12 = now - 12 * 60 * 60 * 1000;
|
|
112
|
+
const todayMidnight = new Date(); todayMidnight.setHours(0, 0, 0, 0);
|
|
113
|
+
const todayStart = todayMidnight.getTime();
|
|
112
114
|
|
|
113
115
|
const empty = () => ({ inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } });
|
|
114
|
-
const acc5h = empty(), accWk = empty();
|
|
116
|
+
const acc5h = empty(), accWk = empty(), accToday = empty();
|
|
115
117
|
|
|
116
118
|
// 12 hourly buckets (index 0 = oldest, 11 = most recent)
|
|
117
119
|
const buckets = Array(12).fill(0);
|
|
@@ -135,8 +137,9 @@ export function readTokenHistory(cwd) {
|
|
|
135
137
|
acc.cost.cacheWrite += (cw / M) * pricing.cacheWrite;
|
|
136
138
|
};
|
|
137
139
|
|
|
138
|
-
if (ts >= wk)
|
|
139
|
-
if (ts >= h5)
|
|
140
|
+
if (ts >= wk) { addTo(accWk); }
|
|
141
|
+
if (ts >= h5) { addTo(acc5h); }
|
|
142
|
+
if (ts >= todayStart) { addTo(accToday); }
|
|
140
143
|
|
|
141
144
|
if (ts >= h12) {
|
|
142
145
|
const hoursAgo = (now - ts) / (60 * 60 * 1000);
|
|
@@ -145,11 +148,11 @@ export function readTokenHistory(cwd) {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
[acc5h, accWk].forEach(acc => {
|
|
151
|
+
[acc5h, accWk, accToday].forEach(acc => {
|
|
149
152
|
acc.cost.total = acc.cost.input + acc.cost.output + acc.cost.cacheRead + acc.cost.cacheWrite;
|
|
150
153
|
});
|
|
151
154
|
|
|
152
|
-
return { last5h: acc5h, lastWeek: accWk, hourlyBuckets: buckets };
|
|
155
|
+
return { last5h: acc5h, lastWeek: accWk, today: accToday, hourlyBuckets: buckets };
|
|
153
156
|
}
|
|
154
157
|
|
|
155
158
|
export function readTokenUsage(cwd) {
|
package/tui/hud.tsx
CHANGED
|
@@ -248,6 +248,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
|
248
248
|
const obj = JSON.parse(line);
|
|
249
249
|
if (obj.type !== 'user') continue;
|
|
250
250
|
const content = obj.message?.content;
|
|
251
|
+
// Skip tool_result messages (not direct user prompts)
|
|
252
|
+
if (Array.isArray(content) && content.some((b: any) => b.type === 'tool_result')) continue;
|
|
251
253
|
const textBlock = Array.isArray(content)
|
|
252
254
|
? content.find((b: any) => b.type === 'text')
|
|
253
255
|
: null;
|
|
@@ -401,6 +403,20 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
|
|
|
401
403
|
);
|
|
402
404
|
})()}
|
|
403
405
|
|
|
406
|
+
{/* Today summary + sparkline */}
|
|
407
|
+
<Section title="TODAY" C={C}>
|
|
408
|
+
<Box>
|
|
409
|
+
<Text color={C.dimmer}>in </Text>
|
|
410
|
+
<Text color={C.brand} bold>{fmtNum(history.today?.inputTokens ?? 0)}</Text>
|
|
411
|
+
<Text color={C.dimmer}> out </Text>
|
|
412
|
+
<Text color={C.purple} bold>{fmtNum(history.today?.outputTokens ?? 0)}</Text>
|
|
413
|
+
<Text color={C.dimmer}> cache </Text>
|
|
414
|
+
<Text color={C.cyan} bold>{fmtNum((history.today?.cacheReadTokens ?? 0) + (history.today?.cacheWriteTokens ?? 0))}</Text>
|
|
415
|
+
<Text color={C.dimmer}> </Text>
|
|
416
|
+
<Text color={costColor(history.today?.cost?.total ?? 0, C)} bold>{fmtCost(history.today?.cost?.total ?? 0)}</Text>
|
|
417
|
+
</Box>
|
|
418
|
+
</Section>
|
|
419
|
+
|
|
404
420
|
{/* Current activity */}
|
|
405
421
|
{currentActivity && (
|
|
406
422
|
<Box borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
@@ -413,7 +429,7 @@ function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }
|
|
|
413
429
|
}
|
|
414
430
|
|
|
415
431
|
// ── Tab 2: PROJECT ─────────────────────────────────────────────────────────
|
|
416
|
-
function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, git, C }: any) {
|
|
432
|
+
function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, fileScroll, termWidth, contentH, git, C }: any) {
|
|
417
433
|
if (!info) return (
|
|
418
434
|
<Box borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
419
435
|
<Text color={C.dimmer}>scanning project…</Text>
|
|
@@ -439,16 +455,26 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
439
455
|
return result;
|
|
440
456
|
}
|
|
441
457
|
|
|
442
|
-
const
|
|
458
|
+
const ch = contentH ?? 30;
|
|
459
|
+
const hasFile = !!selectedFile;
|
|
460
|
+
|
|
461
|
+
// Budget: summary=3, tree border+header+marginTop=4, packages≈14 when shown
|
|
462
|
+
const showPackages = !hasFile && ch >= 28;
|
|
463
|
+
const packagesBudget = showPackages ? 14 : 0;
|
|
464
|
+
const maxTreeRows = Math.max(4, ch - 7 - packagesBudget);
|
|
465
|
+
const treePanelH = Math.max(4, ch - 3 - packagesBudget);
|
|
466
|
+
const VISIBLE_LINES = Math.max(4, ch - 8);
|
|
467
|
+
|
|
468
|
+
// Flatten nodes
|
|
469
|
+
const allFlatNodes: FlatNode[] = info.dirTree ? flatNodes_inner(info.dirTree, 0) : [];
|
|
470
|
+
const flatNodes = allFlatNodes.slice(0, maxTreeRows);
|
|
443
471
|
const safeCursor = Math.min(treeCursor, Math.max(0, flatNodes.length - 1));
|
|
444
472
|
|
|
445
473
|
const totalEndpoints = Object.values(info.endpoints as Record<string, number>).reduce((a: number, b: number) => a + b, 0);
|
|
446
474
|
|
|
447
475
|
// Split layout when file is open
|
|
448
|
-
const hasFile = !!selectedFile;
|
|
449
476
|
const TREE_W = hasFile ? Math.max(28, Math.floor(termWidth * 0.36)) : termWidth - 2;
|
|
450
477
|
const SOURCE_W = hasFile ? termWidth - TREE_W - 5 : 0;
|
|
451
|
-
const VISIBLE_LINES = 22;
|
|
452
478
|
|
|
453
479
|
// Git changed file sets
|
|
454
480
|
const gitModified = new Set<string>([...(git?.modified ?? []), ...(git?.added ?? []), ...(git?.deleted ?? [])]);
|
|
@@ -463,9 +489,9 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
463
489
|
};
|
|
464
490
|
|
|
465
491
|
return (
|
|
466
|
-
<Box flexDirection="column">
|
|
492
|
+
<Box flexDirection="column" height={ch}>
|
|
467
493
|
{/* Summary bar */}
|
|
468
|
-
<Box borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
494
|
+
<Box borderStyle="single" borderColor={C.border} paddingX={1} height={3}>
|
|
469
495
|
<Text color={C.text} bold>{info.totalFiles} files</Text>
|
|
470
496
|
<Text color={C.dim}> │ </Text>
|
|
471
497
|
<Text color={C.text} bold>{info.packages.filter((p: any) => p.depth === 0).length} pkgs</Text>
|
|
@@ -477,10 +503,10 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
477
503
|
</Box>
|
|
478
504
|
|
|
479
505
|
{/* Main area: tree + optional source */}
|
|
480
|
-
<Box flexDirection="row">
|
|
506
|
+
<Box flexDirection="row" height={treePanelH}>
|
|
481
507
|
|
|
482
508
|
{/* ── Tree panel ── */}
|
|
483
|
-
<Box flexDirection="column" borderStyle="single" borderColor={hasFile ? C.brand : C.border} paddingX={1} width={TREE_W}>
|
|
509
|
+
<Box flexDirection="column" borderStyle="single" borderColor={hasFile ? C.brand : C.border} paddingX={1} width={TREE_W} height={treePanelH}>
|
|
484
510
|
<Text color={C.dimmer} bold>▸ <Text color={C.text}>TREE</Text></Text>
|
|
485
511
|
<Box marginTop={1} flexDirection="column">
|
|
486
512
|
{flatNodes.length === 0 && <Text color={C.dimmer}> (empty)</Text>}
|
|
@@ -496,7 +522,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
496
522
|
<Box key={`d_${fn.node.path}_${idx}`}>
|
|
497
523
|
<Text color={C.dimmer}>{indent}</Text>
|
|
498
524
|
<Text color={isSelected ? C.brand : C.dimmer}>{expIcon}</Text>
|
|
499
|
-
<Text color={nameColor} bold={isSelected}>{fn.node.name}/</Text>
|
|
525
|
+
<Text color={nameColor} bold={isSelected} wrap="truncate">{fn.node.name}/</Text>
|
|
500
526
|
<Text color={C.dimmer}> {fn.node.totalFiles}f</Text>
|
|
501
527
|
</Box>
|
|
502
528
|
);
|
|
@@ -513,7 +539,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
513
539
|
<Box key={`f_${fn.filePath}_${idx}`}>
|
|
514
540
|
<Text color={C.dimmer}>{indent}</Text>
|
|
515
541
|
<Text color={isSelected ? C.brand : C.dimmer}>{isOpen ? '▶ ' : ' '}</Text>
|
|
516
|
-
<Text color={fileColor} bold={isSelected || isOpen}>{fn.fileName}</Text>
|
|
542
|
+
<Text color={fileColor} bold={isSelected || isOpen} wrap="truncate">{fn.fileName}</Text>
|
|
517
543
|
{gitBadge ? <Text color={gitColor!} bold>{gitBadge}</Text> : null}
|
|
518
544
|
</Box>
|
|
519
545
|
);
|
|
@@ -524,7 +550,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
524
550
|
|
|
525
551
|
{/* ── Source viewer panel ── */}
|
|
526
552
|
{hasFile && (
|
|
527
|
-
<Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} width={SOURCE_W}>
|
|
553
|
+
<Box flexDirection="column" borderStyle="single" borderColor={C.brand} paddingX={1} width={SOURCE_W} height={treePanelH}>
|
|
528
554
|
<Text color={C.dimmer} bold>▸ <Text color={C.text}>SOURCE <Text color={C.dim}>{selectedFile}</Text></Text></Text>
|
|
529
555
|
<Box marginTop={1} flexDirection="column">
|
|
530
556
|
{(fileLines as string[]).slice(fileScroll, fileScroll + VISIBLE_LINES).map((line, i) => {
|
|
@@ -536,7 +562,7 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
536
562
|
<Text color={C.dimmer}>{lineNo}</Text>
|
|
537
563
|
</Box>
|
|
538
564
|
<Text color={C.dimmer}> </Text>
|
|
539
|
-
<Text color={C.text}>{truncated}</Text>
|
|
565
|
+
<Text color={C.text} wrap="truncate">{truncated}</Text>
|
|
540
566
|
</Box>
|
|
541
567
|
);
|
|
542
568
|
})}
|
|
@@ -549,8 +575,8 @@ function ProjectTab({ info, treeCursor, treeExpanded, selectedFile, fileLines, f
|
|
|
549
575
|
</Box>
|
|
550
576
|
|
|
551
577
|
{/* Packages (hidden when file open to save space) */}
|
|
552
|
-
{
|
|
553
|
-
<Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
578
|
+
{showPackages && (
|
|
579
|
+
<Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1} height={packagesBudget}>
|
|
554
580
|
<Text color={C.dimmer} bold>▸ <Text color={C.text}>PACKAGES</Text></Text>
|
|
555
581
|
<Box marginTop={1} flexDirection="column">
|
|
556
582
|
{info.packages.slice(0, 10).map((p: any, i: number) => {
|
|
@@ -733,6 +759,17 @@ function App() {
|
|
|
733
759
|
const [fileLines, setFileLines] = useState<string[]>([]);
|
|
734
760
|
const [fileScroll, setFileScroll] = useState(0);
|
|
735
761
|
|
|
762
|
+
// Help overlay state
|
|
763
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
764
|
+
|
|
765
|
+
// Token warning blink state
|
|
766
|
+
const [blinkOn, setBlinkOn] = useState(true);
|
|
767
|
+
|
|
768
|
+
// Loading spinner state
|
|
769
|
+
const [loading, setLoading] = useState(true);
|
|
770
|
+
const [spinFrame, setSpinFrame] = useState(0);
|
|
771
|
+
const SPIN = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
772
|
+
|
|
736
773
|
// Branch switcher state
|
|
737
774
|
const [branchMode, setBranchMode] = useState(false);
|
|
738
775
|
const [branchList, setBranchList] = useState<string[]>([]);
|
|
@@ -760,7 +797,7 @@ function App() {
|
|
|
760
797
|
|
|
761
798
|
useEffect(() => {
|
|
762
799
|
// Scan project once
|
|
763
|
-
scanProject(cwd).then(setProject).catch(() => {});
|
|
800
|
+
scanProject(cwd).then(p => { setProject(p); setLoading(false); }).catch(() => { setLoading(false); });
|
|
764
801
|
// Initial API usage fetch
|
|
765
802
|
getUsage().then(setRateLimits).catch(() => {});
|
|
766
803
|
// Initial timeline load
|
|
@@ -789,11 +826,15 @@ function App() {
|
|
|
789
826
|
depth: 2, persistent: true, ignoreInitial: true,
|
|
790
827
|
ignored: (p: string) => !p.endsWith('.jsonl'),
|
|
791
828
|
});
|
|
792
|
-
|
|
829
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
830
|
+
watcher.on('change', () => {
|
|
831
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
832
|
+
debounceTimer = setTimeout(refresh, 800);
|
|
833
|
+
});
|
|
793
834
|
});
|
|
794
835
|
}
|
|
795
836
|
|
|
796
|
-
const tickInterval = setInterval(() => setTick(t => t + 1),
|
|
837
|
+
const tickInterval = setInterval(() => setTick(t => t + 1), 5000);
|
|
797
838
|
|
|
798
839
|
return () => {
|
|
799
840
|
stdout?.off('resize', onResize);
|
|
@@ -803,7 +844,16 @@ function App() {
|
|
|
803
844
|
};
|
|
804
845
|
}, []);
|
|
805
846
|
|
|
847
|
+
useEffect(() => {
|
|
848
|
+
if (!loading) return;
|
|
849
|
+
const id = setInterval(() => setSpinFrame(f => f + 1), 80);
|
|
850
|
+
return () => clearInterval(id);
|
|
851
|
+
}, [loading]);
|
|
852
|
+
|
|
806
853
|
useInput((input, key) => {
|
|
854
|
+
if (input === '?') { setShowHelp(h => !h); return; }
|
|
855
|
+
if (key.escape && showHelp) { setShowHelp(false); return; }
|
|
856
|
+
|
|
807
857
|
// Branch switcher intercepts input when active
|
|
808
858
|
if (branchMode) {
|
|
809
859
|
if (input === 'j' || key.downArrow) {
|
|
@@ -935,11 +985,28 @@ function App() {
|
|
|
935
985
|
const uptime = fmtSince(SESSION_START - Date.now() + (Date.now() - SESSION_START)); // forces tick dep
|
|
936
986
|
void tick;
|
|
937
987
|
|
|
988
|
+
const ctxPct = usage.contextWindow > 0 ? usage.totalTokens / usage.contextWindow : 0;
|
|
989
|
+
|
|
990
|
+
useEffect(() => {
|
|
991
|
+
if (ctxPct <= 0.85) { setBlinkOn(true); return; }
|
|
992
|
+
const id = setInterval(() => setBlinkOn(b => !b), 600);
|
|
993
|
+
return () => clearInterval(id);
|
|
994
|
+
}, [ctxPct > 0.85]);
|
|
995
|
+
|
|
996
|
+
if (termWidth < 60 || termHeight < 15) {
|
|
997
|
+
return (
|
|
998
|
+
<Box width={termWidth} height={termHeight} alignItems="center" justifyContent="center" flexDirection="column">
|
|
999
|
+
<Text color={C.yellow} bold>⚠ terminal too small</Text>
|
|
1000
|
+
<Text color={C.dimmer}>{termWidth}×{termHeight} — min 60×15</Text>
|
|
1001
|
+
</Box>
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
938
1005
|
return (
|
|
939
|
-
<Box flexDirection="column">
|
|
1006
|
+
<Box flexDirection="column" height={termHeight}>
|
|
940
1007
|
|
|
941
1008
|
{/* ── Header / Tab bar ── */}
|
|
942
|
-
<Box borderStyle="single" borderColor={
|
|
1009
|
+
<Box height={3} borderStyle="single" borderColor={ctxPct > 0.85 ? (blinkOn ? C.red : C.border) : ctxPct > 0.65 ? C.yellow : C.brand} paddingX={1} justifyContent="space-between">
|
|
943
1010
|
<Box>
|
|
944
1011
|
<Text color={C.brand} bold>◆ HUD</Text>
|
|
945
1012
|
{TAB_NAMES.map((name, i) => (
|
|
@@ -964,31 +1031,53 @@ function App() {
|
|
|
964
1031
|
const contentH = Math.max(4, termHeight - 7);
|
|
965
1032
|
return (
|
|
966
1033
|
<Box flexDirection="column" height={contentH} overflow="hidden">
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
</
|
|
1034
|
+
{showHelp ? (
|
|
1035
|
+
<Box flexDirection="column" borderStyle="round" borderColor={C.brand} paddingX={2} paddingY={1}>
|
|
1036
|
+
<Text color={C.brand} bold> Keyboard Shortcuts</Text>
|
|
1037
|
+
<Text> </Text>
|
|
1038
|
+
<Text><Text color={C.dim}> 1 / 2 / 3 / 4 </Text><Text color={C.text}>switch tabs</Text></Text>
|
|
1039
|
+
<Text><Text color={C.dim}> j / k </Text><Text color={C.text}>scroll / tree move</Text></Text>
|
|
1040
|
+
<Text><Text color={C.dim}> → / Enter </Text><Text color={C.text}>expand dir / open file</Text></Text>
|
|
1041
|
+
<Text><Text color={C.dim}> ← / Esc </Text><Text color={C.text}>collapse / close</Text></Text>
|
|
1042
|
+
<Text><Text color={C.dim}> b </Text><Text color={C.text}>branch switcher (GIT tab)</Text></Text>
|
|
1043
|
+
<Text><Text color={C.dim}> d </Text><Text color={C.text}>cycle accent color</Text></Text>
|
|
1044
|
+
<Text><Text color={C.dim}> r </Text><Text color={C.text}>refresh</Text></Text>
|
|
1045
|
+
<Text><Text color={C.dim}> q / Esc </Text><Text color={C.text}>quit</Text></Text>
|
|
1046
|
+
<Text><Text color={C.dim}> ? </Text><Text color={C.text}>toggle this help</Text></Text>
|
|
1047
|
+
<Text> </Text>
|
|
1048
|
+
<Text color={C.dimmer}> Korean: ㅓ/ㅏ (j/k) ㅇ (d) ㄱ (r) ㅂ (q) ㅠ (b)</Text>
|
|
1049
|
+
</Box>
|
|
1050
|
+
) : loading ? (
|
|
1051
|
+
<Box height={contentH} alignItems="center" justifyContent="center">
|
|
1052
|
+
<Text color={C.brand} bold>{SPIN[spinFrame % SPIN.length]} scanning project…</Text>
|
|
1053
|
+
</Box>
|
|
1054
|
+
) : (
|
|
1055
|
+
<Box flexDirection="column" height={contentH} marginTop={-scrollY}>
|
|
1056
|
+
{tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} currentActivity={currentActivity} C={C} />}
|
|
1057
|
+
{tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} contentH={contentH - 1} git={git} C={C} />}
|
|
1058
|
+
{tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
|
|
1059
|
+
{tab === 3 && <TimelineTab timeline={timeline} timelineScroll={timelineScroll} C={C} />}
|
|
1060
|
+
</Box>
|
|
1061
|
+
)}
|
|
973
1062
|
</Box>
|
|
974
1063
|
);
|
|
975
1064
|
})()}
|
|
976
1065
|
|
|
977
1066
|
{/* ── Footer row 1: keys ── */}
|
|
978
|
-
<Box justifyContent="space-between" paddingX={1}>
|
|
1067
|
+
<Box height={1} justifyContent="space-between" paddingX={1}>
|
|
979
1068
|
<Box>
|
|
980
1069
|
<Text color={C.green}>● </Text>
|
|
981
1070
|
<Text color={C.dimmer}>[1/2/3/4] tabs </Text>
|
|
982
1071
|
<Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
|
|
983
|
-
<Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc
|
|
1072
|
+
<Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc] close ' : '[↵/→←] open ') : ''}</Text>
|
|
984
1073
|
{tab === 2 && !branchMode && <Text color={C.brand}>[b] branch </Text>}
|
|
985
|
-
<Text color={C.dimmer}>[r] refresh [d] color [q] quit</Text>
|
|
1074
|
+
<Text color={C.dimmer}>[r] refresh [d] color [?] help [q] quit</Text>
|
|
986
1075
|
</Box>
|
|
987
1076
|
<Text color={C.dimmer}>↻ {since}</Text>
|
|
988
1077
|
</Box>
|
|
989
1078
|
|
|
990
1079
|
{/* ── Footer row 2: current dir ── */}
|
|
991
|
-
<Box paddingX={1} borderStyle="single" borderColor={C.brand}>
|
|
1080
|
+
<Box height={3} paddingX={1} borderStyle="single" borderColor={C.brand}>
|
|
992
1081
|
<Text color={C.brand} bold>◆ </Text>
|
|
993
1082
|
<Text color={C.text} bold>~/{basename(cwd)}</Text>
|
|
994
1083
|
</Box>
|