agenthud 0.9.1 → 0.9.3

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
  }
@@ -241,21 +348,65 @@ function parseArgs(args) {
241
348
  return { mode: "watch" };
242
349
  }
243
350
 
351
+ // src/utils/altScreen.ts
352
+ var ENTER = "\x1B[?1049h";
353
+ var LEAVE = "\x1B[?1049l";
354
+ var entered = false;
355
+ var left = false;
356
+ function enterAltScreen() {
357
+ if (entered) return;
358
+ entered = true;
359
+ process.stdout.write(ENTER);
360
+ }
361
+ function leaveAltScreen() {
362
+ if (left || !entered) return;
363
+ left = true;
364
+ process.stdout.write(LEAVE);
365
+ }
366
+ var hooksInstalled = false;
367
+ function installAltScreenCleanup() {
368
+ if (hooksInstalled) return;
369
+ hooksInstalled = true;
370
+ process.on("exit", () => {
371
+ leaveAltScreen();
372
+ });
373
+ process.on("SIGINT", () => {
374
+ leaveAltScreen();
375
+ process.exit(130);
376
+ });
377
+ process.on("SIGTERM", () => {
378
+ leaveAltScreen();
379
+ process.exit(143);
380
+ });
381
+ process.on("uncaughtException", (err) => {
382
+ leaveAltScreen();
383
+ setImmediate(() => {
384
+ throw err;
385
+ });
386
+ });
387
+ }
388
+
244
389
  // src/config/globalConfig.ts
245
390
  import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
246
391
  import { homedir } from "os";
247
- import { join as join2 } from "path";
392
+ import { join as join3 } from "path";
248
393
  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");
394
+ var CONFIG_PATH = join3(homedir(), ".agenthud", "config.yaml");
395
+ var STATE_PATH = join3(homedir(), ".agenthud", "state.yaml");
251
396
  var DEFAULT_GLOBAL_CONFIG = {
252
397
  refreshIntervalMs: 2e3,
253
- logDir: join2(homedir(), ".agenthud", "logs"),
254
398
  hiddenSessions: [],
255
399
  hiddenSubAgents: [],
256
- filterPresets: [[], ["response"], ["commit"]],
400
+ // [] means "show all"; conversation preset bundles assistant + user;
401
+ // commits-only preset filters down to git activity.
402
+ filterPresets: [[], ["response", "user"], ["commit"]],
257
403
  hiddenProjects: []
258
404
  };
405
+ var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
406
+ function normalizePreset(tokens) {
407
+ if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
408
+ return tokens;
409
+ }
259
410
  function parseInterval(value) {
260
411
  const match = value.match(/^(\d+)(s|m)$/);
261
412
  if (!match) return null;
@@ -263,7 +414,7 @@ function parseInterval(value) {
263
414
  return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
264
415
  }
265
416
  function ensureAgenthudDir() {
266
- const dir = join2(homedir(), ".agenthud");
417
+ const dir = join3(homedir(), ".agenthud");
267
418
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
268
419
  }
269
420
  function writeDefaultConfig() {
@@ -274,14 +425,12 @@ function writeDefaultConfig() {
274
425
  # How often to poll for activity updates
275
426
  refreshInterval: 2s
276
427
 
277
- # Where 's' key saves activity logs
278
- logDir: ~/.agenthud/logs
279
-
280
428
  # Activity filter presets (cycle with 'f' key in viewer)
281
- # Each list is one preset; [] means "all". First preset is the default.
429
+ # Each list is one preset. Use "all" (or "*") to show everything.
430
+ # Types: response, user, bash, edit, thinking, read, glob, commit
282
431
  filterPresets:
283
- - []
284
- - ["response"]
432
+ - ["all"]
433
+ - ["response", "user"]
285
434
  - ["commit"]
286
435
  `;
287
436
  try {
@@ -326,13 +475,13 @@ function loadGlobalConfig() {
326
475
  const ms = parseInterval(configRaw.refreshInterval);
327
476
  if (ms !== null) config.refreshIntervalMs = ms;
328
477
  }
329
- if (typeof configRaw.logDir === "string") {
330
- config.logDir = configRaw.logDir.replace(/^~/, homedir());
331
- }
332
478
  if (Array.isArray(configRaw.filterPresets)) {
333
- const presets = configRaw.filterPresets.filter(Array.isArray).map(
334
- (p) => p.filter((t) => typeof t === "string")
335
- );
479
+ const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
480
+ const tokens = p.filter(
481
+ (t) => typeof t === "string"
482
+ );
483
+ return normalizePreset(tokens);
484
+ });
336
485
  if (presets.length > 0) config.filterPresets = presets;
337
486
  }
338
487
  const legacyHidden = {};
@@ -432,13 +581,10 @@ function hideProject(name) {
432
581
  if (config.hiddenProjects.includes(name)) return;
433
582
  updateState({ hiddenProjects: [...config.hiddenProjects, name] });
434
583
  }
435
- function ensureLogDir(logDir) {
436
- if (!existsSync(logDir)) {
437
- mkdirSync(logDir, { recursive: true });
438
- }
439
- }
440
584
  function hasProjectLevelConfig() {
441
- return existsSync(join2(process.cwd(), ".agenthud", "config.yaml"));
585
+ const candidate = join3(process.cwd(), ".agenthud", "config.yaml");
586
+ if (candidate === join3(homedir(), ".agenthud", "config.yaml")) return false;
587
+ return existsSync(candidate);
442
588
  }
443
589
 
444
590
  // src/data/reportGenerator.ts
@@ -475,7 +621,7 @@ function getCommitDetail(projectPath, hash) {
475
621
  if (!projectPath) return null;
476
622
  try {
477
623
  return execSync(
478
- `git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
624
+ `git --git-dir="${projectPath}/.git" show --stat --patch --no-color ${hash}`,
479
625
  { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
480
626
  ).trim();
481
627
  } catch {
@@ -786,16 +932,18 @@ import {
786
932
  createWriteStream,
787
933
  existsSync as existsSync4,
788
934
  mkdirSync as mkdirSync2,
789
- readFileSync as readFileSync5
935
+ readFileSync as readFileSync5,
936
+ unlinkSync
790
937
  } from "fs";
791
938
  import { homedir as homedir3 } from "os";
792
- import { dirname as dirname2, join as join4 } from "path";
939
+ import { dirname as dirname2, join as join5 } from "path";
940
+ import { createInterface } from "readline";
793
941
  import { fileURLToPath as fileURLToPath2 } from "url";
794
942
 
795
943
  // src/data/sessions.ts
796
944
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
797
945
  import { homedir as homedir2 } from "os";
798
- import { basename as basename2, join as join3 } from "path";
946
+ import { basename as basename2, join as join4 } from "path";
799
947
 
800
948
  // src/ui/constants.ts
801
949
  import stringWidth from "string-width";
@@ -832,6 +980,21 @@ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
832
980
  return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
833
981
  }
834
982
  var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
983
+ function truncateByWidth(text, maxWidth) {
984
+ if (maxWidth <= 0) return "";
985
+ if (getDisplayWidth(text) <= maxWidth) return text;
986
+ if (maxWidth === 1) return "\u2026";
987
+ const ellipsisWidth = 1;
988
+ let acc = "";
989
+ let used = 0;
990
+ for (const ch of text) {
991
+ const w = getDisplayWidth(ch);
992
+ if (used + w + ellipsisWidth > maxWidth) break;
993
+ acc += ch;
994
+ used += w;
995
+ }
996
+ return `${acc}\u2026`;
997
+ }
835
998
  var widthCache = /* @__PURE__ */ new Map();
836
999
  function getDisplayWidth(s) {
837
1000
  const cached = widthCache.get(s);
@@ -843,7 +1006,7 @@ function getDisplayWidth(s) {
843
1006
 
844
1007
  // src/data/sessions.ts
845
1008
  function getProjectsDir() {
846
- return process.env.CLAUDE_PROJECTS_DIR ?? join3(homedir2(), ".claude", "projects");
1009
+ return process.env.CLAUDE_PROJECTS_DIR ?? join4(homedir2(), ".claude", "projects");
847
1010
  }
848
1011
  function decodeProjectPath(encoded) {
849
1012
  const windowsDriveMatch = encoded.match(/^([A-Za-z])--(.*)$/);
@@ -866,13 +1029,19 @@ function getSessionStatus(mtimeMs) {
866
1029
  }
867
1030
  return "cold";
868
1031
  }
1032
+ var MAX_TITLE_LEN = 300;
1033
+ function capWithEllipsis(s, max = MAX_TITLE_LEN) {
1034
+ const trimmed = s.trim();
1035
+ if (trimmed.length <= max) return trimmed;
1036
+ return `${trimmed.slice(0, max - 1)}\u2026`;
1037
+ }
869
1038
  function extractTaskDescription(content) {
870
1039
  const headerMatch = content.match(/##\s*(Task\s+\d+[:\s].+)/m);
871
- if (headerMatch) return headerMatch[1].trim().slice(0, 60);
1040
+ if (headerMatch) return capWithEllipsis(headerMatch[1]);
872
1041
  const thisTaskMatch = content.match(/\*\*This Task[^:]+:\*\*\s*(.+)/);
873
- if (thisTaskMatch) return thisTaskMatch[1].trim().slice(0, 60);
1042
+ if (thisTaskMatch) return capWithEllipsis(thisTaskMatch[1]);
874
1043
  const firstLine = content.split("\n").find((l) => l.trim());
875
- return (firstLine ?? "").trim().slice(0, 60);
1044
+ return capWithEllipsis(firstLine ?? "");
876
1045
  }
877
1046
  function readSubAgentInfo(filePath) {
878
1047
  if (!existsSync3(filePath)) return { agentId: null, taskDescription: null };
@@ -955,8 +1124,7 @@ function readFirstUserPrompt(filePath) {
955
1124
  if (!text || isSystemNoise(text)) continue;
956
1125
  const firstLine = text.split("\n").find((l) => l.trim()) ?? "";
957
1126
  if (!firstLine || isSystemNoise(firstLine)) continue;
958
- const trimmed = firstLine.trim();
959
- return trimmed.length > 80 ? trimmed.slice(0, 80) : trimmed;
1127
+ return capWithEllipsis(firstLine);
960
1128
  }
961
1129
  return null;
962
1130
  }
@@ -972,7 +1140,7 @@ function readEntrypoint(filePath) {
972
1140
  }
973
1141
  }
974
1142
  function buildSubAgents(parentId, projectDir, config, projectName) {
975
- const subagentsDir = join3(projectDir, parentId, "subagents");
1143
+ const subagentsDir = join4(projectDir, parentId, "subagents");
976
1144
  if (!existsSync3(subagentsDir)) return [];
977
1145
  let files;
978
1146
  try {
@@ -985,7 +1153,7 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
985
1153
  return files.map((file) => {
986
1154
  const id = file.replace(/\.jsonl$/, "");
987
1155
  const hideKey = `${projectName}/${id}`;
988
- const filePath = join3(subagentsDir, file);
1156
+ const filePath = join4(subagentsDir, file);
989
1157
  try {
990
1158
  const stat = statSync2(filePath);
991
1159
  const { agentId, taskDescription } = readSubAgentInfo(filePath);
@@ -1025,7 +1193,7 @@ function discoverSessions(config) {
1025
1193
  try {
1026
1194
  projectDirs = readdirSync(projectsDir).filter((entry) => {
1027
1195
  try {
1028
- return statSync2(join3(projectsDir, entry)).isDirectory();
1196
+ return statSync2(join4(projectsDir, entry)).isDirectory();
1029
1197
  } catch {
1030
1198
  return false;
1031
1199
  }
@@ -1040,7 +1208,7 @@ function discoverSessions(config) {
1040
1208
  }
1041
1209
  const allSessions = [];
1042
1210
  for (const encodedDir of projectDirs) {
1043
- const projectDir = join3(projectsDir, encodedDir);
1211
+ const projectDir = join4(projectsDir, encodedDir);
1044
1212
  const decodedPath = decodeProjectPath(encodedDir);
1045
1213
  const projectName = basename2(decodedPath);
1046
1214
  let files;
@@ -1054,7 +1222,7 @@ function discoverSessions(config) {
1054
1222
  for (const file of files) {
1055
1223
  const id = file.replace(/\.jsonl$/, "");
1056
1224
  const hideKey = `${projectName}/${id}`;
1057
- const filePath = join3(projectDir, file);
1225
+ const filePath = join4(projectDir, file);
1058
1226
  try {
1059
1227
  const stat = statSync2(filePath);
1060
1228
  const subAgents = buildSubAgents(id, projectDir, config, projectName);
@@ -1126,44 +1294,50 @@ function discoverSessions(config) {
1126
1294
 
1127
1295
  // src/data/summaryRunner.ts
1128
1296
  function agenthudHomeDir() {
1129
- const dir = join4(homedir3(), ".agenthud");
1297
+ const dir = join5(homedir3(), ".agenthud");
1130
1298
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1131
1299
  return dir;
1132
1300
  }
1133
1301
  function summariesDir() {
1134
- const dir = join4(agenthudHomeDir(), "summaries");
1302
+ const dir = join5(agenthudHomeDir(), "summaries");
1135
1303
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1136
1304
  return dir;
1137
1305
  }
1138
- function userPromptPath() {
1139
- return join4(homedir3(), ".agenthud", "summary-prompt.md");
1306
+ function promptFilename(kind) {
1307
+ return kind === "daily" ? "summary-prompt.md" : "summary-range-prompt.md";
1308
+ }
1309
+ function userPromptPath(kind) {
1310
+ return join5(homedir3(), ".agenthud", promptFilename(kind));
1140
1311
  }
1141
- function templatePath() {
1312
+ function templatePath(kind) {
1142
1313
  const here = dirname2(fileURLToPath2(import.meta.url));
1143
- return join4(here, "templates", "summary-prompt.md");
1314
+ return join5(here, "templates", promptFilename(kind));
1144
1315
  }
1145
1316
  function dateKey(d) {
1146
1317
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1147
1318
  }
1148
- function cachePath(date) {
1149
- return join4(summariesDir(), `${dateKey(date)}.md`);
1319
+ function dailyCachePath(date) {
1320
+ return join5(summariesDir(), `${dateKey(date)}.md`);
1321
+ }
1322
+ function rangeCachePath(from, to) {
1323
+ return join5(summariesDir(), `range-${dateKey(from)}_${dateKey(to)}.md`);
1150
1324
  }
1151
1325
  function isSameLocalDay2(a, b) {
1152
1326
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1153
1327
  }
1154
- function ensureUserPromptFile() {
1155
- const p = userPromptPath();
1328
+ function ensureUserPromptFile(kind) {
1329
+ const p = userPromptPath(kind);
1156
1330
  if (existsSync4(p)) return;
1157
1331
  const dir = dirname2(p);
1158
1332
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1159
1333
  try {
1160
- copyFileSync(templatePath(), p);
1334
+ copyFileSync(templatePath(kind), p);
1161
1335
  } catch {
1162
1336
  }
1163
1337
  }
1164
- function resolvePrompt(override) {
1338
+ function resolvePrompt(kind, override) {
1165
1339
  if (override) return override;
1166
- const p = userPromptPath();
1340
+ const p = userPromptPath(kind);
1167
1341
  if (existsSync4(p)) {
1168
1342
  try {
1169
1343
  return readFileSync5(p, "utf-8");
@@ -1171,90 +1345,411 @@ function resolvePrompt(override) {
1171
1345
  }
1172
1346
  }
1173
1347
  try {
1174
- return readFileSync5(templatePath(), "utf-8");
1348
+ return readFileSync5(templatePath(kind), "utf-8");
1175
1349
  } catch {
1176
- return "Summarize the activity log below.";
1350
+ return "Summarize the input below.";
1177
1351
  }
1178
1352
  }
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
- }
1353
+ function shouldUseRangeCache(force, dates, today, cacheExists) {
1354
+ if (force) return false;
1355
+ if (!cacheExists) return false;
1356
+ if (dates.some((d) => isSameLocalDay2(d, today))) return false;
1357
+ return true;
1358
+ }
1359
+ function enumerateDates(from, to) {
1360
+ const dates = [];
1361
+ const cursor = new Date(from);
1362
+ while (cursor.getTime() <= to.getTime()) {
1363
+ dates.push(new Date(cursor));
1364
+ cursor.setDate(cursor.getDate() + 1);
1191
1365
  }
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()
1366
+ return dates;
1367
+ }
1368
+ async function ask(question, defaultYes = false) {
1369
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1370
+ return new Promise((resolve2) => {
1371
+ rl.question(question, (answer) => {
1372
+ rl.close();
1373
+ const trimmed = answer.trim();
1374
+ if (trimmed.length === 0) return resolve2(defaultYes);
1375
+ if (/^y(es)?$/i.test(trimmed)) return resolve2(true);
1376
+ if (/^n(o)?$/i.test(trimmed)) return resolve2(false);
1377
+ resolve2(defaultYes);
1206
1378
  });
1379
+ });
1380
+ }
1381
+ function formatUsage(u) {
1382
+ const fmt = (n) => n.toLocaleString("en-US");
1383
+ const parts = [`${fmt(u.inputTokens)} in / ${fmt(u.outputTokens)} out`];
1384
+ const cacheParts = [];
1385
+ if (u.cacheReadTokens > 0) cacheParts.push(`${fmt(u.cacheReadTokens)} read`);
1386
+ if (u.cacheCreationTokens > 0)
1387
+ cacheParts.push(`${fmt(u.cacheCreationTokens)} written`);
1388
+ if (cacheParts.length > 0) parts.push(`cache: ${cacheParts.join(", ")}`);
1389
+ if (u.costUsd != null) parts.push(`$${u.costUsd.toFixed(4)}`);
1390
+ return parts.join(" \xB7 ");
1391
+ }
1392
+ function spawnClaude(opts) {
1393
+ return new Promise((resolve2) => {
1394
+ const proc = spawn(
1395
+ "claude",
1396
+ [
1397
+ "-p",
1398
+ "--no-session-persistence",
1399
+ "--output-format",
1400
+ "stream-json",
1401
+ "--verbose",
1402
+ opts.prompt
1403
+ ],
1404
+ {
1405
+ stdio: ["pipe", "pipe", "pipe"],
1406
+ cwd: agenthudHomeDir()
1407
+ }
1408
+ );
1207
1409
  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})
1410
+ if (opts.cachePath) {
1411
+ cacheStream = createWriteStream(opts.cachePath, { encoding: "utf-8" });
1412
+ cacheStream.on("error", (err) => {
1413
+ process.stderr.write(
1414
+ `agenthud: warning: cannot write cache (${err.message})
1212
1415
  `
1213
- );
1214
- cacheStream = null;
1215
- });
1416
+ );
1417
+ cacheStream = null;
1418
+ });
1419
+ }
1216
1420
  let stderrBuf = "";
1421
+ let stdoutErrBuf = "";
1422
+ let lineBuf = "";
1423
+ let assembledText = "";
1424
+ let usage = null;
1217
1425
  proc.on("error", (err) => {
1218
1426
  if (err.code === "ENOENT") {
1219
1427
  process.stderr.write(
1220
1428
  "Error: claude CLI not found. Install: npm i -g @anthropic-ai/claude-code\n"
1221
1429
  );
1222
- resolve(1);
1430
+ resolve2({ code: 1, text: "", usage: null });
1223
1431
  } else {
1224
1432
  process.stderr.write(`Error: ${err.message}
1225
1433
  `);
1226
- resolve(1);
1434
+ resolve2({ code: 1, text: "", usage: null });
1227
1435
  }
1228
1436
  });
1437
+ const writeText = (text) => {
1438
+ assembledText += text;
1439
+ if (opts.streamToStdout) process.stdout.write(text);
1440
+ cacheStream?.write(text);
1441
+ };
1442
+ const handleEvent = (event) => {
1443
+ const type = event.type;
1444
+ if (type === "assistant") {
1445
+ const msg = event.message;
1446
+ for (const block of msg?.content ?? []) {
1447
+ if (block.type === "text" && typeof block.text === "string") {
1448
+ writeText(block.text);
1449
+ }
1450
+ }
1451
+ } else if (type === "result") {
1452
+ const u = event.usage;
1453
+ const cost = event.total_cost_usd;
1454
+ if (u) {
1455
+ usage = {
1456
+ inputTokens: u.input_tokens ?? 0,
1457
+ outputTokens: u.output_tokens ?? 0,
1458
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
1459
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
1460
+ costUsd: cost ?? null
1461
+ };
1462
+ }
1463
+ }
1464
+ };
1229
1465
  proc.stdout.on("data", (chunk) => {
1230
- process.stdout.write(chunk);
1231
- cacheStream?.write(chunk);
1466
+ lineBuf += chunk.toString();
1467
+ let nl = lineBuf.indexOf("\n");
1468
+ while (nl !== -1) {
1469
+ const line = lineBuf.slice(0, nl).trim();
1470
+ lineBuf = lineBuf.slice(nl + 1);
1471
+ if (line.length > 0) {
1472
+ try {
1473
+ handleEvent(JSON.parse(line));
1474
+ } catch {
1475
+ if (stdoutErrBuf.length < 1024) stdoutErrBuf += `${line}
1476
+ `;
1477
+ }
1478
+ }
1479
+ nl = lineBuf.indexOf("\n");
1480
+ }
1232
1481
  });
1233
1482
  proc.stderr.on("data", (chunk) => {
1234
1483
  stderrBuf += chunk.toString();
1235
1484
  process.stderr.write(chunk);
1236
1485
  });
1237
1486
  proc.on("close", (code) => {
1487
+ if (lineBuf.trim().length > 0) {
1488
+ try {
1489
+ handleEvent(JSON.parse(lineBuf.trim()));
1490
+ } catch {
1491
+ if (stdoutErrBuf.length < 1024) stdoutErrBuf += lineBuf;
1492
+ }
1493
+ lineBuf = "";
1494
+ }
1495
+ if (opts.streamToStdout) process.stdout.write("\n");
1238
1496
  cacheStream?.end();
1239
1497
  if (code !== 0) {
1240
- const lower = stderrBuf.toLowerCase();
1241
- if (lower.includes("not authenticated") || lower.includes("login") || lower.includes(" auth")) {
1498
+ if (opts.cachePath) {
1499
+ try {
1500
+ unlinkSync(opts.cachePath);
1501
+ } catch {
1502
+ }
1503
+ }
1504
+ const combined = (stderrBuf + stdoutErrBuf).toLowerCase();
1505
+ if (combined.includes("not logged in") || combined.includes("not authenticated") || combined.includes("please run /login") || combined.includes(" auth")) {
1242
1506
  process.stderr.write(
1243
- "\nHint: claude appears to be unauthenticated. Run: claude\n"
1507
+ "\nHint: claude appears to be unauthenticated. Run: claude /login\n"
1244
1508
  );
1245
1509
  }
1246
1510
  }
1247
- resolve(code ?? 1);
1511
+ resolve2({ code: code ?? 1, text: assembledText, usage });
1512
+ });
1513
+ proc.stdin.end(opts.stdin);
1514
+ });
1515
+ }
1516
+ async function generateDailySummary(opts) {
1517
+ ensureUserPromptFile("daily");
1518
+ const isToday = isSameLocalDay2(opts.date, opts.today);
1519
+ const cached = dailyCachePath(opts.date);
1520
+ const dateLabel = dateKey(opts.date);
1521
+ if (!isToday && !opts.force && existsSync4(cached)) {
1522
+ try {
1523
+ const content = readFileSync5(cached, "utf-8");
1524
+ if (opts.announce) {
1525
+ process.stderr.write(`agenthud: cached summary from ${cached}
1526
+ `);
1527
+ }
1528
+ if (opts.streamToStdout) {
1529
+ process.stdout.write(content);
1530
+ if (!content.endsWith("\n")) process.stdout.write("\n");
1531
+ }
1532
+ return {
1533
+ code: 0,
1534
+ markdown: content,
1535
+ fromCache: true,
1536
+ skipped: false,
1537
+ usage: null
1538
+ };
1539
+ } catch {
1540
+ }
1541
+ }
1542
+ if (opts.announce) {
1543
+ process.stderr.write(`agenthud: scanning sessions for ${dateLabel}...
1544
+ `);
1545
+ }
1546
+ const config = loadGlobalConfig();
1547
+ const tree = discoverSessions(config);
1548
+ const flatSessions = [
1549
+ ...tree.projects.flatMap((p) => p.sessions),
1550
+ ...tree.coldProjects.flatMap((p) => p.sessions)
1551
+ ];
1552
+ const reportMarkdown = generateReport(flatSessions, {
1553
+ date: opts.date,
1554
+ include: ["response", "bash", "edit", "thinking"],
1555
+ format: "markdown",
1556
+ detailLimit: 0,
1557
+ withGit: true
1558
+ });
1559
+ if (opts.announce) {
1560
+ const reportLines = reportMarkdown.split("\n");
1561
+ const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
1562
+ const activityCount = reportLines.filter(
1563
+ (l) => /^\[\d{2}:\d{2}\]/.test(l)
1564
+ ).length;
1565
+ const commitCount = reportLines.filter(
1566
+ (l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
1567
+ ).length;
1568
+ const sizeKb = (Buffer.byteLength(reportMarkdown, "utf-8") / 1024).toFixed(
1569
+ 1
1570
+ );
1571
+ process.stderr.write(
1572
+ `agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB)
1573
+ `
1574
+ );
1575
+ }
1576
+ if (opts.confirmBeforeSpawn) {
1577
+ const proceed = await opts.confirmBeforeSpawn();
1578
+ if (!proceed) {
1579
+ return {
1580
+ code: 0,
1581
+ markdown: "",
1582
+ fromCache: false,
1583
+ skipped: true,
1584
+ usage: null
1585
+ };
1586
+ }
1587
+ }
1588
+ if (opts.announce) {
1589
+ process.stderr.write(
1590
+ `agenthud: sending to claude (this may take a minute)...
1591
+
1592
+ `
1593
+ );
1594
+ }
1595
+ const prompt = resolvePrompt("daily", opts.promptOverride);
1596
+ const result = await spawnClaude({
1597
+ prompt,
1598
+ stdin: reportMarkdown,
1599
+ cachePath: cached,
1600
+ streamToStdout: opts.streamToStdout
1601
+ });
1602
+ if (opts.announce && result.code === 0) {
1603
+ process.stderr.write("\n");
1604
+ process.stderr.write(`agenthud: saved to ${cached}
1605
+ `);
1606
+ if (result.usage) {
1607
+ process.stderr.write(`agenthud: ${formatUsage(result.usage)}
1608
+ `);
1609
+ }
1610
+ }
1611
+ return {
1612
+ code: result.code,
1613
+ markdown: result.text,
1614
+ fromCache: false,
1615
+ skipped: false,
1616
+ usage: result.usage
1617
+ };
1618
+ }
1619
+ async function runSummary(options2) {
1620
+ const res = await generateDailySummary({
1621
+ date: options2.date,
1622
+ today: options2.today,
1623
+ force: options2.force,
1624
+ promptOverride: options2.prompt,
1625
+ streamToStdout: true,
1626
+ announce: true
1627
+ });
1628
+ return res.code;
1629
+ }
1630
+ async function runRangeSummary(options2) {
1631
+ ensureUserPromptFile("daily");
1632
+ ensureUserPromptFile("range");
1633
+ const dates = enumerateDates(options2.from, options2.to);
1634
+ const fromLabel = dateKey(options2.from);
1635
+ const toLabel = dateKey(options2.to);
1636
+ const rangeCache = rangeCachePath(options2.from, options2.to);
1637
+ if (shouldUseRangeCache(
1638
+ options2.force,
1639
+ dates,
1640
+ options2.today,
1641
+ existsSync4(rangeCache)
1642
+ )) {
1643
+ try {
1644
+ const content = readFileSync5(rangeCache, "utf-8");
1645
+ process.stderr.write(
1646
+ `agenthud: cached range summary from ${rangeCache}
1647
+ `
1648
+ );
1649
+ process.stdout.write(content);
1650
+ if (!content.endsWith("\n")) process.stdout.write("\n");
1651
+ return 0;
1652
+ } catch {
1653
+ }
1654
+ }
1655
+ let cachedCount = 0;
1656
+ let missingCount = 0;
1657
+ for (const d of dates) {
1658
+ const isToday = isSameLocalDay2(d, options2.today);
1659
+ if (!isToday && existsSync4(dailyCachePath(d))) cachedCount++;
1660
+ else missingCount++;
1661
+ }
1662
+ process.stderr.write(
1663
+ `agenthud: range ${fromLabel} \u2192 ${toLabel} (${dates.length} days)
1664
+ `
1665
+ );
1666
+ process.stderr.write(
1667
+ `agenthud: ${cachedCount} cached, ${missingCount} to generate
1668
+ `
1669
+ );
1670
+ const dailyMarkdowns = [];
1671
+ let skippedCount = 0;
1672
+ for (const d of dates) {
1673
+ const label = dateKey(d);
1674
+ const isToday = isSameLocalDay2(d, options2.today);
1675
+ process.stderr.write(`
1676
+ agenthud: --- ${label} ---
1677
+ `);
1678
+ const willPrompt = !options2.assumeYes && (isToday || !existsSync4(dailyCachePath(d)));
1679
+ const confirmer = willPrompt ? async () => {
1680
+ const hint = isToday ? " (today \u2014 regenerated every time)" : "";
1681
+ return ask(`Generate this summary${hint}? [Y/n] `, true);
1682
+ } : void 0;
1683
+ const res = await generateDailySummary({
1684
+ date: d,
1685
+ today: options2.today,
1686
+ force: false,
1687
+ streamToStdout: false,
1688
+ announce: true,
1689
+ confirmBeforeSpawn: confirmer
1248
1690
  });
1249
- proc.stdin.end(reportMarkdown);
1691
+ if (res.skipped) {
1692
+ process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
1693
+ `);
1694
+ skippedCount++;
1695
+ continue;
1696
+ }
1697
+ if (res.code !== 0) {
1698
+ process.stderr.write(
1699
+ `agenthud: aborted (failed to generate daily summary for ${label}).
1700
+ `
1701
+ );
1702
+ return res.code;
1703
+ }
1704
+ const text = res.markdown.trim();
1705
+ if (text.length === 0 || /^no activity found/i.test(text)) {
1706
+ process.stderr.write(`agenthud: ${label} has no activity \u2014 skipping.
1707
+ `);
1708
+ continue;
1709
+ }
1710
+ dailyMarkdowns.push({ date: d, markdown: text });
1711
+ }
1712
+ if (dailyMarkdowns.length === 0) {
1713
+ process.stderr.write("agenthud: no daily summaries to combine.\n");
1714
+ return 1;
1715
+ }
1716
+ const metaInput = dailyMarkdowns.map(({ date, markdown }) => `# ${dateKey(date)}
1717
+
1718
+ ${markdown}`).join("\n\n---\n\n");
1719
+ process.stderr.write(
1720
+ `
1721
+ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary...
1722
+ `
1723
+ );
1724
+ process.stderr.write(
1725
+ `agenthud: sending to claude (this may take a minute)...
1726
+
1727
+ `
1728
+ );
1729
+ const metaPrompt = resolvePrompt("range");
1730
+ const metaResult = await spawnClaude({
1731
+ prompt: metaPrompt,
1732
+ stdin: metaInput,
1733
+ cachePath: rangeCache,
1734
+ streamToStdout: true
1250
1735
  });
1736
+ if (metaResult.code !== 0) {
1737
+ return metaResult.code;
1738
+ }
1739
+ process.stderr.write("\n");
1740
+ process.stderr.write(`agenthud: saved to ${rangeCache}
1741
+ `);
1742
+ if (metaResult.usage) {
1743
+ process.stderr.write(`agenthud: ${formatUsage(metaResult.usage)}
1744
+ `);
1745
+ }
1746
+ return 0;
1251
1747
  }
1252
1748
 
1253
1749
  // src/ui/App.tsx
1254
- import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1255
- import { join as join5 } from "path";
1750
+ import { existsSync as existsSync5, watch } from "fs";
1256
1751
  import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
1257
- import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
1752
+ import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
1258
1753
 
1259
1754
  // src/ui/ActivityViewerPanel.tsx
1260
1755
  import { Box, Text } from "ink";
@@ -1314,6 +1809,8 @@ function ActivityViewerPanel({
1314
1809
  isLive,
1315
1810
  newCount,
1316
1811
  visibleRows,
1812
+ trailingBlankRows = 0,
1813
+ liveIndicatorPosition = null,
1317
1814
  width,
1318
1815
  cursorLine,
1319
1816
  hasFocus,
@@ -1327,18 +1824,18 @@ function ActivityViewerPanel({
1327
1824
  if (isLive) {
1328
1825
  titleSuffix = `[LIVE ${spinner || "\u25BC"}${filterSuffix}]`;
1329
1826
  } else {
1330
- const badge = newCount > 0 ? ` +${newCount}\u2191` : "";
1331
- titleSuffix = `[PAUSED \u2193${scrollOffset}${badge}${filterSuffix}]`;
1827
+ const badge = newCount > 0 ? ` +${newCount}\u2193` : "";
1828
+ titleSuffix = `[PAUSED \u2191${scrollOffset}${badge}${filterSuffix}]`;
1332
1829
  }
1333
1830
  let visibleActivities;
1334
1831
  if (activities.length === 0) {
1335
1832
  visibleActivities = [];
1336
1833
  } else if (isLive) {
1337
- visibleActivities = activities.slice(-visibleRows).reverse();
1834
+ visibleActivities = activities.slice(-visibleRows);
1338
1835
  } else {
1339
1836
  const end = Math.max(0, activities.length - scrollOffset);
1340
1837
  const start = Math.max(0, end - visibleRows);
1341
- visibleActivities = activities.slice(start, end).reverse();
1838
+ visibleActivities = activities.slice(start, end);
1342
1839
  }
1343
1840
  const now = /* @__PURE__ */ new Date();
1344
1841
  const lines = [];
@@ -1356,10 +1853,11 @@ function ActivityViewerPanel({
1356
1853
  );
1357
1854
  } else {
1358
1855
  const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
1856
+ const cursorIndexInSlice = visibleActivities.length - 1 - effectiveCursor;
1359
1857
  for (let i = 0; i < visibleActivities.length; i++) {
1360
1858
  const activity = visibleActivities[i];
1361
1859
  const style = getActivityStyle(activity);
1362
- const isCursor = hasFocus && i === effectiveCursor;
1860
+ const isCursor = hasFocus && i === cursorIndexInSlice;
1363
1861
  const time = formatActivityTime(activity.timestamp, now);
1364
1862
  const timestamp = `[${time}] `;
1365
1863
  const timestampWidth = timestamp.length;
@@ -1411,28 +1909,110 @@ function ActivityViewerPanel({
1411
1909
  }
1412
1910
  }
1413
1911
  const emptyRow = `${BOX.v}${" ".repeat(contentWidth + 1)}${BOX.v}`;
1414
- while (lines.length < visibleRows) {
1415
- lines.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${lines.length}`));
1912
+ const padCount = Math.max(0, visibleRows - lines.length);
1913
+ const padded = [];
1914
+ for (let i = 0; i < padCount; i++) {
1915
+ padded.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${i}`));
1916
+ }
1917
+ const hasContent = visibleActivities.length > 0;
1918
+ const trailing = [];
1919
+ for (let i = 0; i < trailingBlankRows; i++) {
1920
+ if (i === 0 && isLive && liveIndicatorPosition != null && hasContent) {
1921
+ const pos = Math.max(0, liveIndicatorPosition);
1922
+ const arrow = "\u203A";
1923
+ const safePos = Math.min(pos, Math.max(0, contentWidth - 1));
1924
+ const padAfter = Math.max(0, contentWidth - safePos - 1);
1925
+ trailing.push(
1926
+ /* @__PURE__ */ jsxs(Text, { children: [
1927
+ BOX.v,
1928
+ " ",
1929
+ " ".repeat(safePos),
1930
+ /* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: arrow }),
1931
+ " ".repeat(padAfter),
1932
+ BOX.v
1933
+ ] }, `trail-${i}`)
1934
+ );
1935
+ } else {
1936
+ trailing.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `trail-${i}`));
1937
+ }
1416
1938
  }
1939
+ const finalLines = [...padded, ...lines, ...trailing];
1417
1940
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
1418
1941
  /* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
1419
- lines,
1942
+ finalLines,
1420
1943
  /* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
1421
1944
  ] });
1422
1945
  }
1423
1946
 
1424
1947
  // src/ui/DetailViewPanel.tsx
1425
1948
  import { Box as Box2, Text as Text2 } from "ink";
1949
+
1950
+ // src/ui/lineColoring.ts
1951
+ var DIFF_META_PREFIXES = [
1952
+ "diff --git",
1953
+ "index ",
1954
+ "commit ",
1955
+ "Author:",
1956
+ "Date:",
1957
+ "Merge:"
1958
+ ];
1959
+ function classifyDiffLines(lines) {
1960
+ return lines.map((line) => {
1961
+ if (line.startsWith("+++") || line.startsWith("---")) return "diff-meta";
1962
+ if (DIFF_META_PREFIXES.some((p) => line.startsWith(p))) return "diff-meta";
1963
+ if (line.startsWith("@@")) return "diff-hunk";
1964
+ if (line.startsWith("+")) return "diff-add";
1965
+ if (line.startsWith("-")) return "diff-remove";
1966
+ return "prose";
1967
+ });
1968
+ }
1969
+ function classifyCodeFences(lines) {
1970
+ const out = [];
1971
+ let inCode = false;
1972
+ for (const line of lines) {
1973
+ if (/^\s*```/.test(line)) {
1974
+ out.push("code-fence");
1975
+ inCode = !inCode;
1976
+ } else {
1977
+ out.push(inCode ? "code" : "prose");
1978
+ }
1979
+ }
1980
+ return out;
1981
+ }
1982
+ function getLineStyle(category) {
1983
+ switch (category) {
1984
+ case "diff-add":
1985
+ return { color: "green" };
1986
+ case "diff-remove":
1987
+ return { color: "red" };
1988
+ case "diff-hunk":
1989
+ return { color: "cyan" };
1990
+ case "diff-meta":
1991
+ return { dimColor: true };
1992
+ case "code-fence":
1993
+ return { color: "cyan", dimColor: true };
1994
+ case "code":
1995
+ return { color: "cyan" };
1996
+ case "prose":
1997
+ return {};
1998
+ }
1999
+ }
2000
+
2001
+ // src/ui/DetailViewPanel.tsx
1426
2002
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1427
- function wrapText(text, maxWidth) {
1428
- if (!text) return ["(empty)"];
1429
- const result = [];
1430
- for (const rawLine of text.split("\n")) {
1431
- if (!rawLine) {
1432
- result.push("");
2003
+ function wrapClassified(text, maxWidth, classifier) {
2004
+ if (!text) return [{ text: "(empty)", category: "prose" }];
2005
+ const sourceLines = text.split("\n");
2006
+ const categories = classifier(sourceLines);
2007
+ const out = [];
2008
+ for (let i = 0; i < sourceLines.length; i++) {
2009
+ const line = sourceLines[i];
2010
+ const cat = categories[i] ?? "prose";
2011
+ if (!line) {
2012
+ out.push({ text: "", category: cat });
1433
2013
  continue;
1434
2014
  }
1435
- const words = rawLine.split(" ");
2015
+ const words = line.split(" ");
1436
2016
  let current = "";
1437
2017
  for (const word of words) {
1438
2018
  if (!current) {
@@ -1440,13 +2020,13 @@ function wrapText(text, maxWidth) {
1440
2020
  } else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
1441
2021
  current += ` ${word}`;
1442
2022
  } else {
1443
- result.push(current);
2023
+ out.push({ text: current, category: cat });
1444
2024
  current = word;
1445
2025
  }
1446
2026
  }
1447
- if (current) result.push(current);
2027
+ if (current) out.push({ text: current, category: cat });
1448
2028
  }
1449
- return result.length > 0 ? result : ["(empty)"];
2029
+ return out.length > 0 ? out : [{ text: "(empty)", category: "prose" }];
1450
2030
  }
1451
2031
  function DetailViewPanel({
1452
2032
  activity,
@@ -1456,7 +2036,8 @@ function DetailViewPanel({
1456
2036
  }) {
1457
2037
  const innerWidth = getInnerWidth(width);
1458
2038
  const contentWidth = innerWidth - 1;
1459
- const allLines = wrapText(activity.detail, contentWidth);
2039
+ const classifier = activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
2040
+ const allLines = wrapClassified(activity.detail, contentWidth, classifier);
1460
2041
  const totalLines = allLines.length;
1461
2042
  const clampedOffset = Math.min(
1462
2043
  scrollOffset,
@@ -1480,13 +2061,14 @@ function DetailViewPanel({
1480
2061
  const titleRight = `${dashes}${scrollPart}${BOX.tr}`;
1481
2062
  const contentRows = [];
1482
2063
  for (let i = 0; i < visibleRows; i++) {
1483
- const line = visibleSlice[i] ?? "";
1484
- const padding = Math.max(0, contentWidth - getDisplayWidth(line));
2064
+ const entry = visibleSlice[i] ?? { text: "", category: "prose" };
2065
+ const padding = Math.max(0, contentWidth - getDisplayWidth(entry.text));
2066
+ const lineStyle = getLineStyle(entry.category);
1485
2067
  contentRows.push(
1486
2068
  /* @__PURE__ */ jsxs2(Text2, { children: [
1487
2069
  BOX.v,
1488
2070
  " ",
1489
- line,
2071
+ /* @__PURE__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: entry.text }),
1490
2072
  " ".repeat(padding),
1491
2073
  BOX.v
1492
2074
  ] }, i)
@@ -1513,7 +2095,7 @@ import { Box as Box3, Text as Text3 } from "ink";
1513
2095
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1514
2096
  var SECTIONS = [
1515
2097
  {
1516
- title: "Session tree",
2098
+ title: "Project tree",
1517
2099
  rows: [
1518
2100
  ["\u2191 \u2193 / k j", "Move selection"],
1519
2101
  ["PgUp / Ctrl+B", "Page up"],
@@ -1530,12 +2112,11 @@ var SECTIONS = [
1530
2112
  ["\u2191 \u2193 / k j", "Scroll one line"],
1531
2113
  ["PgUp/Dn, Ctrl+B/F", "Scroll one page"],
1532
2114
  ["Ctrl+U / Ctrl+D", "Scroll half page"],
1533
- ["g", "Jump to live (newest)"],
1534
- ["G", "Jump to oldest"],
2115
+ ["g", "Jump to top (oldest)"],
2116
+ ["G", "Jump to live (newest, bottom)"],
1535
2117
  ["\u21B5", "Open detail view for selected activity"],
1536
2118
  ["f", "Cycle filter preset (set in config.yaml)"],
1537
- ["s", "Save activity log to ~/.agenthud/logs/"],
1538
- ["Tab", "Switch focus to session tree"]
2119
+ ["Tab", "Switch focus to project tree"]
1539
2120
  ]
1540
2121
  },
1541
2122
  {
@@ -1545,6 +2126,15 @@ var SECTIONS = [
1545
2126
  ["\u21B5 / Esc / q", "Close"]
1546
2127
  ]
1547
2128
  },
2129
+ {
2130
+ title: "Session status (by recent activity)",
2131
+ rows: [
2132
+ ["[hot]", "Updated in the last 30 minutes", "green"],
2133
+ ["[warm]", "Updated in the last hour", "yellow"],
2134
+ ["[cool]", "Updated earlier today", "cyan"],
2135
+ ["[cold]", "Last updated yesterday or earlier (collapsed)", "gray"]
2136
+ ]
2137
+ },
1548
2138
  {
1549
2139
  title: "Always available",
1550
2140
  rows: [
@@ -1565,14 +2155,17 @@ var SECTIONS = [
1565
2155
  rows: [
1566
2156
  ["~/.agenthud/config.yaml", "User settings (edit freely)"],
1567
2157
  ["~/.agenthud/state.yaml", "Hidden items (app-managed)"],
1568
- ["~/.agenthud/summary-prompt.md", "LLM prompt template"],
1569
- ["~/.agenthud/summaries/", "Cached daily summaries"]
2158
+ ["~/.agenthud/summary-prompt.md", "Daily summary prompt template"],
2159
+ ["~/.agenthud/summary-range-prompt.md", "Range summary prompt template"],
2160
+ ["~/.agenthud/summaries/", "Cached daily and range summaries"]
1570
2161
  ]
1571
2162
  }
1572
2163
  ];
1573
2164
  function HelpPanel({
1574
2165
  width,
1575
- height
2166
+ height,
2167
+ scrollOffset = 0,
2168
+ onTotalLinesChange
1576
2169
  }) {
1577
2170
  const allKeys = SECTIONS.flatMap((s) => s.rows.map((r) => r[0]));
1578
2171
  const keyColumn = Math.min(
@@ -1594,21 +2187,35 @@ function HelpPanel({
1594
2187
  /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: SECTIONS[s].title }, `title-${s}`)
1595
2188
  );
1596
2189
  for (let r = 0; r < SECTIONS[s].rows.length; r++) {
1597
- const [key, desc] = SECTIONS[s].rows[r];
2190
+ const row = SECTIONS[s].rows[r];
2191
+ const [key, desc] = row;
2192
+ const explicitColor = row.length === 3 ? row[2] : void 0;
1598
2193
  const isCli = key.trim().startsWith("agenthud");
1599
2194
  const isFile = key.includes("~/.agenthud");
2195
+ const color = explicitColor ?? (isCli ? "cyan" : isFile ? "green" : void 0);
1600
2196
  lines.push(
1601
2197
  /* @__PURE__ */ jsxs3(Text3, { children: [
1602
2198
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " }),
1603
- /* @__PURE__ */ jsx3(Text3, { color: isCli ? "cyan" : isFile ? "green" : void 0, children: padTo(key, keyColumn) }),
2199
+ /* @__PURE__ */ jsx3(Text3, { color, children: padTo(key, keyColumn) }),
1604
2200
  /* @__PURE__ */ jsx3(Text3, { children: " " }),
1605
2201
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: desc })
1606
2202
  ] }, `row-${s}-${r}`)
1607
2203
  );
1608
2204
  }
1609
2205
  }
1610
- const visible = lines.slice(0, height);
1611
- return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", width, children: visible });
2206
+ const indicatorReserved = lines.length > height ? 1 : 0;
2207
+ const viewport = Math.max(1, height - indicatorReserved);
2208
+ const maxOffset = Math.max(0, lines.length - viewport);
2209
+ const offset = Math.max(0, Math.min(scrollOffset, maxOffset));
2210
+ if (onTotalLinesChange) onTotalLinesChange(lines.length);
2211
+ const visible = lines.slice(offset, offset + viewport);
2212
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
2213
+ visible,
2214
+ indicatorReserved > 0 && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2215
+ `-- ${offset + viewport} / ${lines.length} `,
2216
+ offset < maxOffset ? "(\u2193 / j / PgDn / Space for more) --" : "(top: g \xB7 \u2191 / k to scroll back) --"
2217
+ ] })
2218
+ ] });
1612
2219
  }
1613
2220
 
1614
2221
  // src/ui/hooks/useHotkeys.ts
@@ -1625,7 +2232,6 @@ function useHotkeys({
1625
2232
  onScrollHalfPageDown,
1626
2233
  onScrollTop,
1627
2234
  onScrollBottom,
1628
- onSaveLog,
1629
2235
  onRefresh,
1630
2236
  onQuit,
1631
2237
  onEnter,
@@ -1635,6 +2241,8 @@ function useHotkeys({
1635
2241
  onDetailScrollDown,
1636
2242
  onFilter,
1637
2243
  onHelp,
2244
+ onHelpScroll,
2245
+ onHelpScrollToTop,
1638
2246
  filterLabel
1639
2247
  }) {
1640
2248
  const handleInput = (input, key) => {
@@ -1643,6 +2251,32 @@ function useHotkeys({
1643
2251
  onHelp();
1644
2252
  return;
1645
2253
  }
2254
+ if (onHelpScroll) {
2255
+ if (key.downArrow || input === "j" || input === " ") {
2256
+ onHelpScroll(1);
2257
+ return;
2258
+ }
2259
+ if (key.upArrow || input === "k") {
2260
+ onHelpScroll(-1);
2261
+ return;
2262
+ }
2263
+ if (key.pageDown || key.ctrl && input === "f") {
2264
+ onHelpScroll(10);
2265
+ return;
2266
+ }
2267
+ if (key.pageUp || key.ctrl && input === "b") {
2268
+ onHelpScroll(-10);
2269
+ return;
2270
+ }
2271
+ if (input === "G") {
2272
+ onHelpScroll(Number.MAX_SAFE_INTEGER);
2273
+ return;
2274
+ }
2275
+ }
2276
+ if (input === "g" && onHelpScrollToTop) {
2277
+ onHelpScrollToTop();
2278
+ return;
2279
+ }
1646
2280
  return;
1647
2281
  }
1648
2282
  if (input === "?") {
@@ -1741,13 +2375,9 @@ function useHotkeys({
1741
2375
  onScrollBottom();
1742
2376
  return;
1743
2377
  }
1744
- if (input === "s") {
1745
- onSaveLog();
1746
- return;
1747
- }
1748
2378
  }
1749
2379
  };
1750
- const statusBarItems = helpMode ? ["\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
2380
+ 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
2381
  "Tab: viewer",
1752
2382
  "\u2191\u2193/jk: select",
1753
2383
  "PgUp/Dn: page",
@@ -1757,11 +2387,11 @@ function useHotkeys({
1757
2387
  "?: help",
1758
2388
  "q: quit"
1759
2389
  ] : [
1760
- "Tab: sessions",
2390
+ "Tab: projects",
1761
2391
  "\u2191\u2193/jk: scroll",
1762
2392
  "PgUp/Dn: page",
1763
- "g: live",
1764
- "G: oldest",
2393
+ "g: oldest",
2394
+ "G: live",
1765
2395
  "\u21B5: detail",
1766
2396
  `f: ${filterLabel}`,
1767
2397
  "?: help",
@@ -1770,12 +2400,29 @@ function useHotkeys({
1770
2400
  return { handleInput, statusBarItems };
1771
2401
  }
1772
2402
 
1773
- // src/ui/hooks/useSpinner.ts
2403
+ // src/ui/hooks/useSlide.ts
1774
2404
  import { useEffect, useState } from "react";
1775
- var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1776
- function useSpinner(active, intervalMs = 100) {
2405
+ function useSlide(active, positions, intervalMs = 180, resetKey) {
1777
2406
  const [index, setIndex] = useState(0);
1778
2407
  useEffect(() => {
2408
+ setIndex(0);
2409
+ }, [resetKey]);
2410
+ useEffect(() => {
2411
+ if (!active) return;
2412
+ const timer = setInterval(() => {
2413
+ setIndex((i) => (i + 1) % positions);
2414
+ }, intervalMs);
2415
+ return () => clearInterval(timer);
2416
+ }, [active, positions, intervalMs]);
2417
+ return index;
2418
+ }
2419
+
2420
+ // src/ui/hooks/useSpinner.ts
2421
+ import { useEffect as useEffect2, useState as useState2 } from "react";
2422
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2423
+ function useSpinner(active, intervalMs = 100) {
2424
+ const [index, setIndex] = useState2(0);
2425
+ useEffect2(() => {
1779
2426
  if (!active) return;
1780
2427
  const timer = setInterval(() => {
1781
2428
  setIndex((i) => (i + 1) % FRAMES.length);
@@ -1789,12 +2436,20 @@ function useSpinner(active, intervalMs = 100) {
1789
2436
  import { homedir as homedir4 } from "os";
1790
2437
  import { Box as Box4, Text as Text4 } from "ink";
1791
2438
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1792
- function formatElapsed(lastModifiedMs) {
1793
- const elapsed = Date.now() - lastModifiedMs;
2439
+ function formatElapsed(lastModifiedMs, now = Date.now()) {
2440
+ const elapsed = Math.max(0, now - lastModifiedMs);
1794
2441
  const seconds = Math.floor(elapsed / 1e3);
1795
2442
  const minutes = Math.floor(seconds / 60);
1796
2443
  const hours = Math.floor(minutes / 60);
1797
- if (hours > 0) return `${hours}h${minutes % 60}m`;
2444
+ const days = Math.floor(hours / 24);
2445
+ const weeks = Math.floor(days / 7);
2446
+ const months = Math.floor(days / 30);
2447
+ const years = Math.floor(days / 365);
2448
+ if (years >= 1) return `${years}y`;
2449
+ if (months >= 1) return `${months}mo`;
2450
+ if (weeks >= 1) return `${weeks}w`;
2451
+ if (days >= 1) return `${days}d`;
2452
+ if (hours > 0) return `${hours}h`;
1798
2453
  if (minutes > 0) return `${minutes}m`;
1799
2454
  if (seconds > 0) return `${seconds}s`;
1800
2455
  return "<1s";
@@ -1816,11 +2471,6 @@ function formatProjectPath(projectPath) {
1816
2471
  const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
1817
2472
  return raw;
1818
2473
  }
1819
- function truncatePath(path, maxWidth) {
1820
- if (getDisplayWidth(path) <= maxWidth) return path;
1821
- if (maxWidth < 4) return "";
1822
- return `...${path.slice(-(maxWidth - 3))}`;
1823
- }
1824
2474
  function SessionRow({
1825
2475
  session,
1826
2476
  isSelected,
@@ -1842,36 +2492,40 @@ function SessionRow({
1842
2492
  const leftCoreBase = `${prefix}${rawName}${shortIdDisplay} ${badge}`;
1843
2493
  const leftCoreWidth = getDisplayWidth(leftCoreBase);
1844
2494
  const rightWidth = getDisplayWidth(rightSide);
1845
- const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - 1;
2495
+ const RIGHT_GAP = 3;
2496
+ const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - RIGHT_GAP;
1846
2497
  let middleText = "";
1847
- if (middleAvailable > 3) {
2498
+ if (middleAvailable > 1) {
1848
2499
  const raw = isParent ? session.firstUserPrompt ?? "" : session.taskDescription ?? "";
1849
2500
  if (raw) {
1850
- const truncated = truncatePath(raw, middleAvailable);
2501
+ const flat = raw.replace(/[\r\n\t]+/g, " ").trim();
2502
+ const truncated = truncateByWidth(flat, middleAvailable);
1851
2503
  if (truncated) middleText = truncated;
1852
2504
  }
1853
2505
  }
1854
2506
  const middleSection = middleText ? ` ${middleText}` : "";
1855
2507
  const gapWidth = Math.max(
1856
- 1,
2508
+ RIGHT_GAP,
1857
2509
  contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
1858
2510
  );
1859
2511
  const gap = " ".repeat(gapWidth);
1860
2512
  const fullLine = leftCoreBase + middleSection + gap + rightSide;
1861
2513
  const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
1862
- const highlight = isSelected && hasFocus;
1863
- const shouldDim = isNonInteractive;
2514
+ const focused = isSelected && hasFocus;
2515
+ const muted = isSelected && !hasFocus;
2516
+ const showBg = focused || muted;
2517
+ const shouldDim = isNonInteractive || muted;
1864
2518
  return /* @__PURE__ */ jsxs4(Text4, { children: [
1865
2519
  BOX.v,
1866
2520
  " ",
1867
2521
  /* @__PURE__ */ jsxs4(
1868
2522
  Text4,
1869
2523
  {
1870
- backgroundColor: highlight ? "blue" : void 0,
1871
- bold: highlight,
1872
- dimColor: shouldDim && !highlight,
2524
+ backgroundColor: showBg ? "blue" : void 0,
2525
+ bold: focused,
2526
+ dimColor: shouldDim && !focused,
1873
2527
  children: [
1874
- /* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !highlight, children: prefix }),
2528
+ /* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !focused, children: prefix }),
1875
2529
  /* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
1876
2530
  shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
1877
2531
  /* @__PURE__ */ jsx4(Text4, { children: " " }),
@@ -1968,23 +2622,47 @@ function ProjectRow({
1968
2622
  }) {
1969
2623
  const nameText = `> ${project.name}`;
1970
2624
  const pathText = project.projectPath ? formatProjectPath(project.projectPath) : "";
2625
+ const latestMtime = project.sessions.reduce(
2626
+ (acc, s) => Math.max(acc, s.lastModifiedMs),
2627
+ 0
2628
+ );
2629
+ const elapsed = latestMtime > 0 ? formatElapsed(latestMtime) : "";
1971
2630
  const nameWidth = getDisplayWidth(nameText);
1972
2631
  const pathWidth = pathText ? getDisplayWidth(pathText) : 0;
1973
- const gapWidth = pathText ? 2 : 0;
1974
- const totalWidth = nameWidth + gapWidth + pathWidth;
2632
+ const elapsedWidth = elapsed ? getDisplayWidth(elapsed) : 0;
2633
+ const middleGap = pathText ? 2 : 0;
2634
+ const leftWidth = nameWidth + middleGap + pathWidth;
2635
+ const PROJECT_RIGHT_GAP = 3;
2636
+ const rightGap = Math.max(
2637
+ PROJECT_RIGHT_GAP,
2638
+ contentWidth - leftWidth - elapsedWidth
2639
+ );
2640
+ const totalWidth = leftWidth + rightGap + elapsedWidth;
1975
2641
  const padding = Math.max(0, contentWidth - totalWidth);
1976
- const highlight = isSelected && hasFocus;
2642
+ const focused = isSelected && hasFocus;
2643
+ const muted = isSelected && !hasFocus;
2644
+ const showBg = focused || muted;
1977
2645
  return /* @__PURE__ */ jsxs4(Text4, { children: [
1978
2646
  BOX.v,
1979
2647
  " ",
1980
- /* @__PURE__ */ jsxs4(Text4, { backgroundColor: highlight ? "blue" : void 0, bold: !highlight, children: [
1981
- nameText,
1982
- pathText ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1983
- " ",
1984
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
1985
- ] }) : null,
1986
- " ".repeat(padding)
1987
- ] }),
2648
+ /* @__PURE__ */ jsxs4(
2649
+ Text4,
2650
+ {
2651
+ backgroundColor: showBg ? "blue" : void 0,
2652
+ bold: !showBg,
2653
+ dimColor: muted,
2654
+ children: [
2655
+ nameText,
2656
+ pathText ? /* @__PURE__ */ jsxs4(Fragment, { children: [
2657
+ " ",
2658
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
2659
+ ] }) : null,
2660
+ " ".repeat(rightGap),
2661
+ elapsed ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }) : null,
2662
+ " ".repeat(padding)
2663
+ ]
2664
+ }
2665
+ ),
1988
2666
  BOX.v
1989
2667
  ] });
1990
2668
  }
@@ -2004,11 +2682,12 @@ function SubagentSummaryRow({
2004
2682
  0,
2005
2683
  contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
2006
2684
  );
2007
- const active = isSelected && hasFocus;
2685
+ const focused = isSelected && hasFocus;
2686
+ const muted = isSelected && !hasFocus;
2008
2687
  return /* @__PURE__ */ jsxs4(Text4, { children: [
2009
2688
  BOX.v,
2010
2689
  " ",
2011
- /* @__PURE__ */ jsxs4(Text4, { dimColor: !active, inverse: active, children: [
2690
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: !focused, inverse: focused || muted, children: [
2012
2691
  text,
2013
2692
  " ".repeat(padding),
2014
2693
  hint
@@ -2030,13 +2709,14 @@ function ColdProjectsSummaryRow({
2030
2709
  const dashCount = Math.max(0, innerWidth - 1 - labelWidth - hintWidth);
2031
2710
  const dashes = BOX.h.repeat(dashCount);
2032
2711
  const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
2033
- const highlight = isSelected && hasFocus;
2712
+ const focused = isSelected && hasFocus;
2713
+ const muted = isSelected && !hasFocus;
2034
2714
  return /* @__PURE__ */ jsx4(
2035
2715
  Text4,
2036
2716
  {
2037
- backgroundColor: highlight ? "blue" : void 0,
2038
- bold: highlight,
2039
- dimColor: !highlight,
2717
+ backgroundColor: focused || muted ? "blue" : void 0,
2718
+ bold: focused,
2719
+ dimColor: !focused,
2040
2720
  children: line
2041
2721
  }
2042
2722
  );
@@ -2052,7 +2732,7 @@ function SessionTreePanel({
2052
2732
  }) {
2053
2733
  const innerWidth = getInnerWidth(width);
2054
2734
  const contentWidth = innerWidth - 1;
2055
- const titleLine = createTitleLine("Sessions", "", width);
2735
+ const titleLine = createTitleLine("Projects", "", width);
2056
2736
  const bottomLine = createBottomLine(width);
2057
2737
  const totalProjectCount = projects.length + coldProjects.length;
2058
2738
  if (totalProjectCount === 0) {
@@ -2159,7 +2839,8 @@ function subSummarySentinel(parentId) {
2159
2839
  status: "cold",
2160
2840
  modelName: null,
2161
2841
  subAgents: [],
2162
- nonInteractive: false
2842
+ nonInteractive: false,
2843
+ firstUserPrompt: null
2163
2844
  };
2164
2845
  }
2165
2846
  function appendSubAgentRows(result, session, expandedIds) {
@@ -2197,7 +2878,8 @@ function flattenSessions2(tree, expandedIds) {
2197
2878
  status: project.hotness,
2198
2879
  modelName: null,
2199
2880
  subAgents: [],
2200
- nonInteractive: false
2881
+ nonInteractive: false,
2882
+ firstUserPrompt: null
2201
2883
  });
2202
2884
  const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
2203
2885
  if (shouldShowSessions) {
@@ -2221,7 +2903,8 @@ function flattenSessions2(tree, expandedIds) {
2221
2903
  status: "cold",
2222
2904
  modelName: null,
2223
2905
  subAgents: [],
2224
- nonInteractive: false
2906
+ nonInteractive: false,
2907
+ firstUserPrompt: null
2225
2908
  });
2226
2909
  if (expandedIds.has("__cold__")) {
2227
2910
  for (const project of tree.coldProjects) {
@@ -2235,14 +2918,14 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
2235
2918
  if (acts.length === 0) return null;
2236
2919
  let visible;
2237
2920
  if (live) {
2238
- visible = acts.slice(-rows).reverse();
2921
+ visible = acts.slice(-rows);
2239
2922
  } else {
2240
2923
  const end = Math.max(0, acts.length - scrollOff);
2241
2924
  const start = Math.max(0, end - rows);
2242
- visible = acts.slice(start, end).reverse();
2925
+ visible = acts.slice(start, end);
2243
2926
  }
2244
2927
  const effectiveCursor = Math.min(cursorLine, visible.length - 1);
2245
- return visible[effectiveCursor] ?? null;
2928
+ return visible[visible.length - 1 - effectiveCursor] ?? null;
2246
2929
  }
2247
2930
  function App({ mode }) {
2248
2931
  const { exit } = useApp();
@@ -2250,45 +2933,47 @@ function App({ mode }) {
2250
2933
  const isWatchMode = mode === "watch";
2251
2934
  const config = useMemo(() => loadGlobalConfig(), []);
2252
2935
  const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
2253
- const [sessionTree, setSessionTree] = useState2(
2936
+ const [sessionTree, setSessionTree] = useState3(
2254
2937
  () => discoverSessions(config)
2255
2938
  );
2256
- const [selectedId, setSelectedId] = useState2(() => {
2939
+ const [selectedId, setSelectedId] = useState3(() => {
2257
2940
  const firstProject = sessionTree.projects[0];
2258
2941
  if (firstProject) return `__proj-${firstProject.name}__`;
2259
2942
  return null;
2260
2943
  });
2261
- const [focus, setFocus] = useState2("tree");
2262
- const [scrollOffset, setScrollOffset] = useState2(0);
2263
- const [isLive, setIsLive] = useState2(true);
2264
- const [activities, setActivities] = useState2([]);
2265
- const [gitActivities, setGitActivities] = useState2([]);
2266
- const [newCount, setNewCount] = useState2(0);
2267
- const [expandedIds, setExpandedIds] = useState2(/* @__PURE__ */ new Set());
2268
- const [viewerCursorLine, setViewerCursorLine] = useState2(0);
2269
- const [detailMode, setDetailMode] = useState2(false);
2270
- const [detailActivity, setDetailActivity] = useState2(
2944
+ const [focus, setFocus] = useState3("tree");
2945
+ const [scrollOffset, setScrollOffset] = useState3(0);
2946
+ const [isLive, setIsLive] = useState3(true);
2947
+ const [activities, setActivities] = useState3([]);
2948
+ const [gitActivities, setGitActivities] = useState3([]);
2949
+ const [newCount, setNewCount] = useState3(0);
2950
+ const [expandedIds, setExpandedIds] = useState3(/* @__PURE__ */ new Set());
2951
+ const [viewerCursorLine, setViewerCursorLine] = useState3(0);
2952
+ const [detailMode, setDetailMode] = useState3(false);
2953
+ const [detailActivity, setDetailActivity] = useState3(
2271
2954
  null
2272
2955
  );
2273
- const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
2274
- const [filterIndex, setFilterIndex] = useState2(0);
2275
- const [helpMode, setHelpMode] = useState2(false);
2956
+ const [detailScrollOffset, setDetailScrollOffset] = useState3(0);
2957
+ const [filterIndex, setFilterIndex] = useState3(0);
2958
+ const [helpMode, setHelpMode] = useState3(false);
2959
+ const [helpScroll, setHelpScroll] = useState3(0);
2960
+ const helpTotalLinesRef = useRef(0);
2276
2961
  const allFlat = useMemo(
2277
2962
  () => flattenSessions2(sessionTree, expandedIds),
2278
2963
  [sessionTree, expandedIds]
2279
2964
  );
2280
2965
  const allFlatRef = useRef(allFlat);
2281
- useEffect2(() => {
2966
+ useEffect3(() => {
2282
2967
  allFlatRef.current = allFlat;
2283
2968
  }, [allFlat]);
2284
2969
  const activitiesLengthRef = useRef(0);
2285
2970
  const activitiesRef = useRef(activities);
2286
- useEffect2(() => {
2971
+ useEffect3(() => {
2287
2972
  activitiesLengthRef.current = activities.length;
2288
2973
  activitiesRef.current = activities;
2289
2974
  }, [activities]);
2290
2975
  const lastLoadedFileRef = useRef(null);
2291
- useEffect2(() => {
2976
+ useEffect3(() => {
2292
2977
  let node = allFlatRef.current.find((s) => s.id === selectedId);
2293
2978
  if (node && selectedId?.startsWith("__proj-") && selectedId.endsWith("__")) {
2294
2979
  const projectName = selectedId.slice(7, -2);
@@ -2316,12 +3001,12 @@ function App({ mode }) {
2316
3001
  if (fileChanged) setGitActivities([]);
2317
3002
  }
2318
3003
  }, [selectedId, sessionTree]);
2319
- useEffect2(() => {
3004
+ useEffect3(() => {
2320
3005
  setScrollOffset(0);
2321
3006
  setIsLive(true);
2322
3007
  setViewerCursorLine(0);
2323
3008
  }, [filterIndex]);
2324
- useEffect2(() => {
3009
+ useEffect3(() => {
2325
3010
  if (!isWatchMode) return;
2326
3011
  const node = allFlatRef.current.find((s) => s.id === selectedId);
2327
3012
  if (!node?.projectPath) return;
@@ -2376,10 +3061,10 @@ function App({ mode }) {
2376
3061
  }
2377
3062
  }, [selectedId, isLive, expandedIds]);
2378
3063
  const refreshRef = useRef(refresh);
2379
- useEffect2(() => {
3064
+ useEffect3(() => {
2380
3065
  refreshRef.current = refresh;
2381
3066
  }, [refresh]);
2382
- useEffect2(() => {
3067
+ useEffect3(() => {
2383
3068
  if (!isWatchMode) return;
2384
3069
  const projectsDir = getProjectsDir();
2385
3070
  const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
@@ -2433,36 +3118,61 @@ function App({ mode }) {
2433
3118
  const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
2434
3119
  const naturalTreeRows = allFlat.length;
2435
3120
  const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
2436
- 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]);
3121
+ const VIEWER_BREATHING_ROWS = 1;
3122
+ const viewerRows = Math.max(
3123
+ 5,
3124
+ height - 7 - treeRows - VIEWER_BREATHING_ROWS
3125
+ );
2454
3126
  const spinner = useSpinner(isWatchMode);
3127
+ const viewerIndicatorWidth = Math.max(1, width - 3);
3128
+ const liveIndicatorPosition = useSlide(
3129
+ isWatchMode,
3130
+ viewerIndicatorWidth,
3131
+ 180,
3132
+ // Reset to 0 whenever the viewer's subject changes so each new
3133
+ // session/sub-agent restarts the arrow from the left.
3134
+ selectedId
3135
+ );
3136
+ const helpViewportRows = Math.max(1, height - 3);
3137
+ const helpScrollStep = (delta) => {
3138
+ const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
3139
+ setHelpScroll((s) => Math.max(0, Math.min(max, s + delta)));
3140
+ };
2455
3141
  const { handleInput, statusBarItems } = useHotkeys({
2456
3142
  focus,
2457
3143
  detailMode,
2458
3144
  helpMode,
2459
- onHelp: () => setHelpMode((m) => !m),
3145
+ onHelp: () => {
3146
+ setHelpScroll(0);
3147
+ setHelpMode((m) => !m);
3148
+ },
3149
+ onHelpScroll: helpScrollStep,
3150
+ onHelpScrollToTop: () => setHelpScroll(0),
2460
3151
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
3152
+ // cursorLine = "entries back from the newest" (0 = newest = bottom row).
3153
+ // Up arrow moves visually upward = older direction = cursorLine++.
3154
+ // Down arrow moves visually downward = newer direction = cursorLine--.
2461
3155
  onScrollUp: () => {
2462
3156
  if (focus === "tree") {
2463
3157
  if (selectedIndex === -1) return;
2464
3158
  const prev = Math.max(0, selectedIndex - 1);
2465
3159
  setSelectedId(allFlat[prev]?.id ?? selectedId);
3160
+ } else {
3161
+ if (viewerCursorLine < viewerRows - 1) {
3162
+ setViewerCursorLine((c) => c + 1);
3163
+ } else {
3164
+ setIsLive(false);
3165
+ setScrollOffset(
3166
+ (o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
3167
+ );
3168
+ }
3169
+ }
3170
+ },
3171
+ onScrollDown: () => {
3172
+ if (focus === "tree") {
3173
+ if (selectedIndex === -1) return;
3174
+ const next = Math.min(allFlat.length - 1, selectedIndex + 1);
3175
+ setSelectedId(allFlat[next]?.id ?? selectedId);
2466
3176
  } else {
2467
3177
  if (viewerCursorLine > 0) {
2468
3178
  setViewerCursorLine((c) => c - 1);
@@ -2478,26 +3188,25 @@ function App({ mode }) {
2478
3188
  }
2479
3189
  }
2480
3190
  },
2481
- onScrollDown: () => {
3191
+ // PgUp/PgDn semantics flip to match the bottom-feed layout:
3192
+ // PgUp = visually up = older direction = scrollOffset++
3193
+ // PgDn = visually down = newer direction = scrollOffset--
3194
+ onScrollPageUp: () => {
2482
3195
  if (focus === "tree") {
2483
- if (selectedIndex === -1) return;
2484
- const next = Math.min(allFlat.length - 1, selectedIndex + 1);
2485
- setSelectedId(allFlat[next]?.id ?? selectedId);
3196
+ const prev = Math.max(0, selectedIndex - 5);
3197
+ setSelectedId(allFlat[prev]?.id ?? selectedId);
2486
3198
  } else {
2487
- if (viewerCursorLine < viewerRows - 1) {
2488
- setViewerCursorLine((c) => c + 1);
2489
- } else {
2490
- setIsLive(false);
2491
- setScrollOffset(
2492
- (o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
2493
- );
2494
- }
3199
+ setViewerCursorLine(0);
3200
+ setIsLive(false);
3201
+ setScrollOffset(
3202
+ (o) => Math.min(o + viewerRows, Math.max(0, activities.length - viewerRows))
3203
+ );
2495
3204
  }
2496
3205
  },
2497
- onScrollPageUp: () => {
3206
+ onScrollPageDown: () => {
2498
3207
  if (focus === "tree") {
2499
- const prev = Math.max(0, selectedIndex - 5);
2500
- setSelectedId(allFlat[prev]?.id ?? selectedId);
3208
+ const next = Math.min(allFlat.length - 1, selectedIndex + 5);
3209
+ setSelectedId(allFlat[next]?.id ?? selectedId);
2501
3210
  } else {
2502
3211
  setViewerCursorLine(0);
2503
3212
  setScrollOffset((o) => {
@@ -2510,22 +3219,28 @@ function App({ mode }) {
2510
3219
  });
2511
3220
  }
2512
3221
  },
2513
- onScrollPageDown: () => {
3222
+ onScrollHalfPageUp: () => {
2514
3223
  if (focus === "tree") {
2515
- const next = Math.min(allFlat.length - 1, selectedIndex + 5);
2516
- setSelectedId(allFlat[next]?.id ?? selectedId);
3224
+ const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
3225
+ setSelectedId(allFlat[prev]?.id ?? selectedId);
2517
3226
  } else {
2518
3227
  setViewerCursorLine(0);
2519
3228
  setIsLive(false);
2520
3229
  setScrollOffset(
2521
- (o) => Math.min(o + viewerRows, Math.max(0, activities.length - viewerRows))
3230
+ (o) => Math.min(
3231
+ o + Math.floor(viewerRows / 2),
3232
+ Math.max(0, activities.length - viewerRows)
3233
+ )
2522
3234
  );
2523
3235
  }
2524
3236
  },
2525
- onScrollHalfPageUp: () => {
3237
+ onScrollHalfPageDown: () => {
2526
3238
  if (focus === "tree") {
2527
- const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
2528
- setSelectedId(allFlat[prev]?.id ?? selectedId);
3239
+ const next = Math.min(
3240
+ allFlat.length - 1,
3241
+ selectedIndex + Math.ceil(5 / 2)
3242
+ );
3243
+ setSelectedId(allFlat[next]?.id ?? selectedId);
2529
3244
  } else {
2530
3245
  setViewerCursorLine(0);
2531
3246
  setScrollOffset((o) => {
@@ -2538,35 +3253,17 @@ function App({ mode }) {
2538
3253
  });
2539
3254
  }
2540
3255
  },
2541
- onScrollHalfPageDown: () => {
2542
- if (focus === "tree") {
2543
- const next = Math.min(
2544
- allFlat.length - 1,
2545
- selectedIndex + Math.ceil(5 / 2)
2546
- );
2547
- setSelectedId(allFlat[next]?.id ?? selectedId);
2548
- } else {
2549
- setViewerCursorLine(0);
2550
- setIsLive(false);
2551
- setScrollOffset(
2552
- (o) => Math.min(
2553
- o + Math.floor(viewerRows / 2),
2554
- Math.max(0, activities.length - viewerRows)
2555
- )
2556
- );
2557
- }
2558
- },
2559
3256
  onScrollTop: () => {
3257
+ setViewerCursorLine(Math.max(0, viewerRows - 1));
3258
+ setIsLive(false);
3259
+ setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
3260
+ },
3261
+ onScrollBottom: () => {
2560
3262
  setViewerCursorLine(0);
2561
3263
  setIsLive(true);
2562
3264
  setScrollOffset(0);
2563
3265
  setNewCount(0);
2564
3266
  },
2565
- onScrollBottom: () => {
2566
- setViewerCursorLine(0);
2567
- setIsLive(false);
2568
- setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
2569
- },
2570
3267
  onDetailClose: () => {
2571
3268
  setDetailMode(false);
2572
3269
  },
@@ -2658,15 +3355,23 @@ function App({ mode }) {
2658
3355
  const toggleKey = isCold ? `__expanded-session-${selectedId}` : `__collapsed-session-${selectedId}`;
2659
3356
  setExpandedIds((prev) => {
2660
3357
  const next = new Set(prev);
2661
- if (next.has(toggleKey)) {
2662
- next.delete(toggleKey);
2663
- if (!isCold) setSelectedId(selectedId);
2664
- } else {
2665
- next.add(toggleKey);
2666
- if (isCold) {
3358
+ if (isCold) {
3359
+ if (next.has(toggleKey)) {
3360
+ next.delete(toggleKey);
3361
+ next.delete(selectedId);
3362
+ } else {
3363
+ next.add(toggleKey);
2667
3364
  const firstSub = selectedSessionObj.subAgents[0];
2668
3365
  if (firstSub) setSelectedId(firstSub.id);
2669
3366
  }
3367
+ } else {
3368
+ if (next.has(toggleKey)) {
3369
+ next.delete(toggleKey);
3370
+ } else {
3371
+ next.add(toggleKey);
3372
+ next.delete(selectedId);
3373
+ setSelectedId(selectedId);
3374
+ }
2670
3375
  }
2671
3376
  return next;
2672
3377
  });
@@ -2711,7 +3416,6 @@ function App({ mode }) {
2711
3416
  }
2712
3417
  }
2713
3418
  },
2714
- onSaveLog: saveLog,
2715
3419
  onRefresh: refresh,
2716
3420
  onQuit: exit,
2717
3421
  onFilter: () => setFilterIndex((i) => (i + 1) % filterPresets.length),
@@ -2729,17 +3433,48 @@ function App({ mode }) {
2729
3433
  }
2730
3434
  }
2731
3435
  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);
3436
+ const sessionDisplayName = isPlaceholderSelected || !selectedSession ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
3437
+ const MIN_WIDTH = 80;
3438
+ const MIN_HEIGHT = 20;
3439
+ if (isWatchMode && (width < MIN_WIDTH || height + 1 < MIN_HEIGHT)) {
3440
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", width, children: [
3441
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "AgentHUD needs a larger terminal." }),
3442
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Minimum: ${MIN_WIDTH} cols \xD7 ${MIN_HEIGHT} rows` }),
3443
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: `Current: ${width} cols \xD7 ${height + 1} rows` }),
3444
+ /* @__PURE__ */ jsx5(Text5, { children: " " }),
3445
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Resize the window and AgentHUD will redraw automatically." }),
3446
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Press q to quit." })
3447
+ ] });
3448
+ }
2733
3449
  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: [
3450
+ isWatchMode && (() => {
3451
+ const branding = `${spinner} AgentHUD v${getVersion()}`;
3452
+ const sep = " \xB7 ";
3453
+ let items = statusBarItems;
3454
+ let shortcuts = items.join(sep);
3455
+ let showBranding = true;
3456
+ const fits = () => (showBranding ? getDisplayWidth(branding) + 1 : 0) + getDisplayWidth(shortcuts) <= width;
3457
+ if (!fits()) showBranding = false;
3458
+ while (!fits() && items.length > 1) {
3459
+ items = items.slice(1);
3460
+ shortcuts = items.join(sep);
3461
+ }
3462
+ return /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, justifyContent: "space-between", width, children: [
3463
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: showBranding ? branding : "" }),
3464
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: shortcuts })
3465
+ ] });
3466
+ })(),
3467
+ helpMode ? /* @__PURE__ */ jsx5(
3468
+ HelpPanel,
3469
+ {
3470
+ width,
3471
+ height: height - 2,
3472
+ scrollOffset: helpScroll,
3473
+ onTotalLinesChange: (n) => {
3474
+ helpTotalLinesRef.current = n;
3475
+ }
3476
+ }
3477
+ ) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2743
3478
  migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
2744
3479
  /* @__PURE__ */ jsx5(
2745
3480
  SessionTreePanel,
@@ -2771,6 +3506,8 @@ function App({ mode }) {
2771
3506
  isLive,
2772
3507
  newCount,
2773
3508
  visibleRows: viewerRows,
3509
+ trailingBlankRows: VIEWER_BREATHING_ROWS,
3510
+ liveIndicatorPosition,
2774
3511
  width,
2775
3512
  cursorLine: viewerCursorLine,
2776
3513
  hasFocus: focus === "viewer",
@@ -2798,13 +3535,13 @@ if (options.command === "version") {
2798
3535
  process.exit(0);
2799
3536
  }
2800
3537
  var legacyConfig = join6(process.cwd(), ".agenthud", "config.yaml");
2801
- if (existsSync6(legacyConfig)) {
3538
+ if (isLegacyProjectConfig(process.cwd(), homedir5()) && existsSync6(legacyConfig)) {
2802
3539
  console.log(
2803
3540
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
2804
3541
  );
2805
3542
  console.log("Settings have moved to ~/.agenthud/config.yaml.");
2806
- const rl = createInterface({ input: process.stdin, output: process.stdout });
2807
- await new Promise((resolve) => {
3543
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
3544
+ await new Promise((resolve2) => {
2808
3545
  rl.question("Delete the old config file and continue? [y/N] ", (answer) => {
2809
3546
  rl.close();
2810
3547
  if (answer.trim().toLowerCase() === "y") {
@@ -2814,7 +3551,7 @@ if (existsSync6(legacyConfig)) {
2814
3551
  console.log("Aborted.");
2815
3552
  process.exit(0);
2816
3553
  }
2817
- resolve();
3554
+ resolve2();
2818
3555
  });
2819
3556
  });
2820
3557
  }
@@ -2826,7 +3563,11 @@ if (options.mode === "report") {
2826
3563
  }
2827
3564
  const config = loadGlobalConfig();
2828
3565
  const tree = discoverSessions(config);
2829
- const markdown = generateReport(tree.sessions, {
3566
+ const flatSessions = [
3567
+ ...tree.projects.flatMap((p) => p.sessions),
3568
+ ...tree.coldProjects.flatMap((p) => p.sessions)
3569
+ ];
3570
+ const markdown = generateReport(flatSessions, {
2830
3571
  date: options.reportDate,
2831
3572
  include: options.reportInclude,
2832
3573
  format: options.reportFormat,
@@ -2843,15 +3584,29 @@ if (options.mode === "summary") {
2843
3584
  `);
2844
3585
  process.exit(1);
2845
3586
  }
3587
+ const today = /* @__PURE__ */ new Date();
3588
+ if (options.summaryFrom && options.summaryTo) {
3589
+ const exitCode2 = await runRangeSummary({
3590
+ from: options.summaryFrom,
3591
+ to: options.summaryTo,
3592
+ today,
3593
+ force: options.summaryForce ?? false,
3594
+ assumeYes: options.summaryAssumeYes ?? false
3595
+ });
3596
+ process.exit(exitCode2);
3597
+ }
2846
3598
  const exitCode = await runSummary({
2847
3599
  date: options.summaryDate,
2848
3600
  prompt: options.summaryPrompt,
2849
3601
  force: options.summaryForce ?? false,
2850
- today: /* @__PURE__ */ new Date()
3602
+ today
2851
3603
  });
2852
3604
  process.exit(exitCode);
2853
3605
  }
2854
3606
  if (options.mode === "watch") {
2855
- clearScreen();
3607
+ installAltScreenCleanup();
3608
+ enterAltScreen();
3609
+ } else {
3610
+ if (options.mode === "once") clearScreen();
2856
3611
  }
2857
3612
  render(React.createElement(App, { mode: options.mode }));