agenthud 0.8.2 → 0.8.4

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/dist/index.js CHANGED
@@ -14,4 +14,4 @@ Error: Node.js ${MIN_NODE_VERSION}+ is required (current: ${process.version})
14
14
  console.error(" https://nodejs.org/\n");
15
15
  process.exit(1);
16
16
  }
17
- import("./main-QZW2W2BX.js");
17
+ import("./main-WBZ2KBF2.js");
@@ -28,7 +28,13 @@ var KNOWN_WATCH_FLAGS = /* @__PURE__ */ new Set([
28
28
  "-h",
29
29
  "--help"
30
30
  ]);
31
- var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set(["--date", "--include", "--format"]);
31
+ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
32
+ "--date",
33
+ "--include",
34
+ "--format",
35
+ "--detail-limit",
36
+ "--with-git"
37
+ ]);
32
38
  var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report"]);
33
39
  function getHelp() {
34
40
  return `Usage: agenthud [options]
@@ -49,6 +55,8 @@ Commands:
49
55
  Types: response,bash,edit,thinking,read,glob,user
50
56
  Default: response,bash,edit,thinking
51
57
  --format FORMAT Output format: markdown (default) or json
58
+ --detail-limit N Max chars per activity detail (default: 120, 0 = unlimited)
59
+ --with-git Append today's git commits from cwd to report
52
60
 
53
61
  Environment:
54
62
  CLAUDE_PROJECTS_DIR Path to Claude projects directory
@@ -68,25 +76,21 @@ function getVersion() {
68
76
  function clearScreen() {
69
77
  console.clear();
70
78
  }
71
- function parseUTCMidnight(dateStr) {
79
+ function parseLocalMidnight(dateStr) {
72
80
  if (dateStr === "today") {
73
81
  const now = /* @__PURE__ */ new Date();
74
- return new Date(
75
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
76
- );
82
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
77
83
  }
78
84
  const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
79
85
  if (!match) return null;
80
86
  const [, y, m, d] = match.map(Number);
81
- const date = new Date(Date.UTC(y, m - 1, d));
87
+ const date = new Date(y, m - 1, d);
82
88
  if (Number.isNaN(date.getTime())) return null;
83
89
  return date;
84
90
  }
85
- function todayUTCMidnight() {
91
+ function todayLocalMidnight() {
86
92
  const now = /* @__PURE__ */ new Date();
87
- return new Date(
88
- Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
89
- );
93
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
90
94
  }
91
95
  function parseArgs(args) {
92
96
  if (args.includes("--help") || args.includes("-h")) {
@@ -100,7 +104,7 @@ function parseArgs(args) {
100
104
  }
101
105
  if (args[0] === "report") {
102
106
  const rest = args.slice(1);
103
- let reportDate = todayUTCMidnight();
107
+ let reportDate = todayLocalMidnight();
104
108
  let reportInclude = DEFAULT_TYPES;
105
109
  let reportError;
106
110
  for (const arg of rest) {
@@ -115,7 +119,7 @@ function parseArgs(args) {
115
119
  if (!dateStr) {
116
120
  reportError = "Invalid date: missing value for --date";
117
121
  } else {
118
- const parsed = parseUTCMidnight(dateStr);
122
+ const parsed = parseLocalMidnight(dateStr);
119
123
  if (!parsed) {
120
124
  reportError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
121
125
  } else {
@@ -144,11 +148,25 @@ function parseArgs(args) {
144
148
  reportError = "Invalid format: missing value for --format.";
145
149
  }
146
150
  }
151
+ let reportDetailLimit;
152
+ const detailLimitIdx = rest.indexOf("--detail-limit");
153
+ if (detailLimitIdx !== -1) {
154
+ const val = rest[detailLimitIdx + 1];
155
+ const n = Number(val);
156
+ if (!val || Number.isNaN(n) || n < 0 || !Number.isInteger(n)) {
157
+ reportError = `Invalid --detail-limit: "${val}". Must be a non-negative integer.`;
158
+ } else {
159
+ reportDetailLimit = n;
160
+ }
161
+ }
162
+ const reportWithGit = rest.includes("--with-git");
147
163
  return {
148
164
  mode: "report",
149
165
  reportDate,
150
166
  reportInclude,
151
167
  reportFormat,
168
+ reportDetailLimit,
169
+ reportWithGit,
152
170
  reportError
153
171
  };
154
172
  }
@@ -179,7 +197,8 @@ var DEFAULT_GLOBAL_CONFIG = {
179
197
  refreshIntervalMs: 2e3,
180
198
  logDir: join2(homedir(), ".agenthud", "logs"),
181
199
  hiddenSessions: [],
182
- hiddenSubAgents: []
200
+ hiddenSubAgents: [],
201
+ filterPresets: [[], ["response"], ["commit"]]
183
202
  };
184
203
  function parseInterval(value) {
185
204
  const match = value.match(/^(\d+)(s|m)$/);
@@ -221,6 +240,12 @@ function loadGlobalConfig() {
221
240
  (s) => typeof s === "string"
222
241
  );
223
242
  }
243
+ if (Array.isArray(parsed.filterPresets)) {
244
+ const presets = parsed.filterPresets.filter(Array.isArray).map(
245
+ (p) => p.filter((t) => typeof t === "string")
246
+ );
247
+ if (presets.length > 0) config.filterPresets = presets;
248
+ }
224
249
  return config;
225
250
  }
226
251
  function writeConfig(updates) {
@@ -264,11 +289,8 @@ function hasProjectLevelConfig() {
264
289
  // src/data/reportGenerator.ts
265
290
  import { statSync } from "fs";
266
291
 
267
- // src/data/sessionHistory.ts
268
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
269
-
270
- // src/data/activityParser.ts
271
- import { basename } from "path";
292
+ // src/data/gitCommits.ts
293
+ import { execSync } from "child_process";
272
294
 
273
295
  // src/types/index.ts
274
296
  var ICONS = {
@@ -286,10 +308,61 @@ var ICONS = {
286
308
  Task: "\xBB",
287
309
  TodoWrite: "~",
288
310
  AskUserQuestion: "?",
311
+ Commit: "\u25C6",
289
312
  Default: "$"
290
313
  };
291
314
 
315
+ // src/data/gitCommits.ts
316
+ function formatDateString(date) {
317
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
318
+ }
319
+ function getCommitDetail(projectPath, hash) {
320
+ try {
321
+ return execSync(`git show --stat --no-color ${hash}`, {
322
+ cwd: projectPath,
323
+ encoding: "utf-8"
324
+ }).trim();
325
+ } catch {
326
+ return null;
327
+ }
328
+ }
329
+ function parseGitCommits(projectPath, startDate, endDate) {
330
+ if (!projectPath) return [];
331
+ const start = formatDateString(startDate);
332
+ const end = formatDateString(endDate ?? startDate);
333
+ let raw;
334
+ try {
335
+ raw = execSync(
336
+ `git log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
337
+ { cwd: projectPath, encoding: "utf-8" }
338
+ ).trim();
339
+ } catch {
340
+ return [];
341
+ }
342
+ if (!raw) return [];
343
+ const entries = [];
344
+ for (const line of raw.split("\n")) {
345
+ const parts = line.trim().split("|");
346
+ if (parts.length < 3) continue;
347
+ const [tsStr, hash, ...rest] = parts;
348
+ const ts = Number(tsStr);
349
+ if (Number.isNaN(ts)) continue;
350
+ entries.push({
351
+ timestamp: new Date(ts * 1e3),
352
+ type: "commit",
353
+ icon: ICONS.Commit,
354
+ label: hash,
355
+ detail: rest.join("|")
356
+ });
357
+ }
358
+ return entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
359
+ }
360
+
361
+ // src/data/sessionHistory.ts
362
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
363
+
292
364
  // src/data/activityParser.ts
365
+ import { basename } from "path";
293
366
  function stripAnsi(text) {
294
367
  return text.replace(/\x1b\[[0-9;]*m/g, "");
295
368
  }
@@ -430,37 +503,51 @@ function activityMatchesInclude(activity, include) {
430
503
  return true;
431
504
  return false;
432
505
  }
433
- function isSameUTCDay(a, b) {
434
- return a.getUTCFullYear() === b.getUTCFullYear() && a.getUTCMonth() === b.getUTCMonth() && a.getUTCDate() === b.getUTCDate();
506
+ function isSameLocalDay(a, b) {
507
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
435
508
  }
436
509
  function formatTime(date) {
437
- return `${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}`;
510
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
438
511
  }
439
- function formatActivity(activity) {
512
+ function formatActivity(activity, limit) {
440
513
  const time = formatTime(activity.timestamp);
441
- const detail = activity.detail.length > 120 ? activity.detail.slice(0, 120) : activity.detail;
514
+ const detail = truncateDetail(activity.detail, limit);
442
515
  const suffix = detail ? `: ${detail}` : "";
443
516
  return `[${time}] ${activity.icon} ${activity.label}${suffix}`;
444
517
  }
445
- function formatDateString(date) {
446
- return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
518
+ function formatDateString2(date) {
519
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
447
520
  }
448
521
  function sessionIsOnDate(session, date, activities) {
449
522
  try {
450
523
  const mtime = new Date(statSync(session.filePath).mtimeMs);
451
- if (isSameUTCDay(mtime, date)) return true;
524
+ if (isSameLocalDay(mtime, date)) return true;
452
525
  } catch {
453
526
  }
454
- return activities.some((a) => isSameUTCDay(a.timestamp, date));
527
+ return activities.some((a) => isSameLocalDay(a.timestamp, date));
528
+ }
529
+ function truncateDetail(detail, limit) {
530
+ if (limit === 0 || detail.length <= limit) return detail;
531
+ return detail.slice(0, limit);
455
532
  }
456
533
  function generateReport(sessions, options2) {
457
- const { date, include, format = "markdown" } = options2;
458
- const dateStr = formatDateString(date);
534
+ const {
535
+ date,
536
+ include,
537
+ format = "markdown",
538
+ detailLimit = 120,
539
+ withGit = false
540
+ } = options2;
541
+ const dateStr = formatDateString2(date);
459
542
  const blocks = [];
460
543
  for (const session of sessions) {
461
544
  const allActivities = parseSessionHistory(session.filePath);
462
545
  if (!sessionIsOnDate(session, date, allActivities)) continue;
463
- const dayActivities = allActivities.filter((a) => isSameUTCDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include));
546
+ const commits = withGit ? parseGitCommits(session.projectPath, date) : [];
547
+ const dayActivities = [
548
+ ...allActivities.filter((a) => isSameLocalDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include)),
549
+ ...commits
550
+ ].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
464
551
  if (dayActivities.length === 0) continue;
465
552
  blocks.push({
466
553
  session,
@@ -476,20 +563,39 @@ function generateReport(sessions, options2) {
476
563
  }
477
564
  blocks.sort((a, b) => a.firstTime - b.firstTime);
478
565
  if (format === "json") {
479
- return JSON.stringify(
480
- {
481
- date: dateStr,
482
- sessions: blocks.map(({ session, activities }) => ({
483
- project: session.projectName,
484
- start: formatTime(activities[0].timestamp),
485
- end: formatTime(activities[activities.length - 1].timestamp),
486
- activities: activities.map((a) => ({
566
+ const buildJsonSession = (session, acts) => {
567
+ const subAgentBlocks = session.subAgents.map((sa) => {
568
+ const saActivities = parseSessionHistory(sa.filePath).filter((a) => isSameLocalDay(a.timestamp, date)).filter((a) => activityMatchesInclude(a, include));
569
+ return {
570
+ agentId: sa.agentId,
571
+ taskDescription: sa.taskDescription,
572
+ activities: saActivities.map((a) => ({
487
573
  time: formatTime(a.timestamp),
488
574
  icon: a.icon,
489
575
  label: a.label,
490
- detail: a.detail.length > 120 ? a.detail.slice(0, 120) : a.detail
576
+ detail: truncateDetail(a.detail, detailLimit)
491
577
  }))
492
- }))
578
+ };
579
+ });
580
+ return {
581
+ project: session.projectName,
582
+ start: formatTime(acts[0].timestamp),
583
+ end: formatTime(acts[acts.length - 1].timestamp),
584
+ activities: acts.map((a) => ({
585
+ time: formatTime(a.timestamp),
586
+ icon: a.icon,
587
+ label: a.label,
588
+ detail: truncateDetail(a.detail, detailLimit)
589
+ })),
590
+ subAgents: subAgentBlocks
591
+ };
592
+ };
593
+ return JSON.stringify(
594
+ {
595
+ date: dateStr,
596
+ sessions: blocks.map(
597
+ ({ session, activities }) => buildJsonSession(session, activities)
598
+ )
493
599
  },
494
600
  null,
495
601
  2
@@ -502,7 +608,7 @@ function generateReport(sessions, options2) {
502
608
  lines.push(`## ${session.projectName} (${first} \u2013 ${last})`);
503
609
  lines.push("");
504
610
  for (const activity of activities) {
505
- lines.push(formatActivity(activity));
611
+ lines.push(formatActivity(activity, detailLimit));
506
612
  }
507
613
  lines.push("");
508
614
  }
@@ -747,6 +853,9 @@ function getActivityStyle(activity) {
747
853
  if (activity.type === "thinking") {
748
854
  return { color: "magenta", dimColor: true };
749
855
  }
856
+ if (activity.type === "commit") {
857
+ return { color: "yellow", dimColor: false };
858
+ }
750
859
  if (activity.type === "tool") {
751
860
  if (activity.label === "Bash") {
752
861
  return { color: "gray", dimColor: false };
@@ -766,7 +875,7 @@ function formatActivityTime(date, now) {
766
875
  const day = String(date.getDate()).padStart(2, "0");
767
876
  return `${month}/${day} ${time}`;
768
877
  }
769
- function truncateDetail(detail, maxWidth) {
878
+ function truncateDetail2(detail, maxWidth) {
770
879
  if (getDisplayWidth(detail) <= maxWidth) return detail;
771
880
  let truncated = "";
772
881
  let currentWidth = 0;
@@ -791,16 +900,18 @@ function ActivityViewerPanel({
791
900
  width,
792
901
  cursorLine,
793
902
  hasFocus,
794
- spinner = ""
903
+ spinner = "",
904
+ filterLabel
795
905
  }) {
796
906
  const innerWidth = getInnerWidth(width);
797
907
  const contentWidth = innerWidth - 1;
908
+ const filterSuffix = filterLabel && filterLabel !== "all" ? ` \xB7 ${filterLabel}` : "";
798
909
  let titleSuffix;
799
910
  if (isLive) {
800
- titleSuffix = `[LIVE ${spinner || "\u25BC"}]`;
911
+ titleSuffix = `[LIVE ${spinner || "\u25BC"}${filterSuffix}]`;
801
912
  } else {
802
913
  const badge = newCount > 0 ? ` +${newCount}\u2191` : "";
803
- titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}]`;
914
+ titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}${filterSuffix}]`;
804
915
  }
805
916
  let visibleActivities;
806
917
  if (activities.length === 0) {
@@ -850,7 +961,7 @@ function ActivityViewerPanel({
850
961
  let labelContent;
851
962
  let _displayWidth;
852
963
  if (detail) {
853
- const truncated = truncateDetail(detail, Math.max(0, detailMaxWidth));
964
+ const truncated = truncateDetail2(detail, Math.max(0, detailMaxWidth));
854
965
  labelContent = `${labelPart}${truncated}${countSuffix}`;
855
966
  _displayWidth = prefixWidth - 1 + labelWidth + getDisplayWidth(truncated) + countSuffixWidth;
856
967
  } else {
@@ -898,21 +1009,27 @@ import { Box as Box2, Text as Text2 } from "ink";
898
1009
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
899
1010
  function wrapText(text, maxWidth) {
900
1011
  if (!text) return ["(empty)"];
901
- const words = text.split(" ");
902
- const lines = [];
903
- let current = "";
904
- for (const word of words) {
905
- if (!current) {
906
- current = word;
907
- } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
908
- current += ` ${word}`;
909
- } else {
910
- lines.push(current);
911
- current = word;
1012
+ const result = [];
1013
+ for (const rawLine of text.split("\n")) {
1014
+ if (!rawLine) {
1015
+ result.push("");
1016
+ continue;
912
1017
  }
1018
+ const words = rawLine.split(" ");
1019
+ let current = "";
1020
+ for (const word of words) {
1021
+ if (!current) {
1022
+ current = word;
1023
+ } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
1024
+ current += ` ${word}`;
1025
+ } else {
1026
+ result.push(current);
1027
+ current = word;
1028
+ }
1029
+ }
1030
+ if (current) result.push(current);
913
1031
  }
914
- if (current) lines.push(current);
915
- return lines.length > 0 ? lines : ["(empty)"];
1032
+ return result.length > 0 ? result : ["(empty)"];
916
1033
  }
917
1034
  function DetailViewPanel({
918
1035
  activity,
@@ -994,7 +1111,9 @@ function useHotkeys({
994
1111
  onHide,
995
1112
  onDetailClose,
996
1113
  onDetailScrollUp,
997
- onDetailScrollDown
1114
+ onDetailScrollDown,
1115
+ onFilter,
1116
+ filterLabel
998
1117
  }) {
999
1118
  const handleInput = (input, key) => {
1000
1119
  if (detailMode) {
@@ -1028,6 +1147,10 @@ function useHotkeys({
1028
1147
  onRefresh();
1029
1148
  return;
1030
1149
  }
1150
+ if (input === "f" && !key.ctrl && focus === "viewer") {
1151
+ onFilter();
1152
+ return;
1153
+ }
1031
1154
  if (key.pageUp) {
1032
1155
  onScrollPageUp();
1033
1156
  return;
@@ -1103,9 +1226,10 @@ function useHotkeys({
1103
1226
  "Tab: sessions",
1104
1227
  "\u2191\u2193/jk: scroll",
1105
1228
  "PgUp/Dn: page",
1106
- "g: top",
1107
- "G: live",
1229
+ "g: live",
1230
+ "G: oldest",
1108
1231
  "\u21B5: detail",
1232
+ `f: ${filterLabel}`,
1109
1233
  "q: quit"
1110
1234
  ];
1111
1235
  return { handleInput, statusBarItems };
@@ -1508,6 +1632,7 @@ function App({ mode }) {
1508
1632
  const [scrollOffset, setScrollOffset] = useState2(0);
1509
1633
  const [isLive, setIsLive] = useState2(true);
1510
1634
  const [activities, setActivities] = useState2([]);
1635
+ const [gitActivities, setGitActivities] = useState2([]);
1511
1636
  const [newCount, setNewCount] = useState2(0);
1512
1637
  const [expandedIds, setExpandedIds] = useState2(/* @__PURE__ */ new Set());
1513
1638
  const [viewerCursorLine, setViewerCursorLine] = useState2(0);
@@ -1516,6 +1641,7 @@ function App({ mode }) {
1516
1641
  null
1517
1642
  );
1518
1643
  const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
1644
+ const [filterIndex, setFilterIndex] = useState2(0);
1519
1645
  const allFlat = useMemo(
1520
1646
  () => flattenSessions2(sessionTree, expandedIds),
1521
1647
  [sessionTree, expandedIds]
@@ -1539,13 +1665,54 @@ function App({ mode }) {
1539
1665
  } else {
1540
1666
  setActivities([]);
1541
1667
  }
1668
+ setGitActivities([]);
1542
1669
  }, [selectedId]);
1670
+ useEffect2(() => {
1671
+ setScrollOffset(0);
1672
+ setIsLive(true);
1673
+ setViewerCursorLine(0);
1674
+ }, [filterIndex]);
1675
+ useEffect2(() => {
1676
+ if (!isWatchMode) return;
1677
+ const node = allFlatRef.current.find((s) => s.id === selectedId);
1678
+ if (!node?.projectPath) return;
1679
+ const load = () => {
1680
+ const acts = node.filePath ? parseSessionHistory(node.filePath) : [];
1681
+ const today = /* @__PURE__ */ new Date();
1682
+ const todayMidnight = new Date(
1683
+ today.getFullYear(),
1684
+ today.getMonth(),
1685
+ today.getDate()
1686
+ );
1687
+ const startDate = acts.length > 0 ? new Date(
1688
+ acts[0].timestamp.getFullYear(),
1689
+ acts[0].timestamp.getMonth(),
1690
+ acts[0].timestamp.getDate()
1691
+ ) : todayMidnight;
1692
+ const endDate = acts.length > 0 ? new Date(
1693
+ acts[acts.length - 1].timestamp.getFullYear(),
1694
+ acts[acts.length - 1].timestamp.getMonth(),
1695
+ acts[acts.length - 1].timestamp.getDate()
1696
+ ) : todayMidnight;
1697
+ const commits = parseGitCommits(node.projectPath, startDate, endDate);
1698
+ setGitActivities(commits);
1699
+ };
1700
+ load();
1701
+ const timer = setInterval(load, 3e4);
1702
+ return () => clearInterval(timer);
1703
+ }, [selectedId, isWatchMode]);
1543
1704
  const refresh = useCallback(() => {
1544
1705
  const freshConfig = loadGlobalConfig();
1545
1706
  const tree = discoverSessions(freshConfig);
1546
- setSessionTree(tree);
1547
1707
  const updatedFlat = flattenSessions2(tree, expandedIds);
1548
1708
  const node = updatedFlat.find((s) => s.id === selectedId);
1709
+ if (!node) {
1710
+ const parentSession = tree.sessions.find(
1711
+ (s) => s.subAgents.some((sa) => sa.id === selectedId)
1712
+ );
1713
+ if (parentSession) setSelectedId(parentSession.id);
1714
+ }
1715
+ setSessionTree(tree);
1549
1716
  if (!node || !node.filePath) return;
1550
1717
  const newActivities = parseSessionHistory(node.filePath);
1551
1718
  const delta = newActivities.length - activitiesLengthRef.current;
@@ -1589,6 +1756,18 @@ function App({ mode }) {
1589
1756
  if (debounce) clearTimeout(debounce);
1590
1757
  };
1591
1758
  }, [isWatchMode, config.refreshIntervalMs]);
1759
+ const filterPresets = config.filterPresets;
1760
+ const activePreset = filterPresets[filterIndex % filterPresets.length] ?? [];
1761
+ const filterLabel = activePreset.length === 0 ? "all" : activePreset.join("+");
1762
+ const mergedActivities = useMemo(() => {
1763
+ const merged = [...activities, ...gitActivities].sort(
1764
+ (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
1765
+ );
1766
+ if (activePreset.length === 0) return merged;
1767
+ return merged.filter(
1768
+ (a) => activePreset.includes(a.type) || a.type === "tool" && activePreset.some((p) => a.label.toLowerCase() === p)
1769
+ );
1770
+ }, [activities, gitActivities, activePreset]);
1592
1771
  const selectedIndex = allFlat.findIndex((s) => s.id === selectedId);
1593
1772
  const height = (stdout?.rows ?? 41) - 1;
1594
1773
  const width = stdout?.columns ?? 80;
@@ -1620,6 +1799,7 @@ function App({ mode }) {
1620
1799
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
1621
1800
  onScrollUp: () => {
1622
1801
  if (focus === "tree") {
1802
+ if (selectedIndex === -1) return;
1623
1803
  const prev = Math.max(0, selectedIndex - 1);
1624
1804
  setSelectedId(allFlat[prev]?.id ?? selectedId);
1625
1805
  } else {
@@ -1639,6 +1819,7 @@ function App({ mode }) {
1639
1819
  },
1640
1820
  onScrollDown: () => {
1641
1821
  if (focus === "tree") {
1822
+ if (selectedIndex === -1) return;
1642
1823
  const next = Math.min(allFlat.length - 1, selectedIndex + 1);
1643
1824
  setSelectedId(allFlat[next]?.id ?? selectedId);
1644
1825
  } else {
@@ -1715,16 +1896,16 @@ function App({ mode }) {
1715
1896
  }
1716
1897
  },
1717
1898
  onScrollTop: () => {
1718
- setViewerCursorLine(0);
1719
- setIsLive(false);
1720
- setScrollOffset(Math.max(0, activities.length - viewerRows));
1721
- },
1722
- onScrollBottom: () => {
1723
1899
  setViewerCursorLine(0);
1724
1900
  setIsLive(true);
1725
1901
  setScrollOffset(0);
1726
1902
  setNewCount(0);
1727
1903
  },
1904
+ onScrollBottom: () => {
1905
+ setViewerCursorLine(0);
1906
+ setIsLive(false);
1907
+ setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
1908
+ },
1728
1909
  onDetailClose: () => {
1729
1910
  setDetailMode(false);
1730
1911
  },
@@ -1737,14 +1918,20 @@ function App({ mode }) {
1737
1918
  onEnter: () => {
1738
1919
  if (focus === "viewer") {
1739
1920
  const act = getSelectedActivity(
1740
- activities,
1921
+ mergedActivities,
1741
1922
  isLive,
1742
1923
  scrollOffset,
1743
1924
  viewerRows,
1744
1925
  viewerCursorLine
1745
1926
  );
1746
1927
  if (act) {
1747
- setDetailActivity(act);
1928
+ if (act.type === "commit") {
1929
+ const node = allFlatRef.current.find((s) => s.id === selectedId);
1930
+ const detail = node?.projectPath ? getCommitDetail(node.projectPath, act.label) ?? act.detail : act.detail;
1931
+ setDetailActivity({ ...act, detail });
1932
+ } else {
1933
+ setDetailActivity(act);
1934
+ }
1748
1935
  setDetailMode(true);
1749
1936
  setDetailScrollOffset(0);
1750
1937
  }
@@ -1769,8 +1956,14 @@ function App({ mode }) {
1769
1956
  const next = new Set(prev);
1770
1957
  if (next.has(parentId)) {
1771
1958
  next.delete(parentId);
1959
+ setSelectedId(parentId);
1772
1960
  } else {
1773
1961
  next.add(parentId);
1962
+ const parent = sessionTree.sessions.find((s) => s.id === parentId);
1963
+ const firstNew = parent?.subAgents.find(
1964
+ (sa) => sa.status === "cool" || sa.status === "cold"
1965
+ );
1966
+ if (firstNew) setSelectedId(firstNew.id);
1774
1967
  }
1775
1968
  return next;
1776
1969
  });
@@ -1828,7 +2021,9 @@ function App({ mode }) {
1828
2021
  },
1829
2022
  onSaveLog: saveLog,
1830
2023
  onRefresh: refresh,
1831
- onQuit: exit
2024
+ onQuit: exit,
2025
+ onFilter: () => setFilterIndex((i) => (i + 1) % filterPresets.length),
2026
+ filterLabel
1832
2027
  });
1833
2028
  useInput((input, key) => handleInput(input, key), { isActive: isWatchMode });
1834
2029
  const selectedSession = allFlat.find((s) => s.id === selectedId);
@@ -1867,7 +2062,7 @@ function App({ mode }) {
1867
2062
  ) : /* @__PURE__ */ jsx4(
1868
2063
  ActivityViewerPanel,
1869
2064
  {
1870
- activities,
2065
+ activities: mergedActivities,
1871
2066
  sessionName: sessionDisplayName,
1872
2067
  scrollOffset,
1873
2068
  isLive,
@@ -1876,7 +2071,8 @@ function App({ mode }) {
1876
2071
  width,
1877
2072
  cursorLine: viewerCursorLine,
1878
2073
  hasFocus: focus === "viewer",
1879
- spinner
2074
+ spinner,
2075
+ filterLabel
1880
2076
  }
1881
2077
  ) })
1882
2078
  ] });
@@ -1929,7 +2125,9 @@ if (options.mode === "report") {
1929
2125
  const markdown = generateReport(tree.sessions, {
1930
2126
  date: options.reportDate,
1931
2127
  include: options.reportInclude,
1932
- format: options.reportFormat
2128
+ format: options.reportFormat,
2129
+ detailLimit: options.reportDetailLimit,
2130
+ withGit: options.reportWithGit
1933
2131
  });
1934
2132
  process.stdout.write(`${markdown}
1935
2133
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agenthud",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
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",
@@ -0,0 +1,10 @@
1
+ #!/bin/zsh
2
+ # Daily summary: pipe today's agenthud report to claude for summarization
3
+ # Usage: daily-summary.sh [--date YYYY-MM-DD]
4
+
5
+ DATE_ARG=""
6
+ if [[ "$1" == "--date" && -n "$2" ]]; then
7
+ DATE_ARG="--date $2"
8
+ fi
9
+
10
+ agenthud report ${DATE_ARG} --detail-limit 0 --with-git | claude -p "다음은 오늘 Claude Code로 작업한 활동 로그입니다. 이를 바탕으로 오늘 작업 내용을 한국어로 간결하게 정리해주세요. 완료한 작업, 주요 변경사항, 커밋 내역 순으로 bullet point로 작성해주세요."