agenthud 0.10.0 → 0.11.1
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 +3 -2
- package/dist/index.js +1 -1
- package/dist/{main-HPL3AG6B.js → main-3J6HDPKN.js} +283 -129
- package/package.json +1 -1
- package/scripts/record-demo.sh +72 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
An observability layer for [Claude Code](https://github.com/anthropics/claude-code). **See** your live sessions, **export** structured activity logs, and **summarize** a day or a week into an LLM digest — all from one CLI.
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
11
|
AgentHUD reads Claude Code's session files from `~/.claude/projects/` and gives you three things:
|
|
12
12
|
|
|
@@ -134,7 +134,8 @@ When tracking is on, the tree panel's title shows `[LIVE ⠧]` and the status ba
|
|
|
134
134
|
|
|
135
135
|
| Key | Action |
|
|
136
136
|
|-----|--------|
|
|
137
|
-
| `↑` / `k` / `↓` / `j` | Scroll |
|
|
137
|
+
| `↑` / `k` / `↓` / `j` | Scroll one line |
|
|
138
|
+
| `Ctrl+U` / `Ctrl+D` | Half page up / down |
|
|
138
139
|
| `↵` / `Esc` / `q` | Close |
|
|
139
140
|
|
|
140
141
|
Detail view colors the content based on activity type:
|
package/dist/index.js
CHANGED
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
// src/main.ts
|
|
2
|
-
import { existsSync as existsSync6, rmSync } from "fs";
|
|
2
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, realpathSync, rmSync } from "fs";
|
|
3
3
|
import { homedir as homedir5 } from "os";
|
|
4
4
|
import { join as join6 } from "path";
|
|
5
5
|
import { createInterface as createInterface2 } from "readline";
|
|
6
|
-
|
|
7
|
-
// src/utils/legacyConfig.ts
|
|
8
|
-
import { join, resolve } from "path";
|
|
9
|
-
function isLegacyProjectConfig(cwd, home) {
|
|
10
|
-
const legacy = resolve(join(cwd, ".agenthud", "config.yaml"));
|
|
11
|
-
const global = resolve(join(home, ".agenthud", "config.yaml"));
|
|
12
|
-
return legacy !== global;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// src/main.ts
|
|
16
6
|
import { render } from "ink";
|
|
17
7
|
import React from "react";
|
|
18
8
|
|
|
19
9
|
// src/cli.ts
|
|
20
10
|
import { readFileSync } from "fs";
|
|
21
|
-
import { dirname, join
|
|
11
|
+
import { dirname, join } from "path";
|
|
22
12
|
import { fileURLToPath } from "url";
|
|
23
13
|
var ALL_TYPES = [
|
|
24
14
|
"response",
|
|
@@ -37,7 +27,8 @@ var KNOWN_WATCH_FLAGS = /* @__PURE__ */ new Set([
|
|
|
37
27
|
"-V",
|
|
38
28
|
"--version",
|
|
39
29
|
"-h",
|
|
40
|
-
"--help"
|
|
30
|
+
"--help",
|
|
31
|
+
"--cwd"
|
|
41
32
|
]);
|
|
42
33
|
var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
|
|
43
34
|
"--date",
|
|
@@ -66,6 +57,9 @@ Monitors all running Claude Code sessions in real-time.
|
|
|
66
57
|
Options:
|
|
67
58
|
-w, --watch Watch mode (default) \u2014 live updates
|
|
68
59
|
--once Print once and exit
|
|
60
|
+
--cwd Scope the view to the Claude project
|
|
61
|
+
containing the current directory.
|
|
62
|
+
Exits 1 if no such project is found.
|
|
69
63
|
-V, --version Show version number
|
|
70
64
|
-h, --help Show this help message
|
|
71
65
|
|
|
@@ -103,7 +97,7 @@ Config: ~/.agenthud/config.yaml
|
|
|
103
97
|
function getVersion() {
|
|
104
98
|
const __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
105
99
|
const packageJson = JSON.parse(
|
|
106
|
-
readFileSync(
|
|
100
|
+
readFileSync(join(__dirname2, "..", "package.json"), "utf-8")
|
|
107
101
|
);
|
|
108
102
|
return packageJson.version;
|
|
109
103
|
}
|
|
@@ -144,7 +138,7 @@ function parseArgs(args) {
|
|
|
144
138
|
return { mode: "watch", command: "version" };
|
|
145
139
|
}
|
|
146
140
|
if (args.includes("--once")) {
|
|
147
|
-
return { mode: "once" };
|
|
141
|
+
return args.includes("--cwd") ? { mode: "once", scopeToCwd: true } : { mode: "once" };
|
|
148
142
|
}
|
|
149
143
|
if (args[0] === "report") {
|
|
150
144
|
const rest = args.slice(1);
|
|
@@ -367,54 +361,16 @@ function parseArgs(args) {
|
|
|
367
361
|
};
|
|
368
362
|
}
|
|
369
363
|
}
|
|
370
|
-
return { mode: "watch" };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// src/utils/altScreen.ts
|
|
374
|
-
var ENTER = "\x1B[?1049h";
|
|
375
|
-
var LEAVE = "\x1B[?1049l";
|
|
376
|
-
var entered = false;
|
|
377
|
-
var left = false;
|
|
378
|
-
function enterAltScreen() {
|
|
379
|
-
if (entered) return;
|
|
380
|
-
entered = true;
|
|
381
|
-
process.stdout.write(ENTER);
|
|
382
|
-
}
|
|
383
|
-
function leaveAltScreen() {
|
|
384
|
-
if (left || !entered) return;
|
|
385
|
-
left = true;
|
|
386
|
-
process.stdout.write(LEAVE);
|
|
387
|
-
}
|
|
388
|
-
var hooksInstalled = false;
|
|
389
|
-
function installAltScreenCleanup() {
|
|
390
|
-
if (hooksInstalled) return;
|
|
391
|
-
hooksInstalled = true;
|
|
392
|
-
process.on("exit", () => {
|
|
393
|
-
leaveAltScreen();
|
|
394
|
-
});
|
|
395
|
-
process.on("SIGINT", () => {
|
|
396
|
-
leaveAltScreen();
|
|
397
|
-
process.exit(130);
|
|
398
|
-
});
|
|
399
|
-
process.on("SIGTERM", () => {
|
|
400
|
-
leaveAltScreen();
|
|
401
|
-
process.exit(143);
|
|
402
|
-
});
|
|
403
|
-
process.on("uncaughtException", (err) => {
|
|
404
|
-
leaveAltScreen();
|
|
405
|
-
setImmediate(() => {
|
|
406
|
-
throw err;
|
|
407
|
-
});
|
|
408
|
-
});
|
|
364
|
+
return args.includes("--cwd") ? { mode: "watch", scopeToCwd: true } : { mode: "watch" };
|
|
409
365
|
}
|
|
410
366
|
|
|
411
367
|
// src/config/globalConfig.ts
|
|
412
368
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
413
369
|
import { homedir } from "os";
|
|
414
|
-
import { join as
|
|
370
|
+
import { join as join2 } from "path";
|
|
415
371
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
416
|
-
var CONFIG_PATH =
|
|
417
|
-
var STATE_PATH =
|
|
372
|
+
var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
|
|
373
|
+
var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
|
|
418
374
|
var DEFAULT_GLOBAL_CONFIG = {
|
|
419
375
|
refreshIntervalMs: 2e3,
|
|
420
376
|
hiddenSessions: [],
|
|
@@ -436,7 +392,7 @@ function parseInterval(value) {
|
|
|
436
392
|
return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
|
|
437
393
|
}
|
|
438
394
|
function ensureAgenthudDir() {
|
|
439
|
-
const dir =
|
|
395
|
+
const dir = join2(homedir(), ".agenthud");
|
|
440
396
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
441
397
|
}
|
|
442
398
|
function writeDefaultConfig() {
|
|
@@ -604,8 +560,8 @@ function hideProject(name) {
|
|
|
604
560
|
updateState({ hiddenProjects: [...config.hiddenProjects, name] });
|
|
605
561
|
}
|
|
606
562
|
function hasProjectLevelConfig() {
|
|
607
|
-
const candidate =
|
|
608
|
-
if (candidate ===
|
|
563
|
+
const candidate = join2(process.cwd(), ".agenthud", "config.yaml");
|
|
564
|
+
if (candidate === join2(homedir(), ".agenthud", "config.yaml")) return false;
|
|
609
565
|
return existsSync(candidate);
|
|
610
566
|
}
|
|
611
567
|
|
|
@@ -643,7 +599,7 @@ function getCommitDetail(projectPath, hash) {
|
|
|
643
599
|
if (!projectPath) return null;
|
|
644
600
|
try {
|
|
645
601
|
return execSync(
|
|
646
|
-
`git
|
|
602
|
+
`git -C "${projectPath}" show --stat --patch --no-color ${hash}`,
|
|
647
603
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
648
604
|
).trim();
|
|
649
605
|
} catch {
|
|
@@ -657,7 +613,7 @@ function parseGitCommits(projectPath, startDate, endDate) {
|
|
|
657
613
|
let raw;
|
|
658
614
|
try {
|
|
659
615
|
raw = execSync(
|
|
660
|
-
`git
|
|
616
|
+
`git -C "${projectPath}" log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
|
|
661
617
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
662
618
|
).trim();
|
|
663
619
|
} catch {
|
|
@@ -769,11 +725,24 @@ function summarizeToolDetail(name, input, result) {
|
|
|
769
725
|
}
|
|
770
726
|
return getToolDetail(name, input);
|
|
771
727
|
}
|
|
728
|
+
function numberLines(content, start) {
|
|
729
|
+
const lines = content.split("\n");
|
|
730
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
731
|
+
const width = String(start + lines.length - 1).length;
|
|
732
|
+
return lines.map((line, i) => `${String(start + i).padStart(width)}: ${line}`).join("\n");
|
|
733
|
+
}
|
|
772
734
|
function buildToolDetailBody(name, input, result) {
|
|
773
735
|
if (name === "Write") {
|
|
774
736
|
const content = result?.content ?? input?.content;
|
|
775
737
|
if (content) return { text: content, kind: "code" };
|
|
776
738
|
}
|
|
739
|
+
if (name === "Read") {
|
|
740
|
+
const content = result?.file?.content;
|
|
741
|
+
if (content) {
|
|
742
|
+
const start = result?.file?.startLine ?? input?.offset ?? 1;
|
|
743
|
+
return { text: numberLines(content, start), kind: "code", numbered: true };
|
|
744
|
+
}
|
|
745
|
+
}
|
|
777
746
|
if (name === "Edit" || name === "Write") {
|
|
778
747
|
const hunks = result?.structuredPatch;
|
|
779
748
|
if (hunks && hunks.length > 0) {
|
|
@@ -893,6 +862,7 @@ function parseActivitiesFromLines(lines) {
|
|
|
893
862
|
if (body) {
|
|
894
863
|
entry2.detailBody = body.text;
|
|
895
864
|
entry2.detailKind = body.kind;
|
|
865
|
+
if (body.numbered) entry2.detailNumbered = true;
|
|
896
866
|
}
|
|
897
867
|
activities.push(entry2);
|
|
898
868
|
}
|
|
@@ -1062,25 +1032,10 @@ function generateReport(sessions, options2) {
|
|
|
1062
1032
|
return lines.join("\n").trimEnd();
|
|
1063
1033
|
}
|
|
1064
1034
|
|
|
1065
|
-
// src/data/summaryRunner.ts
|
|
1066
|
-
import { spawn } from "child_process";
|
|
1067
|
-
import {
|
|
1068
|
-
copyFileSync,
|
|
1069
|
-
createWriteStream,
|
|
1070
|
-
existsSync as existsSync4,
|
|
1071
|
-
mkdirSync as mkdirSync2,
|
|
1072
|
-
readFileSync as readFileSync5,
|
|
1073
|
-
unlinkSync
|
|
1074
|
-
} from "fs";
|
|
1075
|
-
import { homedir as homedir3 } from "os";
|
|
1076
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
1077
|
-
import { createInterface } from "readline";
|
|
1078
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1079
|
-
|
|
1080
1035
|
// src/data/sessions.ts
|
|
1081
1036
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
1082
1037
|
import { homedir as homedir2 } from "os";
|
|
1083
|
-
import { basename as basename2, join as
|
|
1038
|
+
import { basename as basename2, join as join3 } from "path";
|
|
1084
1039
|
|
|
1085
1040
|
// src/ui/constants.ts
|
|
1086
1041
|
import stringWidth from "string-width";
|
|
@@ -1170,7 +1125,7 @@ function detectLiveState(tailLines, mtimeMs, now) {
|
|
|
1170
1125
|
|
|
1171
1126
|
// src/data/sessions.ts
|
|
1172
1127
|
function getProjectsDir() {
|
|
1173
|
-
return process.env.CLAUDE_PROJECTS_DIR ??
|
|
1128
|
+
return process.env.CLAUDE_PROJECTS_DIR ?? join3(homedir2(), ".claude", "projects");
|
|
1174
1129
|
}
|
|
1175
1130
|
function decodeProjectPath(encoded) {
|
|
1176
1131
|
const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
|
|
@@ -1307,7 +1262,7 @@ function readEntrypoint(filePath) {
|
|
|
1307
1262
|
}
|
|
1308
1263
|
}
|
|
1309
1264
|
function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
1310
|
-
const subagentsDir =
|
|
1265
|
+
const subagentsDir = join3(projectDir, parentId, "subagents");
|
|
1311
1266
|
if (!existsSync3(subagentsDir)) return [];
|
|
1312
1267
|
let files;
|
|
1313
1268
|
try {
|
|
@@ -1320,7 +1275,7 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1320
1275
|
return files.map((file) => {
|
|
1321
1276
|
const id = file.replace(/\.jsonl$/, "");
|
|
1322
1277
|
const hideKey = `${projectName}/${id}`;
|
|
1323
|
-
const filePath =
|
|
1278
|
+
const filePath = join3(subagentsDir, file);
|
|
1324
1279
|
try {
|
|
1325
1280
|
const stat = statSync2(filePath);
|
|
1326
1281
|
const { agentId, taskDescription } = readSubAgentInfo(filePath);
|
|
@@ -1352,7 +1307,36 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1352
1307
|
(n) => n !== null && !config.hiddenSubAgents.includes(n.hideKey)
|
|
1353
1308
|
).sort((a, b) => b.lastModifiedMs - a.lastModifiedMs);
|
|
1354
1309
|
}
|
|
1355
|
-
function
|
|
1310
|
+
function findContainingProject(cwd, projectPaths, options2) {
|
|
1311
|
+
const resolve2 = options2?.realpath ?? ((p) => p);
|
|
1312
|
+
const cwdR = resolve2(cwd);
|
|
1313
|
+
let best = null;
|
|
1314
|
+
let bestLen = -1;
|
|
1315
|
+
for (const raw of projectPaths) {
|
|
1316
|
+
let pR;
|
|
1317
|
+
try {
|
|
1318
|
+
pR = resolve2(raw);
|
|
1319
|
+
} catch {
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (cwdR === pR) {
|
|
1323
|
+
if (pR.length > bestLen) {
|
|
1324
|
+
best = raw;
|
|
1325
|
+
bestLen = pR.length;
|
|
1326
|
+
}
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const boundary = cwdR[pR.length];
|
|
1330
|
+
if ((boundary === "/" || boundary === "\\") && cwdR.startsWith(pR)) {
|
|
1331
|
+
if (pR.length > bestLen) {
|
|
1332
|
+
best = raw;
|
|
1333
|
+
bestLen = pR.length;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return best;
|
|
1338
|
+
}
|
|
1339
|
+
function discoverSessions(config, options2) {
|
|
1356
1340
|
const projectsDir = getProjectsDir();
|
|
1357
1341
|
if (!existsSync3(projectsDir)) {
|
|
1358
1342
|
return {
|
|
@@ -1366,7 +1350,7 @@ function discoverSessions(config) {
|
|
|
1366
1350
|
try {
|
|
1367
1351
|
projectDirs = readdirSync(projectsDir).filter((entry) => {
|
|
1368
1352
|
try {
|
|
1369
|
-
return statSync2(
|
|
1353
|
+
return statSync2(join3(projectsDir, entry)).isDirectory();
|
|
1370
1354
|
} catch {
|
|
1371
1355
|
return false;
|
|
1372
1356
|
}
|
|
@@ -1380,9 +1364,11 @@ function discoverSessions(config) {
|
|
|
1380
1364
|
};
|
|
1381
1365
|
}
|
|
1382
1366
|
const allSessions = [];
|
|
1367
|
+
const scope = options2?.scopeToProject ?? null;
|
|
1383
1368
|
for (const encodedDir of projectDirs) {
|
|
1384
|
-
const projectDir =
|
|
1369
|
+
const projectDir = join3(projectsDir, encodedDir);
|
|
1385
1370
|
const decodedPath = decodeProjectPath(encodedDir);
|
|
1371
|
+
if (scope !== null && decodedPath !== scope) continue;
|
|
1386
1372
|
const projectName = basename2(decodedPath);
|
|
1387
1373
|
let files;
|
|
1388
1374
|
try {
|
|
@@ -1395,7 +1381,7 @@ function discoverSessions(config) {
|
|
|
1395
1381
|
for (const file of files) {
|
|
1396
1382
|
const id = file.replace(/\.jsonl$/, "");
|
|
1397
1383
|
const hideKey = `${projectName}/${id}`;
|
|
1398
|
-
const filePath =
|
|
1384
|
+
const filePath = join3(projectDir, file);
|
|
1399
1385
|
try {
|
|
1400
1386
|
const stat = statSync2(filePath);
|
|
1401
1387
|
const subAgents = buildSubAgents(id, projectDir, config, projectName);
|
|
@@ -1473,13 +1459,26 @@ function discoverSessions(config) {
|
|
|
1473
1459
|
}
|
|
1474
1460
|
|
|
1475
1461
|
// src/data/summaryRunner.ts
|
|
1462
|
+
import { spawn } from "child_process";
|
|
1463
|
+
import {
|
|
1464
|
+
copyFileSync,
|
|
1465
|
+
createWriteStream,
|
|
1466
|
+
existsSync as existsSync4,
|
|
1467
|
+
mkdirSync as mkdirSync2,
|
|
1468
|
+
readFileSync as readFileSync5,
|
|
1469
|
+
unlinkSync
|
|
1470
|
+
} from "fs";
|
|
1471
|
+
import { homedir as homedir3 } from "os";
|
|
1472
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1473
|
+
import { createInterface } from "readline";
|
|
1474
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1476
1475
|
function agenthudHomeDir() {
|
|
1477
|
-
const dir =
|
|
1476
|
+
const dir = join4(homedir3(), ".agenthud");
|
|
1478
1477
|
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
1479
1478
|
return dir;
|
|
1480
1479
|
}
|
|
1481
1480
|
function summariesDir() {
|
|
1482
|
-
const dir =
|
|
1481
|
+
const dir = join4(agenthudHomeDir(), "summaries");
|
|
1483
1482
|
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
1484
1483
|
return dir;
|
|
1485
1484
|
}
|
|
@@ -1487,20 +1486,20 @@ function promptFilename(kind) {
|
|
|
1487
1486
|
return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
|
|
1488
1487
|
}
|
|
1489
1488
|
function userPromptPath(kind) {
|
|
1490
|
-
return
|
|
1489
|
+
return join4(homedir3(), ".agenthud", promptFilename(kind));
|
|
1491
1490
|
}
|
|
1492
1491
|
function templatePath(kind) {
|
|
1493
1492
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1494
|
-
return
|
|
1493
|
+
return join4(here, "templates", promptFilename(kind));
|
|
1495
1494
|
}
|
|
1496
1495
|
function dateKey(d) {
|
|
1497
1496
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1498
1497
|
}
|
|
1499
1498
|
function dailyCachePath(date) {
|
|
1500
|
-
return
|
|
1499
|
+
return join4(summariesDir(), `${dateKey(date)}.md`);
|
|
1501
1500
|
}
|
|
1502
1501
|
function rangeCachePath(from, to) {
|
|
1503
|
-
return
|
|
1502
|
+
return join4(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
|
|
1504
1503
|
}
|
|
1505
1504
|
function isSameLocalDay2(a, b) {
|
|
1506
1505
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
@@ -1959,6 +1958,7 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1959
1958
|
|
|
1960
1959
|
// src/ui/App.tsx
|
|
1961
1960
|
import { existsSync as existsSync5, watch } from "fs";
|
|
1961
|
+
import { basename as basename3 } from "path";
|
|
1962
1962
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1963
1963
|
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
1964
1964
|
|
|
@@ -2255,8 +2255,31 @@ function getLineStyle(category) {
|
|
|
2255
2255
|
}
|
|
2256
2256
|
|
|
2257
2257
|
// src/ui/DetailViewPanel.tsx
|
|
2258
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2259
|
-
function
|
|
2258
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2259
|
+
function splitLineNumberGutter(text) {
|
|
2260
|
+
const m = text.match(/^(\s*\d+: )(.*)$/);
|
|
2261
|
+
return m ? [m[1], m[2]] : null;
|
|
2262
|
+
}
|
|
2263
|
+
function hardWrapByWidth(line, maxWidth) {
|
|
2264
|
+
if (maxWidth <= 0) return [line];
|
|
2265
|
+
const out = [];
|
|
2266
|
+
let cur = "";
|
|
2267
|
+
let curW = 0;
|
|
2268
|
+
for (const ch of line) {
|
|
2269
|
+
const w = getDisplayWidth(ch);
|
|
2270
|
+
if (curW + w > maxWidth && cur !== "") {
|
|
2271
|
+
out.push(cur);
|
|
2272
|
+
cur = ch;
|
|
2273
|
+
curW = w;
|
|
2274
|
+
} else {
|
|
2275
|
+
cur += ch;
|
|
2276
|
+
curW += w;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (cur !== "") out.push(cur);
|
|
2280
|
+
return out.length > 0 ? out : [line];
|
|
2281
|
+
}
|
|
2282
|
+
function wrapClassified(text, maxWidth, classifier, preserveWhitespace = false) {
|
|
2260
2283
|
if (!text) return [{ text: "(empty)", category: "prose" }];
|
|
2261
2284
|
const sourceLines = text.split("\n");
|
|
2262
2285
|
const categories = classifier(sourceLines);
|
|
@@ -2268,6 +2291,12 @@ function wrapClassified(text, maxWidth, classifier) {
|
|
|
2268
2291
|
out.push({ text: "", category: cat });
|
|
2269
2292
|
continue;
|
|
2270
2293
|
}
|
|
2294
|
+
if (preserveWhitespace) {
|
|
2295
|
+
for (const chunk of hardWrapByWidth(line, maxWidth)) {
|
|
2296
|
+
out.push({ text: chunk, category: cat });
|
|
2297
|
+
}
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2271
2300
|
const words = line.split(" ");
|
|
2272
2301
|
let current = "";
|
|
2273
2302
|
for (const word of words) {
|
|
@@ -2294,7 +2323,13 @@ function DetailViewPanel({
|
|
|
2294
2323
|
const contentWidth = innerWidth - 1;
|
|
2295
2324
|
const body = activity.detailBody ?? activity.detail;
|
|
2296
2325
|
const classifier = activity.detailKind === "diff" ? classifyDiffLines : activity.detailKind === "code" ? classifyCodeFences : activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
2297
|
-
const
|
|
2326
|
+
const preserveWhitespace = activity.detailKind === "diff" || activity.detailKind === "code" || activity.type === "commit";
|
|
2327
|
+
const allLines = wrapClassified(
|
|
2328
|
+
body,
|
|
2329
|
+
contentWidth,
|
|
2330
|
+
classifier,
|
|
2331
|
+
preserveWhitespace
|
|
2332
|
+
);
|
|
2298
2333
|
const totalLines = allLines.length;
|
|
2299
2334
|
const clampedOffset = Math.min(
|
|
2300
2335
|
scrollOffset,
|
|
@@ -2321,11 +2356,15 @@ function DetailViewPanel({
|
|
|
2321
2356
|
const entry = visibleSlice[i] ?? { text: "", category: "prose" };
|
|
2322
2357
|
const padding = Math.max(0, contentWidth - getDisplayWidth(entry.text));
|
|
2323
2358
|
const lineStyle = getLineStyle(entry.category);
|
|
2359
|
+
const gutterSplit = activity.detailNumbered ? splitLineNumberGutter(entry.text) : null;
|
|
2324
2360
|
contentRows.push(
|
|
2325
2361
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2326
2362
|
BOX.v,
|
|
2327
2363
|
" ",
|
|
2328
|
-
/* @__PURE__ */
|
|
2364
|
+
gutterSplit ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
2365
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: gutterSplit[0] }),
|
|
2366
|
+
/* @__PURE__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: gutterSplit[1] })
|
|
2367
|
+
] }) : /* @__PURE__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: entry.text }),
|
|
2329
2368
|
" ".repeat(padding),
|
|
2330
2369
|
BOX.v
|
|
2331
2370
|
] }, i)
|
|
@@ -2497,6 +2536,8 @@ function useHotkeys({
|
|
|
2497
2536
|
onDetailClose,
|
|
2498
2537
|
onDetailScrollUp,
|
|
2499
2538
|
onDetailScrollDown,
|
|
2539
|
+
onDetailScrollHalfPageUp,
|
|
2540
|
+
onDetailScrollHalfPageDown,
|
|
2500
2541
|
onFilter,
|
|
2501
2542
|
onHelp,
|
|
2502
2543
|
onHelpScroll,
|
|
@@ -2544,6 +2585,14 @@ function useHotkeys({
|
|
|
2544
2585
|
return;
|
|
2545
2586
|
}
|
|
2546
2587
|
if (detailMode) {
|
|
2588
|
+
if (key.ctrl && input === "u") {
|
|
2589
|
+
onDetailScrollHalfPageUp();
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2592
|
+
if (key.ctrl && input === "d") {
|
|
2593
|
+
onDetailScrollHalfPageDown();
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2547
2596
|
if (key.upArrow || input === "k") {
|
|
2548
2597
|
onDetailScrollUp();
|
|
2549
2598
|
return;
|
|
@@ -2642,7 +2691,7 @@ function useHotkeys({
|
|
|
2642
2691
|
}
|
|
2643
2692
|
};
|
|
2644
2693
|
const trackingItems = trackingOn ? ["TRK \u25CF"] : ["t: track"];
|
|
2645
|
-
const statusBarItems = helpMode ? ["\u2191\u2193/jk: scroll", "PgDn/Space: page", "\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
|
|
2694
|
+
const statusBarItems = helpMode ? ["\u2191\u2193/jk: scroll", "PgDn/Space: page", "\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "C-u/d: \xBDpage", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
|
|
2646
2695
|
...trackingItems,
|
|
2647
2696
|
"Tab: viewer",
|
|
2648
2697
|
"\u2191\u2193/jk: select",
|
|
@@ -2667,24 +2716,12 @@ function useHotkeys({
|
|
|
2667
2716
|
return { handleInput, statusBarItems };
|
|
2668
2717
|
}
|
|
2669
2718
|
|
|
2670
|
-
// src/ui/hooks/useTick.ts
|
|
2671
|
-
import { useEffect, useState } from "react";
|
|
2672
|
-
function useTick(active, intervalMs = 100) {
|
|
2673
|
-
const [n, setN] = useState(0);
|
|
2674
|
-
useEffect(() => {
|
|
2675
|
-
if (!active) return;
|
|
2676
|
-
const id = setInterval(() => setN((x) => x + 1), intervalMs);
|
|
2677
|
-
return () => clearInterval(id);
|
|
2678
|
-
}, [active, intervalMs]);
|
|
2679
|
-
return n;
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
2719
|
// src/ui/hooks/useSpinner.ts
|
|
2683
|
-
import { useEffect
|
|
2720
|
+
import { useEffect, useState } from "react";
|
|
2684
2721
|
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2685
2722
|
function useSpinner(active, intervalMs = 100) {
|
|
2686
|
-
const [index, setIndex] =
|
|
2687
|
-
|
|
2723
|
+
const [index, setIndex] = useState(0);
|
|
2724
|
+
useEffect(() => {
|
|
2688
2725
|
if (!active) return;
|
|
2689
2726
|
const timer = setInterval(() => {
|
|
2690
2727
|
setIndex((i) => (i + 1) % FRAMES.length);
|
|
@@ -2694,10 +2731,22 @@ function useSpinner(active, intervalMs = 100) {
|
|
|
2694
2731
|
return active ? FRAMES[index] : "";
|
|
2695
2732
|
}
|
|
2696
2733
|
|
|
2734
|
+
// src/ui/hooks/useTick.ts
|
|
2735
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
2736
|
+
function useTick(active, intervalMs = 100) {
|
|
2737
|
+
const [n, setN] = useState2(0);
|
|
2738
|
+
useEffect2(() => {
|
|
2739
|
+
if (!active) return;
|
|
2740
|
+
const id = setInterval(() => setN((x) => x + 1), intervalMs);
|
|
2741
|
+
return () => clearInterval(id);
|
|
2742
|
+
}, [active, intervalMs]);
|
|
2743
|
+
return n;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2697
2746
|
// src/ui/SessionTreePanel.tsx
|
|
2698
2747
|
import { homedir as homedir4 } from "os";
|
|
2699
2748
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2700
|
-
import { Fragment as
|
|
2749
|
+
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2701
2750
|
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2702
2751
|
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2703
2752
|
const seconds = Math.floor(elapsed / 1e3);
|
|
@@ -2921,7 +2970,7 @@ function ProjectRow({
|
|
|
2921
2970
|
dimColor: muted,
|
|
2922
2971
|
children: [
|
|
2923
2972
|
nameText,
|
|
2924
|
-
pathText ? /* @__PURE__ */ jsxs4(
|
|
2973
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
2925
2974
|
" ",
|
|
2926
2975
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2927
2976
|
] }) : null,
|
|
@@ -2998,12 +3047,14 @@ function SessionTreePanel({
|
|
|
2998
3047
|
maxRows,
|
|
2999
3048
|
expandedIds = /* @__PURE__ */ new Set(),
|
|
3000
3049
|
trackingOn = false,
|
|
3001
|
-
spinner = ""
|
|
3050
|
+
spinner = "",
|
|
3051
|
+
scopeLabel
|
|
3002
3052
|
}) {
|
|
3003
3053
|
const innerWidth = getInnerWidth(width);
|
|
3004
3054
|
const contentWidth = innerWidth - 1;
|
|
3005
3055
|
const titleSuffix = trackingOn ? `[LIVE ${spinner || "\u25BC"}]` : "";
|
|
3006
|
-
const
|
|
3056
|
+
const titleText = scopeLabel ? `Projects [${scopeLabel}]` : "Projects";
|
|
3057
|
+
const titleLine = createTitleLine(titleText, titleSuffix, width);
|
|
3007
3058
|
const bottomLine = createBottomLine(width);
|
|
3008
3059
|
const totalProjectCount = projects.length + coldProjects.length;
|
|
3009
3060
|
if (totalProjectCount === 0) {
|
|
@@ -3097,7 +3148,7 @@ function SessionTreePanel({
|
|
|
3097
3148
|
}
|
|
3098
3149
|
|
|
3099
3150
|
// src/ui/App.tsx
|
|
3100
|
-
import { Fragment as
|
|
3151
|
+
import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3101
3152
|
var VIEWER_HEIGHT_FRACTION = 0.55;
|
|
3102
3153
|
function subSummarySentinel(parentId) {
|
|
3103
3154
|
return {
|
|
@@ -3249,27 +3300,46 @@ function collectAllIds(tree) {
|
|
|
3249
3300
|
}
|
|
3250
3301
|
return ids;
|
|
3251
3302
|
}
|
|
3252
|
-
function
|
|
3303
|
+
function initialSelectedId(tree) {
|
|
3304
|
+
const firstProject = tree.projects[0];
|
|
3305
|
+
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
3306
|
+
if (tree.coldProjects.length > 0) return "__cold__";
|
|
3307
|
+
return null;
|
|
3308
|
+
}
|
|
3309
|
+
function initialExpandedIds(tree) {
|
|
3310
|
+
if (tree.projects.length === 0 && tree.coldProjects.length > 0) {
|
|
3311
|
+
return /* @__PURE__ */ new Set(["__cold__"]);
|
|
3312
|
+
}
|
|
3313
|
+
return /* @__PURE__ */ new Set();
|
|
3314
|
+
}
|
|
3315
|
+
function App({
|
|
3316
|
+
mode,
|
|
3317
|
+
scopeToProject: scopeToProject2
|
|
3318
|
+
}) {
|
|
3253
3319
|
const { exit } = useApp();
|
|
3254
3320
|
const { stdout } = useStdout();
|
|
3255
3321
|
const isWatchMode = mode === "watch";
|
|
3256
3322
|
const config = useMemo(() => loadGlobalConfig(), []);
|
|
3257
3323
|
const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
|
|
3324
|
+
const discoverOptions = useMemo(
|
|
3325
|
+
() => scopeToProject2 ? { scopeToProject: scopeToProject2 } : void 0,
|
|
3326
|
+
[scopeToProject2]
|
|
3327
|
+
);
|
|
3258
3328
|
const [sessionTree, setSessionTree] = useState3(
|
|
3259
|
-
() => discoverSessions(config)
|
|
3329
|
+
() => discoverSessions(config, discoverOptions)
|
|
3330
|
+
);
|
|
3331
|
+
const [selectedId, setSelectedId] = useState3(
|
|
3332
|
+
() => initialSelectedId(sessionTree)
|
|
3260
3333
|
);
|
|
3261
|
-
const [selectedId, setSelectedId] = useState3(() => {
|
|
3262
|
-
const firstProject = sessionTree.projects[0];
|
|
3263
|
-
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
3264
|
-
return null;
|
|
3265
|
-
});
|
|
3266
3334
|
const [focus, setFocus] = useState3("tree");
|
|
3267
3335
|
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
3268
3336
|
const [isLive, setIsLive] = useState3(true);
|
|
3269
3337
|
const [activities, setActivities] = useState3([]);
|
|
3270
3338
|
const [gitActivities, setGitActivities] = useState3([]);
|
|
3271
3339
|
const [newCount, setNewCount] = useState3(0);
|
|
3272
|
-
const [expandedIds, setExpandedIds] = useState3(
|
|
3340
|
+
const [expandedIds, setExpandedIds] = useState3(
|
|
3341
|
+
() => initialExpandedIds(sessionTree)
|
|
3342
|
+
);
|
|
3273
3343
|
const [viewerCursorLine, setViewerCursorLine] = useState3(0);
|
|
3274
3344
|
const [detailMode, setDetailMode] = useState3(false);
|
|
3275
3345
|
const [detailActivity, setDetailActivity] = useState3(
|
|
@@ -3364,7 +3434,7 @@ function App({ mode }) {
|
|
|
3364
3434
|
}, [selectedId, isWatchMode]);
|
|
3365
3435
|
const refresh = useCallback(() => {
|
|
3366
3436
|
const freshConfig = loadGlobalConfig();
|
|
3367
|
-
const tree = discoverSessions(freshConfig);
|
|
3437
|
+
const tree = discoverSessions(freshConfig, discoverOptions);
|
|
3368
3438
|
const updatedFlat = flattenSessions2(tree, expandedIds);
|
|
3369
3439
|
let nextSelected = selectedId;
|
|
3370
3440
|
if (tracking) {
|
|
@@ -3396,7 +3466,7 @@ function App({ mode }) {
|
|
|
3396
3466
|
setScrollOffset((o) => o + delta);
|
|
3397
3467
|
setNewCount((n) => n + delta);
|
|
3398
3468
|
}
|
|
3399
|
-
}, [selectedId, isLive, expandedIds, tracking]);
|
|
3469
|
+
}, [selectedId, isLive, expandedIds, tracking, discoverOptions]);
|
|
3400
3470
|
const refreshRef = useRef(refresh);
|
|
3401
3471
|
useEffect3(() => {
|
|
3402
3472
|
refreshRef.current = refresh;
|
|
@@ -3629,6 +3699,14 @@ function App({ mode }) {
|
|
|
3629
3699
|
onDetailScrollDown: () => {
|
|
3630
3700
|
setDetailScrollOffset((o) => o + 1);
|
|
3631
3701
|
},
|
|
3702
|
+
onDetailScrollHalfPageUp: () => {
|
|
3703
|
+
const step = Math.max(1, Math.floor(viewerRows / 2));
|
|
3704
|
+
setDetailScrollOffset((o) => Math.max(0, o - step));
|
|
3705
|
+
},
|
|
3706
|
+
onDetailScrollHalfPageDown: () => {
|
|
3707
|
+
const step = Math.max(1, Math.floor(viewerRows / 2));
|
|
3708
|
+
setDetailScrollOffset((o) => o + step);
|
|
3709
|
+
},
|
|
3632
3710
|
onEnter: () => {
|
|
3633
3711
|
if (focus === "viewer") {
|
|
3634
3712
|
const act = getSelectedActivity(
|
|
@@ -3838,7 +3916,7 @@ function App({ mode }) {
|
|
|
3838
3916
|
helpTotalLinesRef.current = n;
|
|
3839
3917
|
}
|
|
3840
3918
|
}
|
|
3841
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3919
|
+
) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
3842
3920
|
migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
|
|
3843
3921
|
/* @__PURE__ */ jsx5(
|
|
3844
3922
|
SessionTreePanel,
|
|
@@ -3851,7 +3929,8 @@ function App({ mode }) {
|
|
|
3851
3929
|
maxRows: treeRows,
|
|
3852
3930
|
expandedIds,
|
|
3853
3931
|
trackingOn: tracking,
|
|
3854
|
-
spinner
|
|
3932
|
+
spinner,
|
|
3933
|
+
scopeLabel: scopeToProject2 ? basename3(scopeToProject2) : void 0
|
|
3855
3934
|
}
|
|
3856
3935
|
),
|
|
3857
3936
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
|
|
@@ -3885,6 +3964,52 @@ function App({ mode }) {
|
|
|
3885
3964
|
] });
|
|
3886
3965
|
}
|
|
3887
3966
|
|
|
3967
|
+
// src/utils/altScreen.ts
|
|
3968
|
+
var ENTER = "\x1B[?1049h";
|
|
3969
|
+
var LEAVE = "\x1B[?1049l";
|
|
3970
|
+
var entered = false;
|
|
3971
|
+
var left = false;
|
|
3972
|
+
function enterAltScreen() {
|
|
3973
|
+
if (entered) return;
|
|
3974
|
+
entered = true;
|
|
3975
|
+
process.stdout.write(ENTER);
|
|
3976
|
+
}
|
|
3977
|
+
function leaveAltScreen() {
|
|
3978
|
+
if (left || !entered) return;
|
|
3979
|
+
left = true;
|
|
3980
|
+
process.stdout.write(LEAVE);
|
|
3981
|
+
}
|
|
3982
|
+
var hooksInstalled = false;
|
|
3983
|
+
function installAltScreenCleanup() {
|
|
3984
|
+
if (hooksInstalled) return;
|
|
3985
|
+
hooksInstalled = true;
|
|
3986
|
+
process.on("exit", () => {
|
|
3987
|
+
leaveAltScreen();
|
|
3988
|
+
});
|
|
3989
|
+
process.on("SIGINT", () => {
|
|
3990
|
+
leaveAltScreen();
|
|
3991
|
+
process.exit(130);
|
|
3992
|
+
});
|
|
3993
|
+
process.on("SIGTERM", () => {
|
|
3994
|
+
leaveAltScreen();
|
|
3995
|
+
process.exit(143);
|
|
3996
|
+
});
|
|
3997
|
+
process.on("uncaughtException", (err) => {
|
|
3998
|
+
leaveAltScreen();
|
|
3999
|
+
setImmediate(() => {
|
|
4000
|
+
throw err;
|
|
4001
|
+
});
|
|
4002
|
+
});
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
// src/utils/legacyConfig.ts
|
|
4006
|
+
import { join as join5, resolve } from "path";
|
|
4007
|
+
function isLegacyProjectConfig(cwd, home) {
|
|
4008
|
+
const legacy = resolve(join5(cwd, ".agenthud", "config.yaml"));
|
|
4009
|
+
const global = resolve(join5(home, ".agenthud", "config.yaml"));
|
|
4010
|
+
return legacy !== global;
|
|
4011
|
+
}
|
|
4012
|
+
|
|
3888
4013
|
// src/main.ts
|
|
3889
4014
|
var options = parseArgs(process.argv.slice(2));
|
|
3890
4015
|
if (options.error) {
|
|
@@ -3971,10 +4096,39 @@ if (options.mode === "summary") {
|
|
|
3971
4096
|
});
|
|
3972
4097
|
process.exit(exitCode);
|
|
3973
4098
|
}
|
|
4099
|
+
var scopeToProject;
|
|
4100
|
+
if (options.scopeToCwd) {
|
|
4101
|
+
const projectsDir = getProjectsDir();
|
|
4102
|
+
let registered = [];
|
|
4103
|
+
try {
|
|
4104
|
+
registered = readdirSync2(projectsDir).map(decodeProjectPath);
|
|
4105
|
+
} catch {
|
|
4106
|
+
}
|
|
4107
|
+
const safeReal = (p) => {
|
|
4108
|
+
try {
|
|
4109
|
+
return realpathSync(p);
|
|
4110
|
+
} catch {
|
|
4111
|
+
return p;
|
|
4112
|
+
}
|
|
4113
|
+
};
|
|
4114
|
+
const match = findContainingProject(process.cwd(), registered, {
|
|
4115
|
+
realpath: safeReal
|
|
4116
|
+
});
|
|
4117
|
+
if (!match) {
|
|
4118
|
+
process.stderr.write(
|
|
4119
|
+
`agenthud: --cwd: no Claude project found at or above ${process.cwd()}
|
|
4120
|
+
`
|
|
4121
|
+
);
|
|
4122
|
+
process.exit(1);
|
|
4123
|
+
}
|
|
4124
|
+
scopeToProject = match;
|
|
4125
|
+
process.stderr.write(`agenthud: scope = ${match}
|
|
4126
|
+
`);
|
|
4127
|
+
}
|
|
3974
4128
|
if (options.mode === "watch") {
|
|
3975
4129
|
installAltScreenCleanup();
|
|
3976
4130
|
enterAltScreen();
|
|
3977
4131
|
} else {
|
|
3978
4132
|
if (options.mode === "once") clearScreen();
|
|
3979
4133
|
}
|
|
3980
|
-
render(React.createElement(App, { mode: options.mode }));
|
|
4134
|
+
render(React.createElement(App, { mode: options.mode, scopeToProject }));
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Interactive screencast workflow: record what you do in the terminal,
|
|
3
|
+
# automatically encode to demo/live.gif on Ctrl+D.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# scripts/record-demo.sh
|
|
7
|
+
#
|
|
8
|
+
# Override defaults with env vars, e.g.:
|
|
9
|
+
# COLS=120 ROWS=32 AGG_THEME=dracula scripts/record-demo.sh
|
|
10
|
+
#
|
|
11
|
+
# Dependencies (install once):
|
|
12
|
+
# brew install asciinema agg
|
|
13
|
+
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
17
|
+
CAST="$ROOT/demo/.cache/live.cast"
|
|
18
|
+
GIF="$ROOT/demo/live.gif"
|
|
19
|
+
|
|
20
|
+
# Wider than the VHS tape's effective area so the split-pane TUI has
|
|
21
|
+
# room to breathe (tree gets ~22 rows, activity viewer ~25 rows at
|
|
22
|
+
# 48 total). For an even more generous recording use e.g.
|
|
23
|
+
# COLS=220 ROWS=56. Most modern emulators honor the xterm resize
|
|
24
|
+
# escape used below.
|
|
25
|
+
COLS="${COLS:-180}"
|
|
26
|
+
ROWS="${ROWS:-48}"
|
|
27
|
+
|
|
28
|
+
# agg ships: asciinema, dracula, github-dark, github-light, monokai,
|
|
29
|
+
# solarized-dark, solarized-light. Pass a path to a TOML file for
|
|
30
|
+
# anything else (e.g. Catppuccin Mocha).
|
|
31
|
+
AGG_THEME="${AGG_THEME:-monokai}"
|
|
32
|
+
AGG_FONT_SIZE="${AGG_FONT_SIZE:-16}"
|
|
33
|
+
|
|
34
|
+
for cmd in asciinema agg; do
|
|
35
|
+
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
36
|
+
echo "[record-demo] $cmd not found. Install with: brew install $cmd" >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
done
|
|
40
|
+
|
|
41
|
+
mkdir -p "$(dirname "$CAST")"
|
|
42
|
+
rm -f "$CAST"
|
|
43
|
+
|
|
44
|
+
# Ask the terminal to resize itself. Works in iTerm2, Terminal.app,
|
|
45
|
+
# kitty, alacritty, wezterm; silently ignored elsewhere.
|
|
46
|
+
printf '\033[8;%d;%dt' "$ROWS" "$COLS"
|
|
47
|
+
|
|
48
|
+
cat <<EOF
|
|
49
|
+
[record-demo]
|
|
50
|
+
size : ${COLS} cols × ${ROWS} rows
|
|
51
|
+
cast : ${CAST}
|
|
52
|
+
gif : ${GIF}
|
|
53
|
+
|
|
54
|
+
Tips:
|
|
55
|
+
- Type 'clear' once the recording shell appears so the prompt
|
|
56
|
+
starts on a clean screen (your normal PS1 will be captured
|
|
57
|
+
otherwise).
|
|
58
|
+
- Run your scenario at a natural pace.
|
|
59
|
+
- When you're done, press Ctrl+D (or type 'exit') to stop.
|
|
60
|
+
|
|
61
|
+
EOF
|
|
62
|
+
|
|
63
|
+
read -r -p "Press Enter to start recording..." _
|
|
64
|
+
|
|
65
|
+
asciinema rec "$CAST"
|
|
66
|
+
|
|
67
|
+
echo
|
|
68
|
+
echo "[record-demo] Encoding ${CAST} → ${GIF}"
|
|
69
|
+
agg --font-size "$AGG_FONT_SIZE" --theme "$AGG_THEME" "$CAST" "$GIF"
|
|
70
|
+
|
|
71
|
+
echo "[record-demo] Done: ${GIF}"
|
|
72
|
+
echo "[record-demo] Preview: open '${GIF}'"
|