agenthud 0.9.4 → 0.10.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 +5 -3
- package/dist/index.js +1 -1
- package/dist/{main-26QL33AJ.js → main-HPL3AG6B.js} +459 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,8 +43,7 @@ AgentHUD's TUI splits the screen into a project tree and an activity viewer:
|
|
|
43
43
|
│ [10:23] ~ Edit src/ui/App.tsx │
|
|
44
44
|
│ [10:23] $ Bash npm test │
|
|
45
45
|
│ [10:23] < Response Tests passed successfully │
|
|
46
|
-
│ [10:25]
|
|
47
|
-
│ › │
|
|
46
|
+
│ [10:25] ⠧ Edit src/auth/oauth.ts ← bold + spinner = live │
|
|
48
47
|
└──────────────────────────────────────────────────────────┘
|
|
49
48
|
```
|
|
50
49
|
|
|
@@ -59,7 +58,7 @@ AgentHUD's TUI splits the screen into a project tree and an activity viewer:
|
|
|
59
58
|
|
|
60
59
|
**Activity viewer (bottom pane)**
|
|
61
60
|
- Real-time feed for the selected session: file reads, edits, bash, responses, thinking, git commits. Newest at the bottom, like `tail -f`.
|
|
62
|
-
-
|
|
61
|
+
- The **newest visible row is rendered "alive"** while in LIVE mode: its icon is replaced with a spinning glyph and the text turns bold. When a new activity arrives, the spinner moves to the new bottom row. Hidden when paused or empty.
|
|
63
62
|
- Press `f` to cycle through filter presets (configurable).
|
|
64
63
|
- Press `↵` on any row to open a scrollable detail view; on a commit row this shows `git show --stat --patch`.
|
|
65
64
|
|
|
@@ -106,10 +105,13 @@ Full reference is also available inside the app — press `?`.
|
|
|
106
105
|
| `Tab` | Switch focus to activity viewer |
|
|
107
106
|
| `PgUp` / `Ctrl+B` | Page up |
|
|
108
107
|
| `PgDn` / `Ctrl+F` | Page down |
|
|
108
|
+
| `t` | Track — auto-follow the newest live sub-agent (any nav key turns it off) |
|
|
109
109
|
| `r` | Refresh now |
|
|
110
110
|
| `?` | Help |
|
|
111
111
|
| `q` | Quit |
|
|
112
112
|
|
|
113
|
+
When tracking is on, the tree panel's title shows `[LIVE ⠧]` and the status bar replaces `t: track` with `TRK ●`. Any explicit selection-changing key (`↑/k`, `↓/j`, `PgUp/PgDn`, `↵`, `h`, or `t` again) turns tracking off.
|
|
114
|
+
|
|
113
115
|
#### Activity viewer focus
|
|
114
116
|
|
|
115
117
|
| Key | Action |
|
package/dist/index.js
CHANGED
|
@@ -685,20 +685,11 @@ function parseGitCommits(projectPath, startDate, endDate) {
|
|
|
685
685
|
// src/data/sessionHistory.ts
|
|
686
686
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
687
687
|
|
|
688
|
-
// src/data/
|
|
688
|
+
// src/data/toolDetails.ts
|
|
689
689
|
import { basename } from "path";
|
|
690
690
|
function stripAnsi(text) {
|
|
691
691
|
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
692
692
|
}
|
|
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
693
|
function getToolDetail(_toolName, input) {
|
|
703
694
|
if (!input) return "";
|
|
704
695
|
if (input.command) return stripAnsi(input.command);
|
|
@@ -708,11 +699,128 @@ function getToolDetail(_toolName, input) {
|
|
|
708
699
|
if (input.description) return stripAnsi(input.description);
|
|
709
700
|
return "";
|
|
710
701
|
}
|
|
702
|
+
function rangeStr(start, lines) {
|
|
703
|
+
return `L${start}-${start + Math.max(lines, 1) - 1}`;
|
|
704
|
+
}
|
|
705
|
+
function patchSpan(hunks) {
|
|
706
|
+
if (hunks.length === 0) return null;
|
|
707
|
+
const start = Math.min(...hunks.map((h) => h.newStart));
|
|
708
|
+
const end = Math.max(
|
|
709
|
+
...hunks.map((h) => h.newStart + Math.max(h.newLines, 1) - 1)
|
|
710
|
+
);
|
|
711
|
+
return `L${start}-${end}`;
|
|
712
|
+
}
|
|
713
|
+
function countChanges(hunks) {
|
|
714
|
+
let add = 0;
|
|
715
|
+
let del = 0;
|
|
716
|
+
for (const h of hunks) {
|
|
717
|
+
for (const line of h.lines ?? []) {
|
|
718
|
+
if (line.startsWith("+")) add++;
|
|
719
|
+
else if (line.startsWith("-")) del++;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const parts = [];
|
|
723
|
+
if (add > 0) parts.push(`+${add}`);
|
|
724
|
+
if (del > 0) parts.push(`-${del}`);
|
|
725
|
+
return parts.join(" ");
|
|
726
|
+
}
|
|
727
|
+
function joinParts(...parts) {
|
|
728
|
+
return parts.filter((p) => !!p).join(" ");
|
|
729
|
+
}
|
|
730
|
+
function summarizeToolDetail(name, input, result) {
|
|
731
|
+
const file = input?.file_path ? basename(input.file_path) : "";
|
|
732
|
+
if (name === "Edit" || name === "Write") {
|
|
733
|
+
const hunks = result?.structuredPatch;
|
|
734
|
+
if (hunks && hunks.length > 0) {
|
|
735
|
+
return joinParts(file, patchSpan(hunks), countChanges(hunks));
|
|
736
|
+
}
|
|
737
|
+
if (name === "Write") {
|
|
738
|
+
const content = result?.content ?? input?.content;
|
|
739
|
+
if (content) {
|
|
740
|
+
const n = content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
|
|
741
|
+
return joinParts(file, rangeStr(1, n), `+${n}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return file;
|
|
745
|
+
}
|
|
746
|
+
if (name === "Read") {
|
|
747
|
+
const f = result?.file;
|
|
748
|
+
if (typeof f?.startLine === "number" && typeof f?.numLines === "number") {
|
|
749
|
+
return joinParts(file, rangeStr(f.startLine, f.numLines));
|
|
750
|
+
}
|
|
751
|
+
if (typeof input?.offset === "number" && typeof input?.limit === "number") {
|
|
752
|
+
return joinParts(file, rangeStr(input.offset, input.limit));
|
|
753
|
+
}
|
|
754
|
+
return file;
|
|
755
|
+
}
|
|
756
|
+
if (name === "TaskUpdate") {
|
|
757
|
+
const id = input?.taskId ?? result?.taskId;
|
|
758
|
+
const idStr = id ? `#${id}` : "";
|
|
759
|
+
const sc = result?.statusChange;
|
|
760
|
+
if (sc?.from && sc?.to) return joinParts(idStr, `${sc.from}\u2192${sc.to}`);
|
|
761
|
+
if (result?.updatedFields?.length) {
|
|
762
|
+
return joinParts(idStr, result.updatedFields.join(", "));
|
|
763
|
+
}
|
|
764
|
+
if (input?.status) return joinParts(idStr, input.status);
|
|
765
|
+
return idStr;
|
|
766
|
+
}
|
|
767
|
+
if (name === "TaskCreate") {
|
|
768
|
+
return input?.subject ?? "";
|
|
769
|
+
}
|
|
770
|
+
return getToolDetail(name, input);
|
|
771
|
+
}
|
|
772
|
+
function buildToolDetailBody(name, input, result) {
|
|
773
|
+
if (name === "Write") {
|
|
774
|
+
const content = result?.content ?? input?.content;
|
|
775
|
+
if (content) return { text: content, kind: "code" };
|
|
776
|
+
}
|
|
777
|
+
if (name === "Edit" || name === "Write") {
|
|
778
|
+
const hunks = result?.structuredPatch;
|
|
779
|
+
if (hunks && hunks.length > 0) {
|
|
780
|
+
const text = hunks.map(
|
|
781
|
+
(h) => `@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@
|
|
782
|
+
${(h.lines ?? []).join("\n")}`
|
|
783
|
+
).join("\n");
|
|
784
|
+
return { text, kind: "diff" };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// src/data/activityParser.ts
|
|
791
|
+
function parseModelName(modelId) {
|
|
792
|
+
const opusMatch = modelId.match(/claude-opus-(\d+)-(\d+)/);
|
|
793
|
+
if (opusMatch) return `opus-${opusMatch[1]}.${opusMatch[2]}`;
|
|
794
|
+
const sonnetMatch = modelId.match(/claude-sonnet-(\d+)/);
|
|
795
|
+
if (sonnetMatch) return `sonnet-${sonnetMatch[1]}`;
|
|
796
|
+
const haikuMatch = modelId.match(/claude-(\d+)-(\d+)-haiku/);
|
|
797
|
+
if (haikuMatch) return `haiku-${haikuMatch[1]}.${haikuMatch[2]}`;
|
|
798
|
+
return modelId.replace(/-\d{8}$/, "");
|
|
799
|
+
}
|
|
711
800
|
function parseActivitiesFromLines(lines) {
|
|
712
801
|
const activities = [];
|
|
713
802
|
let tokenCount = 0;
|
|
714
803
|
let modelName = null;
|
|
715
804
|
let sessionStartTime = null;
|
|
805
|
+
const resultsById = /* @__PURE__ */ new Map();
|
|
806
|
+
for (const line of lines) {
|
|
807
|
+
let entry;
|
|
808
|
+
try {
|
|
809
|
+
entry = JSON.parse(line);
|
|
810
|
+
} catch {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
if (entry.type !== "user") continue;
|
|
814
|
+
const tur = entry.toolUseResult;
|
|
815
|
+
if (!tur || typeof tur !== "object") continue;
|
|
816
|
+
const content = entry.message?.content;
|
|
817
|
+
if (!Array.isArray(content)) continue;
|
|
818
|
+
for (const b of content) {
|
|
819
|
+
if (b?.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
820
|
+
resultsById.set(b.tool_use_id, tur);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
716
824
|
for (const line of lines) {
|
|
717
825
|
let entry;
|
|
718
826
|
try {
|
|
@@ -767,19 +875,26 @@ function parseActivitiesFromLines(lines) {
|
|
|
767
875
|
} else if (block.type === "tool_use" && block.name) {
|
|
768
876
|
if (block.name === "TodoWrite") continue;
|
|
769
877
|
const icon = ICONS[block.name] ?? ICONS.Default;
|
|
770
|
-
const
|
|
878
|
+
const result = block.id ? resultsById.get(block.id) : void 0;
|
|
879
|
+
const detail = summarizeToolDetail(block.name, block.input, result);
|
|
880
|
+
const body = buildToolDetailBody(block.name, block.input, result);
|
|
771
881
|
const last = activities[activities.length - 1];
|
|
772
882
|
if (last && last.type === "tool" && last.label === block.name && last.detail === detail) {
|
|
773
883
|
last.count = (last.count ?? 1) + 1;
|
|
774
884
|
last.timestamp = timestamp;
|
|
775
885
|
} else {
|
|
776
|
-
|
|
886
|
+
const entry2 = {
|
|
777
887
|
timestamp,
|
|
778
888
|
type: "tool",
|
|
779
889
|
icon,
|
|
780
890
|
label: block.name,
|
|
781
891
|
detail
|
|
782
|
-
}
|
|
892
|
+
};
|
|
893
|
+
if (body) {
|
|
894
|
+
entry2.detailBody = body.text;
|
|
895
|
+
entry2.detailKind = body.kind;
|
|
896
|
+
}
|
|
897
|
+
activities.push(entry2);
|
|
783
898
|
}
|
|
784
899
|
} else if (block.type === "text" && block.text && block.text.length > 10) {
|
|
785
900
|
activities.push({
|
|
@@ -1026,6 +1141,33 @@ function getDisplayWidth(s) {
|
|
|
1026
1141
|
return w;
|
|
1027
1142
|
}
|
|
1028
1143
|
|
|
1144
|
+
// src/data/sessionLiveness.ts
|
|
1145
|
+
function detectLiveState(tailLines, mtimeMs, now) {
|
|
1146
|
+
if (now - mtimeMs > THIRTY_MINUTES_MS) return null;
|
|
1147
|
+
for (let i = tailLines.length - 1; i >= 0; i--) {
|
|
1148
|
+
const line = tailLines[i];
|
|
1149
|
+
if (!line || !line.trim()) continue;
|
|
1150
|
+
let entry;
|
|
1151
|
+
try {
|
|
1152
|
+
entry = JSON.parse(line);
|
|
1153
|
+
} catch {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (entry.type === "assistant") {
|
|
1157
|
+
const content = entry.message?.content;
|
|
1158
|
+
const blocks = Array.isArray(content) ? content : [];
|
|
1159
|
+
const toolUses = blocks.filter((b) => b && b.type === "tool_use");
|
|
1160
|
+
if (toolUses.length === 0) return "waiting";
|
|
1161
|
+
if (toolUses.some((b) => b.name === "AskUserQuestion")) return "waiting";
|
|
1162
|
+
return "working";
|
|
1163
|
+
}
|
|
1164
|
+
if (entry.type === "user") {
|
|
1165
|
+
return "working";
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1029
1171
|
// src/data/sessions.ts
|
|
1030
1172
|
function getProjectsDir() {
|
|
1031
1173
|
return process.env.CLAUDE_PROJECTS_DIR ?? join4(homedir2(), ".claude", "projects");
|
|
@@ -1079,23 +1221,26 @@ function readSubAgentInfo(filePath) {
|
|
|
1079
1221
|
return { agentId: null, taskDescription: null };
|
|
1080
1222
|
}
|
|
1081
1223
|
}
|
|
1082
|
-
function
|
|
1083
|
-
if (!existsSync3(filePath)) return null;
|
|
1224
|
+
function readSessionTail(filePath, mtimeMs, now) {
|
|
1225
|
+
if (!existsSync3(filePath)) return { modelName: null, liveState: null };
|
|
1084
1226
|
try {
|
|
1085
1227
|
const content = readFileSync4(filePath, "utf-8");
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1228
|
+
const tail = content.trim().split("\n").filter(Boolean).slice(-50);
|
|
1229
|
+
let modelName = null;
|
|
1230
|
+
for (const line of [...tail].reverse()) {
|
|
1088
1231
|
try {
|
|
1089
1232
|
const entry = JSON.parse(line);
|
|
1090
1233
|
if (entry.type === "assistant" && entry.message?.model) {
|
|
1091
|
-
|
|
1234
|
+
modelName = parseModelName(entry.message.model);
|
|
1235
|
+
break;
|
|
1092
1236
|
}
|
|
1093
1237
|
} catch {
|
|
1094
1238
|
}
|
|
1095
1239
|
}
|
|
1240
|
+
return { modelName, liveState: detectLiveState(tail, mtimeMs, now) };
|
|
1096
1241
|
} catch {
|
|
1242
|
+
return { modelName: null, liveState: null };
|
|
1097
1243
|
}
|
|
1098
|
-
return null;
|
|
1099
1244
|
}
|
|
1100
1245
|
var SYSTEM_PREFIXES = [
|
|
1101
1246
|
"<command-name>",
|
|
@@ -1179,6 +1324,11 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1179
1324
|
try {
|
|
1180
1325
|
const stat = statSync2(filePath);
|
|
1181
1326
|
const { agentId, taskDescription } = readSubAgentInfo(filePath);
|
|
1327
|
+
const { modelName, liveState } = readSessionTail(
|
|
1328
|
+
filePath,
|
|
1329
|
+
stat.mtimeMs,
|
|
1330
|
+
Date.now()
|
|
1331
|
+
);
|
|
1182
1332
|
return {
|
|
1183
1333
|
id,
|
|
1184
1334
|
hideKey,
|
|
@@ -1187,12 +1337,13 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
|
|
|
1187
1337
|
projectName: "",
|
|
1188
1338
|
lastModifiedMs: stat.mtimeMs,
|
|
1189
1339
|
status: getSessionStatus(stat.mtimeMs),
|
|
1190
|
-
modelName
|
|
1340
|
+
modelName,
|
|
1191
1341
|
subAgents: [],
|
|
1192
1342
|
agentId: agentId ?? void 0,
|
|
1193
1343
|
taskDescription: taskDescription ?? void 0,
|
|
1194
1344
|
nonInteractive: false,
|
|
1195
|
-
firstUserPrompt: null
|
|
1345
|
+
firstUserPrompt: null,
|
|
1346
|
+
liveState
|
|
1196
1347
|
};
|
|
1197
1348
|
} catch {
|
|
1198
1349
|
return null;
|
|
@@ -1248,6 +1399,12 @@ function discoverSessions(config) {
|
|
|
1248
1399
|
try {
|
|
1249
1400
|
const stat = statSync2(filePath);
|
|
1250
1401
|
const subAgents = buildSubAgents(id, projectDir, config, projectName);
|
|
1402
|
+
const nonInteractive = readEntrypoint(filePath) === "sdk-cli";
|
|
1403
|
+
const { modelName, liveState } = readSessionTail(
|
|
1404
|
+
filePath,
|
|
1405
|
+
stat.mtimeMs,
|
|
1406
|
+
Date.now()
|
|
1407
|
+
);
|
|
1251
1408
|
allSessions.push({
|
|
1252
1409
|
id,
|
|
1253
1410
|
hideKey,
|
|
@@ -1256,10 +1413,11 @@ function discoverSessions(config) {
|
|
|
1256
1413
|
projectName,
|
|
1257
1414
|
lastModifiedMs: stat.mtimeMs,
|
|
1258
1415
|
status: getSessionStatus(stat.mtimeMs),
|
|
1259
|
-
modelName
|
|
1416
|
+
modelName,
|
|
1260
1417
|
subAgents,
|
|
1261
|
-
nonInteractive
|
|
1262
|
-
firstUserPrompt: readFirstUserPrompt(filePath)
|
|
1418
|
+
nonInteractive,
|
|
1419
|
+
firstUserPrompt: readFirstUserPrompt(filePath),
|
|
1420
|
+
liveState: nonInteractive ? null : liveState
|
|
1263
1421
|
});
|
|
1264
1422
|
} catch {
|
|
1265
1423
|
}
|
|
@@ -1806,7 +1964,33 @@ import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useS
|
|
|
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,
|
|
@@ -2089,8 +2292,9 @@ function DetailViewPanel({
|
|
|
2089
2292
|
}) {
|
|
2090
2293
|
const innerWidth = getInnerWidth(width);
|
|
2091
2294
|
const contentWidth = innerWidth - 1;
|
|
2092
|
-
const
|
|
2093
|
-
const
|
|
2295
|
+
const body = activity.detailBody ?? activity.detail;
|
|
2296
|
+
const classifier = activity.detailKind === "diff" ? classifyDiffLines : activity.detailKind === "code" ? classifyCodeFences : activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
2297
|
+
const allLines = wrapClassified(body, contentWidth, classifier);
|
|
2094
2298
|
const totalLines = allLines.length;
|
|
2095
2299
|
const clampedOffset = Math.min(
|
|
2096
2300
|
scrollOffset,
|
|
@@ -2155,6 +2359,7 @@ var SECTIONS = [
|
|
|
2155
2359
|
["PgDn / Ctrl+F", "Page down"],
|
|
2156
2360
|
["\u21B5", "Expand/collapse project, session, or summary"],
|
|
2157
2361
|
["h", "Hide selected (project/session/sub-agent)"],
|
|
2362
|
+
["t", "Track: auto-follow the newest live sub-agent (any nav key turns it off)"],
|
|
2158
2363
|
["Tab", "Switch focus to activity viewer"],
|
|
2159
2364
|
["r", "Refresh now"]
|
|
2160
2365
|
]
|
|
@@ -2296,7 +2501,9 @@ function useHotkeys({
|
|
|
2296
2501
|
onHelp,
|
|
2297
2502
|
onHelpScroll,
|
|
2298
2503
|
onHelpScrollToTop,
|
|
2299
|
-
|
|
2504
|
+
onToggleTracking,
|
|
2505
|
+
filterLabel,
|
|
2506
|
+
trackingOn = false
|
|
2300
2507
|
}) {
|
|
2301
2508
|
const handleInput = (input, key) => {
|
|
2302
2509
|
if (helpMode) {
|
|
@@ -2371,6 +2578,10 @@ function useHotkeys({
|
|
|
2371
2578
|
onFilter();
|
|
2372
2579
|
return;
|
|
2373
2580
|
}
|
|
2581
|
+
if (input === "t" && !key.ctrl && onToggleTracking) {
|
|
2582
|
+
onToggleTracking();
|
|
2583
|
+
return;
|
|
2584
|
+
}
|
|
2374
2585
|
if (key.pageUp) {
|
|
2375
2586
|
onScrollPageUp();
|
|
2376
2587
|
return;
|
|
@@ -2430,7 +2641,9 @@ function useHotkeys({
|
|
|
2430
2641
|
}
|
|
2431
2642
|
}
|
|
2432
2643
|
};
|
|
2644
|
+
const trackingItems = trackingOn ? ["TRK \u25CF"] : ["t: track"];
|
|
2433
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" ? [
|
|
2646
|
+
...trackingItems,
|
|
2434
2647
|
"Tab: viewer",
|
|
2435
2648
|
"\u2191\u2193/jk: select",
|
|
2436
2649
|
"PgUp/Dn: page",
|
|
@@ -2440,6 +2653,7 @@ function useHotkeys({
|
|
|
2440
2653
|
"?: help",
|
|
2441
2654
|
"q: quit"
|
|
2442
2655
|
] : [
|
|
2656
|
+
...trackingItems,
|
|
2443
2657
|
"Tab: projects",
|
|
2444
2658
|
"\u2191\u2193/jk: scroll",
|
|
2445
2659
|
"PgUp/Dn: page",
|
|
@@ -2453,21 +2667,16 @@ function useHotkeys({
|
|
|
2453
2667
|
return { handleInput, statusBarItems };
|
|
2454
2668
|
}
|
|
2455
2669
|
|
|
2456
|
-
// src/ui/hooks/
|
|
2670
|
+
// src/ui/hooks/useTick.ts
|
|
2457
2671
|
import { useEffect, useState } from "react";
|
|
2458
|
-
function
|
|
2459
|
-
const [
|
|
2460
|
-
useEffect(() => {
|
|
2461
|
-
setIndex(0);
|
|
2462
|
-
}, [resetKey]);
|
|
2672
|
+
function useTick(active, intervalMs = 100) {
|
|
2673
|
+
const [n, setN] = useState(0);
|
|
2463
2674
|
useEffect(() => {
|
|
2464
2675
|
if (!active) return;
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
}, [active, positions, intervalMs]);
|
|
2470
|
-
return index;
|
|
2676
|
+
const id = setInterval(() => setN((x) => x + 1), intervalMs);
|
|
2677
|
+
return () => clearInterval(id);
|
|
2678
|
+
}, [active, intervalMs]);
|
|
2679
|
+
return n;
|
|
2471
2680
|
}
|
|
2472
2681
|
|
|
2473
2682
|
// src/ui/hooks/useSpinner.ts
|
|
@@ -2488,7 +2697,7 @@ function useSpinner(active, intervalMs = 100) {
|
|
|
2488
2697
|
// src/ui/SessionTreePanel.tsx
|
|
2489
2698
|
import { homedir as homedir4 } from "os";
|
|
2490
2699
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2491
|
-
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2700
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2492
2701
|
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2493
2702
|
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2494
2703
|
const seconds = Math.floor(elapsed / 1e3);
|
|
@@ -2519,6 +2728,13 @@ function getStatusColor(status) {
|
|
|
2519
2728
|
return "gray";
|
|
2520
2729
|
}
|
|
2521
2730
|
}
|
|
2731
|
+
function getBadge(session) {
|
|
2732
|
+
if (session.liveState === "working")
|
|
2733
|
+
return { text: "[working]", color: "green" };
|
|
2734
|
+
if (session.liveState === "waiting")
|
|
2735
|
+
return { text: "[waiting]", color: "magenta" };
|
|
2736
|
+
return { text: `[${session.status}]`, color: getStatusColor(session.status) };
|
|
2737
|
+
}
|
|
2522
2738
|
function formatProjectPath(projectPath) {
|
|
2523
2739
|
const home = homedir4();
|
|
2524
2740
|
const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
|
|
@@ -2532,8 +2748,7 @@ function SessionRow({
|
|
|
2532
2748
|
contentWidth
|
|
2533
2749
|
}) {
|
|
2534
2750
|
const isParent = prefix === " ";
|
|
2535
|
-
const
|
|
2536
|
-
const badge = `[${session.status}]`;
|
|
2751
|
+
const { text: badge, color: badgeColor } = getBadge(session);
|
|
2537
2752
|
const elapsed = formatElapsed(session.lastModifiedMs);
|
|
2538
2753
|
const model = session.modelName ?? "";
|
|
2539
2754
|
const isNonInteractive = session.nonInteractive;
|
|
@@ -2582,7 +2797,7 @@ function SessionRow({
|
|
|
2582
2797
|
/* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
|
|
2583
2798
|
shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
|
|
2584
2799
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
2585
|
-
/* @__PURE__ */ jsx4(Text4, { color:
|
|
2800
|
+
/* @__PURE__ */ jsx4(Text4, { color: badgeColor, children: badge }),
|
|
2586
2801
|
middleText ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: middleSection }) : null,
|
|
2587
2802
|
/* @__PURE__ */ jsx4(Text4, { children: gap }),
|
|
2588
2803
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }),
|
|
@@ -2706,7 +2921,7 @@ function ProjectRow({
|
|
|
2706
2921
|
dimColor: muted,
|
|
2707
2922
|
children: [
|
|
2708
2923
|
nameText,
|
|
2709
|
-
pathText ? /* @__PURE__ */ jsxs4(
|
|
2924
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
2710
2925
|
" ",
|
|
2711
2926
|
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2712
2927
|
] }) : null,
|
|
@@ -2781,11 +2996,14 @@ function SessionTreePanel({
|
|
|
2781
2996
|
hasFocus,
|
|
2782
2997
|
width = DEFAULT_PANEL_WIDTH,
|
|
2783
2998
|
maxRows,
|
|
2784
|
-
expandedIds = /* @__PURE__ */ new Set()
|
|
2999
|
+
expandedIds = /* @__PURE__ */ new Set(),
|
|
3000
|
+
trackingOn = false,
|
|
3001
|
+
spinner = ""
|
|
2785
3002
|
}) {
|
|
2786
3003
|
const innerWidth = getInnerWidth(width);
|
|
2787
3004
|
const contentWidth = innerWidth - 1;
|
|
2788
|
-
const
|
|
3005
|
+
const titleSuffix = trackingOn ? `[LIVE ${spinner || "\u25BC"}]` : "";
|
|
3006
|
+
const titleLine = createTitleLine("Projects", titleSuffix, width);
|
|
2789
3007
|
const bottomLine = createBottomLine(width);
|
|
2790
3008
|
const totalProjectCount = projects.length + coldProjects.length;
|
|
2791
3009
|
if (totalProjectCount === 0) {
|
|
@@ -2879,7 +3097,7 @@ function SessionTreePanel({
|
|
|
2879
3097
|
}
|
|
2880
3098
|
|
|
2881
3099
|
// src/ui/App.tsx
|
|
2882
|
-
import { Fragment as
|
|
3100
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2883
3101
|
var VIEWER_HEIGHT_FRACTION = 0.55;
|
|
2884
3102
|
function subSummarySentinel(parentId) {
|
|
2885
3103
|
return {
|
|
@@ -2893,7 +3111,8 @@ function subSummarySentinel(parentId) {
|
|
|
2893
3111
|
modelName: null,
|
|
2894
3112
|
subAgents: [],
|
|
2895
3113
|
nonInteractive: false,
|
|
2896
|
-
firstUserPrompt: null
|
|
3114
|
+
firstUserPrompt: null,
|
|
3115
|
+
liveState: null
|
|
2897
3116
|
};
|
|
2898
3117
|
}
|
|
2899
3118
|
function appendSubAgentRows(result, session, expandedIds) {
|
|
@@ -2932,7 +3151,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2932
3151
|
modelName: null,
|
|
2933
3152
|
subAgents: [],
|
|
2934
3153
|
nonInteractive: false,
|
|
2935
|
-
firstUserPrompt: null
|
|
3154
|
+
firstUserPrompt: null,
|
|
3155
|
+
liveState: null
|
|
2936
3156
|
});
|
|
2937
3157
|
const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
|
|
2938
3158
|
if (shouldShowSessions) {
|
|
@@ -2957,7 +3177,8 @@ function flattenSessions2(tree, expandedIds) {
|
|
|
2957
3177
|
modelName: null,
|
|
2958
3178
|
subAgents: [],
|
|
2959
3179
|
nonInteractive: false,
|
|
2960
|
-
firstUserPrompt: null
|
|
3180
|
+
firstUserPrompt: null,
|
|
3181
|
+
liveState: null
|
|
2961
3182
|
});
|
|
2962
3183
|
if (expandedIds.has("__cold__")) {
|
|
2963
3184
|
for (const project of tree.coldProjects) {
|
|
@@ -2980,6 +3201,54 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
|
|
|
2980
3201
|
const effectiveCursor = Math.min(cursorLine, visible.length - 1);
|
|
2981
3202
|
return visible[visible.length - 1 - effectiveCursor] ?? null;
|
|
2982
3203
|
}
|
|
3204
|
+
function pickTrackingTarget(tree, selectedId, seen) {
|
|
3205
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3206
|
+
const liveSubs = [];
|
|
3207
|
+
const liveParents = [];
|
|
3208
|
+
for (const p of tree.projects) {
|
|
3209
|
+
for (const s of p.sessions) {
|
|
3210
|
+
ids.add(s.id);
|
|
3211
|
+
if (s.status === "hot" || s.status === "warm") {
|
|
3212
|
+
liveParents.push({
|
|
3213
|
+
id: s.id,
|
|
3214
|
+
mtime: s.lastModifiedMs,
|
|
3215
|
+
isNew: !seen.has(s.id)
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
for (const sa of s.subAgents) {
|
|
3219
|
+
ids.add(sa.id);
|
|
3220
|
+
if (sa.status === "hot" || sa.status === "warm") {
|
|
3221
|
+
liveSubs.push({
|
|
3222
|
+
id: sa.id,
|
|
3223
|
+
mtime: sa.lastModifiedMs,
|
|
3224
|
+
isNew: !seen.has(sa.id)
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
const newest = (xs) => xs.length === 0 ? null : xs.reduce((a, b) => a.mtime > b.mtime ? a : b).id;
|
|
3231
|
+
const newSubTarget = newest(liveSubs.filter((s) => s.isNew));
|
|
3232
|
+
if (newSubTarget) return { target: newSubTarget, ids };
|
|
3233
|
+
const newParentTarget = newest(liveParents.filter((p) => p.isNew));
|
|
3234
|
+
if (newParentTarget) return { target: newParentTarget, ids };
|
|
3235
|
+
const currentIsLive = selectedId != null && (liveSubs.some((s) => s.id === selectedId) || liveParents.some((p) => p.id === selectedId));
|
|
3236
|
+
if (currentIsLive) return { target: null, ids };
|
|
3237
|
+
return {
|
|
3238
|
+
target: newest(liveSubs) ?? newest(liveParents),
|
|
3239
|
+
ids
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
function collectAllIds(tree) {
|
|
3243
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3244
|
+
for (const p of tree.projects) {
|
|
3245
|
+
for (const s of p.sessions) {
|
|
3246
|
+
ids.add(s.id);
|
|
3247
|
+
for (const sa of s.subAgents) ids.add(sa.id);
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return ids;
|
|
3251
|
+
}
|
|
2983
3252
|
function App({ mode }) {
|
|
2984
3253
|
const { exit } = useApp();
|
|
2985
3254
|
const { stdout } = useStdout();
|
|
@@ -3011,6 +3280,8 @@ function App({ mode }) {
|
|
|
3011
3280
|
const [helpMode, setHelpMode] = useState3(false);
|
|
3012
3281
|
const [helpScroll, setHelpScroll] = useState3(0);
|
|
3013
3282
|
const helpTotalLinesRef = useRef(0);
|
|
3283
|
+
const [tracking, setTracking] = useState3(false);
|
|
3284
|
+
const seenIdsRef = useRef(/* @__PURE__ */ new Set());
|
|
3014
3285
|
const allFlat = useMemo(
|
|
3015
3286
|
() => flattenSessions2(sessionTree, expandedIds),
|
|
3016
3287
|
[sessionTree, expandedIds]
|
|
@@ -3095,11 +3366,24 @@ function App({ mode }) {
|
|
|
3095
3366
|
const freshConfig = loadGlobalConfig();
|
|
3096
3367
|
const tree = discoverSessions(freshConfig);
|
|
3097
3368
|
const updatedFlat = flattenSessions2(tree, expandedIds);
|
|
3098
|
-
|
|
3369
|
+
let nextSelected = selectedId;
|
|
3370
|
+
if (tracking) {
|
|
3371
|
+
const { target, ids } = pickTrackingTarget(
|
|
3372
|
+
tree,
|
|
3373
|
+
selectedId,
|
|
3374
|
+
seenIdsRef.current
|
|
3375
|
+
);
|
|
3376
|
+
if (target && target !== selectedId) {
|
|
3377
|
+
setSelectedId(target);
|
|
3378
|
+
nextSelected = target;
|
|
3379
|
+
}
|
|
3380
|
+
seenIdsRef.current = ids;
|
|
3381
|
+
}
|
|
3382
|
+
const node = updatedFlat.find((s) => s.id === nextSelected);
|
|
3099
3383
|
if (!node) {
|
|
3100
3384
|
const allSessions = tree.projects?.flatMap((p) => p.sessions) ?? [];
|
|
3101
3385
|
const parentSession = allSessions.find(
|
|
3102
|
-
(s) => s.subAgents.some((sa) => sa.id ===
|
|
3386
|
+
(s) => s.subAgents.some((sa) => sa.id === nextSelected)
|
|
3103
3387
|
);
|
|
3104
3388
|
if (parentSession) setSelectedId(parentSession.id);
|
|
3105
3389
|
}
|
|
@@ -3112,7 +3396,7 @@ function App({ mode }) {
|
|
|
3112
3396
|
setScrollOffset((o) => o + delta);
|
|
3113
3397
|
setNewCount((n) => n + delta);
|
|
3114
3398
|
}
|
|
3115
|
-
}, [selectedId, isLive, expandedIds]);
|
|
3399
|
+
}, [selectedId, isLive, expandedIds, tracking]);
|
|
3116
3400
|
const refreshRef = useRef(refresh);
|
|
3117
3401
|
useEffect3(() => {
|
|
3118
3402
|
refreshRef.current = refresh;
|
|
@@ -3147,6 +3431,11 @@ function App({ mode }) {
|
|
|
3147
3431
|
if (debounce) clearTimeout(debounce);
|
|
3148
3432
|
};
|
|
3149
3433
|
}, [isWatchMode, config.refreshIntervalMs]);
|
|
3434
|
+
useEffect3(() => {
|
|
3435
|
+
if (!isWatchMode || !tracking) return;
|
|
3436
|
+
const timer = setInterval(() => refreshRef.current(), 1e3);
|
|
3437
|
+
return () => clearInterval(timer);
|
|
3438
|
+
}, [isWatchMode, tracking]);
|
|
3150
3439
|
const filterPresets = config.filterPresets;
|
|
3151
3440
|
const activePreset = useMemo(
|
|
3152
3441
|
() => filterPresets[filterIndex % filterPresets.length] ?? [],
|
|
@@ -3171,26 +3460,18 @@ function App({ mode }) {
|
|
|
3171
3460
|
const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
|
|
3172
3461
|
const naturalTreeRows = allFlat.length;
|
|
3173
3462
|
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
|
-
);
|
|
3463
|
+
const viewerRows = Math.max(5, height - 7 - treeRows);
|
|
3464
|
+
const spinner = useSpinner(isWatchMode, 150);
|
|
3465
|
+
const tickActive = isWatchMode && isLive && !helpMode && !detailMode && activities.length > 0;
|
|
3466
|
+
const liveTick = useTick(tickActive, 150);
|
|
3189
3467
|
const helpViewportRows = Math.max(1, height - 3);
|
|
3190
3468
|
const helpScrollStep = (delta) => {
|
|
3191
3469
|
const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
|
|
3192
3470
|
setHelpScroll((s) => Math.max(0, Math.min(max, s + delta)));
|
|
3193
3471
|
};
|
|
3472
|
+
const stopTracking = () => {
|
|
3473
|
+
if (tracking) setTracking(false);
|
|
3474
|
+
};
|
|
3194
3475
|
const { handleInput, statusBarItems } = useHotkeys({
|
|
3195
3476
|
focus,
|
|
3196
3477
|
detailMode,
|
|
@@ -3201,12 +3482,29 @@ function App({ mode }) {
|
|
|
3201
3482
|
},
|
|
3202
3483
|
onHelpScroll: helpScrollStep,
|
|
3203
3484
|
onHelpScrollToTop: () => setHelpScroll(0),
|
|
3485
|
+
onToggleTracking: () => {
|
|
3486
|
+
setTracking((on) => {
|
|
3487
|
+
const next = !on;
|
|
3488
|
+
if (next) {
|
|
3489
|
+
seenIdsRef.current = collectAllIds(sessionTree);
|
|
3490
|
+
const { target } = pickTrackingTarget(
|
|
3491
|
+
sessionTree,
|
|
3492
|
+
selectedId,
|
|
3493
|
+
seenIdsRef.current
|
|
3494
|
+
);
|
|
3495
|
+
if (target) setSelectedId(target);
|
|
3496
|
+
}
|
|
3497
|
+
return next;
|
|
3498
|
+
});
|
|
3499
|
+
},
|
|
3500
|
+
trackingOn: tracking,
|
|
3204
3501
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3205
3502
|
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3206
3503
|
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
3207
3504
|
// Down arrow moves visually downward = newer direction = cursorLine--.
|
|
3208
3505
|
onScrollUp: () => {
|
|
3209
3506
|
if (focus === "tree") {
|
|
3507
|
+
stopTracking();
|
|
3210
3508
|
if (selectedIndex === -1) return;
|
|
3211
3509
|
const prev = Math.max(0, selectedIndex - 1);
|
|
3212
3510
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
@@ -3223,6 +3521,7 @@ function App({ mode }) {
|
|
|
3223
3521
|
},
|
|
3224
3522
|
onScrollDown: () => {
|
|
3225
3523
|
if (focus === "tree") {
|
|
3524
|
+
stopTracking();
|
|
3226
3525
|
if (selectedIndex === -1) return;
|
|
3227
3526
|
const next = Math.min(allFlat.length - 1, selectedIndex + 1);
|
|
3228
3527
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
@@ -3246,6 +3545,7 @@ function App({ mode }) {
|
|
|
3246
3545
|
// PgDn = visually down = newer direction = scrollOffset--
|
|
3247
3546
|
onScrollPageUp: () => {
|
|
3248
3547
|
if (focus === "tree") {
|
|
3548
|
+
stopTracking();
|
|
3249
3549
|
const prev = Math.max(0, selectedIndex - 5);
|
|
3250
3550
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3251
3551
|
} else {
|
|
@@ -3258,6 +3558,7 @@ function App({ mode }) {
|
|
|
3258
3558
|
},
|
|
3259
3559
|
onScrollPageDown: () => {
|
|
3260
3560
|
if (focus === "tree") {
|
|
3561
|
+
stopTracking();
|
|
3261
3562
|
const next = Math.min(allFlat.length - 1, selectedIndex + 5);
|
|
3262
3563
|
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3263
3564
|
} else {
|
|
@@ -3274,6 +3575,7 @@ function App({ mode }) {
|
|
|
3274
3575
|
},
|
|
3275
3576
|
onScrollHalfPageUp: () => {
|
|
3276
3577
|
if (focus === "tree") {
|
|
3578
|
+
stopTracking();
|
|
3277
3579
|
const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
|
|
3278
3580
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3279
3581
|
} else {
|
|
@@ -3289,6 +3591,7 @@ function App({ mode }) {
|
|
|
3289
3591
|
},
|
|
3290
3592
|
onScrollHalfPageDown: () => {
|
|
3291
3593
|
if (focus === "tree") {
|
|
3594
|
+
stopTracking();
|
|
3292
3595
|
const next = Math.min(
|
|
3293
3596
|
allFlat.length - 1,
|
|
3294
3597
|
selectedIndex + Math.ceil(5 / 2)
|
|
@@ -3349,6 +3652,7 @@ function App({ mode }) {
|
|
|
3349
3652
|
return;
|
|
3350
3653
|
}
|
|
3351
3654
|
if (focus !== "tree" || !selectedId) return;
|
|
3655
|
+
stopTracking();
|
|
3352
3656
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3353
3657
|
const projectName = selectedId.slice(7, -2);
|
|
3354
3658
|
const isCold = sessionTree.coldProjects.some(
|
|
@@ -3433,6 +3737,7 @@ function App({ mode }) {
|
|
|
3433
3737
|
},
|
|
3434
3738
|
onHide: () => {
|
|
3435
3739
|
if (focus !== "tree" || !selectedId) return;
|
|
3740
|
+
stopTracking();
|
|
3436
3741
|
if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
3437
3742
|
const projectName = selectedId.slice(7, -2);
|
|
3438
3743
|
hideProject(projectName);
|
|
@@ -3492,7 +3797,13 @@ function App({ mode }) {
|
|
|
3492
3797
|
if (isWatchMode && (width < MIN_WIDTH || height + 1 < MIN_HEIGHT)) {
|
|
3493
3798
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
|
|
3494
3799
|
/* @__PURE__ */ jsx5(Text5, { bold: true, children: "AgentHUD needs a larger terminal." }),
|
|
3495
|
-
/* @__PURE__ */ jsx5(
|
|
3800
|
+
/* @__PURE__ */ jsx5(
|
|
3801
|
+
Text5,
|
|
3802
|
+
{
|
|
3803
|
+
dimColor: true,
|
|
3804
|
+
children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows`
|
|
3805
|
+
}
|
|
3806
|
+
),
|
|
3496
3807
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Current: ${width} cols \xD7 ${height + 1} rows` }),
|
|
3497
3808
|
/* @__PURE__ */ jsx5(Text5, { children: " " }),
|
|
3498
3809
|
/* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Resize the window and AgentHUD will redraw automatically." }),
|
|
@@ -3527,7 +3838,7 @@ function App({ mode }) {
|
|
|
3527
3838
|
helpTotalLinesRef.current = n;
|
|
3528
3839
|
}
|
|
3529
3840
|
}
|
|
3530
|
-
) : /* @__PURE__ */ jsxs5(
|
|
3841
|
+
) : /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
3531
3842
|
migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
|
|
3532
3843
|
/* @__PURE__ */ jsx5(
|
|
3533
3844
|
SessionTreePanel,
|
|
@@ -3538,7 +3849,9 @@ function App({ mode }) {
|
|
|
3538
3849
|
hasFocus: focus === "tree",
|
|
3539
3850
|
width,
|
|
3540
3851
|
maxRows: treeRows,
|
|
3541
|
-
expandedIds
|
|
3852
|
+
expandedIds,
|
|
3853
|
+
trackingOn: tracking,
|
|
3854
|
+
spinner
|
|
3542
3855
|
}
|
|
3543
3856
|
),
|
|
3544
3857
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
|
|
@@ -3559,8 +3872,8 @@ function App({ mode }) {
|
|
|
3559
3872
|
isLive,
|
|
3560
3873
|
newCount,
|
|
3561
3874
|
visibleRows: viewerRows,
|
|
3562
|
-
|
|
3563
|
-
|
|
3875
|
+
liveSpinnerFrame: spinner,
|
|
3876
|
+
liveTick,
|
|
3564
3877
|
width,
|
|
3565
3878
|
cursorLine: viewerCursorLine,
|
|
3566
3879
|
hasFocus: focus === "viewer",
|
package/package.json
CHANGED