claude-code-hud 0.3.3 → 0.3.5
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 +36 -31
- package/package.json +1 -1
- package/tui/hud.tsx +171 -26
package/README.md
CHANGED
|
@@ -8,27 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Claude Code로 작업할 때 토큰 사용량, git 상태, 파일 구조를 IDE나 별도 탭 없이 터미널 하나에서 확인할 수 있는 HUD입니다.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
13
|
-
│ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
|
|
14
|
-
├──────────────────────────────────────────────────────────────────────────────┤
|
|
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 │
|
|
30
|
-
└──────────────────────────────────────────────────────────────────────────────┘
|
|
31
|
-
```
|
|
11
|
+
<img src="./demo.gif" width="700" alt="demo">
|
|
32
12
|
|
|
33
13
|
### 사용법
|
|
34
14
|
|
|
@@ -59,9 +39,6 @@ npx claude-code-hud
|
|
|
59
39
|
# 전역 설치
|
|
60
40
|
npm install -g claude-code-hud
|
|
61
41
|
claude-hud
|
|
62
|
-
|
|
63
|
-
# Claude Code 플러그인
|
|
64
|
-
/plugin install letsgojh0810/hud-plugin
|
|
65
42
|
```
|
|
66
43
|
|
|
67
44
|
### 기능
|
|
@@ -117,17 +94,30 @@ claude-hud
|
|
|
117
94
|
- Claude Pro / Max 플랜 권장 (5h / 주간 사용률 표시)
|
|
118
95
|
- Git (GIT 탭 사용 시)
|
|
119
96
|
|
|
97
|
+
### 플랫폼 지원
|
|
98
|
+
|
|
99
|
+
| 기능 | macOS | Windows |
|
|
100
|
+
|------|-------|---------|
|
|
101
|
+
| 기본 실행 | ✅ | ✅ Node.js 설치 후 `npx` |
|
|
102
|
+
| 토큰 / Git / 파일 브라우저 | ✅ | ✅ |
|
|
103
|
+
| 5h / 주간 사용률 | ✅ Keychain 자동 인식 | ⚠️ `~/.claude/.credentials.json` 폴백 |
|
|
104
|
+
| 터미널 렌더링 | ✅ | ✅ Windows Terminal 권장 (cmd.exe 깨짐) |
|
|
105
|
+
| 한글 키보드 | ✅ | ⚠️ IME 방식 차이로 미지원 가능 |
|
|
106
|
+
|
|
107
|
+
**Windows 권장 환경:**
|
|
108
|
+
- [Windows Terminal](https://aka.ms/terminal) 사용
|
|
109
|
+
- WSL2 환경이면 macOS와 동일하게 동작
|
|
110
|
+
|
|
111
|
+
**Windows에서 5h/wk 사용률이 안 보일 때:**
|
|
112
|
+
Claude Code를 한 번 실행하면 `~/.claude/.credentials.json`에 credentials이 저장됩니다. HUD는 이 파일을 자동으로 읽습니다.
|
|
113
|
+
|
|
120
114
|
---
|
|
121
115
|
|
|
122
116
|
## English
|
|
123
117
|
|
|
124
118
|
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.
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
┌──────────────────────────────────────────────────────────────────────────────┐
|
|
128
|
-
│ ◆ HUD │ ◉ TOKENS │ ○ PROJECT │ ○ GIT sonnet-4-6 · up 4m │
|
|
129
|
-
└──────────────────────────────────────────────────────────────────────────────┘
|
|
130
|
-
```
|
|
120
|
+
<img src="./demo.gif" width="700" alt="demo">
|
|
131
121
|
|
|
132
122
|
### Usage
|
|
133
123
|
|
|
@@ -157,8 +147,6 @@ npx claude-code-hud
|
|
|
157
147
|
npm install -g claude-code-hud
|
|
158
148
|
claude-hud
|
|
159
149
|
|
|
160
|
-
# Claude Code plugin
|
|
161
|
-
/plugin install letsgojh0810/hud-plugin
|
|
162
150
|
```
|
|
163
151
|
|
|
164
152
|
### Features
|
|
@@ -198,6 +186,23 @@ claude-hud
|
|
|
198
186
|
|
|
199
187
|
> Korean keyboard layout supported — `ㅓ/ㅏ` (j/k), `ㅇ` (d), `ㄱ` (r), `ㅂ` (q), `ㅠ` (b)
|
|
200
188
|
|
|
189
|
+
### Platform Support
|
|
190
|
+
|
|
191
|
+
| Feature | macOS | Windows |
|
|
192
|
+
|---------|-------|---------|
|
|
193
|
+
| Basic run | ✅ | ✅ via `npx` with Node.js |
|
|
194
|
+
| Tokens / Git / File browser | ✅ | ✅ |
|
|
195
|
+
| 5h / weekly usage % | ✅ Keychain auto-detected | ⚠️ Falls back to `~/.claude/.credentials.json` |
|
|
196
|
+
| Terminal rendering | ✅ | ✅ Windows Terminal recommended (cmd.exe may break) |
|
|
197
|
+
| Korean keyboard | ✅ | ⚠️ May not work depending on IME |
|
|
198
|
+
|
|
199
|
+
**Windows recommendations:**
|
|
200
|
+
- Use [Windows Terminal](https://aka.ms/terminal) for proper Unicode rendering
|
|
201
|
+
- WSL2 works identically to macOS
|
|
202
|
+
|
|
203
|
+
**5h / weekly usage not showing on Windows?**
|
|
204
|
+
Run `claude` once to authenticate — credentials are saved to `~/.claude/.credentials.json` which the HUD reads automatically.
|
|
205
|
+
|
|
201
206
|
### How it works
|
|
202
207
|
|
|
203
208
|
- **Token data**: Watches `~/.claude/projects/*/sessions/*.jsonl` with chokidar — updates instantly on each Claude response
|
package/package.json
CHANGED
package/tui/hud.tsx
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
7
7
|
import { render, Box, Text, useStdout, useInput } from 'ink';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
-
import { dirname, join } from 'path';
|
|
9
|
+
import { dirname, join, basename } from 'path';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import os from 'os';
|
|
12
12
|
import { execSync } from 'child_process';
|
|
@@ -16,23 +16,34 @@ const { readTokenUsage, readTokenHistory } = await import(join(__dir, '../script
|
|
|
16
16
|
const { readGitInfo } = await import(join(__dir, '../scripts/lib/git-info.mjs'));
|
|
17
17
|
const { getUsage, getUsageSync } = await import(join(__dir, '../scripts/lib/usage-api.mjs'));
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
process.stdout.write('\x1b[
|
|
19
|
+
// Enter alternate screen buffer (like vim/htop — terminal never scrolls, header stays fixed)
|
|
20
|
+
process.stdout.write('\x1b[?1049h\x1b[2J\x1b[H');
|
|
21
|
+
process.on('exit', () => process.stdout.write('\x1b[?1049l'));
|
|
22
|
+
process.on('SIGINT', () => { process.stdout.write('\x1b[?1049l'); process.exit(0); });
|
|
23
|
+
process.on('SIGTERM', () => { process.stdout.write('\x1b[?1049l'); process.exit(0); });
|
|
21
24
|
|
|
22
25
|
const SESSION_START = Date.now();
|
|
23
26
|
|
|
24
27
|
// ── Themes ─────────────────────────────────────────────────────────────────
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
// Base is always dark. Only accent colors cycle with `d`.
|
|
29
|
+
const BASE = {
|
|
30
|
+
text: '#E6EDF3', dim: '#8B949E', dimmer: '#6E7681',
|
|
27
31
|
border: '#30363D', green: '#3FB950', yellow: '#D29922', red: '#F85149',
|
|
28
|
-
purple: '#A371F7', cyan: '#58A6FF',
|
|
29
|
-
};
|
|
30
|
-
const LIGHT = {
|
|
31
|
-
brand: '#3182F6', text: '#1F2328', dim: '#656D76', dimmer: '#8C959F',
|
|
32
|
-
border: '#D8DEE4', green: '#1A7F37', yellow: '#9A6700', red: '#CF222E',
|
|
33
|
-
purple: '#8250DF', cyan: '#0969DA',
|
|
34
32
|
};
|
|
35
33
|
|
|
34
|
+
const ACCENTS = [
|
|
35
|
+
{ brand: '#3B82F6', cyan: '#60A5FA', purple: '#A78BFA' }, // blue
|
|
36
|
+
{ brand: '#F43F5E', cyan: '#FB7185', purple: '#F9A8D4' }, // red
|
|
37
|
+
{ brand: '#F59E0B', cyan: '#FCD34D', purple: '#FDE68A' }, // amber
|
|
38
|
+
{ brand: '#10B981', cyan: '#34D399', purple: '#6EE7B7' }, // emerald
|
|
39
|
+
{ brand: '#EC4899', cyan: '#F472B6', purple: '#F9A8D4' }, // pink
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
type Theme = typeof BASE & typeof ACCENTS[number];
|
|
43
|
+
function makeTheme(accentIdx: number): Theme {
|
|
44
|
+
return { ...BASE, ...ACCENTS[accentIdx] };
|
|
45
|
+
}
|
|
46
|
+
|
|
36
47
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
37
48
|
const fmtNum = (n: number) =>
|
|
38
49
|
n >= 1_000_000 ? (n / 1_000_000).toFixed(1) + 'M' :
|
|
@@ -197,6 +208,62 @@ function getBranches(cwd: string): string[] {
|
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
210
|
|
|
211
|
+
// ── Timeline ────────────────────────────────────────────────────────────────
|
|
212
|
+
type TimelineEntry = {
|
|
213
|
+
time: string;
|
|
214
|
+
text: string;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
218
|
+
const projectsDir = join(os.homedir(), '.claude', 'projects');
|
|
219
|
+
if (!fs.existsSync(projectsDir)) return [];
|
|
220
|
+
|
|
221
|
+
let latestFile: string | null = null;
|
|
222
|
+
let latestMtime = 0;
|
|
223
|
+
try {
|
|
224
|
+
for (const projectHash of fs.readdirSync(projectsDir)) {
|
|
225
|
+
const sessionDir = join(projectsDir, projectHash);
|
|
226
|
+
if (!fs.statSync(sessionDir).isDirectory()) continue;
|
|
227
|
+
for (const file of fs.readdirSync(sessionDir)) {
|
|
228
|
+
if (!file.endsWith('.jsonl')) continue;
|
|
229
|
+
const filePath = join(sessionDir, file);
|
|
230
|
+
try {
|
|
231
|
+
const mtime = fs.statSync(filePath).mtimeMs;
|
|
232
|
+
if (mtime > latestMtime) { latestMtime = mtime; latestFile = filePath; }
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch {}
|
|
237
|
+
|
|
238
|
+
if (!latestFile) return [];
|
|
239
|
+
|
|
240
|
+
const lines = fs.readFileSync(latestFile, 'utf-8').split('\n').filter(Boolean);
|
|
241
|
+
const entries: TimelineEntry[] = [];
|
|
242
|
+
|
|
243
|
+
for (const line of lines) {
|
|
244
|
+
try {
|
|
245
|
+
const obj = JSON.parse(line);
|
|
246
|
+
if (obj.type !== 'user') continue;
|
|
247
|
+
const content = obj.message?.content;
|
|
248
|
+
const textBlock = Array.isArray(content)
|
|
249
|
+
? content.find((b: any) => b.type === 'text')
|
|
250
|
+
: null;
|
|
251
|
+
const text: string = textBlock?.text ?? (typeof content === 'string' ? content : '');
|
|
252
|
+
if (!text.trim()) continue;
|
|
253
|
+
|
|
254
|
+
const ts: string = obj.timestamp ?? '';
|
|
255
|
+
let time = '';
|
|
256
|
+
if (ts) {
|
|
257
|
+
try { time = new Date(ts).toTimeString().slice(0, 5); } catch {}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
entries.push({ time, text: text.replace(/\n/g, ' ').slice(0, 80) });
|
|
261
|
+
} catch {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return entries.slice(-30);
|
|
265
|
+
}
|
|
266
|
+
|
|
200
267
|
// ── UI Components ──────────────────────────────────────────────────────────
|
|
201
268
|
function Bar({ ratio, width, color, C }: { ratio: number; width: number; color: string; C: typeof DARK }) {
|
|
202
269
|
const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
|
|
@@ -218,7 +285,7 @@ function Section({ title, children, C, accent }: { title: string; children: Reac
|
|
|
218
285
|
}
|
|
219
286
|
|
|
220
287
|
// ── Tab 1: TOKENS ──────────────────────────────────────────────────────────
|
|
221
|
-
function TokensTab({ usage, history, rateLimits, termWidth, C }: any) {
|
|
288
|
+
function TokensTab({ usage, history, rateLimits, termWidth, currentActivity, C }: any) {
|
|
222
289
|
const ctxPct = usage.contextWindow > 0 ? usage.totalTokens / usage.contextWindow : 0;
|
|
223
290
|
const ctxColor = ctxPct > 0.85 ? C.red : ctxPct > 0.65 ? C.yellow : C.brand;
|
|
224
291
|
const ctxLabel = ctxPct > 0.85 ? 'WARN' : ctxPct > 0.65 ? 'MID' : 'OK';
|
|
@@ -330,6 +397,14 @@ function TokensTab({ usage, history, rateLimits, termWidth, C }: any) {
|
|
|
330
397
|
</Section>
|
|
331
398
|
);
|
|
332
399
|
})()}
|
|
400
|
+
|
|
401
|
+
{/* Current activity */}
|
|
402
|
+
{currentActivity && (
|
|
403
|
+
<Box borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
404
|
+
<Text color={C.dimmer}>now </Text>
|
|
405
|
+
<Text color={C.brand}>{currentActivity.slice(0, termWidth - 12)}</Text>
|
|
406
|
+
</Box>
|
|
407
|
+
)}
|
|
333
408
|
</Box>
|
|
334
409
|
);
|
|
335
410
|
}
|
|
@@ -599,18 +674,46 @@ function GitTab({ git, C, termWidth, branchMode, branchList, branchCursor }: any
|
|
|
599
674
|
);
|
|
600
675
|
}
|
|
601
676
|
|
|
677
|
+
// ── Tab 4: TIMELINE ────────────────────────────────────────────────────────
|
|
678
|
+
const TIMELINE_VISIBLE = 10;
|
|
679
|
+
|
|
680
|
+
function TimelineTab({ timeline, timelineScroll, C }: any) {
|
|
681
|
+
const entries = timeline as TimelineEntry[];
|
|
682
|
+
const visible = entries.slice(timelineScroll, timelineScroll + TIMELINE_VISIBLE);
|
|
683
|
+
return (
|
|
684
|
+
<Box flexDirection="column" borderStyle="single" borderColor={C.border} paddingX={1}>
|
|
685
|
+
<Text color={C.dimmer} bold>▸ <Text color={C.text}>SESSION HISTORY</Text>
|
|
686
|
+
{entries.length > 0 && <Text color={C.dimmer}> {timelineScroll + 1}–{Math.min(timelineScroll + TIMELINE_VISIBLE, entries.length)} / {entries.length}</Text>}
|
|
687
|
+
</Text>
|
|
688
|
+
<Box flexDirection="column" marginTop={1}>
|
|
689
|
+
{entries.length === 0 && <Text color={C.dimmer}> no messages yet</Text>}
|
|
690
|
+
{visible.map((entry, i) => (
|
|
691
|
+
<Box key={i} marginBottom={1}>
|
|
692
|
+
<Box width={6}><Text color={C.dimmer}>{entry.time}</Text></Box>
|
|
693
|
+
<Text color={C.text}>{entry.text}</Text>
|
|
694
|
+
</Box>
|
|
695
|
+
))}
|
|
696
|
+
{entries.length > TIMELINE_VISIBLE && (
|
|
697
|
+
<Text color={C.dimmer}> [j/k] scroll</Text>
|
|
698
|
+
)}
|
|
699
|
+
</Box>
|
|
700
|
+
</Box>
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
602
704
|
// ── Main App ───────────────────────────────────────────────────────────────
|
|
603
705
|
function App() {
|
|
604
706
|
const { stdout } = useStdout();
|
|
605
|
-
const [termWidth,
|
|
707
|
+
const [termWidth, setTermWidth] = useState(stdout?.columns ?? 80);
|
|
708
|
+
const [termHeight, setTermHeight] = useState(stdout?.rows ?? 24);
|
|
606
709
|
const [tab, setTab] = useState(0); // 0=TOKENS 1=PROJECT 2=GIT
|
|
607
|
-
const [
|
|
710
|
+
const [accent, setAccent] = useState(3); // 0=blue 1=red 2=amber 3=emerald 4=pink
|
|
608
711
|
const [scrollY, setScrollY] = useState(0);
|
|
609
712
|
const [tick, setTick] = useState(0);
|
|
610
713
|
const [updatedAt, setUpdatedAt] = useState(Date.now());
|
|
611
714
|
|
|
612
715
|
const cwd = process.env.CLAUDE_PROJECT_ROOT || process.cwd();
|
|
613
|
-
const C =
|
|
716
|
+
const C = makeTheme(accent);
|
|
614
717
|
|
|
615
718
|
const [usage, setUsage] = useState<any>(readTokenUsage());
|
|
616
719
|
const [history, setHistory] = useState<any>(readTokenHistory());
|
|
@@ -632,12 +735,24 @@ function App() {
|
|
|
632
735
|
const [branchList, setBranchList] = useState<string[]>([]);
|
|
633
736
|
const [branchCursor, setBranchCursor] = useState(0);
|
|
634
737
|
|
|
738
|
+
// Timeline state
|
|
739
|
+
const [timeline, setTimeline] = useState<TimelineEntry[]>([]);
|
|
740
|
+
const [timelineScroll, setTimelineScroll] = useState(0);
|
|
741
|
+
const [currentActivity, setCurrentActivity] = useState<string>('');
|
|
742
|
+
|
|
635
743
|
const refresh = useCallback(() => {
|
|
636
744
|
setUsage(readTokenUsage());
|
|
637
745
|
setHistory(readTokenHistory());
|
|
638
746
|
setGit(readGitInfo(cwd));
|
|
639
747
|
setUpdatedAt(Date.now());
|
|
640
748
|
getUsage().then(setRateLimits).catch(() => {});
|
|
749
|
+
readSessionTimeline(cwd).then(entries => {
|
|
750
|
+
setTimeline(entries);
|
|
751
|
+
if (entries.length > 0) {
|
|
752
|
+
const last = entries[entries.length - 1];
|
|
753
|
+
setCurrentActivity(last.text);
|
|
754
|
+
}
|
|
755
|
+
}).catch(() => {});
|
|
641
756
|
}, [cwd]);
|
|
642
757
|
|
|
643
758
|
useEffect(() => {
|
|
@@ -645,10 +760,19 @@ function App() {
|
|
|
645
760
|
scanProject(cwd).then(setProject).catch(() => {});
|
|
646
761
|
// Initial API usage fetch
|
|
647
762
|
getUsage().then(setRateLimits).catch(() => {});
|
|
763
|
+
// Initial timeline load
|
|
764
|
+
readSessionTimeline(cwd).then(entries => {
|
|
765
|
+
setTimeline(entries);
|
|
766
|
+
if (entries.length > 0) {
|
|
767
|
+
const last = entries[entries.length - 1];
|
|
768
|
+
setCurrentActivity(last.text);
|
|
769
|
+
}
|
|
770
|
+
}).catch(() => {});
|
|
648
771
|
|
|
649
772
|
const onResize = () => {
|
|
650
773
|
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
651
774
|
setTermWidth(stdout?.columns ?? 80);
|
|
775
|
+
setTermHeight(stdout?.rows ?? 24);
|
|
652
776
|
};
|
|
653
777
|
stdout?.on('resize', onResize);
|
|
654
778
|
|
|
@@ -692,6 +816,7 @@ function App() {
|
|
|
692
816
|
if (selected && selected !== git.branch) {
|
|
693
817
|
try {
|
|
694
818
|
execSync(`git checkout ${selected}`, { cwd });
|
|
819
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
695
820
|
refresh();
|
|
696
821
|
} catch {}
|
|
697
822
|
}
|
|
@@ -726,7 +851,8 @@ function App() {
|
|
|
726
851
|
if (input === '1') { setTab(0); setScrollY(0); }
|
|
727
852
|
if (input === '2') { setTab(1); setScrollY(0); }
|
|
728
853
|
if (input === '3') { setTab(2); setScrollY(0); }
|
|
729
|
-
if (input === '
|
|
854
|
+
if (input === '4') { setTab(3); setScrollY(0); }
|
|
855
|
+
if (input === 'd' || input === 'ㅇ') setAccent(a => (a + 1) % ACCENTS.length);
|
|
730
856
|
|
|
731
857
|
// r = manual refresh
|
|
732
858
|
if (input === 'r' || input === 'ㄱ') {
|
|
@@ -742,6 +868,8 @@ function App() {
|
|
|
742
868
|
} else if (tab === 1) {
|
|
743
869
|
const flat = project?.dirTree ? flattenTree(project.dirTree, 0, treeExpanded) : [];
|
|
744
870
|
setTreeCursor(c => Math.min(c + 1, flat.length - 1));
|
|
871
|
+
} else if (tab === 3) {
|
|
872
|
+
setTimelineScroll(s => Math.min(s + 1, Math.max(0, timeline.length - 10)));
|
|
745
873
|
} else {
|
|
746
874
|
setScrollY(s => Math.min(s + 1, 20));
|
|
747
875
|
}
|
|
@@ -751,6 +879,8 @@ function App() {
|
|
|
751
879
|
setFileScroll(s => Math.max(s - 1, 0));
|
|
752
880
|
} else if (tab === 1) {
|
|
753
881
|
setTreeCursor(c => Math.max(c - 1, 0));
|
|
882
|
+
} else if (tab === 3) {
|
|
883
|
+
setTimelineScroll(s => Math.max(s - 1, 0));
|
|
754
884
|
} else {
|
|
755
885
|
setScrollY(s => Math.max(s - 1, 0));
|
|
756
886
|
}
|
|
@@ -797,7 +927,7 @@ function App() {
|
|
|
797
927
|
}
|
|
798
928
|
});
|
|
799
929
|
|
|
800
|
-
const TAB_NAMES = ['TOKENS', 'PROJECT', 'GIT'];
|
|
930
|
+
const TAB_NAMES = ['TOKENS', 'PROJECT', 'GIT', 'TIMELINE'];
|
|
801
931
|
const since = fmtSince(Date.now() - updatedAt);
|
|
802
932
|
const uptime = fmtSince(SESSION_START - Date.now() + (Date.now() - SESSION_START)); // forces tick dep
|
|
803
933
|
void tick;
|
|
@@ -825,26 +955,41 @@ function App() {
|
|
|
825
955
|
</Box>
|
|
826
956
|
</Box>
|
|
827
957
|
|
|
828
|
-
{/* ── Content
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
958
|
+
{/* ── Content: fixed height so header/footer never get pushed off screen ── */}
|
|
959
|
+
{(() => {
|
|
960
|
+
// header ~3 rows, footer key row ~1, footer dir row ~3 = 7 total chrome
|
|
961
|
+
const contentH = Math.max(4, termHeight - 7);
|
|
962
|
+
return (
|
|
963
|
+
<Box flexDirection="column" height={contentH} overflow="hidden">
|
|
964
|
+
<Box flexDirection="column" marginTop={-scrollY}>
|
|
965
|
+
{tab === 0 && <TokensTab usage={usage} history={history} rateLimits={rateLimits} termWidth={termWidth} currentActivity={currentActivity} C={C} />}
|
|
966
|
+
{tab === 1 && <ProjectTab info={project} treeCursor={treeCursor} treeExpanded={treeExpanded} selectedFile={selectedFile} fileLines={fileLines} fileScroll={fileScroll} termWidth={termWidth} git={git} C={C} />}
|
|
967
|
+
{tab === 2 && <GitTab git={git} termWidth={termWidth} branchMode={branchMode} branchList={branchList} branchCursor={branchCursor} C={C} />}
|
|
968
|
+
{tab === 3 && <TimelineTab timeline={timeline} timelineScroll={timelineScroll} C={C} />}
|
|
969
|
+
</Box>
|
|
970
|
+
</Box>
|
|
971
|
+
);
|
|
972
|
+
})()}
|
|
834
973
|
|
|
835
|
-
{/* ── Footer ── */}
|
|
974
|
+
{/* ── Footer row 1: keys ── */}
|
|
836
975
|
<Box justifyContent="space-between" paddingX={1}>
|
|
837
976
|
<Box>
|
|
838
977
|
<Text color={C.green}>● </Text>
|
|
839
|
-
<Text color={C.dimmer}>[1/2/3] tabs </Text>
|
|
978
|
+
<Text color={C.dimmer}>[1/2/3/4] tabs </Text>
|
|
840
979
|
<Text color={tab === 1 ? C.brand : C.dimmer}>[j/k] {tab === 1 ? 'tree' : 'scroll'} </Text>
|
|
841
980
|
<Text color={tab === 1 ? C.brand : C.dimmer}>{tab === 1 ? (selectedFile ? '[esc/←] close [j/k] scroll ' : '[enter] open [→←] expand ') : ''}</Text>
|
|
842
981
|
{tab === 2 && !branchMode && <Text color={C.brand}>[b] branch </Text>}
|
|
843
|
-
<Text color={C.dimmer}>[r] refresh [d]
|
|
982
|
+
<Text color={C.dimmer}>[r] refresh [d] color [q] quit</Text>
|
|
844
983
|
</Box>
|
|
845
984
|
<Text color={C.dimmer}>↻ {since}</Text>
|
|
846
985
|
</Box>
|
|
847
986
|
|
|
987
|
+
{/* ── Footer row 2: current dir ── */}
|
|
988
|
+
<Box paddingX={1} borderStyle="single" borderColor={C.brand}>
|
|
989
|
+
<Text color={C.brand} bold>◆ </Text>
|
|
990
|
+
<Text color={C.text} bold>~/{basename(cwd)}</Text>
|
|
991
|
+
</Box>
|
|
992
|
+
|
|
848
993
|
</Box>
|
|
849
994
|
);
|
|
850
995
|
}
|