claude-code-hud 0.3.11 → 0.3.13
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/package.json +1 -1
- package/scripts/lib/git-info.mjs +18 -15
- package/tui/hud.tsx +32 -32
package/package.json
CHANGED
package/scripts/lib/git-info.mjs
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Git status via child_process
|
|
2
|
+
* Git status via child_process exec (async — non-blocking).
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const execAsync = promisify(exec);
|
|
8
|
+
|
|
9
|
+
async function run(cmd, cwd) {
|
|
7
10
|
try {
|
|
8
|
-
|
|
11
|
+
const { stdout } = await execAsync(cmd, { cwd, timeout: 3000 });
|
|
12
|
+
return stdout.trim();
|
|
9
13
|
} catch {
|
|
10
14
|
return '';
|
|
11
15
|
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export function readGitInfo(cwd = process.cwd()) {
|
|
15
|
-
const branch = run('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown';
|
|
18
|
+
export async function readGitInfo(cwd = process.cwd()) {
|
|
19
|
+
const branch = await run('git rev-parse --abbrev-ref HEAD', cwd) || 'unknown';
|
|
16
20
|
if (branch === 'unknown' || branch === 'HEAD') {
|
|
17
21
|
return { isRepo: false, branch: 'unknown', ahead: 0, behind: 0, modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 };
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const [aheadBehind, statusOut, logOut, numstatOut] = await Promise.all([
|
|
25
|
+
run('git rev-list --left-right --count @{upstream}...HEAD 2>/dev/null || echo "0\t0"', cwd),
|
|
26
|
+
run('git status --porcelain', cwd),
|
|
27
|
+
run('git log --oneline -5 --format="%h|%s|%cr"', cwd),
|
|
28
|
+
run('git diff --numstat HEAD 2>/dev/null', cwd),
|
|
29
|
+
]);
|
|
30
|
+
|
|
22
31
|
const [behind = 0, ahead = 0] = aheadBehind.split('\t').map(Number);
|
|
23
32
|
|
|
24
|
-
// status
|
|
25
|
-
const statusOut = run('git status --porcelain', cwd);
|
|
26
33
|
const modified = [], added = [], deleted = [];
|
|
27
34
|
for (const line of statusOut.split('\n').filter(Boolean)) {
|
|
28
35
|
const st = line.slice(0, 2).trim();
|
|
29
36
|
const file = line.slice(2).trimStart();
|
|
30
37
|
if (st === 'M' || st === 'MM' || st === 'AM') modified.push(file);
|
|
31
|
-
else if (st === 'A' || st === '??'
|
|
38
|
+
else if (st === 'A' || st === '??') added.push(file);
|
|
32
39
|
else if (st === 'D') deleted.push(file);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
// recent commits
|
|
36
|
-
const logOut = run('git log --oneline -5 --format="%h|%s|%cr"', cwd);
|
|
37
42
|
const recentCommits = logOut.split('\n').filter(Boolean).map(l => {
|
|
38
43
|
const [hash, ...rest] = l.split('|');
|
|
39
44
|
const time = rest.pop();
|
|
@@ -41,8 +46,6 @@ export function readGitInfo(cwd = process.cwd()) {
|
|
|
41
46
|
return { hash, msg, time };
|
|
42
47
|
});
|
|
43
48
|
|
|
44
|
-
// diff stats: actual +/- line counts per file
|
|
45
|
-
const numstatOut = run('git diff --numstat HEAD 2>/dev/null', cwd);
|
|
46
49
|
const diffStats = {};
|
|
47
50
|
for (const line of numstatOut.split('\n').filter(Boolean)) {
|
|
48
51
|
const [addStr, delStr, ...fileParts] = line.split('\t');
|
package/tui/hud.tsx
CHANGED
|
@@ -219,9 +219,8 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
|
219
219
|
if (!fs.existsSync(projectsDir)) return [];
|
|
220
220
|
|
|
221
221
|
const targetDirName = cwd.replace(/\//g, '-');
|
|
222
|
+
const allFiles: string[] = [];
|
|
222
223
|
|
|
223
|
-
let latestFile: string | null = null;
|
|
224
|
-
let latestMtime = 0;
|
|
225
224
|
try {
|
|
226
225
|
for (const projectHash of fs.readdirSync(projectsDir)) {
|
|
227
226
|
if (projectHash !== targetDirName) continue;
|
|
@@ -229,44 +228,43 @@ async function readSessionTimeline(cwd: string): Promise<TimelineEntry[]> {
|
|
|
229
228
|
if (!fs.statSync(sessionDir).isDirectory()) continue;
|
|
230
229
|
for (const file of fs.readdirSync(sessionDir)) {
|
|
231
230
|
if (!file.endsWith('.jsonl')) continue;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const mtime = fs.statSync(filePath).mtimeMs;
|
|
235
|
-
if (mtime > latestMtime) { latestMtime = mtime; latestFile = filePath; }
|
|
236
|
-
} catch {}
|
|
231
|
+
allFiles.push(join(sessionDir, file));
|
|
237
232
|
}
|
|
238
233
|
}
|
|
239
234
|
} catch {}
|
|
240
235
|
|
|
241
|
-
if (
|
|
236
|
+
if (allFiles.length === 0) return [];
|
|
242
237
|
|
|
243
|
-
const
|
|
244
|
-
const entries: TimelineEntry[] = [];
|
|
238
|
+
const entries: (TimelineEntry & { ts: number })[] = [];
|
|
245
239
|
|
|
246
|
-
for (const
|
|
240
|
+
for (const filePath of allFiles) {
|
|
247
241
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
242
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
|
|
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
|
+
if (Array.isArray(content) && content.some((b: any) => b.type === 'tool_result')) continue;
|
|
249
|
+
const textBlock = Array.isArray(content)
|
|
250
|
+
? content.find((b: any) => b.type === 'text')
|
|
251
|
+
: null;
|
|
252
|
+
const text: string = textBlock?.text ?? (typeof content === 'string' ? content : '');
|
|
253
|
+
if (!text.trim()) continue;
|
|
254
|
+
|
|
255
|
+
const tsStr: string = obj.timestamp ?? '';
|
|
256
|
+
const tsNum = tsStr ? new Date(tsStr).getTime() : 0;
|
|
257
|
+
const time = tsStr ? new Date(tsStr).toTimeString().slice(0, 5) : '';
|
|
258
|
+
|
|
259
|
+
entries.push({ ts: tsNum, time, text: text.replace(/\n/g, ' ').slice(0, 80) });
|
|
260
|
+
} catch {}
|
|
263
261
|
}
|
|
264
|
-
|
|
265
|
-
entries.push({ time, text: text.replace(/\n/g, ' ').slice(0, 80) });
|
|
266
262
|
} catch {}
|
|
267
263
|
}
|
|
268
264
|
|
|
269
|
-
return
|
|
265
|
+
// Sort all sessions by time, return last 50
|
|
266
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
267
|
+
return entries.slice(-50).map(({ time, text }) => ({ time, text }));
|
|
270
268
|
}
|
|
271
269
|
|
|
272
270
|
// ── UI Components ──────────────────────────────────────────────────────────
|
|
@@ -753,7 +751,7 @@ function App() {
|
|
|
753
751
|
|
|
754
752
|
const [usage, setUsage] = useState<any>(readTokenUsage(cwd));
|
|
755
753
|
const [history, setHistory] = useState<any>(readTokenHistory(cwd));
|
|
756
|
-
const [git, setGit] = useState<any>(
|
|
754
|
+
const [git, setGit] = useState<any>({ isRepo: false, branch: 'loading…', modified: [], added: [], deleted: [], recentCommits: [], totalChanges: 0 });
|
|
757
755
|
const [project, setProject] = useState<ProjectInfo | null>(null);
|
|
758
756
|
const [rateLimits, setRateLimits] = useState<any>(getUsageSync());
|
|
759
757
|
|
|
@@ -793,8 +791,8 @@ function App() {
|
|
|
793
791
|
const refresh = useCallback(() => {
|
|
794
792
|
setUsage(readTokenUsage(cwd));
|
|
795
793
|
setHistory(readTokenHistory(cwd));
|
|
796
|
-
setGit(readGitInfo(cwd));
|
|
797
794
|
setUpdatedAt(Date.now());
|
|
795
|
+
readGitInfo(cwd).then(setGit).catch(() => {});
|
|
798
796
|
getUsage().then(setRateLimits).catch(() => {});
|
|
799
797
|
readSessionTimeline(cwd).then(entries => {
|
|
800
798
|
setTimeline(entries);
|
|
@@ -812,6 +810,8 @@ function App() {
|
|
|
812
810
|
.catch(() => { setLoading(false); });
|
|
813
811
|
// Full deep scan in background → update silently
|
|
814
812
|
scanProject(cwd, 8).then(p => { setProject(p); }).catch(() => {});
|
|
813
|
+
// Initial git load (async)
|
|
814
|
+
readGitInfo(cwd).then(setGit).catch(() => {});
|
|
815
815
|
// Initial API usage fetch
|
|
816
816
|
getUsage().then(setRateLimits).catch(() => {});
|
|
817
817
|
// Initial timeline load
|
|
@@ -843,7 +843,7 @@ function App() {
|
|
|
843
843
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
844
844
|
watcher.on('change', () => {
|
|
845
845
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
846
|
-
debounceTimer = setTimeout(refresh,
|
|
846
|
+
debounceTimer = setTimeout(refresh, 2000);
|
|
847
847
|
});
|
|
848
848
|
});
|
|
849
849
|
}
|