agenthud 0.9.4 → 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 +8 -5
- package/dist/index.js +1 -1
- package/dist/{main-26QL33AJ.js → main-KTFHI6KH.js} +705 -250
- package/package.json +1 -1
- package/scripts/record-demo.sh +72 -0
|
@@ -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 {
|
|
@@ -685,20 +641,11 @@ function parseGitCommits(projectPath, startDate, endDate) {
|
|
|
685
641
|
// src/data/sessionHistory.ts
|
|
686
642
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
687
643
|
|
|
688
|
-
// src/data/
|
|
644
|
+
// src/data/toolDetails.ts
|
|
689
645
|
import { basename } from "path";
|
|
690
646
|
function stripAnsi(text) {
|
|
691
647
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
692
648
|
}
|
|
693
|
-
function parseModelName(modelId) {
|
|
694
|
-
const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
|
|
695
|
-
if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
|
|
696
|
-
const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
|
|
697
|
-
if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
|
|
698
|
-
const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
|
|
699
|
-
if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
|
|
700
|
-
return modelId.replace(/-\d{8}$/, "");
|
|
701
|
-
}
|
|
702
649
|
function getToolDetail(_toolName, input) {
|
|
703
650
|
if (!input) return "";
|
|
704
651
|
if (input.command) return stripAnsi(input.command);
|
|
@@ -708,11 +655,141 @@ function getToolDetail(_toolName, input) {
|
|
|
708
655
|
if (input.description) return stripAnsi(input.description);
|
|
709
656
|
return "";
|
|
710
657
|
}
|
|
658
|
+
function rangeStr(start, lines) {
|
|
659
|
+
return `L${start}-${start + Math.max(lines, 1) - 1}`;
|
|
660
|
+
}
|
|
661
|
+
function patchSpan(hunks) {
|
|
662
|
+
if (hunks.length === 0) return null;
|
|
663
|
+
const start = Math.min(...hunks.map((h) => h.newStart));
|
|
664
|
+
const end = Math.max(
|
|
665
|
+
...hunks.map((h) => h.newStart + Math.max(h.newLines, 1) - 1)
|
|
666
|
+
);
|
|
667
|
+
return `L${start}-${end}`;
|
|
668
|
+
}
|
|
669
|
+
function countChanges(hunks) {
|
|
670
|
+
let add = 0;
|
|
671
|
+
let del = 0;
|
|
672
|
+
for (const h of hunks) {
|
|
673
|
+
for (const line of h.lines ?? []) {
|
|
674
|
+
if (line.startsWith("+")) add++;
|
|
675
|
+
else if (line.startsWith("-")) del++;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const parts = [];
|
|
679
|
+
if (add > 0) parts.push(`+${add}`);
|
|
680
|
+
if (del > 0) parts.push(`-${del}`);
|
|
681
|
+
return parts.join(" ");
|
|
682
|
+
}
|
|
683
|
+
function joinParts(...parts) {
|
|
684
|
+
return parts.filter((p) => !!p).join(" ");
|
|
685
|
+
}
|
|
686
|
+
function summarizeToolDetail(name, input, result) {
|
|
687
|
+
const file = input?.file_path ? basename(input.file_path) : "";
|
|
688
|
+
if (name === "Edit" || name === "Write") {
|
|
689
|
+
const hunks = result?.structuredPatch;
|
|
690
|
+
if (hunks && hunks.length > 0) {
|
|
691
|
+
return joinParts(file, patchSpan(hunks), countChanges(hunks));
|
|
692
|
+
}
|
|
693
|
+
if (name === "Write") {
|
|
694
|
+
const content = result?.content ?? input?.content;
|
|
695
|
+
if (content) {
|
|
696
|
+
const n = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
697
|
+
return joinParts(file, rangeStr(1, n), `+${n}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return file;
|
|
701
|
+
}
|
|
702
|
+
if (name === "Read") {
|
|
703
|
+
const f = result?.file;
|
|
704
|
+
if (typeof f?.startLine === "number" && typeof f?.numLines === "number") {
|
|
705
|
+
return joinParts(file, rangeStr(f.startLine, f.numLines));
|
|
706
|
+
}
|
|
707
|
+
if (typeof input?.offset === "number" && typeof input?.limit === "number") {
|
|
708
|
+
return joinParts(file, rangeStr(input.offset, input.limit));
|
|
709
|
+
}
|
|
710
|
+
return file;
|
|
711
|
+
}
|
|
712
|
+
if (name === "TaskUpdate") {
|
|
713
|
+
const id = input?.taskId ?? result?.taskId;
|
|
714
|
+
const idStr = id ? `#${id}` : "";
|
|
715
|
+
const sc = result?.statusChange;
|
|
716
|
+
if (sc?.from && sc?.to) return joinParts(idStr, `${sc.from}\u2192${sc.to}`);
|
|
717
|
+
if (result?.updatedFields?.length) {
|
|
718
|
+
return joinParts(idStr, result.updatedFields.join(", "));
|
|
719
|
+
}
|
|
720
|
+
if (input?.status) return joinParts(idStr, input.status);
|
|
721
|
+
return idStr;
|
|
722
|
+
}
|
|
723
|
+
if (name === "TaskCreate") {
|
|
724
|
+
return input?.subject ?? "";
|
|
725
|
+
}
|
|
726
|
+
return getToolDetail(name, input);
|
|
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
|
+
}
|
|
734
|
+
function buildToolDetailBody(name, input, result) {
|
|
735
|
+
if (name === "Write") {
|
|
736
|
+
const content = result?.content ?? input?.content;
|
|
737
|
+
if (content) return { text: content, kind: "code" };
|
|
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
|
+
}
|
|
746
|
+
if (name === "Edit" || name === "Write") {
|
|
747
|
+
const hunks = result?.structuredPatch;
|
|
748
|
+
if (hunks && hunks.length > 0) {
|
|
749
|
+
const text = hunks.map(
|
|
750
|
+
(h) => `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@
|
|
751
|
+
${(h.lines ?? []).join("\n")}`
|
|
752
|
+
).join("\n");
|
|
753
|
+
return { text, kind: "diff" };
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/data/activityParser.ts
|
|
760
|
+
function parseModelName(modelId) {
|
|
761
|
+
const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
|
|
762
|
+
if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
|
|
763
|
+
const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
|
|
764
|
+
if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
|
|
765
|
+
const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
|
|
766
|
+
if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
|
|
767
|
+
return modelId.replace(/-\d{8}$/, "");
|
|
768
|
+
}
|
|
711
769
|
function parseActivitiesFromLines(lines) {
|
|
712
770
|
const activities = [];
|
|
713
771
|
let tokenCount = 0;
|
|
714
772
|
let modelName = null;
|
|
715
773
|
let sessionStartTime = null;
|
|
774
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
775
|
+
for (const line of lines) {
|
|
776
|
+
let entry;
|
|
777
|
+
try {
|
|
778
|
+
entry = JSON.parse(line);
|
|
779
|
+
} catch {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (entry.type !== "user") continue;
|
|
783
|
+
const tur = entry.toolUseResult;
|
|
784
|
+
if (!tur || typeof tur !== "object") continue;
|
|
785
|
+
const content = entry.message?.content;
|
|
786
|
+
if (!Array.isArray(content)) continue;
|
|
787
|
+
for (const b of content) {
|
|
788
|
+
if (b?.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
789
|
+
resultsById.set(b.tool_use_id, tur);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
716
793
|
for (const line of lines) {
|
|
717
794
|
let entry;
|
|
718
795
|
try {
|
|
@@ -767,19 +844,27 @@ function parseActivitiesFromLines(lines) {
|
|
|
767
844
|
} else if (block.type === "tool_use" && block.name) {
|
|
768
845
|
if (block.name === "TodoWrite") continue;
|
|
769
846
|
const icon = ICONS[block.name] ?? ICONS.Default;
|
|
770
|
-
const
|
|
847
|
+
const result = block.id ? resultsById.get(block.id) : void 0;
|
|
848
|
+
const detail = summarizeToolDetail(block.name, block.input, result);
|
|
849
|
+
const body = buildToolDetailBody(block.name, block.input, result);
|
|
771
850
|
const last = activities[activities.length - 1];
|
|
772
851
|
if (last && last.type === "tool" && last.label === block.name && last.detail === detail) {
|
|
773
852
|
last.count = (last.count ?? 1) + 1;
|
|
774
853
|
last.timestamp = timestamp;
|
|
775
854
|
} else {
|
|
776
|
-
|
|
855
|
+
const entry2 = {
|
|
777
856
|
timestamp,
|
|
778
857
|
type: "tool",
|
|
779
858
|
icon,
|
|
780
859
|
label: block.name,
|
|
781
860
|
detail
|
|
782
|
-
}
|
|
861
|
+
};
|
|
862
|
+
if (body) {
|
|
863
|
+
entry2.detailBody = body.text;
|
|
864
|
+
entry2.detailKind = body.kind;
|
|
865
|
+
if (body.numbered) entry2.detailNumbered = true;
|
|
866
|
+
}
|
|
867
|
+
activities.push(entry2);
|
|
783
868
|
}
|
|
784
869
|
} else if (block.type === "text" && block.text && block.text.length > 10) {
|
|
785
870
|
activities.push({
|
|
@@ -947,25 +1032,10 @@ function generateReport(sessions, options2) {
|
|
|
947
1032
|
return lines.join("\n").trimEnd();
|
|
948
1033
|
}
|
|
949
1034
|
|
|
950
|
-
// src/data/summaryRunner.ts
|
|
951
|
-
import { spawn } from "child_process";
|
|
952
|
-
import {
|
|
953
|
-
copyFileSync,
|
|
954
|
-
createWriteStream,
|
|
955
|
-
existsSync as existsSync4,
|
|
956
|
-
mkdirSync as mkdirSync2,
|
|
957
|
-
readFileSync as readFileSync5,
|
|
958
|
-
unlinkSync
|
|
959
|
-
} from "fs";
|
|
960
|
-
import { homedir as homedir3 } from "os";
|
|
961
|
-
import { dirname as dirname2, join as join5 } from "path";
|
|
962
|
-
import { createInterface } from "readline";
|
|
963
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
964
|
-
|
|
965
1035
|
// src/data/sessions.ts
|
|
966
1036
|
import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
967
1037
|
import { homedir as homedir2 } from "os";
|
|
968
|
-
import { basename as basename2, join as
|
|
1038
|
+
import { basename as basename2, join as join3 } from "path";
|
|
969
1039
|
|
|
970
1040
|
// src/ui/constants.ts
|
|
971
1041
|
import stringWidth from "string-width";
|
|
@@ -1026,9 +1096,36 @@ function getDisplayWidth(s) {
|
|
|
1026
1096
|
return w;
|
|
1027
1097
|
}
|
|
1028
1098
|
|
|
1099
|
+
// src/data/sessionLiveness.ts
|
|
1100
|
+
function detectLiveState(tailLines, mtimeMs, now) {
|
|
1101
|
+
if (now - mtimeMs > THIRTY_MINUTES_MS) return null;
|
|
1102
|
+
for (let i = tailLines.length - 1; i >= 0; i--) {
|
|
1103
|
+
const line = tailLines[i];
|
|
1104
|
+
if (!line || !line.trim()) continue;
|
|
1105
|
+
let entry;
|
|
1106
|
+
try {
|
|
1107
|
+
entry = JSON.parse(line);
|
|
1108
|
+
} catch {
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
if (entry.type === "assistant") {
|
|
1112
|
+
const content = entry.message?.content;
|
|
1113
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
1114
|
+
const toolUses = blocks.filter((b) => b && b.type === "tool_use");
|
|
1115
|
+
if (toolUses.length === 0) return "waiting";
|
|
1116
|
+
if (toolUses.some((b) => b.name === "AskUserQuestion")) return "waiting";
|
|
1117
|
+
return "working";
|
|
1118
|
+
}
|
|
1119
|
+
if (entry.type === "user") {
|
|
1120
|
+
return "working";
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return null;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1029
1126
|
// src/data/sessions.ts
|
|
1030
1127
|
function getProjectsDir() {
|
|
1031
|
-
return process.env.CLAUDE_PROJECTS_DIR ??
|
|
1128
|
+
return process.env.CLAUDE_PROJECTS_DIR ?? join3(homedir2(), ".claude", "projects");
|
|
1032
1129
|
}
|
|
1033
1130
|
function decodeProjectPath(encoded) {
|
|
1034
1131
|
const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
|
|
@@ -1079,23 +1176,26 @@ function readSubAgentInfo(filePath) {
|
|
|
1079
1176
|
return { agentId: null, taskDescription: null };
|
|
1080
1177
|
}
|
|
1081
1178
|
}
|
|
1082
|
-
function
|
|
1083
|
-
if (!existsSync3(filePath)) return null;
|
|
1179
|
+
function readSessionTail(filePath, mtimeMs, now) {
|
|
1180
|
+
if (!existsSync3(filePath)) return { modelName: null, liveState: null };
|
|
1084
1181
|
try {
|
|
1085
1182
|
const content = readFileSync4(filePath, "utf-8");
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1183
|
+
const tail = content.trim().split("\n").filter(Boolean).slice(-50);
|
|
1184
|
+
let modelName = null;
|
|
1185
|
+
for (const line of [...tail].reverse()) {
|
|
1088
1186
|
try {
|
|
1089
1187
|
const entry = JSON.parse(line);
|
|
1090
1188
|
if (entry.type === "assistant" && entry.message?.model) {
|
|
1091
|
-
|
|
1189
|
+
modelName = parseModelName(entry.message.model);
|
|
1190
|
+
break;
|
|
1092
1191
|
}
|
|
1093
1192
|
} catch {
|
|
1094
1193
|
}
|
|
1095
1194
|
}
|
|
1195
|
+
return { modelName, liveState: detectLiveState(tail, mtimeMs, now) };
|
|
1096
1196
|
} catch {
|
|
1197
|
+
return { modelName: null, liveState: null };
|
|
1097
1198
|
}
|
|
1098
|
-
return null;
|
|
1099
1199
|
}
|
|
1100
1200
|
var SYSTEM_PREFIXES = [
|
|
1101
1201
|
"<command-name>",
|
|
@@ -1162,7 +1262,7 @@ function readEntrypoint(filePath) {
|
|
|
1162
1262
|
}
|
|
1163
1263
|
}
|
|
1164
1264
|
function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
1165
|
-
const subagentsDir =
|
|
1265
|
+
const subagentsDir = join3(projectDir, parentId, "subagents");
|
|
1166
1266
|
if (!existsSync3(subagentsDir)) return [];
|
|
1167
1267
|
let files;
|
|
1168
1268
|
try {
|
|
@@ -1175,10 +1275,15 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1175
1275
|
return files.map((file) => {
|
|
1176
1276
|
const id = file.replace(/\.jsonl$/, "");
|
|
1177
1277
|
const hideKey = `${projectName}/${id}`;
|
|
1178
|
-
const filePath =
|
|
1278
|
+
const filePath = join3(subagentsDir, file);
|
|
1179
1279
|
try {
|
|
1180
1280
|
const stat = statSync2(filePath);
|
|
1181
1281
|
const { agentId, taskDescription } = readSubAgentInfo(filePath);
|
|
1282
|
+
const { modelName, liveState } = readSessionTail(
|
|
1283
|
+
filePath,
|
|
1284
|
+
stat.mtimeMs,
|
|
1285
|
+
Date.now()
|
|
1286
|
+
);
|
|
1182
1287
|
return {
|
|
1183
1288
|
id,
|
|
1184
1289
|
hideKey,
|
|
@@ -1187,12 +1292,13 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1187
1292
|
projectName: "",
|
|
1188
1293
|
lastModifiedMs: stat.mtimeMs,
|
|
1189
1294
|
status: getSessionStatus(stat.mtimeMs),
|
|
1190
|
-
modelName
|
|
1295
|
+
modelName,
|
|
1191
1296
|
subAgents: [],
|
|
1192
1297
|
agentId: agentId ?? void 0,
|
|
1193
1298
|
taskDescription: taskDescription ?? void 0,
|
|
1194
1299
|
nonInteractive: false,
|
|
1195
|
-
firstUserPrompt: null
|
|
1300
|
+
firstUserPrompt: null,
|
|
1301
|
+
liveState
|
|
1196
1302
|
};
|
|
1197
1303
|
} catch {
|
|
1198
1304
|
return null;
|
|
@@ -1201,7 +1307,36 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1201
1307
|
(n) => n !== null && !config.hiddenSubAgents.includes(n.hideKey)
|
|
1202
1308
|
).sort((a, b) => b.lastModifiedMs - a.lastModifiedMs);
|
|
1203
1309
|
}
|
|
1204
|
-
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) {
|
|
1205
1340
|
const projectsDir = getProjectsDir();
|
|
1206
1341
|
if (!existsSync3(projectsDir)) {
|
|
1207
1342
|
return {
|
|
@@ -1215,7 +1350,7 @@ function discoverSessions(config) {
|
|
|
1215
1350
|
try {
|
|
1216
1351
|
projectDirs = readdirSync(projectsDir).filter((entry) => {
|
|
1217
1352
|
try {
|
|
1218
|
-
return statSync2(
|
|
1353
|
+
return statSync2(join3(projectsDir, entry)).isDirectory();
|
|
1219
1354
|
} catch {
|
|
1220
1355
|
return false;
|
|
1221
1356
|
}
|
|
@@ -1229,9 +1364,11 @@ function discoverSessions(config) {
|
|
|
1229
1364
|
};
|
|
1230
1365
|
}
|
|
1231
1366
|
const allSessions = [];
|
|
1367
|
+
const scope = options2?.scopeToProject ?? null;
|
|
1232
1368
|
for (const encodedDir of projectDirs) {
|
|
1233
|
-
const projectDir =
|
|
1369
|
+
const projectDir = join3(projectsDir, encodedDir);
|
|
1234
1370
|
const decodedPath = decodeProjectPath(encodedDir);
|
|
1371
|
+
if (scope !== null && decodedPath !== scope) continue;
|
|
1235
1372
|
const projectName = basename2(decodedPath);
|
|
1236
1373
|
let files;
|
|
1237
1374
|
try {
|
|
@@ -1244,10 +1381,16 @@ function discoverSessions(config) {
|
|
|
1244
1381
|
for (const file of files) {
|
|
1245
1382
|
const id = file.replace(/\.jsonl$/, "");
|
|
1246
1383
|
const hideKey = `${projectName}/${id}`;
|
|
1247
|
-
const filePath =
|
|
1384
|
+
const filePath = join3(projectDir, file);
|
|
1248
1385
|
try {
|
|
1249
1386
|
const stat = statSync2(filePath);
|
|
1250
1387
|
const subAgents = buildSubAgents(id, projectDir, config, projectName);
|
|
1388
|
+
const nonInteractive = readEntrypoint(filePath) === "sdk-cli";
|
|
1389
|
+
const { modelName, liveState } = readSessionTail(
|
|
1390
|
+
filePath,
|
|
1391
|
+
stat.mtimeMs,
|
|
1392
|
+
Date.now()
|
|
1393
|
+
);
|
|
1251
1394
|
allSessions.push({
|
|
1252
1395
|
id,
|
|
1253
1396
|
hideKey,
|
|
@@ -1256,10 +1399,11 @@ function discoverSessions(config) {
|
|
|
1256
1399
|
projectName,
|
|
1257
1400
|
lastModifiedMs: stat.mtimeMs,
|
|
1258
1401
|
status: getSessionStatus(stat.mtimeMs),
|
|
1259
|
-
modelName
|
|
1402
|
+
modelName,
|
|
1260
1403
|
subAgents,
|
|
1261
|
-
nonInteractive
|
|
1262
|
-
firstUserPrompt: readFirstUserPrompt(filePath)
|
|
1404
|
+
nonInteractive,
|
|
1405
|
+
firstUserPrompt: readFirstUserPrompt(filePath),
|
|
1406
|
+
liveState: nonInteractive ? null : liveState
|
|
1263
1407
|
});
|
|
1264
1408
|
} catch {
|
|
1265
1409
|
}
|
|
@@ -1315,13 +1459,26 @@ function discoverSessions(config) {
|
|
|
1315
1459
|
}
|
|
1316
1460
|
|
|
1317
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";
|
|
1318
1475
|
function agenthudHomeDir() {
|
|
1319
|
-
const dir =
|
|
1476
|
+
const dir = join4(homedir3(), ".agenthud");
|
|
1320
1477
|
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
1321
1478
|
return dir;
|
|
1322
1479
|
}
|
|
1323
1480
|
function summariesDir() {
|
|
1324
|
-
const dir =
|
|
1481
|
+
const dir = join4(agenthudHomeDir(), "summaries");
|
|
1325
1482
|
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
1326
1483
|
return dir;
|
|
1327
1484
|
}
|
|
@@ -1329,20 +1486,20 @@ function promptFilename(kind) {
|
|
|
1329
1486
|
return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
|
|
1330
1487
|
}
|
|
1331
1488
|
function userPromptPath(kind) {
|
|
1332
|
-
return
|
|
1489
|
+
return join4(homedir3(), ".agenthud", promptFilename(kind));
|
|
1333
1490
|
}
|
|
1334
1491
|
function templatePath(kind) {
|
|
1335
1492
|
const here = dirname2(fileURLToPath2(import.meta.url));
|
|
1336
|
-
return
|
|
1493
|
+
return join4(here, "templates", promptFilename(kind));
|
|
1337
1494
|
}
|
|
1338
1495
|
function dateKey(d) {
|
|
1339
1496
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1340
1497
|
}
|
|
1341
1498
|
function dailyCachePath(date) {
|
|
1342
|
-
return
|
|
1499
|
+
return join4(summariesDir(), `${dateKey(date)}.md`);
|
|
1343
1500
|
}
|
|
1344
1501
|
function rangeCachePath(from, to) {
|
|
1345
|
-
return
|
|
1502
|
+
return join4(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
|
|
1346
1503
|
}
|
|
1347
1504
|
function isSameLocalDay2(a, b) {
|
|
1348
1505
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
@@ -1801,12 +1958,39 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1801
1958
|
|
|
1802
1959
|
// src/ui/App.tsx
|
|
1803
1960
|
import { existsSync as existsSync5, watch } from "fs";
|
|
1961
|
+
import { basename as basename3 } from "path";
|
|
1804
1962
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1805
1963
|
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
1806
1964
|
|
|
1807
1965
|
// src/ui/ActivityViewerPanel.tsx
|
|
1808
1966
|
import { Box, Text } from "ink";
|
|
1809
|
-
import {
|
|
1967
|
+
import { memo } from "react";
|
|
1968
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
1969
|
+
function brighten(color) {
|
|
1970
|
+
switch (color) {
|
|
1971
|
+
case void 0:
|
|
1972
|
+
case "gray":
|
|
1973
|
+
return "white";
|
|
1974
|
+
case "white":
|
|
1975
|
+
return "whiteBright";
|
|
1976
|
+
case "green":
|
|
1977
|
+
return "greenBright";
|
|
1978
|
+
case "yellow":
|
|
1979
|
+
return "yellowBright";
|
|
1980
|
+
case "magenta":
|
|
1981
|
+
return "magentaBright";
|
|
1982
|
+
case "cyan":
|
|
1983
|
+
return "cyanBright";
|
|
1984
|
+
case "red":
|
|
1985
|
+
return "redBright";
|
|
1986
|
+
case "blue":
|
|
1987
|
+
return "blueBright";
|
|
1988
|
+
case "black":
|
|
1989
|
+
return "blackBright";
|
|
1990
|
+
default:
|
|
1991
|
+
return color;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1810
1994
|
function getActivityStyle(activity) {
|
|
1811
1995
|
if (activity.type === "user") {
|
|
1812
1996
|
return { color: "white", dimColor: false };
|
|
@@ -1855,6 +2039,76 @@ function truncateDetail2(detail, maxWidth) {
|
|
|
1855
2039
|
}
|
|
1856
2040
|
return `${truncated}\u2026`;
|
|
1857
2041
|
}
|
|
2042
|
+
var ActivityRow = memo(function ActivityRow2({
|
|
2043
|
+
activity,
|
|
2044
|
+
timestamp,
|
|
2045
|
+
width,
|
|
2046
|
+
contentWidth,
|
|
2047
|
+
isCursor,
|
|
2048
|
+
isLiveRow,
|
|
2049
|
+
liveSpinnerFrame,
|
|
2050
|
+
liveTick
|
|
2051
|
+
}) {
|
|
2052
|
+
const style = getActivityStyle(activity);
|
|
2053
|
+
const timestampWidth = timestamp.length;
|
|
2054
|
+
const icon = isLiveRow && liveSpinnerFrame ? liveSpinnerFrame : activity.icon;
|
|
2055
|
+
const iconWidth = getDisplayWidth(icon);
|
|
2056
|
+
const label = activity.label;
|
|
2057
|
+
const detail = activity.detail;
|
|
2058
|
+
const count = activity.count;
|
|
2059
|
+
const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
|
|
2060
|
+
const countSuffixWidth = countSuffix.length;
|
|
2061
|
+
const prefixWidth = 2 + timestampWidth + iconWidth + 1;
|
|
2062
|
+
const labelPart = detail ? `${label}: ` : label;
|
|
2063
|
+
const labelWidth = labelPart.length;
|
|
2064
|
+
const detailMaxWidth = width - 2 - timestampWidth - iconWidth - 1 - labelWidth - countSuffixWidth - 1;
|
|
2065
|
+
let labelContent;
|
|
2066
|
+
if (detail) {
|
|
2067
|
+
const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
|
|
2068
|
+
labelContent = `${labelPart}${truncated}${countSuffix}`;
|
|
2069
|
+
} else {
|
|
2070
|
+
labelContent = label + countSuffix;
|
|
2071
|
+
}
|
|
2072
|
+
const usedWidth = 1 + 1 + timestampWidth + iconWidth + 1 + getDisplayWidth(labelContent) + 1;
|
|
2073
|
+
const padding = Math.max(0, width - usedWidth);
|
|
2074
|
+
const SWEEP_WIDTH = 10;
|
|
2075
|
+
let labelNode = labelContent;
|
|
2076
|
+
if (isLiveRow && !isCursor && liveTick != null && labelContent.length > 0) {
|
|
2077
|
+
const period = labelContent.length + SWEEP_WIDTH;
|
|
2078
|
+
const offset = liveTick % period - SWEEP_WIDTH;
|
|
2079
|
+
const litStart = Math.max(0, offset);
|
|
2080
|
+
const litEnd = Math.min(labelContent.length, offset + SWEEP_WIDTH);
|
|
2081
|
+
if (litEnd > litStart) {
|
|
2082
|
+
const pre = labelContent.slice(0, litStart);
|
|
2083
|
+
const lit = labelContent.slice(litStart, litEnd);
|
|
2084
|
+
const post = labelContent.slice(litEnd);
|
|
2085
|
+
labelNode = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2086
|
+
pre,
|
|
2087
|
+
/* @__PURE__ */ jsx(Text, { color: brighten(style.color), bold: true, children: lit }),
|
|
2088
|
+
post
|
|
2089
|
+
] });
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2093
|
+
BOX.v,
|
|
2094
|
+
" ",
|
|
2095
|
+
/* @__PURE__ */ jsxs(Text, { backgroundColor: isCursor ? "blue" : void 0, children: [
|
|
2096
|
+
/* @__PURE__ */ jsx(Text, { dimColor: !isCursor && !isLiveRow, children: timestamp }),
|
|
2097
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: isLiveRow, children: icon }),
|
|
2098
|
+
" ",
|
|
2099
|
+
/* @__PURE__ */ jsx(
|
|
2100
|
+
Text,
|
|
2101
|
+
{
|
|
2102
|
+
color: isCursor ? void 0 : style.color,
|
|
2103
|
+
dimColor: !isCursor && !isLiveRow && style.dimColor,
|
|
2104
|
+
children: labelNode
|
|
2105
|
+
}
|
|
2106
|
+
),
|
|
2107
|
+
" ".repeat(padding)
|
|
2108
|
+
] }),
|
|
2109
|
+
BOX.v
|
|
2110
|
+
] });
|
|
2111
|
+
});
|
|
1858
2112
|
function ActivityViewerPanel({
|
|
1859
2113
|
activities,
|
|
1860
2114
|
sessionName,
|
|
@@ -1862,8 +2116,8 @@ function ActivityViewerPanel({
|
|
|
1862
2116
|
isLive,
|
|
1863
2117
|
newCount,
|
|
1864
2118
|
visibleRows,
|
|
1865
|
-
|
|
1866
|
-
|
|
2119
|
+
liveSpinnerFrame = null,
|
|
2120
|
+
liveTick = null,
|
|
1867
2121
|
width,
|
|
1868
2122
|
cursorLine,
|
|
1869
2123
|
hasFocus,
|
|
@@ -1907,57 +2161,28 @@ function ActivityViewerPanel({
|
|
|
1907
2161
|
} else {
|
|
1908
2162
|
const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
|
|
1909
2163
|
const cursorIndexInSlice = visibleActivities.length - 1 - effectiveCursor;
|
|
2164
|
+
const liveRowIndex = visibleActivities.length - 1;
|
|
2165
|
+
const liveTreatment = isLive && !!liveSpinnerFrame;
|
|
1910
2166
|
for (let i = 0; i < visibleActivities.length; i++) {
|
|
1911
2167
|
const activity = visibleActivities[i];
|
|
1912
|
-
const style = getActivityStyle(activity);
|
|
1913
2168
|
const isCursor = hasFocus && i === cursorIndexInSlice;
|
|
1914
|
-
const
|
|
1915
|
-
const timestamp = `[${
|
|
1916
|
-
const timestampWidth = timestamp.length;
|
|
1917
|
-
const icon = activity.icon;
|
|
1918
|
-
const iconWidth = getDisplayWidth(icon);
|
|
1919
|
-
const label = activity.label;
|
|
1920
|
-
const detail = activity.detail;
|
|
1921
|
-
const count = activity.count;
|
|
1922
|
-
const countSuffix = count && count > 1 ? ` (\xD7${count})` : "";
|
|
1923
|
-
const countSuffixWidth = countSuffix.length;
|
|
1924
|
-
const prefixWidth = 2 + timestampWidth + iconWidth + 1;
|
|
1925
|
-
const labelPart = detail ? `${label}: ` : label;
|
|
1926
|
-
const labelWidth = labelPart.length;
|
|
1927
|
-
const _availableForDetail = contentWidth - prefixWidth - labelWidth - countSuffixWidth + 1;
|
|
1928
|
-
const detailMaxWidth = width - 2 - timestampWidth - iconWidth - 1 - labelWidth - countSuffixWidth - 1;
|
|
1929
|
-
let labelContent;
|
|
1930
|
-
let _displayWidth;
|
|
1931
|
-
if (detail) {
|
|
1932
|
-
const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
|
|
1933
|
-
labelContent = `${labelPart}${truncated}${countSuffix}`;
|
|
1934
|
-
_displayWidth = prefixWidth - 1 + labelWidth + getDisplayWidth(truncated) + countSuffixWidth;
|
|
1935
|
-
} else {
|
|
1936
|
-
labelContent = label + countSuffix;
|
|
1937
|
-
_displayWidth = prefixWidth - 1 + label.length + countSuffixWidth;
|
|
1938
|
-
}
|
|
1939
|
-
const usedWidth = 1 + 1 + timestampWidth + iconWidth + 1 + getDisplayWidth(labelContent) + 1;
|
|
1940
|
-
const padding = Math.max(0, width - usedWidth);
|
|
2169
|
+
const isLiveRow = liveTreatment && i === liveRowIndex;
|
|
2170
|
+
const timestamp = `[${formatActivityTime(activity.timestamp, now)}] `;
|
|
1941
2171
|
lines.push(
|
|
1942
|
-
/* @__PURE__ */
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
),
|
|
1957
|
-
" ".repeat(padding)
|
|
1958
|
-
] }),
|
|
1959
|
-
BOX.v
|
|
1960
|
-
] }, `activity-${i}`)
|
|
2172
|
+
/* @__PURE__ */ jsx(
|
|
2173
|
+
ActivityRow,
|
|
2174
|
+
{
|
|
2175
|
+
activity,
|
|
2176
|
+
timestamp,
|
|
2177
|
+
width,
|
|
2178
|
+
contentWidth,
|
|
2179
|
+
isCursor,
|
|
2180
|
+
isLiveRow,
|
|
2181
|
+
liveSpinnerFrame: isLiveRow ? liveSpinnerFrame ?? void 0 : void 0,
|
|
2182
|
+
liveTick: isLiveRow ? liveTick ?? void 0 : void 0
|
|
2183
|
+
},
|
|
2184
|
+
`activity-${i}`
|
|
2185
|
+
)
|
|
1961
2186
|
);
|
|
1962
2187
|
}
|
|
1963
2188
|
}
|
|
@@ -1967,29 +2192,7 @@ function ActivityViewerPanel({
|
|
|
1967
2192
|
for (let i = 0; i < padCount; i++) {
|
|
1968
2193
|
padded.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${i}`));
|
|
1969
2194
|
}
|
|
1970
|
-
const
|
|
1971
|
-
const trailing = [];
|
|
1972
|
-
for (let i = 0; i < trailingBlankRows; i++) {
|
|
1973
|
-
if (i === 0 && isLive && liveIndicatorPosition != null && hasContent) {
|
|
1974
|
-
const pos = Math.max(0, liveIndicatorPosition);
|
|
1975
|
-
const arrow = "\u203A";
|
|
1976
|
-
const safePos = Math.min(pos, Math.max(0, contentWidth - 1));
|
|
1977
|
-
const padAfter = Math.max(0, contentWidth - safePos - 1);
|
|
1978
|
-
trailing.push(
|
|
1979
|
-
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1980
|
-
BOX.v,
|
|
1981
|
-
" ",
|
|
1982
|
-
" ".repeat(safePos),
|
|
1983
|
-
/* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: arrow }),
|
|
1984
|
-
" ".repeat(padAfter),
|
|
1985
|
-
BOX.v
|
|
1986
|
-
] }, `trail-${i}`)
|
|
1987
|
-
);
|
|
1988
|
-
} else {
|
|
1989
|
-
trailing.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `trail-${i}`));
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
const finalLines = [...padded, ...lines, ...trailing];
|
|
2195
|
+
const finalLines = [...padded, ...lines];
|
|
1993
2196
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
1994
2197
|
/* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
|
|
1995
2198
|
finalLines,
|
|
@@ -2052,8 +2255,31 @@ function getLineStyle(category) {
|
|
|
2052
2255
|
}
|
|
2053
2256
|
|
|
2054
2257
|
// src/ui/DetailViewPanel.tsx
|
|
2055
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2056
|
-
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) {
|
|
2057
2283
|
if (!text) return [{ text: "(empty)", category: "prose" }];
|
|
2058
2284
|
const sourceLines = text.split("\n");
|
|
2059
2285
|
const categories = classifier(sourceLines);
|
|
@@ -2065,6 +2291,12 @@ function wrapClassified(text, maxWidth, classifier) {
|
|
|
2065
2291
|
out.push({ text: "", category: cat });
|
|
2066
2292
|
continue;
|
|
2067
2293
|
}
|
|
2294
|
+
if (preserveWhitespace) {
|
|
2295
|
+
for (const chunk of hardWrapByWidth(line, maxWidth)) {
|
|
2296
|
+
out.push({ text: chunk, category: cat });
|
|
2297
|
+
}
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2068
2300
|
const words = line.split(" ");
|
|
2069
2301
|
let current = "";
|
|
2070
2302
|
for (const word of words) {
|
|
@@ -2089,8 +2321,15 @@ function DetailViewPanel({
|
|
|
2089
2321
|
}) {
|
|
2090
2322
|
const innerWidth = getInnerWidth(width);
|
|
2091
2323
|
const contentWidth = innerWidth - 1;
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2324
|
+
const body = activity.detailBody ?? activity.detail;
|
|
2325
|
+
const classifier = activity.detailKind === "diff" ? classifyDiffLines : activity.detailKind === "code" ? classifyCodeFences : activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
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
|
+
);
|
|
2094
2333
|
const totalLines = allLines.length;
|
|
2095
2334
|
const clampedOffset = Math.min(
|
|
2096
2335
|
scrollOffset,
|
|
@@ -2117,11 +2356,15 @@ function DetailViewPanel({
|
|
|
2117
2356
|
const entry = visibleSlice[i] ?? { text: "", category: "prose" };
|
|
2118
2357
|
const padding = Math.max(0, contentWidth - getDisplayWidth(entry.text));
|
|
2119
2358
|
const lineStyle = getLineStyle(entry.category);
|
|
2359
|
+
const gutterSplit = activity.detailNumbered ? splitLineNumberGutter(entry.text) : null;
|
|
2120
2360
|
contentRows.push(
|
|
2121
2361
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2122
2362
|
BOX.v,
|
|
2123
2363
|
" ",
|
|
2124
|
-
/* @__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 }),
|
|
2125
2368
|
" ".repeat(padding),
|
|
2126
2369
|
BOX.v
|
|
2127
2370
|
] }, i)
|
|
@@ -2155,6 +2398,7 @@ var SECTIONS = [
|
|
|
2155
2398
|
["PgDn / Ctrl+F", "Page down"],
|
|
2156
2399
|
["\u21B5", "Expand/collapse project, session, or summary"],
|
|
2157
2400
|
["h", "Hide selected (project/session/sub-agent)"],
|
|
2401
|
+
["t", "Track: auto-follow the newest live sub-agent (any nav key turns it off)"],
|
|
2158
2402
|
["Tab", "Switch focus to activity viewer"],
|
|
2159
2403
|
["r", "Refresh now"]
|
|
2160
2404
|
]
|
|
@@ -2292,11 +2536,15 @@ function useHotkeys({
|
|
|
2292
2536
|
onDetailClose,
|
|
2293
2537
|
onDetailScrollUp,
|
|
2294
2538
|
onDetailScrollDown,
|
|
2539
|
+
onDetailScrollHalfPageUp,
|
|
2540
|
+
onDetailScrollHalfPageDown,
|
|
2295
2541
|
onFilter,
|
|
2296
2542
|
onHelp,
|
|
2297
2543
|
onHelpScroll,
|
|
2298
2544
|
onHelpScrollToTop,
|
|
2299
|
-
|
|
2545
|
+
onToggleTracking,
|
|
2546
|
+
filterLabel,
|
|
2547
|
+
trackingOn = false
|
|
2300
2548
|
}) {
|
|
2301
2549
|
const handleInput = (input, key) => {
|
|
2302
2550
|
if (helpMode) {
|
|
@@ -2337,6 +2585,14 @@ function useHotkeys({
|
|
|
2337
2585
|
return;
|
|
2338
2586
|
}
|
|
2339
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
|
+
}
|
|
2340
2596
|
if (key.upArrow || input === "k") {
|
|
2341
2597
|
onDetailScrollUp();
|
|
2342
2598
|
return;
|
|
@@ -2371,6 +2627,10 @@ function useHotkeys({
|
|
|
2371
2627
|
onFilter();
|
|
2372
2628
|
return;
|
|
2373
2629
|
}
|
|
2630
|
+
if (input === "t" && !key.ctrl && onToggleTracking) {
|
|
2631
|
+
onToggleTracking();
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2374
2634
|
if (key.pageUp) {
|
|
2375
2635
|
onScrollPageUp();
|
|
2376
2636
|
return;
|
|
@@ -2430,7 +2690,9 @@ function useHotkeys({
|
|
|
2430
2690
|
}
|
|
2431
2691
|
}
|
|
2432
2692
|
};
|
|
2433
|
-
const
|
|
2693
|
+
const trackingItems = trackingOn ? ["TRK \u25CF"] : ["t: track"];
|
|
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" ? [
|
|
2695
|
+
...trackingItems,
|
|
2434
2696
|
"Tab: viewer",
|
|
2435
2697
|
"\u2191\u2193/jk: select",
|
|
2436
2698
|
"PgUp/Dn: page",
|
|
@@ -2440,6 +2702,7 @@ function useHotkeys({
|
|
|
2440
2702
|
"?: help",
|
|
2441
2703
|
"q: quit"
|
|
2442
2704
|
] : [
|
|
2705
|
+
...trackingItems,
|
|
2443
2706
|
"Tab: projects",
|
|
2444
2707
|
"\u2191\u2193/jk: scroll",
|
|
2445
2708
|
"PgUp/Dn: page",
|
|
@@ -2453,42 +2716,37 @@ function useHotkeys({
|
|
|
2453
2716
|
return { handleInput, statusBarItems };
|
|
2454
2717
|
}
|
|
2455
2718
|
|
|
2456
|
-
// src/ui/hooks/
|
|
2719
|
+
// src/ui/hooks/useSpinner.ts
|
|
2457
2720
|
import { useEffect, useState } from "react";
|
|
2458
|
-
|
|
2721
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2722
|
+
function useSpinner(active, intervalMs = 100) {
|
|
2459
2723
|
const [index, setIndex] = useState(0);
|
|
2460
|
-
useEffect(() => {
|
|
2461
|
-
setIndex(0);
|
|
2462
|
-
}, [resetKey]);
|
|
2463
2724
|
useEffect(() => {
|
|
2464
2725
|
if (!active) return;
|
|
2465
2726
|
const timer = setInterval(() => {
|
|
2466
|
-
setIndex((i) => (i + 1) %
|
|
2727
|
+
setIndex((i) => (i + 1) % FRAMES.length);
|
|
2467
2728
|
}, intervalMs);
|
|
2468
2729
|
return () => clearInterval(timer);
|
|
2469
|
-
}, [active,
|
|
2470
|
-
return index;
|
|
2730
|
+
}, [active, intervalMs]);
|
|
2731
|
+
return active ? FRAMES[index] : "";
|
|
2471
2732
|
}
|
|
2472
2733
|
|
|
2473
|
-
// src/ui/hooks/
|
|
2734
|
+
// src/ui/hooks/useTick.ts
|
|
2474
2735
|
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
const [index, setIndex] = useState2(0);
|
|
2736
|
+
function useTick(active, intervalMs = 100) {
|
|
2737
|
+
const [n, setN] = useState2(0);
|
|
2478
2738
|
useEffect2(() => {
|
|
2479
2739
|
if (!active) return;
|
|
2480
|
-
const
|
|
2481
|
-
|
|
2482
|
-
}, intervalMs);
|
|
2483
|
-
return () => clearInterval(timer);
|
|
2740
|
+
const id = setInterval(() => setN((x) => x + 1), intervalMs);
|
|
2741
|
+
return () => clearInterval(id);
|
|
2484
2742
|
}, [active, intervalMs]);
|
|
2485
|
-
return
|
|
2743
|
+
return n;
|
|
2486
2744
|
}
|
|
2487
2745
|
|
|
2488
2746
|
// src/ui/SessionTreePanel.tsx
|
|
2489
2747
|
import { homedir as homedir4 } from "os";
|
|
2490
2748
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2491
|
-
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2749
|
+
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2492
2750
|
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2493
2751
|
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2494
2752
|
const seconds = Math.floor(elapsed / 1e3);
|
|
@@ -2519,6 +2777,13 @@ function getStatusColor(status) {
|
|
|
2519
2777
|
return "gray";
|
|
2520
2778
|
}
|
|
2521
2779
|
}
|
|
2780
|
+
function getBadge(session) {
|
|
2781
|
+
if (session.liveState === "working")
|
|
2782
|
+
return { text: "[working]", color: "green" };
|
|
2783
|
+
if (session.liveState === "waiting")
|
|
2784
|
+
return { text: "[waiting]", color: "magenta" };
|
|
2785
|
+
return { text: `[${session.status}]`, color: getStatusColor(session.status) };
|
|
2786
|
+
}
|
|
2522
2787
|
function formatProjectPath(projectPath) {
|
|
2523
2788
|
const home = homedir4();
|
|
2524
2789
|
const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
|
|
@@ -2532,8 +2797,7 @@ function SessionRow({
|
|
|
2532
2797
|
contentWidth
|
|
2533
2798
|
}) {
|
|
2534
2799
|
const isParent = prefix === " ";
|
|
2535
|
-
const
|
|
2536
|
-
const badge = `[${session.status}]`;
|
|
2800
|
+
const { text: badge, color: badgeColor } = getBadge(session);
|
|
2537
2801
|
const elapsed = formatElapsed(session.lastModifiedMs);
|
|
2538
2802
|
const model = session.modelName ?? "";
|
|
2539
2803
|
const isNonInteractive = session.nonInteractive;
|
|
@@ -2582,7 +2846,7 @@ function SessionRow({
|
|
|
2582
2846
|
/* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
|
|
2583
2847
|
shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
|
|
2584
2848
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
2585
|
-
/* @__PURE__ */ jsx4(Text4, { color:
|
|
2849
|
+
/* @__PURE__ */ jsx4(Text4, { color: badgeColor, children: badge }),
|
|
2586
2850
|
middleText ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: middleSection }) : null,
|
|
2587
2851
|
/* @__PURE__ */ jsx4(Text4, { children: gap }),
|
|
2588
2852
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }),
|
|
@@ -2706,7 +2970,7 @@ function ProjectRow({
|
|
|
2706
2970
|
dimColor: muted,
|
|
2707
2971
|
children: [
|
|
2708
2972
|
nameText,
|
|
2709
|
-
pathText ? /* @__PURE__ */ jsxs4(
|
|
2973
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
2710
2974
|
" ",
|
|
2711
2975
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2712
2976
|
] }) : null,
|
|
@@ -2781,11 +3045,16 @@ function SessionTreePanel({
|
|
|
2781
3045
|
hasFocus,
|
|
2782
3046
|
width = DEFAULT_PANEL_WIDTH,
|
|
2783
3047
|
maxRows,
|
|
2784
|
-
expandedIds = /* @__PURE__ */ new Set()
|
|
3048
|
+
expandedIds = /* @__PURE__ */ new Set(),
|
|
3049
|
+
trackingOn = false,
|
|
3050
|
+
spinner = "",
|
|
3051
|
+
scopeLabel
|
|
2785
3052
|
}) {
|
|
2786
3053
|
const innerWidth = getInnerWidth(width);
|
|
2787
3054
|
const contentWidth = innerWidth - 1;
|
|
2788
|
-
const
|
|
3055
|
+
const titleSuffix = trackingOn ? `[LIVE ${spinner || "\u25BC"}]` : "";
|
|
3056
|
+
const titleText = scopeLabel ? `Projects [${scopeLabel}]` : "Projects";
|
|
3057
|
+
const titleLine = createTitleLine(titleText, titleSuffix, width);
|
|
2789
3058
|
const bottomLine = createBottomLine(width);
|
|
2790
3059
|
const totalProjectCount = projects.length + coldProjects.length;
|
|
2791
3060
|
if (totalProjectCount === 0) {
|
|
@@ -2879,7 +3148,7 @@ function SessionTreePanel({
|
|
|
2879
3148
|
}
|
|
2880
3149
|
|
|
2881
3150
|
// src/ui/App.tsx
|
|
2882
|
-
import { Fragment as
|
|
3151
|
+
import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2883
3152
|
var VIEWER_HEIGHT_FRACTION = 0.55;
|
|
2884
3153
|
function subSummarySentinel(parentId) {
|
|
2885
3154
|
return {
|
|
@@ -2893,7 +3162,8 @@ function subSummarySentinel(parentId) {
|
|
|
2893
3162
|
modelName: null,
|
|
2894
3163
|
subAgents: [],
|
|
2895
3164
|
nonInteractive: false,
|
|
2896
|
-
firstUserPrompt: null
|
|
3165
|
+
firstUserPrompt: null,
|
|
3166
|
+
liveState: null
|
|
2897
3167
|
};
|
|
2898
3168
|
}
|
|
2899
3169
|
function appendSubAgentRows(result, session, expandedIds) {
|
|
@@ -2932,7 +3202,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2932
3202
|
modelName: null,
|
|
2933
3203
|
subAgents: [],
|
|
2934
3204
|
nonInteractive: false,
|
|
2935
|
-
firstUserPrompt: null
|
|
3205
|
+
firstUserPrompt: null,
|
|
3206
|
+
liveState: null
|
|
2936
3207
|
});
|
|
2937
3208
|
const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
|
|
2938
3209
|
if (shouldShowSessions) {
|
|
@@ -2957,7 +3228,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2957
3228
|
modelName: null,
|
|
2958
3229
|
subAgents: [],
|
|
2959
3230
|
nonInteractive: false,
|
|
2960
|
-
firstUserPrompt: null
|
|
3231
|
+
firstUserPrompt: null,
|
|
3232
|
+
liveState: null
|
|
2961
3233
|
});
|
|
2962
3234
|
if (expandedIds.has("__cold__")) {
|
|
2963
3235
|
for (const project of tree.coldProjects) {
|
|
@@ -2980,14 +3252,69 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
|
|
|
2980
3252
|
const effectiveCursor = Math.min(cursorLine, visible.length - 1);
|
|
2981
3253
|
return visible[visible.length - 1 - effectiveCursor] ?? null;
|
|
2982
3254
|
}
|
|
2983
|
-
function
|
|
3255
|
+
function pickTrackingTarget(tree, selectedId, seen) {
|
|
3256
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3257
|
+
const liveSubs = [];
|
|
3258
|
+
const liveParents = [];
|
|
3259
|
+
for (const p of tree.projects) {
|
|
3260
|
+
for (const s of p.sessions) {
|
|
3261
|
+
ids.add(s.id);
|
|
3262
|
+
if (s.status === "hot" || s.status === "warm") {
|
|
3263
|
+
liveParents.push({
|
|
3264
|
+
id: s.id,
|
|
3265
|
+
mtime: s.lastModifiedMs,
|
|
3266
|
+
isNew: !seen.has(s.id)
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
for (const sa of s.subAgents) {
|
|
3270
|
+
ids.add(sa.id);
|
|
3271
|
+
if (sa.status === "hot" || sa.status === "warm") {
|
|
3272
|
+
liveSubs.push({
|
|
3273
|
+
id: sa.id,
|
|
3274
|
+
mtime: sa.lastModifiedMs,
|
|
3275
|
+
isNew: !seen.has(sa.id)
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
const newest = (xs) => xs.length === 0 ? null : xs.reduce((a, b) => a.mtime > b.mtime ? a : b).id;
|
|
3282
|
+
const newSubTarget = newest(liveSubs.filter((s) => s.isNew));
|
|
3283
|
+
if (newSubTarget) return { target: newSubTarget, ids };
|
|
3284
|
+
const newParentTarget = newest(liveParents.filter((p) => p.isNew));
|
|
3285
|
+
if (newParentTarget) return { target: newParentTarget, ids };
|
|
3286
|
+
const currentIsLive = selectedId != null && (liveSubs.some((s) => s.id === selectedId) || liveParents.some((p) => p.id === selectedId));
|
|
3287
|
+
if (currentIsLive) return { target: null, ids };
|
|
3288
|
+
return {
|
|
3289
|
+
target: newest(liveSubs) ?? newest(liveParents),
|
|
3290
|
+
ids
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
function collectAllIds(tree) {
|
|
3294
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3295
|
+
for (const p of tree.projects) {
|
|
3296
|
+
for (const s of p.sessions) {
|
|
3297
|
+
ids.add(s.id);
|
|
3298
|
+
for (const sa of s.subAgents) ids.add(sa.id);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
return ids;
|
|
3302
|
+
}
|
|
3303
|
+
function App({
|
|
3304
|
+
mode,
|
|
3305
|
+
scopeToProject: scopeToProject2
|
|
3306
|
+
}) {
|
|
2984
3307
|
const { exit } = useApp();
|
|
2985
3308
|
const { stdout } = useStdout();
|
|
2986
3309
|
const isWatchMode = mode === "watch";
|
|
2987
3310
|
const config = useMemo(() => loadGlobalConfig(), []);
|
|
2988
3311
|
const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
|
|
3312
|
+
const discoverOptions = useMemo(
|
|
3313
|
+
() => scopeToProject2 ? { scopeToProject: scopeToProject2 } : void 0,
|
|
3314
|
+
[scopeToProject2]
|
|
3315
|
+
);
|
|
2989
3316
|
const [sessionTree, setSessionTree] = useState3(
|
|
2990
|
-
() => discoverSessions(config)
|
|
3317
|
+
() => discoverSessions(config, discoverOptions)
|
|
2991
3318
|
);
|
|
2992
3319
|
const [selectedId, setSelectedId] = useState3(() => {
|
|
2993
3320
|
const firstProject = sessionTree.projects[0];
|
|
@@ -3011,6 +3338,8 @@ function App({ mode }) {
|
|
|
3011
3338
|
const [helpMode, setHelpMode] = useState3(false);
|
|
3012
3339
|
const [helpScroll, setHelpScroll] = useState3(0);
|
|
3013
3340
|
const helpTotalLinesRef = useRef(0);
|
|
3341
|
+
const [tracking, setTracking] = useState3(false);
|
|
3342
|
+
const seenIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
3014
3343
|
const allFlat = useMemo(
|
|
3015
3344
|
() => flattenSessions2(sessionTree, expandedIds),
|
|
3016
3345
|
[sessionTree, expandedIds]
|
|
@@ -3093,13 +3422,26 @@ function App({ mode }) {
|
|
|
3093
3422
|
}, [selectedId, isWatchMode]);
|
|
3094
3423
|
const refresh = useCallback(() => {
|
|
3095
3424
|
const freshConfig = loadGlobalConfig();
|
|
3096
|
-
const tree = discoverSessions(freshConfig);
|
|
3425
|
+
const tree = discoverSessions(freshConfig, discoverOptions);
|
|
3097
3426
|
const updatedFlat = flattenSessions2(tree, expandedIds);
|
|
3098
|
-
|
|
3427
|
+
let nextSelected = selectedId;
|
|
3428
|
+
if (tracking) {
|
|
3429
|
+
const { target, ids } = pickTrackingTarget(
|
|
3430
|
+
tree,
|
|
3431
|
+
selectedId,
|
|
3432
|
+
seenIdsRef.current
|
|
3433
|
+
);
|
|
3434
|
+
if (target && target !== selectedId) {
|
|
3435
|
+
setSelectedId(target);
|
|
3436
|
+
nextSelected = target;
|
|
3437
|
+
}
|
|
3438
|
+
seenIdsRef.current = ids;
|
|
3439
|
+
}
|
|
3440
|
+
const node = updatedFlat.find((s) => s.id === nextSelected);
|
|
3099
3441
|
if (!node) {
|
|
3100
3442
|
const allSessions = tree.projects?.flatMap((p) => p.sessions) ?? [];
|
|
3101
3443
|
const parentSession = allSessions.find(
|
|
3102
|
-
(s) => s.subAgents.some((sa) => sa.id ===
|
|
3444
|
+
(s) => s.subAgents.some((sa) => sa.id === nextSelected)
|
|
3103
3445
|
);
|
|
3104
3446
|
if (parentSession) setSelectedId(parentSession.id);
|
|
3105
3447
|
}
|
|
@@ -3112,7 +3454,7 @@ function App({ mode }) {
|
|
|
3112
3454
|
setScrollOffset((o) => o + delta);
|
|
3113
3455
|
setNewCount((n) => n + delta);
|
|
3114
3456
|
}
|
|
3115
|
-
}, [selectedId, isLive, expandedIds]);
|
|
3457
|
+
}, [selectedId, isLive, expandedIds, tracking, discoverOptions]);
|
|
3116
3458
|
const refreshRef = useRef(refresh);
|
|
3117
3459
|
useEffect3(() => {
|
|
3118
3460
|
refreshRef.current = refresh;
|
|
@@ -3147,6 +3489,11 @@ function App({ mode }) {
|
|
|
3147
3489
|
if (debounce) clearTimeout(debounce);
|
|
3148
3490
|
};
|
|
3149
3491
|
}, [isWatchMode, config.refreshIntervalMs]);
|
|
3492
|
+
useEffect3(() => {
|
|
3493
|
+
if (!isWatchMode || !tracking) return;
|
|
3494
|
+
const timer = setInterval(() => refreshRef.current(), 1e3);
|
|
3495
|
+
return () => clearInterval(timer);
|
|
3496
|
+
}, [isWatchMode, tracking]);
|
|
3150
3497
|
const filterPresets = config.filterPresets;
|
|
3151
3498
|
const activePreset = useMemo(
|
|
3152
3499
|
() => filterPresets[filterIndex % filterPresets.length] ?? [],
|
|
@@ -3171,26 +3518,18 @@ function App({ mode }) {
|
|
|
3171
3518
|
const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
|
|
3172
3519
|
const naturalTreeRows = allFlat.length;
|
|
3173
3520
|
const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
|
|
3174
|
-
const
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
);
|
|
3179
|
-
const spinner = useSpinner(isWatchMode);
|
|
3180
|
-
const viewerIndicatorWidth = Math.max(1, width - 3);
|
|
3181
|
-
const liveIndicatorPosition = useSlide(
|
|
3182
|
-
isWatchMode,
|
|
3183
|
-
viewerIndicatorWidth,
|
|
3184
|
-
180,
|
|
3185
|
-
// Reset to 0 whenever the viewer's subject changes so each new
|
|
3186
|
-
// session/sub-agent restarts the arrow from the left.
|
|
3187
|
-
selectedId
|
|
3188
|
-
);
|
|
3521
|
+
const viewerRows = Math.max(5, height - 7 - treeRows);
|
|
3522
|
+
const spinner = useSpinner(isWatchMode, 150);
|
|
3523
|
+
const tickActive = isWatchMode && isLive && !helpMode && !detailMode && activities.length > 0;
|
|
3524
|
+
const liveTick = useTick(tickActive, 150);
|
|
3189
3525
|
const helpViewportRows = Math.max(1, height - 3);
|
|
3190
3526
|
const helpScrollStep = (delta) => {
|
|
3191
3527
|
const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
|
|
3192
3528
|
setHelpScroll((s) => Math.max(0, Math.min(max, s + delta)));
|
|
3193
3529
|
};
|
|
3530
|
+
const stopTracking = () => {
|
|
3531
|
+
if (tracking) setTracking(false);
|
|
3532
|
+
};
|
|
3194
3533
|
const { handleInput, statusBarItems } = useHotkeys({
|
|
3195
3534
|
focus,
|
|
3196
3535
|
detailMode,
|
|
@@ -3201,12 +3540,29 @@ function App({ mode }) {
|
|
|
3201
3540
|
},
|
|
3202
3541
|
onHelpScroll: helpScrollStep,
|
|
3203
3542
|
onHelpScrollToTop: () => setHelpScroll(0),
|
|
3543
|
+
onToggleTracking: () => {
|
|
3544
|
+
setTracking((on) => {
|
|
3545
|
+
const next = !on;
|
|
3546
|
+
if (next) {
|
|
3547
|
+
seenIdsRef.current = collectAllIds(sessionTree);
|
|
3548
|
+
const { target } = pickTrackingTarget(
|
|
3549
|
+
sessionTree,
|
|
3550
|
+
selectedId,
|
|
3551
|
+
seenIdsRef.current
|
|
3552
|
+
);
|
|
3553
|
+
if (target) setSelectedId(target);
|
|
3554
|
+
}
|
|
3555
|
+
return next;
|
|
3556
|
+
});
|
|
3557
|
+
},
|
|
3558
|
+
trackingOn: tracking,
|
|
3204
3559
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3205
3560
|
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3206
3561
|
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
3207
3562
|
// Down arrow moves visually downward = newer direction = cursorLine--.
|
|
3208
3563
|
onScrollUp: () => {
|
|
3209
3564
|
if (focus === "tree") {
|
|
3565
|
+
stopTracking();
|
|
3210
3566
|
if (selectedIndex === -1) return;
|
|
3211
3567
|
const prev = Math.max(0, selectedIndex - 1);
|
|
3212
3568
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
@@ -3223,6 +3579,7 @@ function App({ mode }) {
|
|
|
3223
3579
|
},
|
|
3224
3580
|
onScrollDown: () => {
|
|
3225
3581
|
if (focus === "tree") {
|
|
3582
|
+
stopTracking();
|
|
3226
3583
|
if (selectedIndex === -1) return;
|
|
3227
3584
|
const next = Math.min(allFlat.length - 1, selectedIndex + 1);
|
|
3228
3585
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
@@ -3246,6 +3603,7 @@ function App({ mode }) {
|
|
|
3246
3603
|
// PgDn = visually down = newer direction = scrollOffset--
|
|
3247
3604
|
onScrollPageUp: () => {
|
|
3248
3605
|
if (focus === "tree") {
|
|
3606
|
+
stopTracking();
|
|
3249
3607
|
const prev = Math.max(0, selectedIndex - 5);
|
|
3250
3608
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3251
3609
|
} else {
|
|
@@ -3258,6 +3616,7 @@ function App({ mode }) {
|
|
|
3258
3616
|
},
|
|
3259
3617
|
onScrollPageDown: () => {
|
|
3260
3618
|
if (focus === "tree") {
|
|
3619
|
+
stopTracking();
|
|
3261
3620
|
const next = Math.min(allFlat.length - 1, selectedIndex + 5);
|
|
3262
3621
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3263
3622
|
} else {
|
|
@@ -3274,6 +3633,7 @@ function App({ mode }) {
|
|
|
3274
3633
|
},
|
|
3275
3634
|
onScrollHalfPageUp: () => {
|
|
3276
3635
|
if (focus === "tree") {
|
|
3636
|
+
stopTracking();
|
|
3277
3637
|
const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
|
|
3278
3638
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3279
3639
|
} else {
|
|
@@ -3289,6 +3649,7 @@ function App({ mode }) {
|
|
|
3289
3649
|
},
|
|
3290
3650
|
onScrollHalfPageDown: () => {
|
|
3291
3651
|
if (focus === "tree") {
|
|
3652
|
+
stopTracking();
|
|
3292
3653
|
const next = Math.min(
|
|
3293
3654
|
allFlat.length - 1,
|
|
3294
3655
|
selectedIndex + Math.ceil(5 / 2)
|
|
@@ -3326,6 +3687,14 @@ function App({ mode }) {
|
|
|
3326
3687
|
onDetailScrollDown: () => {
|
|
3327
3688
|
setDetailScrollOffset((o) => o + 1);
|
|
3328
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
|
+
},
|
|
3329
3698
|
onEnter: () => {
|
|
3330
3699
|
if (focus === "viewer") {
|
|
3331
3700
|
const act = getSelectedActivity(
|
|
@@ -3349,6 +3718,7 @@ function App({ mode }) {
|
|
|
3349
3718
|
return;
|
|
3350
3719
|
}
|
|
3351
3720
|
if (focus !== "tree" || !selectedId) return;
|
|
3721
|
+
stopTracking();
|
|
3352
3722
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3353
3723
|
const projectName = selectedId.slice(7, -2);
|
|
3354
3724
|
const isCold = sessionTree.coldProjects.some(
|
|
@@ -3433,6 +3803,7 @@ function App({ mode }) {
|
|
|
3433
3803
|
},
|
|
3434
3804
|
onHide: () => {
|
|
3435
3805
|
if (focus !== "tree" || !selectedId) return;
|
|
3806
|
+
stopTracking();
|
|
3436
3807
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3437
3808
|
const projectName = selectedId.slice(7, -2);
|
|
3438
3809
|
hideProject(projectName);
|
|
@@ -3492,7 +3863,13 @@ function App({ mode }) {
|
|
|
3492
3863
|
if (isWatchMode && (width < MIN_WIDTH || height + 1 < MIN_HEIGHT)) {
|
|
3493
3864
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
|
|
3494
3865
|
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "AgentHUD needs a larger terminal." }),
|
|
3495
|
-
/* @__PURE__ */ jsx5(
|
|
3866
|
+
/* @__PURE__ */ jsx5(
|
|
3867
|
+
Text5,
|
|
3868
|
+
{
|
|
3869
|
+
dimColor: true,
|
|
3870
|
+
children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows`
|
|
3871
|
+
}
|
|
3872
|
+
),
|
|
3496
3873
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Current: ${width} cols \xD7 ${height + 1} rows` }),
|
|
3497
3874
|
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
3498
3875
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Resize the window and AgentHUD will redraw automatically." }),
|
|
@@ -3527,7 +3904,7 @@ function App({ mode }) {
|
|
|
3527
3904
|
helpTotalLinesRef.current = n;
|
|
3528
3905
|
}
|
|
3529
3906
|
}
|
|
3530
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3907
|
+
) : /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
3531
3908
|
migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
|
|
3532
3909
|
/* @__PURE__ */ jsx5(
|
|
3533
3910
|
SessionTreePanel,
|
|
@@ -3538,7 +3915,10 @@ function App({ mode }) {
|
|
|
3538
3915
|
hasFocus: focus === "tree",
|
|
3539
3916
|
width,
|
|
3540
3917
|
maxRows: treeRows,
|
|
3541
|
-
expandedIds
|
|
3918
|
+
expandedIds,
|
|
3919
|
+
trackingOn: tracking,
|
|
3920
|
+
spinner,
|
|
3921
|
+
scopeLabel: scopeToProject2 ? basename3(scopeToProject2) : void 0
|
|
3542
3922
|
}
|
|
3543
3923
|
),
|
|
3544
3924
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
|
|
@@ -3559,8 +3939,8 @@ function App({ mode }) {
|
|
|
3559
3939
|
isLive,
|
|
3560
3940
|
newCount,
|
|
3561
3941
|
visibleRows: viewerRows,
|
|
3562
|
-
|
|
3563
|
-
|
|
3942
|
+
liveSpinnerFrame: spinner,
|
|
3943
|
+
liveTick,
|
|
3564
3944
|
width,
|
|
3565
3945
|
cursorLine: viewerCursorLine,
|
|
3566
3946
|
hasFocus: focus === "viewer",
|
|
@@ -3572,6 +3952,52 @@ function App({ mode }) {
|
|
|
3572
3952
|
] });
|
|
3573
3953
|
}
|
|
3574
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
|
+
|
|
3575
4001
|
// src/main.ts
|
|
3576
4002
|
var options = parseArgs(process.argv.slice(2));
|
|
3577
4003
|
if (options.error) {
|
|
@@ -3658,10 +4084,39 @@ if (options.mode === "summary") {
|
|
|
3658
4084
|
});
|
|
3659
4085
|
process.exit(exitCode);
|
|
3660
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
|
+
}
|
|
3661
4116
|
if (options.mode === "watch") {
|
|
3662
4117
|
installAltScreenCleanup();
|
|
3663
4118
|
enterAltScreen();
|
|
3664
4119
|
} else {
|
|
3665
4120
|
if (options.mode === "once") clearScreen();
|
|
3666
4121
|
}
|
|
3667
|
-
render(React.createElement(App, { mode: options.mode }));
|
|
4122
|
+
render(React.createElement(App, { mode: options.mode, scopeToProject }));
|