agenthud 0.9.0 → 0.9.2

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,13 +1,24 @@
1
1
  // src/main.ts
2
2
  import { existsSync as existsSync6, rmSync } from "fs";
3
+ import { homedir as homedir5 } from "os";
3
4
  import { join as join6 } from "path";
4
- import { createInterface } from "readline";
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
5
16
  import { render } from "ink";
6
17
  import React from "react";
7
18
 
8
19
  // src/cli.ts
9
20
  import { readFileSync } from "fs";
10
- import { dirname, join } from "path";
21
+ import { dirname, join as join2 } from "path";
11
22
  import { fileURLToPath } from "url";
12
23
  var ALL_TYPES = [
13
24
  "response",
@@ -35,7 +46,16 @@ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
35
46
  "--detail-limit",
36
47
  "--with-git"
37
48
  ]);
38
- var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set(["--date", "--prompt", "--force"]);
49
+ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
50
+ "--date",
51
+ "--last",
52
+ "--from",
53
+ "--to",
54
+ "--prompt",
55
+ "--force",
56
+ "-y",
57
+ "--yes"
58
+ ]);
39
59
  var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report", "summary"]);
40
60
  function getHelp() {
41
61
  return `Usage: agenthud [options]
@@ -51,32 +71,37 @@ Options:
51
71
  Commands:
52
72
  report [--date DATE] [--include TYPES] [--format FORMAT] [--detail-limit N] [--with-git]
53
73
  Print activity report for a date (default: today)
54
- --date YYYY-MM-DD|today Date to report on
74
+ --date YYYY-MM-DD|today|yesterday|-Nd Date to report on
55
75
  --include TYPES Comma-separated types or "all"
56
76
  Types: response,bash,edit,thinking,read,glob,user
57
77
  Default: response,bash,edit,thinking
58
78
  --format FORMAT Output format: markdown (default) or json
59
79
  --detail-limit N Max chars per activity detail (default: 120, 0 = unlimited)
60
- --with-git Append today's git commits from cwd to report
80
+ --with-git Merge git commits from each session's project into the timeline
61
81
 
62
- summary [--date DATE] [--prompt TEXT] [--force]
63
- Generate LLM summary of daily activity via claude CLI
64
- --date YYYY-MM-DD|today Date to summarize (default: today)
65
- --prompt TEXT Override prompt for this run
66
- --force Regenerate even if cached (past dates)
82
+ summary [--date DATE | --last Nd | --from DATE --to DATE] [--prompt TEXT] [--force] [-y]
83
+ Generate LLM summary via claude CLI.
84
+ Single day produces a daily summary;
85
+ a date range produces a meta-summary built from daily summaries.
86
+ --date YYYY-MM-DD|today|yesterday|-Nd Date to summarize (default: today)
87
+ --last Nd Date range: last N days, ending today (e.g. --last 7d)
88
+ --from YYYY-MM-DD Date range: start date (use with --to)
89
+ --to YYYY-MM-DD Date range: end date (use with --from)
90
+ --prompt TEXT Override prompt for this run (daily only)
91
+ --force Regenerate even if cached
92
+ -y, --yes Skip confirmation prompts for new daily summaries
67
93
 
68
94
  Environment:
69
95
  CLAUDE_PROJECTS_DIR Path to Claude projects directory
70
96
  (default: ~/.claude/projects)
71
97
 
72
98
  Config: ~/.agenthud/config.yaml
73
- Logs: ~/.agenthud/logs/
74
99
  `;
75
100
  }
76
101
  function getVersion() {
77
102
  const __dirname2 = dirname(fileURLToPath(import.meta.url));
78
103
  const packageJson = JSON.parse(
79
- readFileSync(join(__dirname2, "..", "package.json"), "utf-8")
104
+ readFileSync(join2(__dirname2, "..", "package.json"), "utf-8")
80
105
  );
81
106
  return packageJson.version;
82
107
  }
@@ -88,6 +113,16 @@ function parseLocalMidnight(dateStr) {
88
113
  const now = /* @__PURE__ */ new Date();
89
114
  return new Date(now.getFullYear(), now.getMonth(), now.getDate());
90
115
  }
116
+ if (dateStr === "yesterday") {
117
+ const now = /* @__PURE__ */ new Date();
118
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
119
+ }
120
+ const relMatch = dateStr.match(/^-(\d+)d$/);
121
+ if (relMatch) {
122
+ const days = Number(relMatch[1]);
123
+ const now = /* @__PURE__ */ new Date();
124
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate() - days);
125
+ }
91
126
  const match = dateStr.match(/^(\d{4})-(\d{2})-(\d{2})$/);
92
127
  if (!match) return null;
93
128
  const [, y, m, d] = match.map(Number);
@@ -128,7 +163,7 @@ function parseArgs(args) {
128
163
  } else {
129
164
  const parsed = parseLocalMidnight(dateStr);
130
165
  if (!parsed) {
131
- reportError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
166
+ reportError = `Invalid date: "${dateStr}". Use YYYY-MM-DD, "today", "yesterday", or "-Nd" (N days ago).`;
132
167
  } else {
133
168
  reportDate = parsed;
134
169
  }
@@ -179,10 +214,20 @@ function parseArgs(args) {
179
214
  }
180
215
  if (args[0] === "summary") {
181
216
  const rest = args.slice(1);
182
- let summaryDate = todayLocalMidnight();
217
+ let summaryDate;
218
+ let summaryFrom;
219
+ let summaryTo;
183
220
  let summaryPrompt;
184
221
  let summaryForce = false;
222
+ let summaryAssumeYes = false;
185
223
  let summaryError;
224
+ const FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
225
+ "--date",
226
+ "--last",
227
+ "--from",
228
+ "--to",
229
+ "--prompt"
230
+ ]);
186
231
  for (let i = 0; i < rest.length; i++) {
187
232
  const arg = rest[i];
188
233
  if (!arg.startsWith("-")) continue;
@@ -190,7 +235,7 @@ function parseArgs(args) {
190
235
  summaryError = `Unknown option: "${arg}". Run agenthud --help for usage.`;
191
236
  break;
192
237
  }
193
- if (arg === "--date" || arg === "--prompt") i++;
238
+ if (FLAGS_WITH_VALUE.has(arg)) i++;
194
239
  }
195
240
  const dateIdx = rest.indexOf("--date");
196
241
  if (dateIdx !== -1) {
@@ -200,12 +245,70 @@ function parseArgs(args) {
200
245
  } else {
201
246
  const parsed = parseLocalMidnight(dateStr);
202
247
  if (!parsed) {
203
- summaryError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
248
+ summaryError = `Invalid date: "${dateStr}". Use YYYY-MM-DD, "today", "yesterday", or "-Nd" (N days ago).`;
204
249
  } else {
205
250
  summaryDate = parsed;
206
251
  }
207
252
  }
208
253
  }
254
+ const lastIdx = rest.indexOf("--last");
255
+ if (lastIdx !== -1 && !summaryError) {
256
+ const val = rest[lastIdx + 1];
257
+ if (!val) {
258
+ summaryError = "Invalid --last: missing value (e.g. --last 7d).";
259
+ } else {
260
+ const m = val.match(/^(\d+)d$/);
261
+ if (!m) {
262
+ summaryError = `Invalid --last: "${val}". Use form like "7d".`;
263
+ } else {
264
+ const days = Number(m[1]);
265
+ if (days < 1) {
266
+ summaryError = `Invalid --last: "${val}". Must be at least 1 day.`;
267
+ } else {
268
+ const today = todayLocalMidnight();
269
+ const from = new Date(today);
270
+ from.setDate(today.getDate() - (days - 1));
271
+ summaryFrom = from;
272
+ summaryTo = today;
273
+ }
274
+ }
275
+ }
276
+ }
277
+ const fromIdx = rest.indexOf("--from");
278
+ const toIdx = rest.indexOf("--to");
279
+ if ((fromIdx !== -1 || toIdx !== -1) && !summaryError) {
280
+ if (fromIdx === -1 || toIdx === -1) {
281
+ summaryError = "--from and --to must be used together.";
282
+ } else {
283
+ const fromStr = rest[fromIdx + 1];
284
+ const toStr = rest[toIdx + 1];
285
+ const from = fromStr ? parseLocalMidnight(fromStr) : null;
286
+ const to = toStr ? parseLocalMidnight(toStr) : null;
287
+ if (!from) {
288
+ summaryError = `Invalid --from: "${fromStr}".`;
289
+ } else if (!to) {
290
+ summaryError = `Invalid --to: "${toStr}".`;
291
+ } else if (from.getTime() > to.getTime()) {
292
+ summaryError = `--from (${fromStr}) must be on or before --to (${toStr}).`;
293
+ } else {
294
+ summaryFrom = from;
295
+ summaryTo = to;
296
+ }
297
+ }
298
+ }
299
+ if (!summaryError) {
300
+ const modesUsed = [
301
+ summaryDate !== void 0,
302
+ lastIdx !== -1,
303
+ fromIdx !== -1 || toIdx !== -1
304
+ ].filter(Boolean).length;
305
+ if (modesUsed > 1) {
306
+ summaryError = "--date, --last, and --from/--to are mutually exclusive.";
307
+ }
308
+ }
309
+ if (!summaryError && summaryDate === void 0 && summaryFrom === void 0) {
310
+ summaryDate = todayLocalMidnight();
311
+ }
209
312
  const promptIdx = rest.indexOf("--prompt");
210
313
  if (promptIdx !== -1) {
211
314
  const val = rest[promptIdx + 1];
@@ -216,11 +319,15 @@ function parseArgs(args) {
216
319
  }
217
320
  }
218
321
  if (rest.includes("--force")) summaryForce = true;
322
+ if (rest.includes("-y") || rest.includes("--yes")) summaryAssumeYes = true;
219
323
  return {
220
324
  mode: "summary",
221
325
  summaryDate,
326
+ summaryFrom,
327
+ summaryTo,
222
328
  summaryPrompt,
223
329
  summaryForce,
330
+ summaryAssumeYes,
224
331
  summaryError
225
332
  };
226
333
  }
@@ -244,13 +351,12 @@ function parseArgs(args) {
244
351
  // src/config/globalConfig.ts
245
352
  import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
246
353
  import { homedir } from "os";
247
- import { join as join2 } from "path";
354
+ import { join as join3 } from "path";
248
355
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
249
- var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
250
- var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
356
+ var CONFIG_PATH = join3(homedir(), ".agenthud", "config.yaml");
357
+ var STATE_PATH = join3(homedir(), ".agenthud", "state.yaml");
251
358
  var DEFAULT_GLOBAL_CONFIG = {
252
359
  refreshIntervalMs: 2e3,
253
- logDir: join2(homedir(), ".agenthud", "logs"),
254
360
  hiddenSessions: [],
255
361
  hiddenSubAgents: [],
256
362
  filterPresets: [[], ["response"], ["commit"]],
@@ -263,7 +369,7 @@ function parseInterval(value) {
263
369
  return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
264
370
  }
265
371
  function ensureAgenthudDir() {
266
- const dir = join2(homedir(), ".agenthud");
372
+ const dir = join3(homedir(), ".agenthud");
267
373
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
268
374
  }
269
375
  function writeDefaultConfig() {
@@ -274,9 +380,6 @@ function writeDefaultConfig() {
274
380
  # How often to poll for activity updates
275
381
  refreshInterval: 2s
276
382
 
277
- # Where 's' key saves activity logs
278
- logDir: ~/.agenthud/logs
279
-
280
383
  # Activity filter presets (cycle with 'f' key in viewer)
281
384
  # Each list is one preset; [] means "all". First preset is the default.
282
385
  filterPresets:
@@ -326,9 +429,6 @@ function loadGlobalConfig() {
326
429
  const ms = parseInterval(configRaw.refreshInterval);
327
430
  if (ms !== null) config.refreshIntervalMs = ms;
328
431
  }
329
- if (typeof configRaw.logDir === "string") {
330
- config.logDir = configRaw.logDir.replace(/^~/, homedir());
331
- }
332
432
  if (Array.isArray(configRaw.filterPresets)) {
333
433
  const presets = configRaw.filterPresets.filter(Array.isArray).map(
334
434
  (p) => p.filter((t) => typeof t === "string")
@@ -432,13 +532,10 @@ function hideProject(name) {
432
532
  if (config.hiddenProjects.includes(name)) return;
433
533
  updateState({ hiddenProjects: [...config.hiddenProjects, name] });
434
534
  }
435
- function ensureLogDir(logDir) {
436
- if (!existsSync(logDir)) {
437
- mkdirSync(logDir, { recursive: true });
438
- }
439
- }
440
535
  function hasProjectLevelConfig() {
441
- return existsSync(join2(process.cwd(), ".agenthud", "config.yaml"));
536
+ const candidate = join3(process.cwd(), ".agenthud", "config.yaml");
537
+ if (candidate === join3(homedir(), ".agenthud", "config.yaml")) return false;
538
+ return existsSync(candidate);
442
539
  }
443
540
 
444
541
  // src/data/reportGenerator.ts
@@ -786,16 +883,18 @@ import {
786
883
  createWriteStream,
787
884
  existsSync as existsSync4,
788
885
  mkdirSync as mkdirSync2,
789
- readFileSync as readFileSync5
886
+ readFileSync as readFileSync5,
887
+ unlinkSync
790
888
  } from "fs";
791
889
  import { homedir as homedir3 } from "os";
792
- import { dirname as dirname2, join as join4 } from "path";
890
+ import { dirname as dirname2, join as join5 } from "path";
891
+ import { createInterface } from "readline";
793
892
  import { fileURLToPath as fileURLToPath2 } from "url";
794
893
 
795
894
  // src/data/sessions.ts
796
895
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
797
896
  import { homedir as homedir2 } from "os";
798
- import { basename as basename2, join as join3 } from "path";
897
+ import { basename as basename2, join as join4 } from "path";
799
898
 
800
899
  // src/ui/constants.ts
801
900
  import stringWidth from "string-width";
@@ -843,7 +942,7 @@ function getDisplayWidth(s) {
843
942
 
844
943
  // src/data/sessions.ts
845
944
  function getProjectsDir() {
846
- return process.env.CLAUDE_PROJECTS_DIR ?? join3(homedir2(), ".claude", "projects");
945
+ return process.env.CLAUDE_PROJECTS_DIR ?? join4(homedir2(), ".claude", "projects");
847
946
  }
848
947
  function decodeProjectPath(encoded) {
849
948
  const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
@@ -972,7 +1071,7 @@ function readEntrypoint(filePath) {
972
1071
  }
973
1072
  }
974
1073
  function buildSubAgents(parentId, projectDir, config, projectName) {
975
- const subagentsDir = join3(projectDir, parentId, "subagents");
1074
+ const subagentsDir = join4(projectDir, parentId, "subagents");
976
1075
  if (!existsSync3(subagentsDir)) return [];
977
1076
  let files;
978
1077
  try {
@@ -985,7 +1084,7 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
985
1084
  return files.map((file) => {
986
1085
  const id = file.replace(/\.jsonl$/, "");
987
1086
  const hideKey = `${projectName}/${id}`;
988
- const filePath = join3(subagentsDir, file);
1087
+ const filePath = join4(subagentsDir, file);
989
1088
  try {
990
1089
  const stat = statSync2(filePath);
991
1090
  const { agentId, taskDescription } = readSubAgentInfo(filePath);
@@ -1025,7 +1124,7 @@ function discoverSessions(config) {
1025
1124
  try {
1026
1125
  projectDirs = readdirSync(projectsDir).filter((entry) => {
1027
1126
  try {
1028
- return statSync2(join3(projectsDir, entry)).isDirectory();
1127
+ return statSync2(join4(projectsDir, entry)).isDirectory();
1029
1128
  } catch {
1030
1129
  return false;
1031
1130
  }
@@ -1040,7 +1139,7 @@ function discoverSessions(config) {
1040
1139
  }
1041
1140
  const allSessions = [];
1042
1141
  for (const encodedDir of projectDirs) {
1043
- const projectDir = join3(projectsDir, encodedDir);
1142
+ const projectDir = join4(projectsDir, encodedDir);
1044
1143
  const decodedPath = decodeProjectPath(encodedDir);
1045
1144
  const projectName = basename2(decodedPath);
1046
1145
  let files;
@@ -1054,7 +1153,7 @@ function discoverSessions(config) {
1054
1153
  for (const file of files) {
1055
1154
  const id = file.replace(/\.jsonl$/, "");
1056
1155
  const hideKey = `${projectName}/${id}`;
1057
- const filePath = join3(projectDir, file);
1156
+ const filePath = join4(projectDir, file);
1058
1157
  try {
1059
1158
  const stat = statSync2(filePath);
1060
1159
  const subAgents = buildSubAgents(id, projectDir, config, projectName);
@@ -1126,44 +1225,50 @@ function discoverSessions(config) {
1126
1225
 
1127
1226
  // src/data/summaryRunner.ts
1128
1227
  function agenthudHomeDir() {
1129
- const dir = join4(homedir3(), ".agenthud");
1228
+ const dir = join5(homedir3(), ".agenthud");
1130
1229
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1131
1230
  return dir;
1132
1231
  }
1133
1232
  function summariesDir() {
1134
- const dir = join4(agenthudHomeDir(), "summaries");
1233
+ const dir = join5(agenthudHomeDir(), "summaries");
1135
1234
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1136
1235
  return dir;
1137
1236
  }
1138
- function userPromptPath() {
1139
- return join4(homedir3(), ".agenthud", "summary-prompt.md");
1237
+ function promptFilename(kind) {
1238
+ return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
1239
+ }
1240
+ function userPromptPath(kind) {
1241
+ return join5(homedir3(), ".agenthud", promptFilename(kind));
1140
1242
  }
1141
- function templatePath() {
1243
+ function templatePath(kind) {
1142
1244
  const here = dirname2(fileURLToPath2(import.meta.url));
1143
- return join4(here, "templates", "summary-prompt.md");
1245
+ return join5(here, "templates", promptFilename(kind));
1144
1246
  }
1145
1247
  function dateKey(d) {
1146
1248
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1147
1249
  }
1148
- function cachePath(date) {
1149
- return join4(summariesDir(), `${dateKey(date)}.md`);
1250
+ function dailyCachePath(date) {
1251
+ return join5(summariesDir(), `${dateKey(date)}.md`);
1252
+ }
1253
+ function rangeCachePath(from, to) {
1254
+ return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
1150
1255
  }
1151
1256
  function isSameLocalDay2(a, b) {
1152
1257
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1153
1258
  }
1154
- function ensureUserPromptFile() {
1155
- const p = userPromptPath();
1259
+ function ensureUserPromptFile(kind) {
1260
+ const p = userPromptPath(kind);
1156
1261
  if (existsSync4(p)) return;
1157
1262
  const dir = dirname2(p);
1158
1263
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1159
1264
  try {
1160
- copyFileSync(templatePath(), p);
1265
+ copyFileSync(templatePath(kind), p);
1161
1266
  } catch {
1162
1267
  }
1163
1268
  }
1164
- function resolvePrompt(override) {
1269
+ function resolvePrompt(kind, override) {
1165
1270
  if (override) return override;
1166
- const p = userPromptPath();
1271
+ const p = userPromptPath(kind);
1167
1272
  if (existsSync4(p)) {
1168
1273
  try {
1169
1274
  return readFileSync5(p, "utf-8");
@@ -1171,88 +1276,409 @@ function resolvePrompt(override) {
1171
1276
  }
1172
1277
  }
1173
1278
  try {
1174
- return readFileSync5(templatePath(), "utf-8");
1279
+ return readFileSync5(templatePath(kind), "utf-8");
1175
1280
  } catch {
1176
- return "Summarize the activity log below.";
1281
+ return "Summarize the input below.";
1177
1282
  }
1178
1283
  }
1179
- async function runSummary(options2) {
1180
- ensureUserPromptFile();
1181
- const isToday = isSameLocalDay2(options2.date, options2.today);
1182
- const cached = cachePath(options2.date);
1183
- if (!isToday && !options2.force && existsSync4(cached)) {
1184
- try {
1185
- const content = readFileSync5(cached, "utf-8");
1186
- process.stdout.write(content);
1187
- if (!content.endsWith("\n")) process.stdout.write("\n");
1188
- return 0;
1189
- } catch {
1190
- }
1284
+ function shouldUseRangeCache(force, dates, today, cacheExists) {
1285
+ if (force) return false;
1286
+ if (!cacheExists) return false;
1287
+ if (dates.some((d) => isSameLocalDay2(d, today))) return false;
1288
+ return true;
1289
+ }
1290
+ function enumerateDates(from, to) {
1291
+ const dates = [];
1292
+ const cursor = new Date(from);
1293
+ while (cursor.getTime() <= to.getTime()) {
1294
+ dates.push(new Date(cursor));
1295
+ cursor.setDate(cursor.getDate() + 1);
1191
1296
  }
1192
- const config = loadGlobalConfig();
1193
- const sessions = discoverSessions(config);
1194
- const reportMarkdown = generateReport(sessions.sessions, {
1195
- date: options2.date,
1196
- include: ["response", "bash", "edit", "thinking"],
1197
- format: "markdown",
1198
- detailLimit: 0,
1199
- withGit: true
1200
- });
1201
- const prompt = resolvePrompt(options2.prompt);
1202
- return new Promise((resolve) => {
1203
- const proc = spawn("claude", ["-p", prompt], {
1204
- stdio: ["pipe", "pipe", "pipe"],
1205
- cwd: agenthudHomeDir()
1297
+ return dates;
1298
+ }
1299
+ async function ask(question, defaultYes = false) {
1300
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1301
+ return new Promise((resolve2) => {
1302
+ rl.question(question, (answer) => {
1303
+ rl.close();
1304
+ const trimmed = answer.trim();
1305
+ if (trimmed.length === 0) return resolve2(defaultYes);
1306
+ if (/^y(es)?$/i.test(trimmed)) return resolve2(true);
1307
+ if (/^n(o)?$/i.test(trimmed)) return resolve2(false);
1308
+ resolve2(defaultYes);
1206
1309
  });
1310
+ });
1311
+ }
1312
+ function formatUsage(u) {
1313
+ const fmt = (n) => n.toLocaleString("en-US");
1314
+ const parts = [`${fmt(u.inputTokens)} in / ${fmt(u.outputTokens)} out`];
1315
+ const cacheParts = [];
1316
+ if (u.cacheReadTokens > 0) cacheParts.push(`${fmt(u.cacheReadTokens)} read`);
1317
+ if (u.cacheCreationTokens > 0)
1318
+ cacheParts.push(`${fmt(u.cacheCreationTokens)} written`);
1319
+ if (cacheParts.length > 0) parts.push(`cache: ${cacheParts.join(", ")}`);
1320
+ if (u.costUsd != null) parts.push(`$${u.costUsd.toFixed(4)}`);
1321
+ return parts.join(" \xB7 ");
1322
+ }
1323
+ function spawnClaude(opts) {
1324
+ return new Promise((resolve2) => {
1325
+ const proc = spawn(
1326
+ "claude",
1327
+ [
1328
+ "-p",
1329
+ "--no-session-persistence",
1330
+ "--output-format",
1331
+ "stream-json",
1332
+ "--verbose",
1333
+ opts.prompt
1334
+ ],
1335
+ {
1336
+ stdio: ["pipe", "pipe", "pipe"],
1337
+ cwd: agenthudHomeDir()
1338
+ }
1339
+ );
1207
1340
  let cacheStream = null;
1208
- cacheStream = createWriteStream(cached, { encoding: "utf-8" });
1209
- cacheStream.on("error", (err) => {
1210
- process.stderr.write(
1211
- `agenthud: warning: cannot write cache (${err.message})
1341
+ if (opts.cachePath) {
1342
+ cacheStream = createWriteStream(opts.cachePath, { encoding: "utf-8" });
1343
+ cacheStream.on("error", (err) => {
1344
+ process.stderr.write(
1345
+ `agenthud: warning: cannot write cache (${err.message})
1212
1346
  `
1213
- );
1214
- cacheStream = null;
1215
- });
1347
+ );
1348
+ cacheStream = null;
1349
+ });
1350
+ }
1216
1351
  let stderrBuf = "";
1352
+ let stdoutErrBuf = "";
1353
+ let lineBuf = "";
1354
+ let assembledText = "";
1355
+ let usage = null;
1217
1356
  proc.on("error", (err) => {
1218
1357
  if (err.code === "ENOENT") {
1219
1358
  process.stderr.write(
1220
1359
  "Error: claude CLI not found. Install: npm i -g @anthropic-ai/claude-code\n"
1221
1360
  );
1222
- resolve(1);
1361
+ resolve2({ code: 1, text: "", usage: null });
1223
1362
  } else {
1224
1363
  process.stderr.write(`Error: ${err.message}
1225
1364
  `);
1226
- resolve(1);
1365
+ resolve2({ code: 1, text: "", usage: null });
1227
1366
  }
1228
1367
  });
1368
+ const writeText = (text) => {
1369
+ assembledText += text;
1370
+ if (opts.streamToStdout) process.stdout.write(text);
1371
+ cacheStream?.write(text);
1372
+ };
1373
+ const handleEvent = (event) => {
1374
+ const type = event.type;
1375
+ if (type === "assistant") {
1376
+ const msg = event.message;
1377
+ for (const block of msg?.content ?? []) {
1378
+ if (block.type === "text" && typeof block.text === "string") {
1379
+ writeText(block.text);
1380
+ }
1381
+ }
1382
+ } else if (type === "result") {
1383
+ const u = event.usage;
1384
+ const cost = event.total_cost_usd;
1385
+ if (u) {
1386
+ usage = {
1387
+ inputTokens: u.input_tokens ?? 0,
1388
+ outputTokens: u.output_tokens ?? 0,
1389
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
1390
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
1391
+ costUsd: cost ?? null
1392
+ };
1393
+ }
1394
+ }
1395
+ };
1229
1396
  proc.stdout.on("data", (chunk) => {
1230
- process.stdout.write(chunk);
1231
- cacheStream?.write(chunk);
1397
+ lineBuf += chunk.toString();
1398
+ let nl = lineBuf.indexOf("\n");
1399
+ while (nl !== -1) {
1400
+ const line = lineBuf.slice(0, nl).trim();
1401
+ lineBuf = lineBuf.slice(nl + 1);
1402
+ if (line.length > 0) {
1403
+ try {
1404
+ handleEvent(JSON.parse(line));
1405
+ } catch {
1406
+ if (stdoutErrBuf.length < 1024) stdoutErrBuf += `${line}
1407
+ `;
1408
+ }
1409
+ }
1410
+ nl = lineBuf.indexOf("\n");
1411
+ }
1232
1412
  });
1233
1413
  proc.stderr.on("data", (chunk) => {
1234
1414
  stderrBuf += chunk.toString();
1235
1415
  process.stderr.write(chunk);
1236
1416
  });
1237
1417
  proc.on("close", (code) => {
1418
+ if (lineBuf.trim().length > 0) {
1419
+ try {
1420
+ handleEvent(JSON.parse(lineBuf.trim()));
1421
+ } catch {
1422
+ if (stdoutErrBuf.length < 1024) stdoutErrBuf += lineBuf;
1423
+ }
1424
+ lineBuf = "";
1425
+ }
1426
+ if (opts.streamToStdout) process.stdout.write("\n");
1238
1427
  cacheStream?.end();
1239
1428
  if (code !== 0) {
1240
- const lower = stderrBuf.toLowerCase();
1241
- if (lower.includes("not authenticated") || lower.includes("login") || lower.includes(" auth")) {
1429
+ if (opts.cachePath) {
1430
+ try {
1431
+ unlinkSync(opts.cachePath);
1432
+ } catch {
1433
+ }
1434
+ }
1435
+ const combined = (stderrBuf + stdoutErrBuf).toLowerCase();
1436
+ if (combined.includes("not logged in") || combined.includes("not authenticated") || combined.includes("please run /login") || combined.includes(" auth")) {
1242
1437
  process.stderr.write(
1243
- "\nHint: claude appears to be unauthenticated. Run: claude\n"
1438
+ "\nHint: claude appears to be unauthenticated. Run: claude /login\n"
1244
1439
  );
1245
1440
  }
1246
1441
  }
1247
- resolve(code ?? 1);
1442
+ resolve2({ code: code ?? 1, text: assembledText, usage });
1248
1443
  });
1249
- proc.stdin.end(reportMarkdown);
1444
+ proc.stdin.end(opts.stdin);
1250
1445
  });
1251
1446
  }
1447
+ async function generateDailySummary(opts) {
1448
+ ensureUserPromptFile("daily");
1449
+ const isToday = isSameLocalDay2(opts.date, opts.today);
1450
+ const cached = dailyCachePath(opts.date);
1451
+ const dateLabel = dateKey(opts.date);
1452
+ if (!isToday && !opts.force && existsSync4(cached)) {
1453
+ try {
1454
+ const content = readFileSync5(cached, "utf-8");
1455
+ if (opts.announce) {
1456
+ process.stderr.write(`agenthud: cached summary from ${cached}
1457
+ `);
1458
+ }
1459
+ if (opts.streamToStdout) {
1460
+ process.stdout.write(content);
1461
+ if (!content.endsWith("\n")) process.stdout.write("\n");
1462
+ }
1463
+ return {
1464
+ code: 0,
1465
+ markdown: content,
1466
+ fromCache: true,
1467
+ skipped: false,
1468
+ usage: null
1469
+ };
1470
+ } catch {
1471
+ }
1472
+ }
1473
+ if (opts.announce) {
1474
+ process.stderr.write(`agenthud: scanning sessions for ${dateLabel}...
1475
+ `);
1476
+ }
1477
+ const config = loadGlobalConfig();
1478
+ const tree = discoverSessions(config);
1479
+ const flatSessions = [
1480
+ ...tree.projects.flatMap((p) => p.sessions),
1481
+ ...tree.coldProjects.flatMap((p) => p.sessions)
1482
+ ];
1483
+ const reportMarkdown = generateReport(flatSessions, {
1484
+ date: opts.date,
1485
+ include: ["response", "bash", "edit", "thinking"],
1486
+ format: "markdown",
1487
+ detailLimit: 0,
1488
+ withGit: true
1489
+ });
1490
+ if (opts.announce) {
1491
+ const reportLines = reportMarkdown.split("\n");
1492
+ const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
1493
+ const activityCount = reportLines.filter(
1494
+ (l) => /^\[\d{2}:\d{2}\]/.test(l)
1495
+ ).length;
1496
+ const commitCount = reportLines.filter(
1497
+ (l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
1498
+ ).length;
1499
+ const sizeKb = (Buffer.byteLength(reportMarkdown, "utf-8") / 1024).toFixed(
1500
+ 1
1501
+ );
1502
+ process.stderr.write(
1503
+ `agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB)
1504
+ `
1505
+ );
1506
+ }
1507
+ if (opts.confirmBeforeSpawn) {
1508
+ const proceed = await opts.confirmBeforeSpawn();
1509
+ if (!proceed) {
1510
+ return {
1511
+ code: 0,
1512
+ markdown: "",
1513
+ fromCache: false,
1514
+ skipped: true,
1515
+ usage: null
1516
+ };
1517
+ }
1518
+ }
1519
+ if (opts.announce) {
1520
+ process.stderr.write(
1521
+ `agenthud: sending to claude (this may take a minute)...
1522
+
1523
+ `
1524
+ );
1525
+ }
1526
+ const prompt = resolvePrompt("daily", opts.promptOverride);
1527
+ const result = await spawnClaude({
1528
+ prompt,
1529
+ stdin: reportMarkdown,
1530
+ cachePath: cached,
1531
+ streamToStdout: opts.streamToStdout
1532
+ });
1533
+ if (opts.announce && result.code === 0) {
1534
+ process.stderr.write("\n");
1535
+ process.stderr.write(`agenthud: saved to ${cached}
1536
+ `);
1537
+ if (result.usage) {
1538
+ process.stderr.write(`agenthud: ${formatUsage(result.usage)}
1539
+ `);
1540
+ }
1541
+ }
1542
+ return {
1543
+ code: result.code,
1544
+ markdown: result.text,
1545
+ fromCache: false,
1546
+ skipped: false,
1547
+ usage: result.usage
1548
+ };
1549
+ }
1550
+ async function runSummary(options2) {
1551
+ const res = await generateDailySummary({
1552
+ date: options2.date,
1553
+ today: options2.today,
1554
+ force: options2.force,
1555
+ promptOverride: options2.prompt,
1556
+ streamToStdout: true,
1557
+ announce: true
1558
+ });
1559
+ return res.code;
1560
+ }
1561
+ async function runRangeSummary(options2) {
1562
+ ensureUserPromptFile("daily");
1563
+ ensureUserPromptFile("range");
1564
+ const dates = enumerateDates(options2.from, options2.to);
1565
+ const fromLabel = dateKey(options2.from);
1566
+ const toLabel = dateKey(options2.to);
1567
+ const rangeCache = rangeCachePath(options2.from, options2.to);
1568
+ if (shouldUseRangeCache(
1569
+ options2.force,
1570
+ dates,
1571
+ options2.today,
1572
+ existsSync4(rangeCache)
1573
+ )) {
1574
+ try {
1575
+ const content = readFileSync5(rangeCache, "utf-8");
1576
+ process.stderr.write(
1577
+ `agenthud: cached range summary from ${rangeCache}
1578
+ `
1579
+ );
1580
+ process.stdout.write(content);
1581
+ if (!content.endsWith("\n")) process.stdout.write("\n");
1582
+ return 0;
1583
+ } catch {
1584
+ }
1585
+ }
1586
+ let cachedCount = 0;
1587
+ let missingCount = 0;
1588
+ for (const d of dates) {
1589
+ const isToday = isSameLocalDay2(d, options2.today);
1590
+ if (!isToday && existsSync4(dailyCachePath(d))) cachedCount++;
1591
+ else missingCount++;
1592
+ }
1593
+ process.stderr.write(
1594
+ `agenthud: range ${fromLabel} \u2192 ${toLabel} (${dates.length} days)
1595
+ `
1596
+ );
1597
+ process.stderr.write(
1598
+ `agenthud: ${cachedCount} cached, ${missingCount} to generate
1599
+ `
1600
+ );
1601
+ const dailyMarkdowns = [];
1602
+ let skippedCount = 0;
1603
+ for (const d of dates) {
1604
+ const label = dateKey(d);
1605
+ const isToday = isSameLocalDay2(d, options2.today);
1606
+ process.stderr.write(`
1607
+ agenthud: --- ${label} ---
1608
+ `);
1609
+ const willPrompt = !options2.assumeYes && (isToday || !existsSync4(dailyCachePath(d)));
1610
+ const confirmer = willPrompt ? async () => {
1611
+ const hint = isToday ? " (today \u2014 regenerated every time)" : "";
1612
+ return ask(`Generate this summary${hint}? [Y/n] `, true);
1613
+ } : void 0;
1614
+ const res = await generateDailySummary({
1615
+ date: d,
1616
+ today: options2.today,
1617
+ force: false,
1618
+ streamToStdout: false,
1619
+ announce: true,
1620
+ confirmBeforeSpawn: confirmer
1621
+ });
1622
+ if (res.skipped) {
1623
+ process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
1624
+ `);
1625
+ skippedCount++;
1626
+ continue;
1627
+ }
1628
+ if (res.code !== 0) {
1629
+ process.stderr.write(
1630
+ `agenthud: aborted (failed to generate daily summary for ${label}).
1631
+ `
1632
+ );
1633
+ return res.code;
1634
+ }
1635
+ const text = res.markdown.trim();
1636
+ if (text.length === 0 || /^no activity found/i.test(text)) {
1637
+ process.stderr.write(`agenthud: ${label} has no activity \u2014 skipping.
1638
+ `);
1639
+ continue;
1640
+ }
1641
+ dailyMarkdowns.push({ date: d, markdown: text });
1642
+ }
1643
+ if (dailyMarkdowns.length === 0) {
1644
+ process.stderr.write("agenthud: no daily summaries to combine.\n");
1645
+ return 1;
1646
+ }
1647
+ const metaInput = dailyMarkdowns.map(({ date, markdown }) => `# ${dateKey(date)}
1648
+
1649
+ ${markdown}`).join("\n\n---\n\n");
1650
+ process.stderr.write(
1651
+ `
1652
+ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary...
1653
+ `
1654
+ );
1655
+ process.stderr.write(
1656
+ `agenthud: sending to claude (this may take a minute)...
1657
+
1658
+ `
1659
+ );
1660
+ const metaPrompt = resolvePrompt("range");
1661
+ const metaResult = await spawnClaude({
1662
+ prompt: metaPrompt,
1663
+ stdin: metaInput,
1664
+ cachePath: rangeCache,
1665
+ streamToStdout: true
1666
+ });
1667
+ if (metaResult.code !== 0) {
1668
+ return metaResult.code;
1669
+ }
1670
+ process.stderr.write("\n");
1671
+ process.stderr.write(`agenthud: saved to ${rangeCache}
1672
+ `);
1673
+ if (metaResult.usage) {
1674
+ process.stderr.write(`agenthud: ${formatUsage(metaResult.usage)}
1675
+ `);
1676
+ }
1677
+ return 0;
1678
+ }
1252
1679
 
1253
1680
  // src/ui/App.tsx
1254
- import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1255
- import { join as join5 } from "path";
1681
+ import { existsSync as existsSync5, watch } from "fs";
1256
1682
  import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
1257
1683
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
1258
1684
 
@@ -1513,7 +1939,7 @@ import { Box as Box3, Text as Text3 } from "ink";
1513
1939
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1514
1940
  var SECTIONS = [
1515
1941
  {
1516
- title: "Session tree",
1942
+ title: "Project tree",
1517
1943
  rows: [
1518
1944
  ["\u2191 \u2193 / k j", "Move selection"],
1519
1945
  ["PgUp / Ctrl+B", "Page up"],
@@ -1534,8 +1960,7 @@ var SECTIONS = [
1534
1960
  ["G", "Jump to oldest"],
1535
1961
  ["\u21B5", "Open detail view for selected activity"],
1536
1962
  ["f", "Cycle filter preset (set in config.yaml)"],
1537
- ["s", "Save activity log to ~/.agenthud/logs/"],
1538
- ["Tab", "Switch focus to session tree"]
1963
+ ["Tab", "Switch focus to project tree"]
1539
1964
  ]
1540
1965
  },
1541
1966
  {
@@ -1545,6 +1970,15 @@ var SECTIONS = [
1545
1970
  ["\u21B5 / Esc / q", "Close"]
1546
1971
  ]
1547
1972
  },
1973
+ {
1974
+ title: "Session status (by recent activity)",
1975
+ rows: [
1976
+ ["[hot]", "Updated in the last 30 minutes", "green"],
1977
+ ["[warm]", "Updated in the last hour", "yellow"],
1978
+ ["[cool]", "Updated earlier today", "cyan"],
1979
+ ["[cold]", "Last updated yesterday or earlier (collapsed)", "gray"]
1980
+ ]
1981
+ },
1548
1982
  {
1549
1983
  title: "Always available",
1550
1984
  rows: [
@@ -1565,14 +1999,17 @@ var SECTIONS = [
1565
1999
  rows: [
1566
2000
  ["~/.agenthud/config.yaml", "User settings (edit freely)"],
1567
2001
  ["~/.agenthud/state.yaml", "Hidden items (app-managed)"],
1568
- ["~/.agenthud/summary-prompt.md", "LLM prompt template"],
1569
- ["~/.agenthud/summaries/", "Cached daily summaries"]
2002
+ ["~/.agenthud/summary-prompt.md", "Daily summary prompt template"],
2003
+ ["~/.agenthud/summary-range-prompt.md", "Range summary prompt template"],
2004
+ ["~/.agenthud/summaries/", "Cached daily and range summaries"]
1570
2005
  ]
1571
2006
  }
1572
2007
  ];
1573
2008
  function HelpPanel({
1574
2009
  width,
1575
- height
2010
+ height,
2011
+ scrollOffset = 0,
2012
+ onTotalLinesChange
1576
2013
  }) {
1577
2014
  const allKeys = SECTIONS.flatMap((s) => s.rows.map((r) => r[0]));
1578
2015
  const keyColumn = Math.min(
@@ -1594,21 +2031,35 @@ function HelpPanel({
1594
2031
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: SECTIONS[s].title }, `title-${s}`)
1595
2032
  );
1596
2033
  for (let r = 0; r < SECTIONS[s].rows.length; r++) {
1597
- const [key, desc] = SECTIONS[s].rows[r];
2034
+ const row = SECTIONS[s].rows[r];
2035
+ const [key, desc] = row;
2036
+ const explicitColor = row.length === 3 ? row[2] : void 0;
1598
2037
  const isCli = key.trim().startsWith("agenthud");
1599
2038
  const isFile = key.includes("~/.agenthud");
2039
+ const color = explicitColor ?? (isCli ? "cyan" : isFile ? "green" : void 0);
1600
2040
  lines.push(
1601
2041
  /* @__PURE__ */ jsxs3(Text3, { children: [
1602
2042
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " }),
1603
- /* @__PURE__ */ jsx3(Text3, { color: isCli ? "cyan" : isFile ? "green" : void 0, children: padTo(key, keyColumn) }),
2043
+ /* @__PURE__ */ jsx3(Text3, { color, children: padTo(key, keyColumn) }),
1604
2044
  /* @__PURE__ */ jsx3(Text3, { children: " " }),
1605
2045
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: desc })
1606
2046
  ] }, `row-${s}-${r}`)
1607
2047
  );
1608
2048
  }
1609
2049
  }
1610
- const visible = lines.slice(0, height);
1611
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", width, children: visible });
2050
+ const indicatorReserved = lines.length > height ? 1 : 0;
2051
+ const viewport = Math.max(1, height - indicatorReserved);
2052
+ const maxOffset = Math.max(0, lines.length - viewport);
2053
+ const offset = Math.max(0, Math.min(scrollOffset, maxOffset));
2054
+ if (onTotalLinesChange) onTotalLinesChange(lines.length);
2055
+ const visible = lines.slice(offset, offset + viewport);
2056
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
2057
+ visible,
2058
+ indicatorReserved > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2059
+ `-- ${offset + viewport} / ${lines.length} `,
2060
+ offset < maxOffset ? "(\u2193 / j / PgDn / Space for more) --" : "(top: g \xB7 \u2191 / k to scroll back) --"
2061
+ ] })
2062
+ ] });
1612
2063
  }
1613
2064
 
1614
2065
  // src/ui/hooks/useHotkeys.ts
@@ -1625,7 +2076,6 @@ function useHotkeys({
1625
2076
  onScrollHalfPageDown,
1626
2077
  onScrollTop,
1627
2078
  onScrollBottom,
1628
- onSaveLog,
1629
2079
  onRefresh,
1630
2080
  onQuit,
1631
2081
  onEnter,
@@ -1635,6 +2085,8 @@ function useHotkeys({
1635
2085
  onDetailScrollDown,
1636
2086
  onFilter,
1637
2087
  onHelp,
2088
+ onHelpScroll,
2089
+ onHelpScrollToTop,
1638
2090
  filterLabel
1639
2091
  }) {
1640
2092
  const handleInput = (input, key) => {
@@ -1643,6 +2095,32 @@ function useHotkeys({
1643
2095
  onHelp();
1644
2096
  return;
1645
2097
  }
2098
+ if (onHelpScroll) {
2099
+ if (key.downArrow || input === "j" || input === " ") {
2100
+ onHelpScroll(1);
2101
+ return;
2102
+ }
2103
+ if (key.upArrow || input === "k") {
2104
+ onHelpScroll(-1);
2105
+ return;
2106
+ }
2107
+ if (key.pageDown || key.ctrl && input === "f") {
2108
+ onHelpScroll(10);
2109
+ return;
2110
+ }
2111
+ if (key.pageUp || key.ctrl && input === "b") {
2112
+ onHelpScroll(-10);
2113
+ return;
2114
+ }
2115
+ if (input === "G") {
2116
+ onHelpScroll(Number.MAX_SAFE_INTEGER);
2117
+ return;
2118
+ }
2119
+ }
2120
+ if (input === "g" && onHelpScrollToTop) {
2121
+ onHelpScrollToTop();
2122
+ return;
2123
+ }
1646
2124
  return;
1647
2125
  }
1648
2126
  if (input === "?") {
@@ -1741,13 +2219,9 @@ function useHotkeys({
1741
2219
  onScrollBottom();
1742
2220
  return;
1743
2221
  }
1744
- if (input === "s") {
1745
- onSaveLog();
1746
- return;
1747
- }
1748
2222
  }
1749
2223
  };
1750
- const statusBarItems = helpMode ? ["\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
2224
+ 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" ? [
1751
2225
  "Tab: viewer",
1752
2226
  "\u2191\u2193/jk: select",
1753
2227
  "PgUp/Dn: page",
@@ -1757,7 +2231,7 @@ function useHotkeys({
1757
2231
  "?: help",
1758
2232
  "q: quit"
1759
2233
  ] : [
1760
- "Tab: sessions",
2234
+ "Tab: projects",
1761
2235
  "\u2191\u2193/jk: scroll",
1762
2236
  "PgUp/Dn: page",
1763
2237
  "g: live",
@@ -2052,7 +2526,7 @@ function SessionTreePanel({
2052
2526
  }) {
2053
2527
  const innerWidth = getInnerWidth(width);
2054
2528
  const contentWidth = innerWidth - 1;
2055
- const titleLine = createTitleLine("Sessions", "", width);
2529
+ const titleLine = createTitleLine("Projects", "", width);
2056
2530
  const bottomLine = createBottomLine(width);
2057
2531
  const totalProjectCount = projects.length + coldProjects.length;
2058
2532
  if (totalProjectCount === 0) {
@@ -2159,7 +2633,8 @@ function subSummarySentinel(parentId) {
2159
2633
  status: "cold",
2160
2634
  modelName: null,
2161
2635
  subAgents: [],
2162
- nonInteractive: false
2636
+ nonInteractive: false,
2637
+ firstUserPrompt: null
2163
2638
  };
2164
2639
  }
2165
2640
  function appendSubAgentRows(result, session, expandedIds) {
@@ -2197,7 +2672,8 @@ function flattenSessions2(tree, expandedIds) {
2197
2672
  status: project.hotness,
2198
2673
  modelName: null,
2199
2674
  subAgents: [],
2200
- nonInteractive: false
2675
+ nonInteractive: false,
2676
+ firstUserPrompt: null
2201
2677
  });
2202
2678
  const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
2203
2679
  if (shouldShowSessions) {
@@ -2221,7 +2697,8 @@ function flattenSessions2(tree, expandedIds) {
2221
2697
  status: "cold",
2222
2698
  modelName: null,
2223
2699
  subAgents: [],
2224
- nonInteractive: false
2700
+ nonInteractive: false,
2701
+ firstUserPrompt: null
2225
2702
  });
2226
2703
  if (expandedIds.has("__cold__")) {
2227
2704
  for (const project of tree.coldProjects) {
@@ -2273,6 +2750,8 @@ function App({ mode }) {
2273
2750
  const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
2274
2751
  const [filterIndex, setFilterIndex] = useState2(0);
2275
2752
  const [helpMode, setHelpMode] = useState2(false);
2753
+ const [helpScroll, setHelpScroll] = useState2(0);
2754
+ const helpTotalLinesRef = useRef(0);
2276
2755
  const allFlat = useMemo(
2277
2756
  () => flattenSessions2(sessionTree, expandedIds),
2278
2757
  [sessionTree, expandedIds]
@@ -2434,29 +2913,22 @@ function App({ mode }) {
2434
2913
  const naturalTreeRows = allFlat.length;
2435
2914
  const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
2436
2915
  const viewerRows = Math.max(5, height - 7 - treeRows);
2437
- const saveLog = useCallback(() => {
2438
- if (!activities.length || !selectedId) return;
2439
- ensureLogDir(config.logDir);
2440
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2441
- const filePath = join5(
2442
- config.logDir,
2443
- `${date}-${selectedId.slice(0, 8)}.txt`
2444
- );
2445
- const lines = activities.map(
2446
- (a) => `[${a.timestamp.toISOString()}] ${a.icon} ${a.label} ${a.detail}`
2447
- );
2448
- try {
2449
- writeFileSync2(filePath, `${lines.join("\n")}
2450
- `, "utf-8");
2451
- } catch {
2452
- }
2453
- }, [activities, selectedId, config.logDir]);
2454
2916
  const spinner = useSpinner(isWatchMode);
2917
+ const helpViewportRows = Math.max(1, height - 3);
2918
+ const helpScrollStep = (delta) => {
2919
+ const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
2920
+ setHelpScroll((s) => Math.max(0, Math.min(max, s + delta)));
2921
+ };
2455
2922
  const { handleInput, statusBarItems } = useHotkeys({
2456
2923
  focus,
2457
2924
  detailMode,
2458
2925
  helpMode,
2459
- onHelp: () => setHelpMode((m) => !m),
2926
+ onHelp: () => {
2927
+ setHelpScroll(0);
2928
+ setHelpMode((m) => !m);
2929
+ },
2930
+ onHelpScroll: helpScrollStep,
2931
+ onHelpScrollToTop: () => setHelpScroll(0),
2460
2932
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
2461
2933
  onScrollUp: () => {
2462
2934
  if (focus === "tree") {
@@ -2711,7 +3183,6 @@ function App({ mode }) {
2711
3183
  }
2712
3184
  }
2713
3185
  },
2714
- onSaveLog: saveLog,
2715
3186
  onRefresh: refresh,
2716
3187
  onQuit: exit,
2717
3188
  onFilter: () => setFilterIndex((i) => (i + 1) % filterPresets.length),
@@ -2729,17 +3200,48 @@ function App({ mode }) {
2729
3200
  }
2730
3201
  }
2731
3202
  const isPlaceholderSelected = !selectedSession || selectedId === "__cold__" || !!selectedId && selectedId.startsWith("__sub-") && selectedId.endsWith("__");
2732
- const sessionDisplayName = isPlaceholderSelected ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
3203
+ const sessionDisplayName = isPlaceholderSelected || !selectedSession ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
3204
+ const MIN_WIDTH = 80;
3205
+ const MIN_HEIGHT = 20;
3206
+ if (isWatchMode && (width < MIN_WIDTH || height + 1 < MIN_HEIGHT)) {
3207
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
3208
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "AgentHUD needs a larger terminal." }),
3209
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows` }),
3210
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Current: ${width} cols \xD7 ${height + 1} rows` }),
3211
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
3212
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Resize the window and AgentHUD will redraw automatically." }),
3213
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Press q to quit." })
3214
+ ] });
3215
+ }
2733
3216
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
2734
- isWatchMode && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", width, children: [
2735
- /* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
2736
- spinner,
2737
- " AgentHUD v",
2738
- getVersion()
2739
- ] }),
2740
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
2741
- ] }),
2742
- helpMode ? /* @__PURE__ */ jsx5(HelpPanel, { width, height: height - 2 }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
3217
+ isWatchMode && (() => {
3218
+ const branding = `${spinner} AgentHUD v${getVersion()}`;
3219
+ const sep = " \xB7 ";
3220
+ let items = statusBarItems;
3221
+ let shortcuts = items.join(sep);
3222
+ let showBranding = true;
3223
+ const fits = () => (showBranding ? getDisplayWidth(branding) + 1 : 0) + getDisplayWidth(shortcuts) <= width;
3224
+ if (!fits()) showBranding = false;
3225
+ while (!fits() && items.length > 1) {
3226
+ items = items.slice(1);
3227
+ shortcuts = items.join(sep);
3228
+ }
3229
+ return /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", width, children: [
3230
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: showBranding ? branding : "" }),
3231
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: shortcuts })
3232
+ ] });
3233
+ })(),
3234
+ helpMode ? /* @__PURE__ */ jsx5(
3235
+ HelpPanel,
3236
+ {
3237
+ width,
3238
+ height: height - 2,
3239
+ scrollOffset: helpScroll,
3240
+ onTotalLinesChange: (n) => {
3241
+ helpTotalLinesRef.current = n;
3242
+ }
3243
+ }
3244
+ ) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2743
3245
  migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
2744
3246
  /* @__PURE__ */ jsx5(
2745
3247
  SessionTreePanel,
@@ -2798,13 +3300,13 @@ if (options.command === "version") {
2798
3300
  process.exit(0);
2799
3301
  }
2800
3302
  var legacyConfig = join6(process.cwd(), ".agenthud", "config.yaml");
2801
- if (existsSync6(legacyConfig)) {
3303
+ if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync6(legacyConfig)) {
2802
3304
  console.log(
2803
3305
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
2804
3306
  );
2805
3307
  console.log("Settings have moved to ~/.agenthud/config.yaml.");
2806
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2807
- await new Promise((resolve) => {
3308
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3309
+ await new Promise((resolve2) => {
2808
3310
  rl.question("Delete the old config file and continue? [y/N] ", (answer) => {
2809
3311
  rl.close();
2810
3312
  if (answer.trim().toLowerCase() === "y") {
@@ -2814,7 +3316,7 @@ if (existsSync6(legacyConfig)) {
2814
3316
  console.log("Aborted.");
2815
3317
  process.exit(0);
2816
3318
  }
2817
- resolve();
3319
+ resolve2();
2818
3320
  });
2819
3321
  });
2820
3322
  }
@@ -2826,7 +3328,11 @@ if (options.mode === "report") {
2826
3328
  }
2827
3329
  const config = loadGlobalConfig();
2828
3330
  const tree = discoverSessions(config);
2829
- const markdown = generateReport(tree.sessions, {
3331
+ const flatSessions = [
3332
+ ...tree.projects.flatMap((p) => p.sessions),
3333
+ ...tree.coldProjects.flatMap((p) => p.sessions)
3334
+ ];
3335
+ const markdown = generateReport(flatSessions, {
2830
3336
  date: options.reportDate,
2831
3337
  include: options.reportInclude,
2832
3338
  format: options.reportFormat,
@@ -2843,11 +3349,22 @@ if (options.mode === "summary") {
2843
3349
  `);
2844
3350
  process.exit(1);
2845
3351
  }
3352
+ const today = /* @__PURE__ */ new Date();
3353
+ if (options.summaryFrom && options.summaryTo) {
3354
+ const exitCode2 = await runRangeSummary({
3355
+ from: options.summaryFrom,
3356
+ to: options.summaryTo,
3357
+ today,
3358
+ force: options.summaryForce ?? false,
3359
+ assumeYes: options.summaryAssumeYes ?? false
3360
+ });
3361
+ process.exit(exitCode2);
3362
+ }
2846
3363
  const exitCode = await runSummary({
2847
3364
  date: options.summaryDate,
2848
3365
  prompt: options.summaryPrompt,
2849
3366
  force: options.summaryForce ?? false,
2850
- today: /* @__PURE__ */ new Date()
3367
+ today
2851
3368
  });
2852
3369
  process.exit(exitCode);
2853
3370
  }