agenthud 0.10.0 → 0.11.0
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-KTFHI6KH.js} +265 -123
- 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,14 +3300,21 @@ function collectAllIds(tree) {
|
|
|
3249
3300
|
}
|
|
3250
3301
|
return ids;
|
|
3251
3302
|
}
|
|
3252
|
-
function App({
|
|
3303
|
+
function App({
|
|
3304
|
+
mode,
|
|
3305
|
+
scopeToProject: scopeToProject2
|
|
3306
|
+
}) {
|
|
3253
3307
|
const { exit } = useApp();
|
|
3254
3308
|
const { stdout } = useStdout();
|
|
3255
3309
|
const isWatchMode = mode === "watch";
|
|
3256
3310
|
const config = useMemo(() => loadGlobalConfig(), []);
|
|
3257
3311
|
const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
|
|
3312
|
+
const discoverOptions = useMemo(
|
|
3313
|
+
() => scopeToProject2 ? { scopeToProject: scopeToProject2 } : void 0,
|
|
3314
|
+
[scopeToProject2]
|
|
3315
|
+
);
|
|
3258
3316
|
const [sessionTree, setSessionTree] = useState3(
|
|
3259
|
-
() => discoverSessions(config)
|
|
3317
|
+
() => discoverSessions(config, discoverOptions)
|
|
3260
3318
|
);
|
|
3261
3319
|
const [selectedId, setSelectedId] = useState3(() => {
|
|
3262
3320
|
const firstProject = sessionTree.projects[0];
|
|
@@ -3364,7 +3422,7 @@ function App({ mode }) {
|
|
|
3364
3422
|
}, [selectedId, isWatchMode]);
|
|
3365
3423
|
const refresh = useCallback(() => {
|
|
3366
3424
|
const freshConfig = loadGlobalConfig();
|
|
3367
|
-
const tree = discoverSessions(freshConfig);
|
|
3425
|
+
const tree = discoverSessions(freshConfig, discoverOptions);
|
|
3368
3426
|
const updatedFlat = flattenSessions2(tree, expandedIds);
|
|
3369
3427
|
let nextSelected = selectedId;
|
|
3370
3428
|
if (tracking) {
|
|
@@ -3396,7 +3454,7 @@ function App({ mode }) {
|
|
|
3396
3454
|
setScrollOffset((o) => o + delta);
|
|
3397
3455
|
setNewCount((n) => n + delta);
|
|
3398
3456
|
}
|
|
3399
|
-
}, [selectedId, isLive, expandedIds, tracking]);
|
|
3457
|
+
}, [selectedId, isLive, expandedIds, tracking, discoverOptions]);
|
|
3400
3458
|
const refreshRef = useRef(refresh);
|
|
3401
3459
|
useEffect3(() => {
|
|
3402
3460
|
refreshRef.current = refresh;
|
|
@@ -3629,6 +3687,14 @@ function App({ mode }) {
|
|
|
3629
3687
|
onDetailScrollDown: () => {
|
|
3630
3688
|
setDetailScrollOffset((o) => o + 1);
|
|
3631
3689
|
},
|
|
3690
|
+
onDetailScrollHalfPageUp: () => {
|
|
3691
|
+
const step = Math.max(1, Math.floor(viewerRows / 2));
|
|
3692
|
+
setDetailScrollOffset((o) => Math.max(0, o - step));
|
|
3693
|
+
},
|
|
3694
|
+
onDetailScrollHalfPageDown: () => {
|
|
3695
|
+
const step = Math.max(1, Math.floor(viewerRows / 2));
|
|
3696
|
+
setDetailScrollOffset((o) => o + step);
|
|
3697
|
+
},
|
|
3632
3698
|
onEnter: () => {
|
|
3633
3699
|
if (focus === "viewer") {
|
|
3634
3700
|
const act = getSelectedActivity(
|
|
@@ -3838,7 +3904,7 @@ function App({ mode }) {
|
|
|
3838
3904
|
helpTotalLinesRef.current = n;
|
|
3839
3905
|
}
|
|
3840
3906
|
}
|
|
3841
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3907
|
+
) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
3842
3908
|
migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
|
|
3843
3909
|
/* @__PURE__ */ jsx5(
|
|
3844
3910
|
SessionTreePanel,
|
|
@@ -3851,7 +3917,8 @@ function App({ mode }) {
|
|
|
3851
3917
|
maxRows: treeRows,
|
|
3852
3918
|
expandedIds,
|
|
3853
3919
|
trackingOn: tracking,
|
|
3854
|
-
spinner
|
|
3920
|
+
spinner,
|
|
3921
|
+
scopeLabel: scopeToProject2 ? basename3(scopeToProject2) : void 0
|
|
3855
3922
|
}
|
|
3856
3923
|
),
|
|
3857
3924
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
|
|
@@ -3885,6 +3952,52 @@ function App({ mode }) {
|
|
|
3885
3952
|
] });
|
|
3886
3953
|
}
|
|
3887
3954
|
|
|
3955
|
+
// src/utils/altScreen.ts
|
|
3956
|
+
var ENTER = "\x1B[?1049h";
|
|
3957
|
+
var LEAVE = "\x1B[?1049l";
|
|
3958
|
+
var entered = false;
|
|
3959
|
+
var left = false;
|
|
3960
|
+
function enterAltScreen() {
|
|
3961
|
+
if (entered) return;
|
|
3962
|
+
entered = true;
|
|
3963
|
+
process.stdout.write(ENTER);
|
|
3964
|
+
}
|
|
3965
|
+
function leaveAltScreen() {
|
|
3966
|
+
if (left || !entered) return;
|
|
3967
|
+
left = true;
|
|
3968
|
+
process.stdout.write(LEAVE);
|
|
3969
|
+
}
|
|
3970
|
+
var hooksInstalled = false;
|
|
3971
|
+
function installAltScreenCleanup() {
|
|
3972
|
+
if (hooksInstalled) return;
|
|
3973
|
+
hooksInstalled = true;
|
|
3974
|
+
process.on("exit", () => {
|
|
3975
|
+
leaveAltScreen();
|
|
3976
|
+
});
|
|
3977
|
+
process.on("SIGINT", () => {
|
|
3978
|
+
leaveAltScreen();
|
|
3979
|
+
process.exit(130);
|
|
3980
|
+
});
|
|
3981
|
+
process.on("SIGTERM", () => {
|
|
3982
|
+
leaveAltScreen();
|
|
3983
|
+
process.exit(143);
|
|
3984
|
+
});
|
|
3985
|
+
process.on("uncaughtException", (err) => {
|
|
3986
|
+
leaveAltScreen();
|
|
3987
|
+
setImmediate(() => {
|
|
3988
|
+
throw err;
|
|
3989
|
+
});
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
|
|
3993
|
+
// src/utils/legacyConfig.ts
|
|
3994
|
+
import { join as join5, resolve } from "path";
|
|
3995
|
+
function isLegacyProjectConfig(cwd, home) {
|
|
3996
|
+
const legacy = resolve(join5(cwd, ".agenthud", "config.yaml"));
|
|
3997
|
+
const global = resolve(join5(home, ".agenthud", "config.yaml"));
|
|
3998
|
+
return legacy !== global;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
3888
4001
|
// src/main.ts
|
|
3889
4002
|
var options = parseArgs(process.argv.slice(2));
|
|
3890
4003
|
if (options.error) {
|
|
@@ -3971,10 +4084,39 @@ if (options.mode === "summary") {
|
|
|
3971
4084
|
});
|
|
3972
4085
|
process.exit(exitCode);
|
|
3973
4086
|
}
|
|
4087
|
+
var scopeToProject;
|
|
4088
|
+
if (options.scopeToCwd) {
|
|
4089
|
+
const projectsDir = getProjectsDir();
|
|
4090
|
+
let registered = [];
|
|
4091
|
+
try {
|
|
4092
|
+
registered = readdirSync2(projectsDir).map(decodeProjectPath);
|
|
4093
|
+
} catch {
|
|
4094
|
+
}
|
|
4095
|
+
const safeReal = (p) => {
|
|
4096
|
+
try {
|
|
4097
|
+
return realpathSync(p);
|
|
4098
|
+
} catch {
|
|
4099
|
+
return p;
|
|
4100
|
+
}
|
|
4101
|
+
};
|
|
4102
|
+
const match = findContainingProject(process.cwd(), registered, {
|
|
4103
|
+
realpath: safeReal
|
|
4104
|
+
});
|
|
4105
|
+
if (!match) {
|
|
4106
|
+
process.stderr.write(
|
|
4107
|
+
`agenthud: --cwd: no Claude project found at or above ${process.cwd()}
|
|
4108
|
+
`
|
|
4109
|
+
);
|
|
4110
|
+
process.exit(1);
|
|
4111
|
+
}
|
|
4112
|
+
scopeToProject = match;
|
|
4113
|
+
process.stderr.write(`agenthud: scope = ${match}
|
|
4114
|
+
`);
|
|
4115
|
+
}
|
|
3974
4116
|
if (options.mode === "watch") {
|
|
3975
4117
|
installAltScreenCleanup();
|
|
3976
4118
|
enterAltScreen();
|
|
3977
4119
|
} else {
|
|
3978
4120
|
if (options.mode === "once") clearScreen();
|
|
3979
4121
|
}
|
|
3980
|
-
render(React.createElement(App, { mode: options.mode }));
|
|
4122
|
+
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}'"
|