agenthud 0.8.4 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  // src/main.ts
2
- import { existsSync as existsSync5, rmSync } from "fs";
3
- import { join as join5 } from "path";
2
+ import { existsSync as existsSync6, rmSync } from "fs";
3
+ import { join as join6 } from "path";
4
4
  import { createInterface } from "readline";
5
5
  import { render } from "ink";
6
6
  import React from "react";
@@ -35,7 +35,8 @@ var KNOWN_REPORT_FLAGS = /* @__PURE__ */ new Set([
35
35
  "--detail-limit",
36
36
  "--with-git"
37
37
  ]);
38
- var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report"]);
38
+ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set(["--date", "--prompt", "--force"]);
39
+ var KNOWN_SUBCOMMANDS = /* @__PURE__ */ new Set(["report", "summary"]);
39
40
  function getHelp() {
40
41
  return `Usage: agenthud [options]
41
42
 
@@ -48,7 +49,7 @@ Options:
48
49
  -h, --help Show this help message
49
50
 
50
51
  Commands:
51
- report [--date DATE] [--include TYPES]
52
+ report [--date DATE] [--include TYPES] [--format FORMAT] [--detail-limit N] [--with-git]
52
53
  Print activity report for a date (default: today)
53
54
  --date YYYY-MM-DD|today Date to report on
54
55
  --include TYPES Comma-separated types or "all"
@@ -58,6 +59,12 @@ Commands:
58
59
  --detail-limit N Max chars per activity detail (default: 120, 0 = unlimited)
59
60
  --with-git Append today's git commits from cwd to report
60
61
 
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)
67
+
61
68
  Environment:
62
69
  CLAUDE_PROJECTS_DIR Path to Claude projects directory
63
70
  (default: ~/.claude/projects)
@@ -170,6 +177,53 @@ function parseArgs(args) {
170
177
  reportError
171
178
  };
172
179
  }
180
+ if (args[0] === "summary") {
181
+ const rest = args.slice(1);
182
+ let summaryDate = todayLocalMidnight();
183
+ let summaryPrompt;
184
+ let summaryForce = false;
185
+ let summaryError;
186
+ for (let i = 0; i < rest.length; i++) {
187
+ const arg = rest[i];
188
+ if (!arg.startsWith("-")) continue;
189
+ if (!KNOWN_SUMMARY_FLAGS.has(arg)) {
190
+ summaryError = `Unknown option: "${arg}". Run agenthud --help for usage.`;
191
+ break;
192
+ }
193
+ if (arg === "--date" || arg === "--prompt") i++;
194
+ }
195
+ const dateIdx = rest.indexOf("--date");
196
+ if (dateIdx !== -1) {
197
+ const dateStr = rest[dateIdx + 1];
198
+ if (!dateStr) {
199
+ summaryError = "Invalid date: missing value for --date";
200
+ } else {
201
+ const parsed = parseLocalMidnight(dateStr);
202
+ if (!parsed) {
203
+ summaryError = `Invalid date: "${dateStr}". Use YYYY-MM-DD or "today".`;
204
+ } else {
205
+ summaryDate = parsed;
206
+ }
207
+ }
208
+ }
209
+ const promptIdx = rest.indexOf("--prompt");
210
+ if (promptIdx !== -1) {
211
+ const val = rest[promptIdx + 1];
212
+ if (!val) {
213
+ summaryError = "Invalid --prompt: missing value";
214
+ } else {
215
+ summaryPrompt = val;
216
+ }
217
+ }
218
+ if (rest.includes("--force")) summaryForce = true;
219
+ return {
220
+ mode: "summary",
221
+ summaryDate,
222
+ summaryPrompt,
223
+ summaryForce,
224
+ summaryError
225
+ };
226
+ }
173
227
  if (args[0] && !args[0].startsWith("-") && !KNOWN_SUBCOMMANDS.has(args[0])) {
174
228
  return {
175
229
  mode: "watch",
@@ -193,12 +247,14 @@ import { homedir } from "os";
193
247
  import { join as join2 } from "path";
194
248
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
195
249
  var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
250
+ var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
196
251
  var DEFAULT_GLOBAL_CONFIG = {
197
252
  refreshIntervalMs: 2e3,
198
253
  logDir: join2(homedir(), ".agenthud", "logs"),
199
254
  hiddenSessions: [],
200
255
  hiddenSubAgents: [],
201
- filterPresets: [[], ["response"], ["commit"]]
256
+ filterPresets: [[], ["response"], ["commit"]],
257
+ hiddenProjects: []
202
258
  };
203
259
  function parseInterval(value) {
204
260
  const match = value.match(/^(\d+)(s|m)$/);
@@ -206,76 +262,175 @@ function parseInterval(value) {
206
262
  const n = parseInt(match[1], 10);
207
263
  return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
208
264
  }
209
- function loadGlobalConfig() {
210
- const config = { ...DEFAULT_GLOBAL_CONFIG };
211
- if (!existsSync(CONFIG_PATH)) {
212
- return config;
213
- }
214
- let raw;
265
+ function ensureAgenthudDir() {
266
+ const dir = join2(homedir(), ".agenthud");
267
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
268
+ }
269
+ function writeDefaultConfig() {
270
+ ensureAgenthudDir();
271
+ const defaultYaml = `# AgentHUD user settings.
272
+ # App-managed state (hidden sessions/projects) lives in state.yaml.
273
+
274
+ # How often to poll for activity updates
275
+ refreshInterval: 2s
276
+
277
+ # Where 's' key saves activity logs
278
+ logDir: ~/.agenthud/logs
279
+
280
+ # Activity filter presets (cycle with 'f' key in viewer)
281
+ # Each list is one preset; [] means "all". First preset is the default.
282
+ filterPresets:
283
+ - []
284
+ - ["response"]
285
+ - ["commit"]
286
+ `;
215
287
  try {
216
- raw = readFileSync2(CONFIG_PATH, "utf-8");
288
+ writeFileSync(CONFIG_PATH, defaultYaml, "utf-8");
217
289
  } catch {
218
- return config;
219
290
  }
220
- let parsed;
291
+ }
292
+ function writeState(state) {
293
+ ensureAgenthudDir();
221
294
  try {
222
- parsed = parseYaml(raw) ?? {};
295
+ writeFileSync(STATE_PATH, stringifyYaml(state), "utf-8");
223
296
  } catch {
224
- return config;
225
297
  }
226
- if (typeof parsed.refreshInterval === "string") {
227
- const ms = parseInterval(parsed.refreshInterval);
228
- if (ms !== null) config.refreshIntervalMs = ms;
298
+ }
299
+ function rewriteConfigWithoutHideFields(raw) {
300
+ const cleaned = {};
301
+ for (const [k, v] of Object.entries(raw)) {
302
+ if (k === "hiddenSessions" || k === "hiddenSubAgents" || k === "hiddenProjects")
303
+ continue;
304
+ cleaned[k] = v;
305
+ }
306
+ try {
307
+ writeFileSync(CONFIG_PATH, stringifyYaml(cleaned), "utf-8");
308
+ } catch {
229
309
  }
230
- if (typeof parsed.logDir === "string") {
231
- config.logDir = parsed.logDir.replace(/^~/, homedir());
310
+ }
311
+ function loadGlobalConfig() {
312
+ const config = { ...DEFAULT_GLOBAL_CONFIG };
313
+ let configRaw = {};
314
+ let configHadHideFields = false;
315
+ if (existsSync(CONFIG_PATH)) {
316
+ try {
317
+ const text = readFileSync2(CONFIG_PATH, "utf-8");
318
+ configRaw = parseYaml(text) ?? {};
319
+ } catch {
320
+ configRaw = {};
321
+ }
322
+ } else {
323
+ writeDefaultConfig();
232
324
  }
233
- if (Array.isArray(parsed.hiddenSessions)) {
234
- config.hiddenSessions = parsed.hiddenSessions.filter(
235
- (s) => typeof s === "string"
236
- );
325
+ if (typeof configRaw.refreshInterval === "string") {
326
+ const ms = parseInterval(configRaw.refreshInterval);
327
+ if (ms !== null) config.refreshIntervalMs = ms;
237
328
  }
238
- if (Array.isArray(parsed.hiddenSubAgents)) {
239
- config.hiddenSubAgents = parsed.hiddenSubAgents.filter(
240
- (s) => typeof s === "string"
241
- );
329
+ if (typeof configRaw.logDir === "string") {
330
+ config.logDir = configRaw.logDir.replace(/^~/, homedir());
242
331
  }
243
- if (Array.isArray(parsed.filterPresets)) {
244
- const presets = parsed.filterPresets.filter(Array.isArray).map(
332
+ if (Array.isArray(configRaw.filterPresets)) {
333
+ const presets = configRaw.filterPresets.filter(Array.isArray).map(
245
334
  (p) => p.filter((t) => typeof t === "string")
246
335
  );
247
336
  if (presets.length > 0) config.filterPresets = presets;
248
337
  }
338
+ const legacyHidden = {};
339
+ for (const key of [
340
+ "hiddenSessions",
341
+ "hiddenSubAgents",
342
+ "hiddenProjects"
343
+ ]) {
344
+ if (Array.isArray(configRaw[key])) {
345
+ configHadHideFields = true;
346
+ legacyHidden[key] = configRaw[key].filter(
347
+ (s) => typeof s === "string"
348
+ );
349
+ }
350
+ }
351
+ let stateRaw = {};
352
+ if (existsSync(STATE_PATH)) {
353
+ try {
354
+ const text = readFileSync2(STATE_PATH, "utf-8");
355
+ stateRaw = parseYaml(text) ?? {};
356
+ } catch {
357
+ stateRaw = {};
358
+ }
359
+ }
360
+ for (const key of [
361
+ "hiddenSessions",
362
+ "hiddenSubAgents",
363
+ "hiddenProjects"
364
+ ]) {
365
+ if (Array.isArray(stateRaw[key])) {
366
+ config[key] = stateRaw[key].filter(
367
+ (s) => typeof s === "string"
368
+ );
369
+ }
370
+ }
371
+ if (configHadHideFields) {
372
+ const merged = {
373
+ hiddenSessions: config.hiddenSessions.length > 0 ? config.hiddenSessions : legacyHidden.hiddenSessions ?? [],
374
+ hiddenSubAgents: config.hiddenSubAgents.length > 0 ? config.hiddenSubAgents : legacyHidden.hiddenSubAgents ?? [],
375
+ hiddenProjects: config.hiddenProjects.length > 0 ? config.hiddenProjects : legacyHidden.hiddenProjects ?? []
376
+ };
377
+ writeState(merged);
378
+ rewriteConfigWithoutHideFields(configRaw);
379
+ config.hiddenSessions = merged.hiddenSessions;
380
+ config.hiddenSubAgents = merged.hiddenSubAgents;
381
+ config.hiddenProjects = merged.hiddenProjects;
382
+ }
249
383
  return config;
250
384
  }
251
- function writeConfig(updates) {
252
- const configDir = join2(homedir(), ".agenthud");
253
- if (!existsSync(configDir)) {
254
- mkdirSync(configDir, { recursive: true });
255
- }
256
- let raw = {};
257
- if (existsSync(CONFIG_PATH)) {
385
+ function updateState(updates) {
386
+ let state = {
387
+ hiddenSessions: [],
388
+ hiddenSubAgents: [],
389
+ hiddenProjects: []
390
+ };
391
+ if (existsSync(STATE_PATH)) {
258
392
  try {
259
- raw = parseYaml(readFileSync2(CONFIG_PATH, "utf-8")) ?? {};
393
+ const text = readFileSync2(STATE_PATH, "utf-8");
394
+ const raw = parseYaml(text) ?? {};
395
+ for (const key of [
396
+ "hiddenSessions",
397
+ "hiddenSubAgents",
398
+ "hiddenProjects"
399
+ ]) {
400
+ if (Array.isArray(raw[key])) {
401
+ state[key] = raw[key].filter(
402
+ (s) => typeof s === "string"
403
+ );
404
+ }
405
+ }
260
406
  } catch {
261
- raw = {};
262
407
  }
263
408
  }
264
- if (updates.hiddenSessions !== void 0)
265
- raw.hiddenSessions = updates.hiddenSessions;
266
- if (updates.hiddenSubAgents !== void 0)
267
- raw.hiddenSubAgents = updates.hiddenSubAgents;
268
- writeFileSync(CONFIG_PATH, stringifyYaml(raw), "utf-8");
409
+ for (const key of [
410
+ "hiddenSessions",
411
+ "hiddenSubAgents",
412
+ "hiddenProjects"
413
+ ]) {
414
+ if (updates[key] !== void 0) {
415
+ state[key] = updates[key];
416
+ }
417
+ }
418
+ writeState(state);
269
419
  }
270
420
  function hideSession(id) {
271
421
  const config = loadGlobalConfig();
272
422
  if (config.hiddenSessions.includes(id)) return;
273
- writeConfig({ hiddenSessions: [...config.hiddenSessions, id] });
423
+ updateState({ hiddenSessions: [...config.hiddenSessions, id] });
274
424
  }
275
425
  function hideSubAgent(id) {
276
426
  const config = loadGlobalConfig();
277
427
  if (config.hiddenSubAgents.includes(id)) return;
278
- writeConfig({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
428
+ updateState({ hiddenSubAgents: [...config.hiddenSubAgents, id] });
429
+ }
430
+ function hideProject(name) {
431
+ const config = loadGlobalConfig();
432
+ if (config.hiddenProjects.includes(name)) return;
433
+ updateState({ hiddenProjects: [...config.hiddenProjects, name] });
279
434
  }
280
435
  function ensureLogDir(logDir) {
281
436
  if (!existsSync(logDir)) {
@@ -317,11 +472,12 @@ function formatDateString(date) {
317
472
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
318
473
  }
319
474
  function getCommitDetail(projectPath, hash) {
475
+ if (!projectPath) return null;
320
476
  try {
321
- return execSync(`git show --stat --no-color ${hash}`, {
322
- cwd: projectPath,
323
- encoding: "utf-8"
324
- }).trim();
477
+ return execSync(
478
+ `git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
479
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
480
+ ).trim();
325
481
  } catch {
326
482
  return null;
327
483
  }
@@ -333,8 +489,8 @@ function parseGitCommits(projectPath, startDate, endDate) {
333
489
  let raw;
334
490
  try {
335
491
  raw = execSync(
336
- `git log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
337
- { cwd: projectPath, encoding: "utf-8" }
492
+ `git --git-dir="${projectPath}/.git" log --format="%ct|%h|%s" --after="${start} 00:00:00" --before="${end} 23:59:59"`,
493
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
338
494
  ).trim();
339
495
  } catch {
340
496
  return [];
@@ -377,7 +533,7 @@ function parseModelName(modelId) {
377
533
  }
378
534
  function getToolDetail(_toolName, input) {
379
535
  if (!input) return "";
380
- if (input.command) return stripAnsi(input.command.replace(/\n/g, " "));
536
+ if (input.command) return stripAnsi(input.command);
381
537
  if (input.file_path) return basename(input.file_path);
382
538
  if (input.pattern) return stripAnsi(input.pattern);
383
539
  if (input.query) return stripAnsi(input.query);
@@ -416,7 +572,7 @@ function parseActivitiesFromLines(lines) {
416
572
  type: "user",
417
573
  icon: ICONS.User,
418
574
  label: "User",
419
- detail: userText.replace(/\n/g, " ")
575
+ detail: userText
420
576
  });
421
577
  }
422
578
  }
@@ -438,7 +594,7 @@ function parseActivitiesFromLines(lines) {
438
594
  type: "thinking",
439
595
  icon: ICONS.Thinking,
440
596
  label: "Thinking",
441
- detail: block.thinking.replace(/\n/g, " ")
597
+ detail: block.thinking
442
598
  });
443
599
  } else if (block.type === "tool_use" && block.name) {
444
600
  if (block.name === "TodoWrite") continue;
@@ -463,7 +619,7 @@ function parseActivitiesFromLines(lines) {
463
619
  type: "response",
464
620
  icon: ICONS.Response,
465
621
  label: "Response",
466
- detail: block.text.replace(/\n/g, " ")
622
+ detail: block.text
467
623
  });
468
624
  }
469
625
  }
@@ -526,7 +682,15 @@ function sessionIsOnDate(session, date, activities) {
526
682
  }
527
683
  return activities.some((a) => isSameLocalDay(a.timestamp, date));
528
684
  }
685
+ function flattenForOneLine(s) {
686
+ return s.replace(/[\r\n\t]+/g, " ").trim();
687
+ }
529
688
  function truncateDetail(detail, limit) {
689
+ const flat = flattenForOneLine(detail);
690
+ if (limit === 0 || flat.length <= limit) return flat;
691
+ return flat.slice(0, limit);
692
+ }
693
+ function truncateRaw(detail, limit) {
530
694
  if (limit === 0 || detail.length <= limit) return detail;
531
695
  return detail.slice(0, limit);
532
696
  }
@@ -573,7 +737,7 @@ function generateReport(sessions, options2) {
573
737
  time: formatTime(a.timestamp),
574
738
  icon: a.icon,
575
739
  label: a.label,
576
- detail: truncateDetail(a.detail, detailLimit)
740
+ detail: truncateRaw(a.detail, detailLimit)
577
741
  }))
578
742
  };
579
743
  });
@@ -585,7 +749,7 @@ function generateReport(sessions, options2) {
585
749
  time: formatTime(a.timestamp),
586
750
  icon: a.icon,
587
751
  label: a.label,
588
- detail: truncateDetail(a.detail, detailLimit)
752
+ detail: truncateRaw(a.detail, detailLimit)
589
753
  })),
590
754
  subAgents: subAgentBlocks
591
755
  };
@@ -615,6 +779,19 @@ function generateReport(sessions, options2) {
615
779
  return lines.join("\n").trimEnd();
616
780
  }
617
781
 
782
+ // src/data/summaryRunner.ts
783
+ import { spawn } from "child_process";
784
+ import {
785
+ copyFileSync,
786
+ createWriteStream,
787
+ existsSync as existsSync4,
788
+ mkdirSync as mkdirSync2,
789
+ readFileSync as readFileSync5
790
+ } from "fs";
791
+ import { homedir as homedir3 } from "os";
792
+ import { dirname as dirname2, join as join4 } from "path";
793
+ import { fileURLToPath as fileURLToPath2 } from "url";
794
+
618
795
  // src/data/sessions.ts
619
796
  import { existsSync as existsSync3, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
620
797
  import { homedir as homedir2 } from "os";
@@ -655,7 +832,14 @@ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
655
832
  return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
656
833
  }
657
834
  var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
658
- var getDisplayWidth = stringWidth;
835
+ var widthCache = /* @__PURE__ */ new Map();
836
+ function getDisplayWidth(s) {
837
+ const cached = widthCache.get(s);
838
+ if (cached !== void 0) return cached;
839
+ const w = stringWidth(s);
840
+ widthCache.set(s, w);
841
+ return w;
842
+ }
659
843
 
660
844
  // src/data/sessions.ts
661
845
  function getProjectsDir() {
@@ -722,6 +906,71 @@ function readModelName(filePath) {
722
906
  }
723
907
  return null;
724
908
  }
909
+ var SYSTEM_PREFIXES = [
910
+ "<command-name>",
911
+ "<command-message>",
912
+ "<command-args>",
913
+ "<local-command-stdout>",
914
+ "<local-command-caveat>",
915
+ "<system-reminder>",
916
+ "<bash-input>",
917
+ "<bash-stdout>",
918
+ "<bash-stderr>",
919
+ "<user-prompt-submit-hook>"
920
+ ];
921
+ function isSystemNoise(text) {
922
+ const trimmed = text.trimStart();
923
+ return SYSTEM_PREFIXES.some((p) => trimmed.startsWith(p));
924
+ }
925
+ function readFirstUserPrompt(filePath) {
926
+ if (!existsSync3(filePath)) return null;
927
+ let content;
928
+ try {
929
+ content = readFileSync4(filePath, "utf-8");
930
+ } catch {
931
+ return null;
932
+ }
933
+ for (const line of content.split("\n")) {
934
+ if (!line.trim()) continue;
935
+ let entry;
936
+ try {
937
+ entry = JSON.parse(line);
938
+ } catch {
939
+ continue;
940
+ }
941
+ if (entry.type !== "user") continue;
942
+ if (entry.toolUseResult !== void 0) continue;
943
+ const raw = entry.message?.content;
944
+ let text;
945
+ if (typeof raw === "string") {
946
+ text = raw;
947
+ } else if (Array.isArray(raw)) {
948
+ const textBlock = raw.find(
949
+ (b) => b && b.type === "text" && typeof b.text === "string"
950
+ );
951
+ text = textBlock?.text ?? "";
952
+ } else {
953
+ continue;
954
+ }
955
+ if (!text || isSystemNoise(text)) continue;
956
+ const firstLine = text.split("\n").find((l) => l.trim()) ?? "";
957
+ if (!firstLine || isSystemNoise(firstLine)) continue;
958
+ const trimmed = firstLine.trim();
959
+ return trimmed.length > 80 ? trimmed.slice(0, 80) : trimmed;
960
+ }
961
+ return null;
962
+ }
963
+ function readEntrypoint(filePath) {
964
+ if (!existsSync3(filePath)) return null;
965
+ try {
966
+ const firstLine = readFileSync4(filePath, "utf-8").split("\n")[0];
967
+ if (!firstLine) return null;
968
+ const entry = JSON.parse(firstLine);
969
+ return typeof entry.entrypoint === "string" ? entry.entrypoint : null;
970
+ } catch {
971
+ return null;
972
+ }
973
+ }
725
974
  function buildSubAgents(parentId, projectDir, config, projectName) {
726
975
  const subagentsDir = join3(projectDir, parentId, "subagents");
727
976
  if (!existsSync3(subagentsDir)) return [];
@@ -751,7 +1000,9 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
751
1000
  modelName: readModelName(filePath),
752
1001
  subAgents: [],
753
1002
  agentId: agentId ?? void 0,
754
- taskDescription: taskDescription ?? void 0
1003
+ taskDescription: taskDescription ?? void 0,
1004
+ nonInteractive: false,
1005
+ firstUserPrompt: null
755
1006
  };
756
1007
  } catch {
757
1008
  return null;
@@ -763,7 +1014,12 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
763
1014
  function discoverSessions(config) {
764
1015
  const projectsDir = getProjectsDir();
765
1016
  if (!existsSync3(projectsDir)) {
766
- return { sessions: [], totalCount: 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1017
+ return {
1018
+ projects: [],
1019
+ coldProjects: [],
1020
+ totalCount: 0,
1021
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1022
+ };
767
1023
  }
768
1024
  let projectDirs;
769
1025
  try {
@@ -775,7 +1031,12 @@ function discoverSessions(config) {
775
1031
  }
776
1032
  });
777
1033
  } catch {
778
- return { sessions: [], totalCount: 0, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
1034
+ return {
1035
+ projects: [],
1036
+ coldProjects: [],
1037
+ totalCount: 0,
1038
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1039
+ };
779
1040
  }
780
1041
  const allSessions = [];
781
1042
  for (const encodedDir of projectDirs) {
@@ -806,38 +1067,193 @@ function discoverSessions(config) {
806
1067
  lastModifiedMs: stat.mtimeMs,
807
1068
  status: getSessionStatus(stat.mtimeMs),
808
1069
  modelName: readModelName(filePath),
809
- subAgents
1070
+ subAgents,
1071
+ nonInteractive: readEntrypoint(filePath) === "sdk-cli",
1072
+ firstUserPrompt: readFirstUserPrompt(filePath)
810
1073
  });
811
1074
  } catch {
812
1075
  }
813
1076
  }
814
1077
  }
815
- allSessions.sort((a, b) => {
816
- const statusOrder = {
817
- hot: 0,
818
- warm: 1,
819
- cool: 2,
820
- cold: 3
821
- };
822
- const statusDiff = statusOrder[a.status] - statusOrder[b.status];
1078
+ const byProject = /* @__PURE__ */ new Map();
1079
+ for (const s of allSessions) {
1080
+ if (config.hiddenSessions.includes(s.hideKey)) continue;
1081
+ const arr = byProject.get(s.projectPath) ?? [];
1082
+ arr.push(s);
1083
+ byProject.set(s.projectPath, arr);
1084
+ }
1085
+ const statusOrder = {
1086
+ hot: 0,
1087
+ warm: 1,
1088
+ cool: 2,
1089
+ cold: 3
1090
+ };
1091
+ const allProjects = [];
1092
+ for (const [projectPath, sessions] of byProject) {
1093
+ if (sessions.length === 0) continue;
1094
+ const projectName = sessions[0].projectName;
1095
+ if (config.hiddenProjects.includes(projectName)) continue;
1096
+ sessions.sort((a, b) => {
1097
+ if (a.nonInteractive !== b.nonInteractive) {
1098
+ return a.nonInteractive ? 1 : -1;
1099
+ }
1100
+ const statusDiff = statusOrder[a.status] - statusOrder[b.status];
1101
+ if (statusDiff !== 0) return statusDiff;
1102
+ return b.lastModifiedMs - a.lastModifiedMs;
1103
+ });
1104
+ const hotness = sessions[0].status;
1105
+ allProjects.push({ name: projectName, projectPath, sessions, hotness });
1106
+ }
1107
+ const activeProjects = allProjects.filter((p) => p.hotness !== "cold");
1108
+ const coldProjects = allProjects.filter((p) => p.hotness === "cold");
1109
+ activeProjects.sort((a, b) => {
1110
+ const statusDiff = statusOrder[a.hotness] - statusOrder[b.hotness];
823
1111
  if (statusDiff !== 0) return statusDiff;
824
- return b.lastModifiedMs - a.lastModifiedMs;
1112
+ return b.sessions[0].lastModifiedMs - a.sessions[0].lastModifiedMs;
825
1113
  });
826
- const visible = allSessions.filter(
827
- (s) => !config.hiddenSessions.includes(s.hideKey)
1114
+ const countSessions = (projects) => projects.reduce(
1115
+ (sum, p) => sum + p.sessions.length + p.sessions.reduce((s, sn) => s + sn.subAgents.length, 0),
1116
+ 0
828
1117
  );
829
- const totalCount = visible.length + visible.reduce((sum, s) => sum + s.subAgents.length, 0);
1118
+ const totalCount = countSessions(activeProjects) + countSessions(coldProjects);
830
1119
  return {
831
- sessions: visible,
1120
+ projects: activeProjects,
1121
+ coldProjects,
832
1122
  totalCount,
833
1123
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
834
1124
  };
835
1125
  }
836
1126
 
1127
+ // src/data/summaryRunner.ts
1128
+ function agenthudHomeDir() {
1129
+ const dir = join4(homedir3(), ".agenthud");
1130
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1131
+ return dir;
1132
+ }
1133
+ function summariesDir() {
1134
+ const dir = join4(agenthudHomeDir(), "summaries");
1135
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1136
+ return dir;
1137
+ }
1138
+ function userPromptPath() {
1139
+ return join4(homedir3(), ".agenthud", "summary-prompt.md");
1140
+ }
1141
+ function templatePath() {
1142
+ const here = dirname2(fileURLToPath2(import.meta.url));
1143
+ return join4(here, "templates", "summary-prompt.md");
1144
+ }
1145
+ function dateKey(d) {
1146
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1147
+ }
1148
+ function cachePath(date) {
1149
+ return join4(summariesDir(), `${dateKey(date)}.md`);
1150
+ }
1151
+ function isSameLocalDay2(a, b) {
1152
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1153
+ }
1154
+ function ensureUserPromptFile() {
1155
+ const p = userPromptPath();
1156
+ if (existsSync4(p)) return;
1157
+ const dir = dirname2(p);
1158
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
1159
+ try {
1160
+ copyFileSync(templatePath(), p);
1161
+ } catch {
1162
+ }
1163
+ }
1164
+ function resolvePrompt(override) {
1165
+ if (override) return override;
1166
+ const p = userPromptPath();
1167
+ if (existsSync4(p)) {
1168
+ try {
1169
+ return readFileSync5(p, "utf-8");
1170
+ } catch {
1171
+ }
1172
+ }
1173
+ try {
1174
+ return readFileSync5(templatePath(), "utf-8");
1175
+ } catch {
1176
+ return "Summarize the activity log below.";
1177
+ }
1178
+ }
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
+ }
1191
+ }
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()
1206
+ });
1207
+ 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})
1212
+ `
1213
+ );
1214
+ cacheStream = null;
1215
+ });
1216
+ let stderrBuf = "";
1217
+ proc.on("error", (err) => {
1218
+ if (err.code === "ENOENT") {
1219
+ process.stderr.write(
1220
+ "Error: claude CLI not found. Install: npm i -g @anthropic-ai/claude-code\n"
1221
+ );
1222
+ resolve(1);
1223
+ } else {
1224
+ process.stderr.write(`Error: ${err.message}
1225
+ `);
1226
+ resolve(1);
1227
+ }
1228
+ });
1229
+ proc.stdout.on("data", (chunk) => {
1230
+ process.stdout.write(chunk);
1231
+ cacheStream?.write(chunk);
1232
+ });
1233
+ proc.stderr.on("data", (chunk) => {
1234
+ stderrBuf += chunk.toString();
1235
+ process.stderr.write(chunk);
1236
+ });
1237
+ proc.on("close", (code) => {
1238
+ cacheStream?.end();
1239
+ if (code !== 0) {
1240
+ const lower = stderrBuf.toLowerCase();
1241
+ if (lower.includes("not authenticated") || lower.includes("login") || lower.includes(" auth")) {
1242
+ process.stderr.write(
1243
+ "\nHint: claude appears to be unauthenticated. Run: claude\n"
1244
+ );
1245
+ }
1246
+ }
1247
+ resolve(code ?? 1);
1248
+ });
1249
+ proc.stdin.end(reportMarkdown);
1250
+ });
1251
+ }
1252
+
837
1253
  // src/ui/App.tsx
838
- import { existsSync as existsSync4, watch, writeFileSync as writeFileSync2 } from "fs";
839
- import { join as join4 } from "path";
840
- import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
1254
+ import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1255
+ import { join as join5 } from "path";
1256
+ import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
841
1257
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
842
1258
 
843
1259
  // src/ui/ActivityViewerPanel.tsx
@@ -875,20 +1291,21 @@ function formatActivityTime(date, now) {
875
1291
  const day = String(date.getDate()).padStart(2, "0");
876
1292
  return `${month}/${day} ${time}`;
877
1293
  }
1294
+ function flattenForOneLine2(detail) {
1295
+ return detail.replace(/[\r\n\t]+/g, " ").trim();
1296
+ }
878
1297
  function truncateDetail2(detail, maxWidth) {
879
- if (getDisplayWidth(detail) <= maxWidth) return detail;
1298
+ const flat = flattenForOneLine2(detail);
1299
+ if (getDisplayWidth(flat) <= maxWidth) return flat;
880
1300
  let truncated = "";
881
- let currentWidth = 0;
882
- for (const char of detail) {
1301
+ let width = 0;
1302
+ for (const char of flat) {
883
1303
  const charWidth = getDisplayWidth(char);
884
- if (currentWidth + charWidth > maxWidth - 3) {
885
- truncated += "...";
886
- break;
887
- }
1304
+ if (width + charWidth > maxWidth - 1) break;
888
1305
  truncated += char;
889
- currentWidth += charWidth;
1306
+ width += charWidth;
890
1307
  }
891
- return truncated;
1308
+ return `${truncated}\u2026`;
892
1309
  }
893
1310
  function ActivityViewerPanel({
894
1311
  activities,
@@ -1091,10 +1508,114 @@ function DetailViewPanel({
1091
1508
  ] });
1092
1509
  }
1093
1510
 
1511
+ // src/ui/HelpPanel.tsx
1512
+ import { Box as Box3, Text as Text3 } from "ink";
1513
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1514
+ var SECTIONS = [
1515
+ {
1516
+ title: "Session tree",
1517
+ rows: [
1518
+ ["\u2191 \u2193 / k j", "Move selection"],
1519
+ ["PgUp / Ctrl+B", "Page up"],
1520
+ ["PgDn / Ctrl+F", "Page down"],
1521
+ ["\u21B5", "Expand/collapse project, session, or summary"],
1522
+ ["h", "Hide selected (project/session/sub-agent)"],
1523
+ ["Tab", "Switch focus to activity viewer"],
1524
+ ["r", "Refresh now"]
1525
+ ]
1526
+ },
1527
+ {
1528
+ title: "Activity viewer",
1529
+ rows: [
1530
+ ["\u2191 \u2193 / k j", "Scroll one line"],
1531
+ ["PgUp/Dn, Ctrl+B/F", "Scroll one page"],
1532
+ ["Ctrl+U / Ctrl+D", "Scroll half page"],
1533
+ ["g", "Jump to live (newest)"],
1534
+ ["G", "Jump to oldest"],
1535
+ ["\u21B5", "Open detail view for selected activity"],
1536
+ ["f", "Cycle filter preset (set in config.yaml)"],
1537
+ ["s", "Save activity log to ~/.agenthud/logs/"],
1538
+ ["Tab", "Switch focus to session tree"]
1539
+ ]
1540
+ },
1541
+ {
1542
+ title: "Detail view",
1543
+ rows: [
1544
+ ["\u2191 \u2193 / k j", "Scroll"],
1545
+ ["\u21B5 / Esc / q", "Close"]
1546
+ ]
1547
+ },
1548
+ {
1549
+ title: "Always available",
1550
+ rows: [
1551
+ ["?", "Toggle this help"],
1552
+ ["q", "Quit (or close detail/help)"]
1553
+ ]
1554
+ },
1555
+ {
1556
+ title: "CLI commands",
1557
+ rows: [
1558
+ ["agenthud report", "Print activity for a date as Markdown/JSON"],
1559
+ ["agenthud summary", "LLM-summarize a day via claude -p (cached)"],
1560
+ ["agenthud --help", "Full CLI usage"]
1561
+ ]
1562
+ },
1563
+ {
1564
+ title: "Files",
1565
+ rows: [
1566
+ ["~/.agenthud/config.yaml", "User settings (edit freely)"],
1567
+ ["~/.agenthud/state.yaml", "Hidden items (app-managed)"],
1568
+ ["~/.agenthud/summary-prompt.md", "LLM prompt template"],
1569
+ ["~/.agenthud/summaries/", "Cached daily summaries"]
1570
+ ]
1571
+ }
1572
+ ];
1573
+ function HelpPanel({
1574
+ width,
1575
+ height
1576
+ }) {
1577
+ const allKeys = SECTIONS.flatMap((s) => s.rows.map((r) => r[0]));
1578
+ const keyColumn = Math.min(
1579
+ 30,
1580
+ Math.max(...allKeys.map((k) => getDisplayWidth(k)))
1581
+ );
1582
+ const padTo = (s, w) => {
1583
+ const pad = Math.max(0, w - getDisplayWidth(s));
1584
+ return s + " ".repeat(pad);
1585
+ };
1586
+ const lines = [];
1587
+ lines.push(
1588
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "AgentHUD Help" }, "title")
1589
+ );
1590
+ lines.push(/* @__PURE__ */ jsx3(Text3, { children: " " }, "title-sp"));
1591
+ for (let s = 0; s < SECTIONS.length; s++) {
1592
+ if (s > 0) lines.push(/* @__PURE__ */ jsx3(Text3, { children: " " }, `sp-${s}`));
1593
+ lines.push(
1594
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: SECTIONS[s].title }, `title-${s}`)
1595
+ );
1596
+ for (let r = 0; r < SECTIONS[s].rows.length; r++) {
1597
+ const [key, desc] = SECTIONS[s].rows[r];
1598
+ const isCli = key.trim().startsWith("agenthud");
1599
+ const isFile = key.includes("~/.agenthud");
1600
+ lines.push(
1601
+ /* @__PURE__ */ jsxs3(Text3, { children: [
1602
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " " }),
1603
+ /* @__PURE__ */ jsx3(Text3, { color: isCli ? "cyan" : isFile ? "green" : void 0, children: padTo(key, keyColumn) }),
1604
+ /* @__PURE__ */ jsx3(Text3, { children: " " }),
1605
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: desc })
1606
+ ] }, `row-${s}-${r}`)
1607
+ );
1608
+ }
1609
+ }
1610
+ const visible = lines.slice(0, height);
1611
+ return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", width, children: visible });
1612
+ }
1613
+
1094
1614
  // src/ui/hooks/useHotkeys.ts
1095
1615
  function useHotkeys({
1096
1616
  focus,
1097
1617
  detailMode,
1618
+ helpMode,
1098
1619
  onSwitchFocus,
1099
1620
  onScrollUp,
1100
1621
  onScrollDown,
@@ -1113,9 +1634,21 @@ function useHotkeys({
1113
1634
  onDetailScrollUp,
1114
1635
  onDetailScrollDown,
1115
1636
  onFilter,
1637
+ onHelp,
1116
1638
  filterLabel
1117
1639
  }) {
1118
1640
  const handleInput = (input, key) => {
1641
+ if (helpMode) {
1642
+ if (key.return || key.escape || input === "q" || input === "?") {
1643
+ onHelp();
1644
+ return;
1645
+ }
1646
+ return;
1647
+ }
1648
+ if (input === "?") {
1649
+ onHelp();
1650
+ return;
1651
+ }
1119
1652
  if (detailMode) {
1120
1653
  if (key.upArrow || input === "k") {
1121
1654
  onDetailScrollUp();
@@ -1214,13 +1747,14 @@ function useHotkeys({
1214
1747
  }
1215
1748
  }
1216
1749
  };
1217
- const statusBarItems = detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close"] : focus === "tree" ? [
1750
+ const statusBarItems = helpMode ? ["\u21B5/Esc/q/?: close"] : detailMode ? ["\u2191\u2193/jk: scroll", "\u21B5/Esc: close", "?: help"] : focus === "tree" ? [
1218
1751
  "Tab: viewer",
1219
1752
  "\u2191\u2193/jk: select",
1220
1753
  "PgUp/Dn: page",
1221
1754
  "\u21B5: expand",
1222
1755
  "h: hide",
1223
1756
  "r: refresh",
1757
+ "?: help",
1224
1758
  "q: quit"
1225
1759
  ] : [
1226
1760
  "Tab: sessions",
@@ -1230,6 +1764,7 @@ function useHotkeys({
1230
1764
  "G: oldest",
1231
1765
  "\u21B5: detail",
1232
1766
  `f: ${filterLabel}`,
1767
+ "?: help",
1233
1768
  "q: quit"
1234
1769
  ];
1235
1770
  return { handleInput, statusBarItems };
@@ -1251,9 +1786,9 @@ function useSpinner(active, intervalMs = 100) {
1251
1786
  }
1252
1787
 
1253
1788
  // src/ui/SessionTreePanel.tsx
1254
- import { homedir as homedir3 } from "os";
1255
- import { Box as Box3, Text as Text3 } from "ink";
1256
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1789
+ import { homedir as homedir4 } from "os";
1790
+ import { Box as Box4, Text as Text4 } from "ink";
1791
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1257
1792
  function formatElapsed(lastModifiedMs) {
1258
1793
  const elapsed = Date.now() - lastModifiedMs;
1259
1794
  const seconds = Math.floor(elapsed / 1e3);
@@ -1277,7 +1812,7 @@ function getStatusColor(status) {
1277
1812
  }
1278
1813
  }
1279
1814
  function formatProjectPath(projectPath) {
1280
- const home = homedir3();
1815
+ const home = homedir4();
1281
1816
  const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
1282
1817
  return raw;
1283
1818
  }
@@ -1293,23 +1828,24 @@ function SessionRow({
1293
1828
  prefix,
1294
1829
  contentWidth
1295
1830
  }) {
1296
- const isParent = prefix === "";
1831
+ const isParent = prefix === " ";
1297
1832
  const statusColor = getStatusColor(session.status);
1298
1833
  const badge = `[${session.status}]`;
1299
1834
  const elapsed = formatElapsed(session.lastModifiedMs);
1300
1835
  const model = session.modelName ?? "";
1301
- const name = isParent ? session.projectName || session.id.slice(0, 8) : session.agentId ?? session.id.slice(0, 8);
1302
- const shortId = isParent && session.projectName ? ` #${session.id.slice(0, 4)}` : "";
1836
+ const isNonInteractive = session.nonInteractive;
1837
+ const rawName = isParent ? isNonInteractive ? `(#${session.id.slice(0, 4)})` : `#${session.id.slice(0, 4)}` : session.agentId ?? session.id.slice(0, 8);
1838
+ const shortIdDisplay = "";
1303
1839
  const rightParts = [elapsed];
1304
1840
  if (model) rightParts.push(model);
1305
1841
  const rightSide = rightParts.join(" ");
1306
- const leftCore = `${prefix}${name}${shortId} ${badge}`;
1307
- const leftCoreWidth = getDisplayWidth(leftCore);
1842
+ const leftCoreBase = `${prefix}${rawName}${shortIdDisplay} ${badge}`;
1843
+ const leftCoreWidth = getDisplayWidth(leftCoreBase);
1308
1844
  const rightWidth = getDisplayWidth(rightSide);
1309
1845
  const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - 1;
1310
1846
  let middleText = "";
1311
1847
  if (middleAvailable > 3) {
1312
- const raw = isParent ? session.projectPath ? formatProjectPath(session.projectPath) : "" : session.taskDescription ?? "";
1848
+ const raw = isParent ? session.firstUserPrompt ?? "" : session.taskDescription ?? "";
1313
1849
  if (raw) {
1314
1850
  const truncated = truncatePath(raw, middleAvailable);
1315
1851
  if (truncated) middleText = truncated;
@@ -1321,42 +1857,56 @@ function SessionRow({
1321
1857
  contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
1322
1858
  );
1323
1859
  const gap = " ".repeat(gapWidth);
1324
- const fullLine = leftCore + middleSection + gap + rightSide;
1860
+ const fullLine = leftCoreBase + middleSection + gap + rightSide;
1325
1861
  const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
1326
1862
  const highlight = isSelected && hasFocus;
1327
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1863
+ const shouldDim = isNonInteractive;
1864
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1328
1865
  BOX.v,
1329
1866
  " ",
1330
- /* @__PURE__ */ jsxs3(Text3, { backgroundColor: highlight ? "blue" : void 0, bold: highlight, children: [
1331
- /* @__PURE__ */ jsx3(Text3, { children: prefix }),
1332
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: name }),
1333
- shortId ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: shortId }) : null,
1334
- /* @__PURE__ */ jsx3(Text3, { children: " " }),
1335
- /* @__PURE__ */ jsx3(Text3, { color: statusColor, children: badge }),
1336
- middleText ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: middleSection }) : null,
1337
- /* @__PURE__ */ jsx3(Text3, { children: gap }),
1338
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: elapsed }),
1339
- model ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${model}` }) : null
1340
- ] }),
1867
+ /* @__PURE__ */ jsxs4(
1868
+ Text4,
1869
+ {
1870
+ backgroundColor: highlight ? "blue" : void 0,
1871
+ bold: highlight,
1872
+ dimColor: shouldDim && !highlight,
1873
+ children: [
1874
+ /* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !highlight, children: prefix }),
1875
+ /* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
1876
+ shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
1877
+ /* @__PURE__ */ jsx4(Text4, { children: " " }),
1878
+ /* @__PURE__ */ jsx4(Text4, { color: statusColor, children: badge }),
1879
+ middleText ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: middleSection }) : null,
1880
+ /* @__PURE__ */ jsx4(Text4, { children: gap }),
1881
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }),
1882
+ model ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: ` ${model}` }) : null
1883
+ ]
1884
+ }
1885
+ ),
1341
1886
  " ".repeat(linePadding),
1342
1887
  BOX.v
1343
1888
  ] });
1344
1889
  }
1345
1890
  function appendSessionRows(result, session, expandedIds) {
1346
- const isExpanded = expandedIds.has(session.id);
1891
+ const isCold = session.status === "cold";
1892
+ const sessionCollapsedKey = `__collapsed-session-${session.id}`;
1893
+ const sessionExpandedKey = `__expanded-session-${session.id}`;
1894
+ const sessionHidden = isCold ? !expandedIds.has(sessionExpandedKey) : expandedIds.has(sessionCollapsedKey);
1895
+ if (sessionHidden) return;
1896
+ const subAgentsFullyExpanded = expandedIds.has(session.id) || isCold && expandedIds.has(sessionExpandedKey);
1347
1897
  const hotWarm = session.subAgents.filter(
1348
1898
  (s) => s.status === "hot" || s.status === "warm"
1349
1899
  );
1350
1900
  const cool = session.subAgents.filter((s) => s.status === "cool");
1351
1901
  const cold = session.subAgents.filter((s) => s.status === "cold");
1352
- if (isExpanded) {
1902
+ if (subAgentsFullyExpanded) {
1353
1903
  const all = [...hotWarm, ...cool, ...cold];
1354
1904
  for (let i = 0; i < all.length; i++) {
1355
1905
  const isLast = i === all.length - 1;
1356
1906
  result.push({
1357
1907
  kind: "session",
1358
1908
  session: all[i],
1359
- prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1909
+ prefix: ` ${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1360
1910
  });
1361
1911
  }
1362
1912
  } else {
@@ -1366,7 +1916,7 @@ function appendSessionRows(result, session, expandedIds) {
1366
1916
  result.push({
1367
1917
  kind: "session",
1368
1918
  session: hotWarm[i],
1369
- prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1919
+ prefix: ` ${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1370
1920
  });
1371
1921
  }
1372
1922
  if (hasSummary) {
@@ -1379,25 +1929,65 @@ function appendSessionRows(result, session, expandedIds) {
1379
1929
  }
1380
1930
  }
1381
1931
  }
1382
- function flattenSessions(sessions, expandedIds) {
1932
+ function flattenSessions(projects, coldProjects, expandedIds) {
1383
1933
  const result = [];
1384
- const visibleSessions = sessions.filter((s) => s.status !== "cold");
1385
- const coldSessions = sessions.filter((s) => s.status === "cold");
1386
- for (const session of visibleSessions) {
1387
- result.push({ kind: "session", session, prefix: "" });
1388
- appendSessionRows(result, session, expandedIds);
1389
- }
1390
- if (coldSessions.length > 0) {
1391
- result.push({ kind: "cold-sessions-summary", count: coldSessions.length });
1392
- if (expandedIds.has("__cold__")) {
1393
- for (const session of coldSessions) {
1394
- result.push({ kind: "session", session, prefix: "" });
1934
+ for (const project of projects) {
1935
+ const sentinelId = `__proj-${project.name}__`;
1936
+ result.push({ kind: "project", project, sentinelId });
1937
+ const collapsed = expandedIds.has(`__collapsed-${sentinelId}`);
1938
+ if (!collapsed) {
1939
+ for (const session of project.sessions) {
1940
+ result.push({ kind: "session", session, prefix: " " });
1395
1941
  appendSessionRows(result, session, expandedIds);
1396
1942
  }
1397
1943
  }
1398
1944
  }
1945
+ if (coldProjects.length > 0) {
1946
+ result.push({ kind: "cold-projects-summary", count: coldProjects.length });
1947
+ if (expandedIds.has("__cold__")) {
1948
+ for (const project of coldProjects) {
1949
+ const sentinelId = `__proj-${project.name}__`;
1950
+ result.push({ kind: "project", project, sentinelId });
1951
+ const expanded = expandedIds.has(`__expanded-${sentinelId}`);
1952
+ if (expanded) {
1953
+ for (const session of project.sessions) {
1954
+ result.push({ kind: "session", session, prefix: " " });
1955
+ appendSessionRows(result, session, expandedIds);
1956
+ }
1957
+ }
1958
+ }
1959
+ }
1960
+ }
1399
1961
  return result;
1400
1962
  }
1963
+ function ProjectRow({
1964
+ project,
1965
+ isSelected,
1966
+ hasFocus,
1967
+ contentWidth
1968
+ }) {
1969
+ const nameText = `> ${project.name}`;
1970
+ const pathText = project.projectPath ? formatProjectPath(project.projectPath) : "";
1971
+ const nameWidth = getDisplayWidth(nameText);
1972
+ const pathWidth = pathText ? getDisplayWidth(pathText) : 0;
1973
+ const gapWidth = pathText ? 2 : 0;
1974
+ const totalWidth = nameWidth + gapWidth + pathWidth;
1975
+ const padding = Math.max(0, contentWidth - totalWidth);
1976
+ const highlight = isSelected && hasFocus;
1977
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1978
+ BOX.v,
1979
+ " ",
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
+ ] }),
1988
+ BOX.v
1989
+ ] });
1990
+ }
1401
1991
  function SubagentSummaryRow({
1402
1992
  coolCount,
1403
1993
  coldCount,
@@ -1409,16 +1999,16 @@ function SubagentSummaryRow({
1409
1999
  if (coolCount > 0) parts.push(`${coolCount} cool`);
1410
2000
  if (coldCount > 0) parts.push(`${coldCount} cold`);
1411
2001
  const hint = " +";
1412
- const text = `\u2514\u2500 ... ${parts.join(" ")}`;
2002
+ const text = ` \u2514\u2500 ... ${parts.join(" ")}`;
1413
2003
  const padding = Math.max(
1414
2004
  0,
1415
2005
  contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
1416
2006
  );
1417
2007
  const active = isSelected && hasFocus;
1418
- return /* @__PURE__ */ jsxs3(Text3, { children: [
2008
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1419
2009
  BOX.v,
1420
2010
  " ",
1421
- /* @__PURE__ */ jsxs3(Text3, { dimColor: !active, inverse: active, children: [
2011
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: !active, inverse: active, children: [
1422
2012
  text,
1423
2013
  " ".repeat(padding),
1424
2014
  hint
@@ -1426,7 +2016,7 @@ function SubagentSummaryRow({
1426
2016
  BOX.v
1427
2017
  ] });
1428
2018
  }
1429
- function ColdSessionsSummaryRow({
2019
+ function ColdProjectsSummaryRow({
1430
2020
  count,
1431
2021
  isSelected,
1432
2022
  hasFocus,
@@ -1441,8 +2031,8 @@ function ColdSessionsSummaryRow({
1441
2031
  const dashes = BOX.h.repeat(dashCount);
1442
2032
  const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
1443
2033
  const highlight = isSelected && hasFocus;
1444
- return /* @__PURE__ */ jsx3(
1445
- Text3,
2034
+ return /* @__PURE__ */ jsx4(
2035
+ Text4,
1446
2036
  {
1447
2037
  backgroundColor: highlight ? "blue" : void 0,
1448
2038
  bold: highlight,
@@ -1452,7 +2042,8 @@ function ColdSessionsSummaryRow({
1452
2042
  );
1453
2043
  }
1454
2044
  function SessionTreePanel({
1455
- sessions,
2045
+ projects,
2046
+ coldProjects,
1456
2047
  selectedId,
1457
2048
  hasFocus,
1458
2049
  width = DEFAULT_PANEL_WIDTH,
@@ -1463,28 +2054,30 @@ function SessionTreePanel({
1463
2054
  const contentWidth = innerWidth - 1;
1464
2055
  const titleLine = createTitleLine("Sessions", "", width);
1465
2056
  const bottomLine = createBottomLine(width);
1466
- if (sessions.length === 0) {
2057
+ const totalProjectCount = projects.length + coldProjects.length;
2058
+ if (totalProjectCount === 0) {
1467
2059
  const emptyText = "No Claude sessions";
1468
2060
  const emptyPadding = Math.max(0, contentWidth - emptyText.length);
1469
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1470
- /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
1471
- /* @__PURE__ */ jsxs3(Text3, { children: [
2061
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
2062
+ /* @__PURE__ */ jsx4(Text4, { children: titleLine }),
2063
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1472
2064
  BOX.v,
1473
2065
  " ",
1474
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: emptyText }),
2066
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: emptyText }),
1475
2067
  " ".repeat(emptyPadding),
1476
2068
  BOX.v
1477
2069
  ] }),
1478
- /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
2070
+ /* @__PURE__ */ jsx4(Text4, { children: bottomLine })
1479
2071
  ] });
1480
2072
  }
1481
- const flatRows = flattenSessions(sessions, expandedIds);
2073
+ const flatRows = flattenSessions(projects, coldProjects, expandedIds);
1482
2074
  const totalRows = flatRows.length;
1483
2075
  const selectedFlatIndex = flatRows.findIndex((row) => {
2076
+ if (row.kind === "project") return selectedId === row.sentinelId;
1484
2077
  if (row.kind === "session") return row.session.id === selectedId;
1485
2078
  if (row.kind === "subagent-summary")
1486
2079
  return selectedId === `__sub-${row.parentId}__`;
1487
- if (row.kind === "cold-sessions-summary") return selectedId === "__cold__";
2080
+ if (row.kind === "cold-projects-summary") return selectedId === "__cold__";
1488
2081
  return false;
1489
2082
  });
1490
2083
  const needsOverflow = maxRows !== void 0 && totalRows > maxRows;
@@ -1496,10 +2089,19 @@ function SessionTreePanel({
1496
2089
  }
1497
2090
  const displayRows = flatRows.slice(scrollTop, scrollTop + visibleCount);
1498
2091
  const hiddenBelow = totalRows - (scrollTop + displayRows.length);
1499
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1500
- /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
2092
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
2093
+ /* @__PURE__ */ jsx4(Text4, { children: titleLine }),
1501
2094
  displayRows.map(
1502
- (row, idx) => row.kind === "session" ? /* @__PURE__ */ jsx3(
2095
+ (row, idx) => row.kind === "project" ? /* @__PURE__ */ jsx4(
2096
+ ProjectRow,
2097
+ {
2098
+ project: row.project,
2099
+ isSelected: selectedId === row.sentinelId,
2100
+ hasFocus,
2101
+ contentWidth
2102
+ },
2103
+ `project-${row.project.name}-${idx}`
2104
+ ) : row.kind === "session" ? /* @__PURE__ */ jsx4(
1503
2105
  SessionRow,
1504
2106
  {
1505
2107
  session: row.session,
@@ -1509,7 +2111,7 @@ function SessionTreePanel({
1509
2111
  contentWidth
1510
2112
  },
1511
2113
  `${row.session.id}-${idx}`
1512
- ) : row.kind === "subagent-summary" ? /* @__PURE__ */ jsx3(
2114
+ ) : row.kind === "subagent-summary" ? /* @__PURE__ */ jsx4(
1513
2115
  SubagentSummaryRow,
1514
2116
  {
1515
2117
  coolCount: row.coolCount,
@@ -1519,8 +2121,8 @@ function SessionTreePanel({
1519
2121
  hasFocus
1520
2122
  },
1521
2123
  `subagent-summary-${idx}`
1522
- ) : /* @__PURE__ */ jsx3(
1523
- ColdSessionsSummaryRow,
2124
+ ) : /* @__PURE__ */ jsx4(
2125
+ ColdProjectsSummaryRow,
1524
2126
  {
1525
2127
  count: row.count,
1526
2128
  isSelected: selectedId === "__cold__",
@@ -1530,35 +2132,42 @@ function SessionTreePanel({
1530
2132
  "cold-summary"
1531
2133
  )
1532
2134
  ),
1533
- hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text3, { children: [
2135
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
1534
2136
  BOX.v,
1535
2137
  " ",
1536
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `... ${hiddenBelow} more` }),
2138
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `... ${hiddenBelow} more` }),
1537
2139
  " ".repeat(
1538
2140
  Math.max(0, contentWidth - `... ${hiddenBelow} more`.length - 1)
1539
2141
  ),
1540
2142
  BOX.v
1541
2143
  ] }),
1542
- /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
2144
+ /* @__PURE__ */ jsx4(Text4, { children: bottomLine })
1543
2145
  ] });
1544
2146
  }
1545
2147
 
1546
2148
  // src/ui/App.tsx
1547
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2149
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
1548
2150
  var VIEWER_HEIGHT_FRACTION = 0.55;
1549
2151
  function subSummarySentinel(parentId) {
1550
2152
  return {
1551
2153
  id: `__sub-${parentId}__`,
2154
+ hideKey: "",
1552
2155
  filePath: "",
1553
2156
  projectPath: "",
1554
2157
  projectName: "",
1555
2158
  lastModifiedMs: 0,
1556
2159
  status: "cold",
1557
2160
  modelName: null,
1558
- subAgents: []
2161
+ subAgents: [],
2162
+ nonInteractive: false
1559
2163
  };
1560
2164
  }
1561
2165
  function appendSubAgentRows(result, session, expandedIds) {
2166
+ const isCold = session.status === "cold";
2167
+ const sessionCollapsedKey = `__collapsed-session-${session.id}`;
2168
+ const sessionExpandedKey = `__expanded-session-${session.id}`;
2169
+ const sessionHidden = isCold ? !expandedIds.has(sessionExpandedKey) : expandedIds.has(sessionCollapsedKey);
2170
+ if (sessionHidden) return;
1562
2171
  if (expandedIds.has(session.id)) {
1563
2172
  result.push(...session.subAgents);
1564
2173
  } else {
@@ -1576,27 +2185,47 @@ function appendSubAgentRows(result, session, expandedIds) {
1576
2185
  }
1577
2186
  function flattenSessions2(tree, expandedIds) {
1578
2187
  const result = [];
1579
- const visible = tree.sessions.filter((s) => s.status !== "cold");
1580
- const cold = tree.sessions.filter((s) => s.status === "cold");
1581
- for (const s of visible) {
1582
- result.push(s);
1583
- appendSubAgentRows(result, s, expandedIds);
2188
+ const projectToFlat = (project, isCold) => {
2189
+ const sentinelId = `__proj-${project.name}__`;
2190
+ result.push({
2191
+ id: sentinelId,
2192
+ hideKey: "",
2193
+ filePath: "",
2194
+ projectPath: project.projectPath,
2195
+ projectName: project.name,
2196
+ lastModifiedMs: 0,
2197
+ status: project.hotness,
2198
+ modelName: null,
2199
+ subAgents: [],
2200
+ nonInteractive: false
2201
+ });
2202
+ const shouldShowSessions = isCold ? expandedIds.has(`__expanded-${sentinelId}`) : !expandedIds.has(`__collapsed-${sentinelId}`);
2203
+ if (shouldShowSessions) {
2204
+ for (const session of project.sessions) {
2205
+ result.push(session);
2206
+ appendSubAgentRows(result, session, expandedIds);
2207
+ }
2208
+ }
2209
+ };
2210
+ for (const project of tree.projects) {
2211
+ projectToFlat(project, false);
1584
2212
  }
1585
- if (cold.length > 0) {
2213
+ if (tree.coldProjects.length > 0) {
1586
2214
  result.push({
1587
2215
  id: "__cold__",
2216
+ hideKey: "",
1588
2217
  filePath: "",
1589
2218
  projectPath: "",
1590
- projectName: `${cold.length} cold`,
2219
+ projectName: `${tree.coldProjects.length} cold`,
1591
2220
  lastModifiedMs: 0,
1592
2221
  status: "cold",
1593
2222
  modelName: null,
1594
- subAgents: []
2223
+ subAgents: [],
2224
+ nonInteractive: false
1595
2225
  });
1596
2226
  if (expandedIds.has("__cold__")) {
1597
- for (const s of cold) {
1598
- result.push(s);
1599
- appendSubAgentRows(result, s, expandedIds);
2227
+ for (const project of tree.coldProjects) {
2228
+ projectToFlat(project, true);
1600
2229
  }
1601
2230
  }
1602
2231
  }
@@ -1625,8 +2254,9 @@ function App({ mode }) {
1625
2254
  () => discoverSessions(config)
1626
2255
  );
1627
2256
  const [selectedId, setSelectedId] = useState2(() => {
1628
- const first = sessionTree.sessions[0];
1629
- return first?.id ?? null;
2257
+ const firstProject = sessionTree.projects[0];
2258
+ if (firstProject) return `__proj-${firstProject.name}__`;
2259
+ return null;
1630
2260
  });
1631
2261
  const [focus, setFocus] = useState2("tree");
1632
2262
  const [scrollOffset, setScrollOffset] = useState2(0);
@@ -1642,6 +2272,7 @@ function App({ mode }) {
1642
2272
  );
1643
2273
  const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
1644
2274
  const [filterIndex, setFilterIndex] = useState2(0);
2275
+ const [helpMode, setHelpMode] = useState2(false);
1645
2276
  const allFlat = useMemo(
1646
2277
  () => flattenSessions2(sessionTree, expandedIds),
1647
2278
  [sessionTree, expandedIds]
@@ -1651,22 +2282,40 @@ function App({ mode }) {
1651
2282
  allFlatRef.current = allFlat;
1652
2283
  }, [allFlat]);
1653
2284
  const activitiesLengthRef = useRef(0);
2285
+ const activitiesRef = useRef(activities);
1654
2286
  useEffect2(() => {
1655
2287
  activitiesLengthRef.current = activities.length;
1656
- }, [activities.length]);
2288
+ activitiesRef.current = activities;
2289
+ }, [activities]);
2290
+ const lastLoadedFileRef = useRef(null);
1657
2291
  useEffect2(() => {
1658
- const node = allFlatRef.current.find((s) => s.id === selectedId);
2292
+ let node = allFlatRef.current.find((s) => s.id === selectedId);
2293
+ if (node && selectedId?.startsWith("__proj-") && selectedId.endsWith("__")) {
2294
+ const projectName = selectedId.slice(7, -2);
2295
+ const project = sessionTree.projects.find((p) => p.name === projectName) ?? sessionTree.coldProjects.find((p) => p.name === projectName);
2296
+ if (project && project.sessions.length > 0) {
2297
+ node = project.sessions[0];
2298
+ } else {
2299
+ node = void 0;
2300
+ }
2301
+ }
2302
+ const newFile = node?.filePath ?? null;
2303
+ const fileChanged = lastLoadedFileRef.current !== newFile;
2304
+ lastLoadedFileRef.current = newFile;
1659
2305
  if (node?.filePath) {
1660
2306
  setActivities(parseSessionHistory(node.filePath));
1661
- setScrollOffset(0);
1662
- setIsLive(true);
1663
- setNewCount(0);
1664
- setViewerCursorLine(0);
2307
+ if (fileChanged) {
2308
+ setScrollOffset(0);
2309
+ setIsLive(true);
2310
+ setNewCount(0);
2311
+ setViewerCursorLine(0);
2312
+ setGitActivities([]);
2313
+ }
1665
2314
  } else {
1666
2315
  setActivities([]);
2316
+ if (fileChanged) setGitActivities([]);
1667
2317
  }
1668
- setGitActivities([]);
1669
- }, [selectedId]);
2318
+ }, [selectedId, sessionTree]);
1670
2319
  useEffect2(() => {
1671
2320
  setScrollOffset(0);
1672
2321
  setIsLive(true);
@@ -1677,7 +2326,7 @@ function App({ mode }) {
1677
2326
  const node = allFlatRef.current.find((s) => s.id === selectedId);
1678
2327
  if (!node?.projectPath) return;
1679
2328
  const load = () => {
1680
- const acts = node.filePath ? parseSessionHistory(node.filePath) : [];
2329
+ const acts = activitiesRef.current;
1681
2330
  const today = /* @__PURE__ */ new Date();
1682
2331
  const todayMidnight = new Date(
1683
2332
  today.getFullYear(),
@@ -1697,9 +2346,12 @@ function App({ mode }) {
1697
2346
  const commits = parseGitCommits(node.projectPath, startDate, endDate);
1698
2347
  setGitActivities(commits);
1699
2348
  };
1700
- load();
2349
+ const initial = setTimeout(load, 100);
1701
2350
  const timer = setInterval(load, 3e4);
1702
- return () => clearInterval(timer);
2351
+ return () => {
2352
+ clearTimeout(initial);
2353
+ clearInterval(timer);
2354
+ };
1703
2355
  }, [selectedId, isWatchMode]);
1704
2356
  const refresh = useCallback(() => {
1705
2357
  const freshConfig = loadGlobalConfig();
@@ -1707,7 +2359,8 @@ function App({ mode }) {
1707
2359
  const updatedFlat = flattenSessions2(tree, expandedIds);
1708
2360
  const node = updatedFlat.find((s) => s.id === selectedId);
1709
2361
  if (!node) {
1710
- const parentSession = tree.sessions.find(
2362
+ const allSessions = tree.projects?.flatMap((p) => p.sessions) ?? [];
2363
+ const parentSession = allSessions.find(
1711
2364
  (s) => s.subAgents.some((sa) => sa.id === selectedId)
1712
2365
  );
1713
2366
  if (parentSession) setSelectedId(parentSession.id);
@@ -1729,7 +2382,7 @@ function App({ mode }) {
1729
2382
  useEffect2(() => {
1730
2383
  if (!isWatchMode) return;
1731
2384
  const projectsDir = getProjectsDir();
1732
- const usePolling = process.platform === "linux" || !existsSync4(projectsDir);
2385
+ const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
1733
2386
  if (usePolling) {
1734
2387
  const timer = setInterval(
1735
2388
  () => refreshRef.current(),
@@ -1757,8 +2410,14 @@ function App({ mode }) {
1757
2410
  };
1758
2411
  }, [isWatchMode, config.refreshIntervalMs]);
1759
2412
  const filterPresets = config.filterPresets;
1760
- const activePreset = filterPresets[filterIndex % filterPresets.length] ?? [];
1761
- const filterLabel = activePreset.length === 0 ? "all" : activePreset.join("+");
2413
+ const activePreset = useMemo(
2414
+ () => filterPresets[filterIndex % filterPresets.length] ?? [],
2415
+ [filterPresets, filterIndex]
2416
+ );
2417
+ const filterLabel = useMemo(
2418
+ () => activePreset.length === 0 ? "all" : activePreset.join("+"),
2419
+ [activePreset]
2420
+ );
1762
2421
  const mergedActivities = useMemo(() => {
1763
2422
  const merged = [...activities, ...gitActivities].sort(
1764
2423
  (a, b) => a.timestamp.getTime() - b.timestamp.getTime()
@@ -1779,7 +2438,7 @@ function App({ mode }) {
1779
2438
  if (!activities.length || !selectedId) return;
1780
2439
  ensureLogDir(config.logDir);
1781
2440
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1782
- const filePath = join4(
2441
+ const filePath = join5(
1783
2442
  config.logDir,
1784
2443
  `${date}-${selectedId.slice(0, 8)}.txt`
1785
2444
  );
@@ -1796,6 +2455,8 @@ function App({ mode }) {
1796
2455
  const { handleInput, statusBarItems } = useHotkeys({
1797
2456
  focus,
1798
2457
  detailMode,
2458
+ helpMode,
2459
+ onHelp: () => setHelpMode((m) => !m),
1799
2460
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
1800
2461
  onScrollUp: () => {
1801
2462
  if (focus === "tree") {
@@ -1938,6 +2599,23 @@ function App({ mode }) {
1938
2599
  return;
1939
2600
  }
1940
2601
  if (focus !== "tree" || !selectedId) return;
2602
+ if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
2603
+ const projectName = selectedId.slice(7, -2);
2604
+ const isCold = sessionTree.coldProjects.some(
2605
+ (p) => p.name === projectName
2606
+ );
2607
+ const toggleKey = isCold ? `__expanded-${selectedId}` : `__collapsed-${selectedId}`;
2608
+ setExpandedIds((prev) => {
2609
+ const next = new Set(prev);
2610
+ if (next.has(toggleKey)) {
2611
+ next.delete(toggleKey);
2612
+ } else {
2613
+ next.add(toggleKey);
2614
+ }
2615
+ return next;
2616
+ });
2617
+ return;
2618
+ }
1941
2619
  if (selectedId === "__cold__") {
1942
2620
  setExpandedIds((prev) => {
1943
2621
  const next = new Set(prev);
@@ -1959,7 +2637,8 @@ function App({ mode }) {
1959
2637
  setSelectedId(parentId);
1960
2638
  } else {
1961
2639
  next.add(parentId);
1962
- const parent = sessionTree.sessions.find((s) => s.id === parentId);
2640
+ const allSessions2 = sessionTree.projects?.flatMap((p) => p.sessions) ?? [];
2641
+ const parent = allSessions2.find((s) => s.id === parentId);
1963
2642
  const firstNew = parent?.subAgents.find(
1964
2643
  (sa) => sa.status === "cool" || sa.status === "cold"
1965
2644
  );
@@ -1969,38 +2648,51 @@ function App({ mode }) {
1969
2648
  });
1970
2649
  return;
1971
2650
  }
1972
- const parentSession = sessionTree.sessions.find(
1973
- (s) => s.id === selectedId
1974
- );
1975
- if (!parentSession || !parentSession.subAgents.some(
1976
- (s) => s.status === "cool" || s.status === "cold"
1977
- ))
2651
+ const allSessions3 = [
2652
+ ...sessionTree.projects.flatMap((p) => p.sessions),
2653
+ ...sessionTree.coldProjects.flatMap((p) => p.sessions)
2654
+ ];
2655
+ const selectedSessionObj = allSessions3.find((s) => s.id === selectedId);
2656
+ if (selectedSessionObj && selectedSessionObj.subAgents.length > 0) {
2657
+ const isCold = selectedSessionObj.status === "cold";
2658
+ const toggleKey = isCold ? `__expanded-session-${selectedId}` : `__collapsed-session-${selectedId}`;
2659
+ setExpandedIds((prev) => {
2660
+ 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) {
2667
+ const firstSub = selectedSessionObj.subAgents[0];
2668
+ if (firstSub) setSelectedId(firstSub.id);
2669
+ }
2670
+ }
2671
+ return next;
2672
+ });
1978
2673
  return;
1979
- setExpandedIds((prev) => {
1980
- const next = new Set(prev);
1981
- if (next.has(selectedId)) {
1982
- next.delete(selectedId);
1983
- } else {
1984
- next.add(selectedId);
1985
- }
1986
- return next;
1987
- });
2674
+ }
1988
2675
  },
1989
2676
  onHide: () => {
1990
2677
  if (focus !== "tree" || !selectedId) return;
2678
+ if (selectedId.startsWith("__proj-") && selectedId.endsWith("__")) {
2679
+ const projectName = selectedId.slice(7, -2);
2680
+ hideProject(projectName);
2681
+ refresh();
2682
+ const nextId = allFlat[selectedIndex + 1]?.id ?? allFlat[selectedIndex - 1]?.id ?? null;
2683
+ setSelectedId(nextId);
2684
+ return;
2685
+ }
1991
2686
  if (selectedId === "__cold__") {
1992
- const coldSessions = sessionTree.sessions.filter(
1993
- (s) => s.status === "cold"
1994
- );
2687
+ const coldSessions = sessionTree.coldProjects?.flatMap((p) => p.sessions) ?? [];
1995
2688
  for (const s of coldSessions) hideSession(s.hideKey);
1996
2689
  const nextId = allFlat[selectedIndex - 1]?.id ?? null;
1997
2690
  refresh();
1998
2691
  setSelectedId(nextId);
1999
2692
  return;
2000
2693
  }
2001
- const selectedSession2 = sessionTree.sessions.find(
2002
- (s) => s.id === selectedId
2003
- );
2694
+ const allSessions4 = sessionTree.projects?.flatMap((p) => p.sessions) ?? [];
2695
+ const selectedSession2 = allSessions4.find((s) => s.id === selectedId);
2004
2696
  if (selectedSession2) {
2005
2697
  hideSession(selectedSession2.hideKey);
2006
2698
  const nextId = allFlat[selectedIndex + 1]?.id ?? allFlat[selectedIndex - 1]?.id ?? null;
@@ -2008,7 +2700,7 @@ function App({ mode }) {
2008
2700
  setSelectedId(nextId);
2009
2701
  return;
2010
2702
  }
2011
- for (const s of sessionTree.sessions) {
2703
+ for (const s of allSessions4) {
2012
2704
  const selectedSubAgent = s.subAgents.find((sa) => sa.id === selectedId);
2013
2705
  if (selectedSubAgent) {
2014
2706
  hideSubAgent(selectedSubAgent.hideKey);
@@ -2026,55 +2718,67 @@ function App({ mode }) {
2026
2718
  filterLabel
2027
2719
  });
2028
2720
  useInput((input, key) => handleInput(input, key), { isActive: isWatchMode });
2029
- const selectedSession = allFlat.find((s) => s.id === selectedId);
2721
+ const rawSelected = allFlat.find((s) => s.id === selectedId);
2722
+ const isProjectSentinel = !!selectedId && selectedId.startsWith("__proj-") && selectedId.endsWith("__");
2723
+ let selectedSession = rawSelected;
2724
+ if (isProjectSentinel && selectedId) {
2725
+ const projectName = selectedId.slice(7, -2);
2726
+ const project = sessionTree.projects.find((p) => p.name === projectName) ?? sessionTree.coldProjects.find((p) => p.name === projectName);
2727
+ if (project && project.sessions.length > 0) {
2728
+ selectedSession = project.sessions[0];
2729
+ }
2730
+ }
2030
2731
  const isPlaceholderSelected = !selectedSession || selectedId === "__cold__" || !!selectedId && selectedId.startsWith("__sub-") && selectedId.endsWith("__");
2031
2732
  const sessionDisplayName = isPlaceholderSelected ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
2032
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2033
- migrationWarning && /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
2034
- isWatchMode && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, justifyContent: "space-between", width, children: [
2035
- /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2733
+ 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: [
2036
2736
  spinner,
2037
2737
  " AgentHUD v",
2038
2738
  getVersion()
2039
2739
  ] }),
2040
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
2740
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
2041
2741
  ] }),
2042
- /* @__PURE__ */ jsx4(
2043
- SessionTreePanel,
2044
- {
2045
- sessions: sessionTree.sessions,
2046
- selectedId,
2047
- hasFocus: focus === "tree",
2048
- width,
2049
- maxRows: treeRows,
2050
- expandedIds
2051
- }
2052
- ),
2053
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx4(
2054
- DetailViewPanel,
2055
- {
2056
- activity: detailActivity,
2057
- sessionName: sessionDisplayName,
2058
- scrollOffset: detailScrollOffset,
2059
- visibleRows: viewerRows,
2060
- width
2061
- }
2062
- ) : /* @__PURE__ */ jsx4(
2063
- ActivityViewerPanel,
2064
- {
2065
- activities: mergedActivities,
2066
- sessionName: sessionDisplayName,
2067
- scrollOffset,
2068
- isLive,
2069
- newCount,
2070
- visibleRows: viewerRows,
2071
- width,
2072
- cursorLine: viewerCursorLine,
2073
- hasFocus: focus === "viewer",
2074
- spinner,
2075
- filterLabel
2076
- }
2077
- ) })
2742
+ helpMode ? /* @__PURE__ */ jsx5(HelpPanel, { width, height: height - 2 }) : /* @__PURE__ */ jsxs5(Fragment2, { children: [
2743
+ migrationWarning && /* @__PURE__ */ jsx5(Box5, { marginBottom: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
2744
+ /* @__PURE__ */ jsx5(
2745
+ SessionTreePanel,
2746
+ {
2747
+ projects: sessionTree.projects ?? [],
2748
+ coldProjects: sessionTree.coldProjects ?? [],
2749
+ selectedId,
2750
+ hasFocus: focus === "tree",
2751
+ width,
2752
+ maxRows: treeRows,
2753
+ expandedIds
2754
+ }
2755
+ ),
2756
+ /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx5(
2757
+ DetailViewPanel,
2758
+ {
2759
+ activity: detailActivity,
2760
+ sessionName: sessionDisplayName,
2761
+ scrollOffset: detailScrollOffset,
2762
+ visibleRows: viewerRows,
2763
+ width
2764
+ }
2765
+ ) : /* @__PURE__ */ jsx5(
2766
+ ActivityViewerPanel,
2767
+ {
2768
+ activities: mergedActivities,
2769
+ sessionName: sessionDisplayName,
2770
+ scrollOffset,
2771
+ isLive,
2772
+ newCount,
2773
+ visibleRows: viewerRows,
2774
+ width,
2775
+ cursorLine: viewerCursorLine,
2776
+ hasFocus: focus === "viewer",
2777
+ spinner,
2778
+ filterLabel
2779
+ }
2780
+ ) })
2781
+ ] })
2078
2782
  ] });
2079
2783
  }
2080
2784
 
@@ -2093,8 +2797,8 @@ if (options.command === "version") {
2093
2797
  console.log(getVersion());
2094
2798
  process.exit(0);
2095
2799
  }
2096
- var legacyConfig = join5(process.cwd(), ".agenthud", "config.yaml");
2097
- if (existsSync5(legacyConfig)) {
2800
+ var legacyConfig = join6(process.cwd(), ".agenthud", "config.yaml");
2801
+ if (existsSync6(legacyConfig)) {
2098
2802
  console.log(
2099
2803
  "The project-level config file (.agenthud/config.yaml) is no longer supported."
2100
2804
  );
@@ -2133,6 +2837,20 @@ if (options.mode === "report") {
2133
2837
  `);
2134
2838
  process.exit(0);
2135
2839
  }
2840
+ if (options.mode === "summary") {
2841
+ if (options.summaryError) {
2842
+ process.stderr.write(`agenthud: ${options.summaryError}
2843
+ `);
2844
+ process.exit(1);
2845
+ }
2846
+ const exitCode = await runSummary({
2847
+ date: options.summaryDate,
2848
+ prompt: options.summaryPrompt,
2849
+ force: options.summaryForce ?? false,
2850
+ today: /* @__PURE__ */ new Date()
2851
+ });
2852
+ process.exit(exitCode);
2853
+ }
2136
2854
  if (options.mode === "watch") {
2137
2855
  clearScreen();
2138
2856
  }