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.
@@ -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 as join2 } from "path";
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(join2(__dirname2, "..", "package.json"), "utf-8")
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 join3 } from "path";
370
+ import { join as join2 } from "path";
415
371
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
416
- var CONFIG_PATH = join3(homedir(), ".agenthud", "config.yaml");
417
- var STATE_PATH = join3(homedir(), ".agenthud", "state.yaml");
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 = join3(homedir(), ".agenthud");
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 = join3(process.cwd(), ".agenthud", "config.yaml");
608
- if (candidate === join3(homedir(), ".agenthud", "config.yaml")) return false;
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 --git-dir="${projectPath}/.git" show --stat --patch --no-color ${hash}`,
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 --git-dir="${projectPath}/.git" log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
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/activityParser.ts
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 detail = getToolDetail(block.name, block.input);
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
- activities.push({
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 join4 } from "path";
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 ?? join4(homedir2(), ".claude", "projects");
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 readModelName(filePath) {
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 lines = content.trim().split("\n").filter(Boolean);
1087
- for (const line of lines.slice(-50).reverse()) {
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
- return parseModelName(entry.message.model);
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 = join4(projectDir, parentId, "subagents");
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 = join4(subagentsDir, file);
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: readModelName(filePath),
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 discoverSessions(config) {
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(join4(projectsDir, entry)).isDirectory();
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 = join4(projectsDir, encodedDir);
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 = join4(projectDir, file);
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: readModelName(filePath),
1402
+ modelName,
1260
1403
  subAgents,
1261
- nonInteractive: readEntrypoint(filePath) === "sdk-cli",
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 = join5(homedir3(), ".agenthud");
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 = join5(agenthudHomeDir(), "summaries");
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 join5(homedir3(), ".agenthud", promptFilename(kind));
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 join5(here, "templates", promptFilename(kind));
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 join5(summariesDir(), `${dateKey(date)}.md`);
1499
+ return join4(summariesDir(), `${dateKey(date)}.md`);
1343
1500
  }
1344
1501
  function rangeCachePath(from, to) {
1345
- return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
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 { jsx, jsxs } from "react/jsx-runtime";
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
- trailingBlankRows = 0,
1866
- liveIndicatorPosition = null,
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 time = formatActivityTime(activity.timestamp, now);
1915
- const timestamp = `[${time}] `;
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__ */ jsxs(Text, { children: [
1943
- BOX.v,
1944
- " ",
1945
- /* @__PURE__ */ jsxs(Text, { backgroundColor: isCursor ? "blue" : void 0, children: [
1946
- /* @__PURE__ */ jsx(Text, { dimColor: !isCursor, children: timestamp }),
1947
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: icon }),
1948
- " ",
1949
- /* @__PURE__ */ jsx(
1950
- Text,
1951
- {
1952
- color: isCursor ? void 0 : style.color,
1953
- dimColor: !isCursor && style.dimColor,
1954
- children: labelContent
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 hasContent = visibleActivities.length > 0;
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 wrapClassified(text, maxWidth, classifier) {
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 classifier = activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
2093
- const allLines = wrapClassified(activity.detail, contentWidth, classifier);
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__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: entry.text }),
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
- filterLabel
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 statusBarItems = helpMode ? ["\u2191\u2193/jk: scroll", "PgDn/Space: page", "\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
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/useSlide.ts
2719
+ // src/ui/hooks/useSpinner.ts
2457
2720
  import { useEffect, useState } from "react";
2458
- function useSlide(active, positions, intervalMs = 180, resetKey) {
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) % positions);
2727
+ setIndex((i) => (i + 1) % FRAMES.length);
2467
2728
  }, intervalMs);
2468
2729
  return () => clearInterval(timer);
2469
- }, [active, positions, intervalMs]);
2470
- return index;
2730
+ }, [active, intervalMs]);
2731
+ return active ? FRAMES[index] : "";
2471
2732
  }
2472
2733
 
2473
- // src/ui/hooks/useSpinner.ts
2734
+ // src/ui/hooks/useTick.ts
2474
2735
  import { useEffect as useEffect2, useState as useState2 } from "react";
2475
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2476
- function useSpinner(active, intervalMs = 100) {
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 timer = setInterval(() => {
2481
- setIndex((i) => (i + 1) % FRAMES.length);
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 active ? FRAMES[index] : "";
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 statusColor = getStatusColor(session.status);
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: statusColor, children: badge }),
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(Fragment, { children: [
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 titleLine = createTitleLine("Projects", "", width);
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 Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
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 App({ mode }) {
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
- const node = updatedFlat.find((s) => s.id === selectedId);
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 === selectedId)
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 VIEWER_BREATHING_ROWS = 1;
3175
- const viewerRows = Math.max(
3176
- 5,
3177
- height - 7 - treeRows - VIEWER_BREATHING_ROWS
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(Text5, { dimColor: true, children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows` }),
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(Fragment2, { children: [
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
- trailingBlankRows: VIEWER_BREATHING_ROWS,
3563
- liveIndicatorPosition,
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 }));