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 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] abc1234 feat: fix auth callback
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
- - A `›` slides left right along the bottom row while the viewer is in LIVE mode visible proof the feed is alive. Hidden when paused (scrolled into history) or when the session has no activity yet.
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
@@ -15,4 +15,4 @@ Error: Node.js ${MIN_NODE_VERSION}+ is required (current: ${process.version})
15
15
  process.exit(1);
16
16
  }
17
17
  if (!process.env.NODE_ENV) process.env.NODE_ENV = "production";
18
- import("./main-26QL33AJ.js");
18
+ import("./main-HPL3AG6B.js");
@@ -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/activityParser.ts
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 detail = getToolDetail(block.name, block.input);
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
- activities.push({
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 readModelName(filePath) {
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 lines = content.trim().split("\n").filter(Boolean);
1087
- for (const line of lines.slice(-50).reverse()) {
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
- return parseModelName(entry.message.model);
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: readModelName(filePath),
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: readModelName(filePath),
1416
+ modelName,
1260
1417
  subAgents,
1261
- nonInteractive: readEntrypoint(filePath) === "sdk-cli",
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 { 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,
@@ -2089,8 +2292,9 @@ function DetailViewPanel({
2089
2292
  }) {
2090
2293
  const innerWidth = getInnerWidth(width);
2091
2294
  const contentWidth = innerWidth - 1;
2092
- const classifier = activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
2093
- const allLines = wrapClassified(activity.detail, contentWidth, classifier);
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
- filterLabel
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/useSlide.ts
2670
+ // src/ui/hooks/useTick.ts
2457
2671
  import { useEffect, useState } from "react";
2458
- function useSlide(active, positions, intervalMs = 180, resetKey) {
2459
- const [index, setIndex] = useState(0);
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 timer = setInterval(() => {
2466
- setIndex((i) => (i + 1) % positions);
2467
- }, intervalMs);
2468
- return () => clearInterval(timer);
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 statusColor = getStatusColor(session.status);
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: statusColor, children: badge }),
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(Fragment, { children: [
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 titleLine = createTitleLine("Projects", "", width);
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 Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
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
- const node = updatedFlat.find((s) => s.id === selectedId);
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 === selectedId)
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 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
- );
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(Text5, { dimColor: true, children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows` }),
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(Fragment2, { children: [
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
- trailingBlankRows: VIEWER_BREATHING_ROWS,
3563
- liveIndicatorPosition,
3875
+ liveSpinnerFrame: spinner,
3876
+ liveTick,
3564
3877
  width,
3565
3878
  cursorLine: viewerCursorLine,
3566
3879
  hasFocus: focus === "viewer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.9.4",
3
+ "version": "0.10.0",
4
4
  "description": "CLI tool to monitor agent status in real-time. Works with Claude Code, multi-agent workflows, and any AI agent system.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",