agenthud 0.8.5 → 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.
@@ -247,12 +247,14 @@ import { homedir } from "os";
247
247
  import { join as join2 } from "path";
248
248
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
249
249
  var CONFIG_PATH = join2(homedir(), ".agenthud", "config.yaml");
250
+ var STATE_PATH = join2(homedir(), ".agenthud", "state.yaml");
250
251
  var DEFAULT_GLOBAL_CONFIG = {
251
252
  refreshIntervalMs: 2e3,
252
253
  logDir: join2(homedir(), ".agenthud", "logs"),
253
254
  hiddenSessions: [],
254
255
  hiddenSubAgents: [],
255
- filterPresets: [[], ["response"], ["commit"]]
256
+ filterPresets: [[], ["response"], ["commit"]],
257
+ hiddenProjects: []
256
258
  };
257
259
  function parseInterval(value) {
258
260
  const match = value.match(/^(\d+)(s|m)$/);
@@ -260,76 +262,175 @@ function parseInterval(value) {
260
262
  const n = parseInt(match[1], 10);
261
263
  return match[2] === "m" ? n * 60 * 1e3 : n * 1e3;
262
264
  }
263
- function loadGlobalConfig() {
264
- const config = { ...DEFAULT_GLOBAL_CONFIG };
265
- if (!existsSync(CONFIG_PATH)) {
266
- return config;
267
- }
268
- 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
+ `;
269
287
  try {
270
- raw = readFileSync2(CONFIG_PATH, "utf-8");
288
+ writeFileSync(CONFIG_PATH, defaultYaml, "utf-8");
271
289
  } catch {
272
- return config;
273
290
  }
274
- let parsed;
291
+ }
292
+ function writeState(state) {
293
+ ensureAgenthudDir();
275
294
  try {
276
- parsed = parseYaml(raw) ?? {};
295
+ writeFileSync(STATE_PATH, stringifyYaml(state), "utf-8");
277
296
  } catch {
278
- return config;
279
297
  }
280
- if (typeof parsed.refreshInterval === "string") {
281
- const ms = parseInterval(parsed.refreshInterval);
282
- 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;
283
305
  }
284
- if (typeof parsed.logDir === "string") {
285
- config.logDir = parsed.logDir.replace(/^~/, homedir());
306
+ try {
307
+ writeFileSync(CONFIG_PATH, stringifyYaml(cleaned), "utf-8");
308
+ } catch {
286
309
  }
287
- if (Array.isArray(parsed.hiddenSessions)) {
288
- config.hiddenSessions = parsed.hiddenSessions.filter(
289
- (s) => typeof s === "string"
290
- );
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();
291
324
  }
292
- if (Array.isArray(parsed.hiddenSubAgents)) {
293
- config.hiddenSubAgents = parsed.hiddenSubAgents.filter(
294
- (s) => typeof s === "string"
295
- );
325
+ if (typeof configRaw.refreshInterval === "string") {
326
+ const ms = parseInterval(configRaw.refreshInterval);
327
+ if (ms !== null) config.refreshIntervalMs = ms;
328
+ }
329
+ if (typeof configRaw.logDir === "string") {
330
+ config.logDir = configRaw.logDir.replace(/^~/, homedir());
296
331
  }
297
- if (Array.isArray(parsed.filterPresets)) {
298
- const presets = parsed.filterPresets.filter(Array.isArray).map(
332
+ if (Array.isArray(configRaw.filterPresets)) {
333
+ const presets = configRaw.filterPresets.filter(Array.isArray).map(
299
334
  (p) => p.filter((t) => typeof t === "string")
300
335
  );
301
336
  if (presets.length > 0) config.filterPresets = presets;
302
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
+ }
303
383
  return config;
304
384
  }
305
- function writeConfig(updates) {
306
- const configDir = join2(homedir(), ".agenthud");
307
- if (!existsSync(configDir)) {
308
- mkdirSync(configDir, { recursive: true });
309
- }
310
- let raw = {};
311
- if (existsSync(CONFIG_PATH)) {
385
+ function updateState(updates) {
386
+ let state = {
387
+ hiddenSessions: [],
388
+ hiddenSubAgents: [],
389
+ hiddenProjects: []
390
+ };
391
+ if (existsSync(STATE_PATH)) {
312
392
  try {
313
- 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
+ }
314
406
  } catch {
315
- raw = {};
316
407
  }
317
408
  }
318
- if (updates.hiddenSessions !== void 0)
319
- raw.hiddenSessions = updates.hiddenSessions;
320
- if (updates.hiddenSubAgents !== void 0)
321
- raw.hiddenSubAgents = updates.hiddenSubAgents;
322
- 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);
323
419
  }
324
420
  function hideSession(id) {
325
421
  const config = loadGlobalConfig();
326
422
  if (config.hiddenSessions.includes(id)) return;
327
- writeConfig({ hiddenSessions: [...config.hiddenSessions, id] });
423
+ updateState({ hiddenSessions: [...config.hiddenSessions, id] });
328
424
  }
329
425
  function hideSubAgent(id) {
330
426
  const config = loadGlobalConfig();
331
427
  if (config.hiddenSubAgents.includes(id)) return;
332
- 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] });
333
434
  }
334
435
  function ensureLogDir(logDir) {
335
436
  if (!existsSync(logDir)) {
@@ -432,7 +533,7 @@ function parseModelName(modelId) {
432
533
  }
433
534
  function getToolDetail(_toolName, input) {
434
535
  if (!input) return "";
435
- if (input.command) return stripAnsi(input.command.replace(/\n/g, " "));
536
+ if (input.command) return stripAnsi(input.command);
436
537
  if (input.file_path) return basename(input.file_path);
437
538
  if (input.pattern) return stripAnsi(input.pattern);
438
539
  if (input.query) return stripAnsi(input.query);
@@ -471,7 +572,7 @@ function parseActivitiesFromLines(lines) {
471
572
  type: "user",
472
573
  icon: ICONS.User,
473
574
  label: "User",
474
- detail: userText.replace(/\n/g, " ")
575
+ detail: userText
475
576
  });
476
577
  }
477
578
  }
@@ -493,7 +594,7 @@ function parseActivitiesFromLines(lines) {
493
594
  type: "thinking",
494
595
  icon: ICONS.Thinking,
495
596
  label: "Thinking",
496
- detail: block.thinking.replace(/\n/g, " ")
597
+ detail: block.thinking
497
598
  });
498
599
  } else if (block.type === "tool_use" && block.name) {
499
600
  if (block.name === "TodoWrite") continue;
@@ -518,7 +619,7 @@ function parseActivitiesFromLines(lines) {
518
619
  type: "response",
519
620
  icon: ICONS.Response,
520
621
  label: "Response",
521
- detail: block.text.replace(/\n/g, " ")
622
+ detail: block.text
522
623
  });
523
624
  }
524
625
  }
@@ -581,7 +682,15 @@ function sessionIsOnDate(session, date, activities) {
581
682
  }
582
683
  return activities.some((a) => isSameLocalDay(a.timestamp, date));
583
684
  }
685
+ function flattenForOneLine(s) {
686
+ return s.replace(/[\r\n\t]+/g, " ").trim();
687
+ }
584
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) {
585
694
  if (limit === 0 || detail.length <= limit) return detail;
586
695
  return detail.slice(0, limit);
587
696
  }
@@ -628,7 +737,7 @@ function generateReport(sessions, options2) {
628
737
  time: formatTime(a.timestamp),
629
738
  icon: a.icon,
630
739
  label: a.label,
631
- detail: truncateDetail(a.detail, detailLimit)
740
+ detail: truncateRaw(a.detail, detailLimit)
632
741
  }))
633
742
  };
634
743
  });
@@ -640,7 +749,7 @@ function generateReport(sessions, options2) {
640
749
  time: formatTime(a.timestamp),
641
750
  icon: a.icon,
642
751
  label: a.label,
643
- detail: truncateDetail(a.detail, detailLimit)
752
+ detail: truncateRaw(a.detail, detailLimit)
644
753
  })),
645
754
  subAgents: subAgentBlocks
646
755
  };
@@ -723,7 +832,14 @@ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
723
832
  return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
724
833
  }
725
834
  var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
726
- 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
+ }
727
843
 
728
844
  // src/data/sessions.ts
729
845
  function getProjectsDir() {
@@ -790,6 +906,71 @@ function readModelName(filePath) {
790
906
  }
791
907
  return null;
792
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
+ }
793
974
  function buildSubAgents(parentId, projectDir, config, projectName) {
794
975
  const subagentsDir = join3(projectDir, parentId, "subagents");
795
976
  if (!existsSync3(subagentsDir)) return [];
@@ -819,7 +1000,9 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
819
1000
  modelName: readModelName(filePath),
820
1001
  subAgents: [],
821
1002
  agentId: agentId ?? void 0,
822
- taskDescription: taskDescription ?? void 0
1003
+ taskDescription: taskDescription ?? void 0,
1004
+ nonInteractive: false,
1005
+ firstUserPrompt: null
823
1006
  };
824
1007
  } catch {
825
1008
  return null;
@@ -831,7 +1014,12 @@ function buildSubAgents(parentId, projectDir, config, projectName) {
831
1014
  function discoverSessions(config) {
832
1015
  const projectsDir = getProjectsDir();
833
1016
  if (!existsSync3(projectsDir)) {
834
- 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
+ };
835
1023
  }
836
1024
  let projectDirs;
837
1025
  try {
@@ -843,7 +1031,12 @@ function discoverSessions(config) {
843
1031
  }
844
1032
  });
845
1033
  } catch {
846
- 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
+ };
847
1040
  }
848
1041
  const allSessions = [];
849
1042
  for (const encodedDir of projectDirs) {
@@ -874,37 +1067,71 @@ function discoverSessions(config) {
874
1067
  lastModifiedMs: stat.mtimeMs,
875
1068
  status: getSessionStatus(stat.mtimeMs),
876
1069
  modelName: readModelName(filePath),
877
- subAgents
1070
+ subAgents,
1071
+ nonInteractive: readEntrypoint(filePath) === "sdk-cli",
1072
+ firstUserPrompt: readFirstUserPrompt(filePath)
878
1073
  });
879
1074
  } catch {
880
1075
  }
881
1076
  }
882
1077
  }
883
- allSessions.sort((a, b) => {
884
- const statusOrder = {
885
- hot: 0,
886
- warm: 1,
887
- cool: 2,
888
- cold: 3
889
- };
890
- 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];
891
1111
  if (statusDiff !== 0) return statusDiff;
892
- return b.lastModifiedMs - a.lastModifiedMs;
1112
+ return b.sessions[0].lastModifiedMs - a.sessions[0].lastModifiedMs;
893
1113
  });
894
- const visible = allSessions.filter(
895
- (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
896
1117
  );
897
- const totalCount = visible.length + visible.reduce((sum, s) => sum + s.subAgents.length, 0);
1118
+ const totalCount = countSessions(activeProjects) + countSessions(coldProjects);
898
1119
  return {
899
- sessions: visible,
1120
+ projects: activeProjects,
1121
+ coldProjects,
900
1122
  totalCount,
901
1123
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
902
1124
  };
903
1125
  }
904
1126
 
905
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
+ }
906
1133
  function summariesDir() {
907
- const dir = join4(homedir3(), ".agenthud", "summaries");
1134
+ const dir = join4(agenthudHomeDir(), "summaries");
908
1135
  if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
909
1136
  return dir;
910
1137
  }
@@ -974,7 +1201,8 @@ async function runSummary(options2) {
974
1201
  const prompt = resolvePrompt(options2.prompt);
975
1202
  return new Promise((resolve) => {
976
1203
  const proc = spawn("claude", ["-p", prompt], {
977
- stdio: ["pipe", "pipe", "pipe"]
1204
+ stdio: ["pipe", "pipe", "pipe"],
1205
+ cwd: agenthudHomeDir()
978
1206
  });
979
1207
  let cacheStream = null;
980
1208
  cacheStream = createWriteStream(cached, { encoding: "utf-8" });
@@ -1025,7 +1253,7 @@ async function runSummary(options2) {
1025
1253
  // src/ui/App.tsx
1026
1254
  import { existsSync as existsSync5, watch, writeFileSync as writeFileSync2 } from "fs";
1027
1255
  import { join as join5 } from "path";
1028
- import { Box as Box4, Text as Text4, useApp, useInput, useStdout } from "ink";
1256
+ import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
1029
1257
  import { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
1030
1258
 
1031
1259
  // src/ui/ActivityViewerPanel.tsx
@@ -1063,20 +1291,21 @@ function formatActivityTime(date, now) {
1063
1291
  const day = String(date.getDate()).padStart(2, "0");
1064
1292
  return `${month}/${day} ${time}`;
1065
1293
  }
1294
+ function flattenForOneLine2(detail) {
1295
+ return detail.replace(/[\r\n\t]+/g, " ").trim();
1296
+ }
1066
1297
  function truncateDetail2(detail, maxWidth) {
1067
- if (getDisplayWidth(detail) <= maxWidth) return detail;
1298
+ const flat = flattenForOneLine2(detail);
1299
+ if (getDisplayWidth(flat) <= maxWidth) return flat;
1068
1300
  let truncated = "";
1069
- let currentWidth = 0;
1070
- for (const char of detail) {
1301
+ let width = 0;
1302
+ for (const char of flat) {
1071
1303
  const charWidth = getDisplayWidth(char);
1072
- if (currentWidth + charWidth > maxWidth - 3) {
1073
- truncated += "...";
1074
- break;
1075
- }
1304
+ if (width + charWidth > maxWidth - 1) break;
1076
1305
  truncated += char;
1077
- currentWidth += charWidth;
1306
+ width += charWidth;
1078
1307
  }
1079
- return truncated;
1308
+ return `${truncated}\u2026`;
1080
1309
  }
1081
1310
  function ActivityViewerPanel({
1082
1311
  activities,
@@ -1279,10 +1508,114 @@ function DetailViewPanel({
1279
1508
  ] });
1280
1509
  }
1281
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
+
1282
1614
  // src/ui/hooks/useHotkeys.ts
1283
1615
  function useHotkeys({
1284
1616
  focus,
1285
1617
  detailMode,
1618
+ helpMode,
1286
1619
  onSwitchFocus,
1287
1620
  onScrollUp,
1288
1621
  onScrollDown,
@@ -1301,9 +1634,21 @@ function useHotkeys({
1301
1634
  onDetailScrollUp,
1302
1635
  onDetailScrollDown,
1303
1636
  onFilter,
1637
+ onHelp,
1304
1638
  filterLabel
1305
1639
  }) {
1306
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
+ }
1307
1652
  if (detailMode) {
1308
1653
  if (key.upArrow || input === "k") {
1309
1654
  onDetailScrollUp();
@@ -1402,13 +1747,14 @@ function useHotkeys({
1402
1747
  }
1403
1748
  }
1404
1749
  };
1405
- 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" ? [
1406
1751
  "Tab: viewer",
1407
1752
  "\u2191\u2193/jk: select",
1408
1753
  "PgUp/Dn: page",
1409
1754
  "\u21B5: expand",
1410
1755
  "h: hide",
1411
1756
  "r: refresh",
1757
+ "?: help",
1412
1758
  "q: quit"
1413
1759
  ] : [
1414
1760
  "Tab: sessions",
@@ -1418,6 +1764,7 @@ function useHotkeys({
1418
1764
  "G: oldest",
1419
1765
  "\u21B5: detail",
1420
1766
  `f: ${filterLabel}`,
1767
+ "?: help",
1421
1768
  "q: quit"
1422
1769
  ];
1423
1770
  return { handleInput, statusBarItems };
@@ -1440,8 +1787,8 @@ function useSpinner(active, intervalMs = 100) {
1440
1787
 
1441
1788
  // src/ui/SessionTreePanel.tsx
1442
1789
  import { homedir as homedir4 } from "os";
1443
- import { Box as Box3, Text as Text3 } from "ink";
1444
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1790
+ import { Box as Box4, Text as Text4 } from "ink";
1791
+ import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
1445
1792
  function formatElapsed(lastModifiedMs) {
1446
1793
  const elapsed = Date.now() - lastModifiedMs;
1447
1794
  const seconds = Math.floor(elapsed / 1e3);
@@ -1481,23 +1828,24 @@ function SessionRow({
1481
1828
  prefix,
1482
1829
  contentWidth
1483
1830
  }) {
1484
- const isParent = prefix === "";
1831
+ const isParent = prefix === " ";
1485
1832
  const statusColor = getStatusColor(session.status);
1486
1833
  const badge = `[${session.status}]`;
1487
1834
  const elapsed = formatElapsed(session.lastModifiedMs);
1488
1835
  const model = session.modelName ?? "";
1489
- const name = isParent ? session.projectName || session.id.slice(0, 8) : session.agentId ?? session.id.slice(0, 8);
1490
- 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 = "";
1491
1839
  const rightParts = [elapsed];
1492
1840
  if (model) rightParts.push(model);
1493
1841
  const rightSide = rightParts.join(" ");
1494
- const leftCore = `${prefix}${name}${shortId} ${badge}`;
1495
- const leftCoreWidth = getDisplayWidth(leftCore);
1842
+ const leftCoreBase = `${prefix}${rawName}${shortIdDisplay} ${badge}`;
1843
+ const leftCoreWidth = getDisplayWidth(leftCoreBase);
1496
1844
  const rightWidth = getDisplayWidth(rightSide);
1497
1845
  const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - 1;
1498
1846
  let middleText = "";
1499
1847
  if (middleAvailable > 3) {
1500
- const raw = isParent ? session.projectPath ? formatProjectPath(session.projectPath) : "" : session.taskDescription ?? "";
1848
+ const raw = isParent ? session.firstUserPrompt ?? "" : session.taskDescription ?? "";
1501
1849
  if (raw) {
1502
1850
  const truncated = truncatePath(raw, middleAvailable);
1503
1851
  if (truncated) middleText = truncated;
@@ -1509,42 +1857,56 @@ function SessionRow({
1509
1857
  contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
1510
1858
  );
1511
1859
  const gap = " ".repeat(gapWidth);
1512
- const fullLine = leftCore + middleSection + gap + rightSide;
1860
+ const fullLine = leftCoreBase + middleSection + gap + rightSide;
1513
1861
  const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
1514
1862
  const highlight = isSelected && hasFocus;
1515
- return /* @__PURE__ */ jsxs3(Text3, { children: [
1863
+ const shouldDim = isNonInteractive;
1864
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1516
1865
  BOX.v,
1517
1866
  " ",
1518
- /* @__PURE__ */ jsxs3(Text3, { backgroundColor: highlight ? "blue" : void 0, bold: highlight, children: [
1519
- /* @__PURE__ */ jsx3(Text3, { children: prefix }),
1520
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: name }),
1521
- shortId ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: shortId }) : null,
1522
- /* @__PURE__ */ jsx3(Text3, { children: " " }),
1523
- /* @__PURE__ */ jsx3(Text3, { color: statusColor, children: badge }),
1524
- middleText ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: middleSection }) : null,
1525
- /* @__PURE__ */ jsx3(Text3, { children: gap }),
1526
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: elapsed }),
1527
- model ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: ` ${model}` }) : null
1528
- ] }),
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
+ ),
1529
1886
  " ".repeat(linePadding),
1530
1887
  BOX.v
1531
1888
  ] });
1532
1889
  }
1533
1890
  function appendSessionRows(result, session, expandedIds) {
1534
- 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);
1535
1897
  const hotWarm = session.subAgents.filter(
1536
1898
  (s) => s.status === "hot" || s.status === "warm"
1537
1899
  );
1538
1900
  const cool = session.subAgents.filter((s) => s.status === "cool");
1539
1901
  const cold = session.subAgents.filter((s) => s.status === "cold");
1540
- if (isExpanded) {
1902
+ if (subAgentsFullyExpanded) {
1541
1903
  const all = [...hotWarm, ...cool, ...cold];
1542
1904
  for (let i = 0; i < all.length; i++) {
1543
1905
  const isLast = i === all.length - 1;
1544
1906
  result.push({
1545
1907
  kind: "session",
1546
1908
  session: all[i],
1547
- prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1909
+ prefix: ` ${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1548
1910
  });
1549
1911
  }
1550
1912
  } else {
@@ -1554,7 +1916,7 @@ function appendSessionRows(result, session, expandedIds) {
1554
1916
  result.push({
1555
1917
  kind: "session",
1556
1918
  session: hotWarm[i],
1557
- prefix: `${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1919
+ prefix: ` ${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}\xBB `
1558
1920
  });
1559
1921
  }
1560
1922
  if (hasSummary) {
@@ -1567,25 +1929,65 @@ function appendSessionRows(result, session, expandedIds) {
1567
1929
  }
1568
1930
  }
1569
1931
  }
1570
- function flattenSessions(sessions, expandedIds) {
1932
+ function flattenSessions(projects, coldProjects, expandedIds) {
1571
1933
  const result = [];
1572
- const visibleSessions = sessions.filter((s) => s.status !== "cold");
1573
- const coldSessions = sessions.filter((s) => s.status === "cold");
1574
- for (const session of visibleSessions) {
1575
- result.push({ kind: "session", session, prefix: "" });
1576
- appendSessionRows(result, session, expandedIds);
1577
- }
1578
- if (coldSessions.length > 0) {
1579
- result.push({ kind: "cold-sessions-summary", count: coldSessions.length });
1580
- if (expandedIds.has("__cold__")) {
1581
- for (const session of coldSessions) {
1582
- 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: " " });
1583
1941
  appendSessionRows(result, session, expandedIds);
1584
1942
  }
1585
1943
  }
1586
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
+ }
1587
1961
  return result;
1588
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
+ }
1589
1991
  function SubagentSummaryRow({
1590
1992
  coolCount,
1591
1993
  coldCount,
@@ -1597,16 +1999,16 @@ function SubagentSummaryRow({
1597
1999
  if (coolCount > 0) parts.push(`${coolCount} cool`);
1598
2000
  if (coldCount > 0) parts.push(`${coldCount} cold`);
1599
2001
  const hint = " +";
1600
- const text = `\u2514\u2500 ... ${parts.join(" ")}`;
2002
+ const text = ` \u2514\u2500 ... ${parts.join(" ")}`;
1601
2003
  const padding = Math.max(
1602
2004
  0,
1603
2005
  contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
1604
2006
  );
1605
2007
  const active = isSelected && hasFocus;
1606
- return /* @__PURE__ */ jsxs3(Text3, { children: [
2008
+ return /* @__PURE__ */ jsxs4(Text4, { children: [
1607
2009
  BOX.v,
1608
2010
  " ",
1609
- /* @__PURE__ */ jsxs3(Text3, { dimColor: !active, inverse: active, children: [
2011
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: !active, inverse: active, children: [
1610
2012
  text,
1611
2013
  " ".repeat(padding),
1612
2014
  hint
@@ -1614,7 +2016,7 @@ function SubagentSummaryRow({
1614
2016
  BOX.v
1615
2017
  ] });
1616
2018
  }
1617
- function ColdSessionsSummaryRow({
2019
+ function ColdProjectsSummaryRow({
1618
2020
  count,
1619
2021
  isSelected,
1620
2022
  hasFocus,
@@ -1629,8 +2031,8 @@ function ColdSessionsSummaryRow({
1629
2031
  const dashes = BOX.h.repeat(dashCount);
1630
2032
  const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
1631
2033
  const highlight = isSelected && hasFocus;
1632
- return /* @__PURE__ */ jsx3(
1633
- Text3,
2034
+ return /* @__PURE__ */ jsx4(
2035
+ Text4,
1634
2036
  {
1635
2037
  backgroundColor: highlight ? "blue" : void 0,
1636
2038
  bold: highlight,
@@ -1640,7 +2042,8 @@ function ColdSessionsSummaryRow({
1640
2042
  );
1641
2043
  }
1642
2044
  function SessionTreePanel({
1643
- sessions,
2045
+ projects,
2046
+ coldProjects,
1644
2047
  selectedId,
1645
2048
  hasFocus,
1646
2049
  width = DEFAULT_PANEL_WIDTH,
@@ -1651,28 +2054,30 @@ function SessionTreePanel({
1651
2054
  const contentWidth = innerWidth - 1;
1652
2055
  const titleLine = createTitleLine("Sessions", "", width);
1653
2056
  const bottomLine = createBottomLine(width);
1654
- if (sessions.length === 0) {
2057
+ const totalProjectCount = projects.length + coldProjects.length;
2058
+ if (totalProjectCount === 0) {
1655
2059
  const emptyText = "No Claude sessions";
1656
2060
  const emptyPadding = Math.max(0, contentWidth - emptyText.length);
1657
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1658
- /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
1659
- /* @__PURE__ */ jsxs3(Text3, { children: [
2061
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
2062
+ /* @__PURE__ */ jsx4(Text4, { children: titleLine }),
2063
+ /* @__PURE__ */ jsxs4(Text4, { children: [
1660
2064
  BOX.v,
1661
2065
  " ",
1662
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: emptyText }),
2066
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: emptyText }),
1663
2067
  " ".repeat(emptyPadding),
1664
2068
  BOX.v
1665
2069
  ] }),
1666
- /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
2070
+ /* @__PURE__ */ jsx4(Text4, { children: bottomLine })
1667
2071
  ] });
1668
2072
  }
1669
- const flatRows = flattenSessions(sessions, expandedIds);
2073
+ const flatRows = flattenSessions(projects, coldProjects, expandedIds);
1670
2074
  const totalRows = flatRows.length;
1671
2075
  const selectedFlatIndex = flatRows.findIndex((row) => {
2076
+ if (row.kind === "project") return selectedId === row.sentinelId;
1672
2077
  if (row.kind === "session") return row.session.id === selectedId;
1673
2078
  if (row.kind === "subagent-summary")
1674
2079
  return selectedId === `__sub-${row.parentId}__`;
1675
- if (row.kind === "cold-sessions-summary") return selectedId === "__cold__";
2080
+ if (row.kind === "cold-projects-summary") return selectedId === "__cold__";
1676
2081
  return false;
1677
2082
  });
1678
2083
  const needsOverflow = maxRows !== void 0 && totalRows > maxRows;
@@ -1684,10 +2089,19 @@ function SessionTreePanel({
1684
2089
  }
1685
2090
  const displayRows = flatRows.slice(scrollTop, scrollTop + visibleCount);
1686
2091
  const hiddenBelow = totalRows - (scrollTop + displayRows.length);
1687
- return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", width, children: [
1688
- /* @__PURE__ */ jsx3(Text3, { children: titleLine }),
2092
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", width, children: [
2093
+ /* @__PURE__ */ jsx4(Text4, { children: titleLine }),
1689
2094
  displayRows.map(
1690
- (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(
1691
2105
  SessionRow,
1692
2106
  {
1693
2107
  session: row.session,
@@ -1697,7 +2111,7 @@ function SessionTreePanel({
1697
2111
  contentWidth
1698
2112
  },
1699
2113
  `${row.session.id}-${idx}`
1700
- ) : row.kind === "subagent-summary" ? /* @__PURE__ */ jsx3(
2114
+ ) : row.kind === "subagent-summary" ? /* @__PURE__ */ jsx4(
1701
2115
  SubagentSummaryRow,
1702
2116
  {
1703
2117
  coolCount: row.coolCount,
@@ -1707,8 +2121,8 @@ function SessionTreePanel({
1707
2121
  hasFocus
1708
2122
  },
1709
2123
  `subagent-summary-${idx}`
1710
- ) : /* @__PURE__ */ jsx3(
1711
- ColdSessionsSummaryRow,
2124
+ ) : /* @__PURE__ */ jsx4(
2125
+ ColdProjectsSummaryRow,
1712
2126
  {
1713
2127
  count: row.count,
1714
2128
  isSelected: selectedId === "__cold__",
@@ -1718,35 +2132,42 @@ function SessionTreePanel({
1718
2132
  "cold-summary"
1719
2133
  )
1720
2134
  ),
1721
- hiddenBelow > 0 && /* @__PURE__ */ jsxs3(Text3, { children: [
2135
+ hiddenBelow > 0 && /* @__PURE__ */ jsxs4(Text4, { children: [
1722
2136
  BOX.v,
1723
2137
  " ",
1724
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: `... ${hiddenBelow} more` }),
2138
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: `... ${hiddenBelow} more` }),
1725
2139
  " ".repeat(
1726
2140
  Math.max(0, contentWidth - `... ${hiddenBelow} more`.length - 1)
1727
2141
  ),
1728
2142
  BOX.v
1729
2143
  ] }),
1730
- /* @__PURE__ */ jsx3(Text3, { children: bottomLine })
2144
+ /* @__PURE__ */ jsx4(Text4, { children: bottomLine })
1731
2145
  ] });
1732
2146
  }
1733
2147
 
1734
2148
  // src/ui/App.tsx
1735
- 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";
1736
2150
  var VIEWER_HEIGHT_FRACTION = 0.55;
1737
2151
  function subSummarySentinel(parentId) {
1738
2152
  return {
1739
2153
  id: `__sub-${parentId}__`,
2154
+ hideKey: "",
1740
2155
  filePath: "",
1741
2156
  projectPath: "",
1742
2157
  projectName: "",
1743
2158
  lastModifiedMs: 0,
1744
2159
  status: "cold",
1745
2160
  modelName: null,
1746
- subAgents: []
2161
+ subAgents: [],
2162
+ nonInteractive: false
1747
2163
  };
1748
2164
  }
1749
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;
1750
2171
  if (expandedIds.has(session.id)) {
1751
2172
  result.push(...session.subAgents);
1752
2173
  } else {
@@ -1764,27 +2185,47 @@ function appendSubAgentRows(result, session, expandedIds) {
1764
2185
  }
1765
2186
  function flattenSessions2(tree, expandedIds) {
1766
2187
  const result = [];
1767
- const visible = tree.sessions.filter((s) => s.status !== "cold");
1768
- const cold = tree.sessions.filter((s) => s.status === "cold");
1769
- for (const s of visible) {
1770
- result.push(s);
1771
- 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);
1772
2212
  }
1773
- if (cold.length > 0) {
2213
+ if (tree.coldProjects.length > 0) {
1774
2214
  result.push({
1775
2215
  id: "__cold__",
2216
+ hideKey: "",
1776
2217
  filePath: "",
1777
2218
  projectPath: "",
1778
- projectName: `${cold.length} cold`,
2219
+ projectName: `${tree.coldProjects.length} cold`,
1779
2220
  lastModifiedMs: 0,
1780
2221
  status: "cold",
1781
2222
  modelName: null,
1782
- subAgents: []
2223
+ subAgents: [],
2224
+ nonInteractive: false
1783
2225
  });
1784
2226
  if (expandedIds.has("__cold__")) {
1785
- for (const s of cold) {
1786
- result.push(s);
1787
- appendSubAgentRows(result, s, expandedIds);
2227
+ for (const project of tree.coldProjects) {
2228
+ projectToFlat(project, true);
1788
2229
  }
1789
2230
  }
1790
2231
  }
@@ -1813,8 +2254,9 @@ function App({ mode }) {
1813
2254
  () => discoverSessions(config)
1814
2255
  );
1815
2256
  const [selectedId, setSelectedId] = useState2(() => {
1816
- const first = sessionTree.sessions[0];
1817
- return first?.id ?? null;
2257
+ const firstProject = sessionTree.projects[0];
2258
+ if (firstProject) return `__proj-${firstProject.name}__`;
2259
+ return null;
1818
2260
  });
1819
2261
  const [focus, setFocus] = useState2("tree");
1820
2262
  const [scrollOffset, setScrollOffset] = useState2(0);
@@ -1830,6 +2272,7 @@ function App({ mode }) {
1830
2272
  );
1831
2273
  const [detailScrollOffset, setDetailScrollOffset] = useState2(0);
1832
2274
  const [filterIndex, setFilterIndex] = useState2(0);
2275
+ const [helpMode, setHelpMode] = useState2(false);
1833
2276
  const allFlat = useMemo(
1834
2277
  () => flattenSessions2(sessionTree, expandedIds),
1835
2278
  [sessionTree, expandedIds]
@@ -1844,19 +2287,35 @@ function App({ mode }) {
1844
2287
  activitiesLengthRef.current = activities.length;
1845
2288
  activitiesRef.current = activities;
1846
2289
  }, [activities]);
2290
+ const lastLoadedFileRef = useRef(null);
1847
2291
  useEffect2(() => {
1848
- 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;
1849
2305
  if (node?.filePath) {
1850
2306
  setActivities(parseSessionHistory(node.filePath));
1851
- setScrollOffset(0);
1852
- setIsLive(true);
1853
- setNewCount(0);
1854
- setViewerCursorLine(0);
2307
+ if (fileChanged) {
2308
+ setScrollOffset(0);
2309
+ setIsLive(true);
2310
+ setNewCount(0);
2311
+ setViewerCursorLine(0);
2312
+ setGitActivities([]);
2313
+ }
1855
2314
  } else {
1856
2315
  setActivities([]);
2316
+ if (fileChanged) setGitActivities([]);
1857
2317
  }
1858
- setGitActivities([]);
1859
- }, [selectedId]);
2318
+ }, [selectedId, sessionTree]);
1860
2319
  useEffect2(() => {
1861
2320
  setScrollOffset(0);
1862
2321
  setIsLive(true);
@@ -1900,7 +2359,8 @@ function App({ mode }) {
1900
2359
  const updatedFlat = flattenSessions2(tree, expandedIds);
1901
2360
  const node = updatedFlat.find((s) => s.id === selectedId);
1902
2361
  if (!node) {
1903
- const parentSession = tree.sessions.find(
2362
+ const allSessions = tree.projects?.flatMap((p) => p.sessions) ?? [];
2363
+ const parentSession = allSessions.find(
1904
2364
  (s) => s.subAgents.some((sa) => sa.id === selectedId)
1905
2365
  );
1906
2366
  if (parentSession) setSelectedId(parentSession.id);
@@ -1995,6 +2455,8 @@ function App({ mode }) {
1995
2455
  const { handleInput, statusBarItems } = useHotkeys({
1996
2456
  focus,
1997
2457
  detailMode,
2458
+ helpMode,
2459
+ onHelp: () => setHelpMode((m) => !m),
1998
2460
  onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
1999
2461
  onScrollUp: () => {
2000
2462
  if (focus === "tree") {
@@ -2137,6 +2599,23 @@ function App({ mode }) {
2137
2599
  return;
2138
2600
  }
2139
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
+ }
2140
2619
  if (selectedId === "__cold__") {
2141
2620
  setExpandedIds((prev) => {
2142
2621
  const next = new Set(prev);
@@ -2158,7 +2637,8 @@ function App({ mode }) {
2158
2637
  setSelectedId(parentId);
2159
2638
  } else {
2160
2639
  next.add(parentId);
2161
- 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);
2162
2642
  const firstNew = parent?.subAgents.find(
2163
2643
  (sa) => sa.status === "cool" || sa.status === "cold"
2164
2644
  );
@@ -2168,38 +2648,51 @@ function App({ mode }) {
2168
2648
  });
2169
2649
  return;
2170
2650
  }
2171
- const parentSession = sessionTree.sessions.find(
2172
- (s) => s.id === selectedId
2173
- );
2174
- if (!parentSession || !parentSession.subAgents.some(
2175
- (s) => s.status === "cool" || s.status === "cold"
2176
- ))
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
+ });
2177
2673
  return;
2178
- setExpandedIds((prev) => {
2179
- const next = new Set(prev);
2180
- if (next.has(selectedId)) {
2181
- next.delete(selectedId);
2182
- } else {
2183
- next.add(selectedId);
2184
- }
2185
- return next;
2186
- });
2674
+ }
2187
2675
  },
2188
2676
  onHide: () => {
2189
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
+ }
2190
2686
  if (selectedId === "__cold__") {
2191
- const coldSessions = sessionTree.sessions.filter(
2192
- (s) => s.status === "cold"
2193
- );
2687
+ const coldSessions = sessionTree.coldProjects?.flatMap((p) => p.sessions) ?? [];
2194
2688
  for (const s of coldSessions) hideSession(s.hideKey);
2195
2689
  const nextId = allFlat[selectedIndex - 1]?.id ?? null;
2196
2690
  refresh();
2197
2691
  setSelectedId(nextId);
2198
2692
  return;
2199
2693
  }
2200
- const selectedSession2 = sessionTree.sessions.find(
2201
- (s) => s.id === selectedId
2202
- );
2694
+ const allSessions4 = sessionTree.projects?.flatMap((p) => p.sessions) ?? [];
2695
+ const selectedSession2 = allSessions4.find((s) => s.id === selectedId);
2203
2696
  if (selectedSession2) {
2204
2697
  hideSession(selectedSession2.hideKey);
2205
2698
  const nextId = allFlat[selectedIndex + 1]?.id ?? allFlat[selectedIndex - 1]?.id ?? null;
@@ -2207,7 +2700,7 @@ function App({ mode }) {
2207
2700
  setSelectedId(nextId);
2208
2701
  return;
2209
2702
  }
2210
- for (const s of sessionTree.sessions) {
2703
+ for (const s of allSessions4) {
2211
2704
  const selectedSubAgent = s.subAgents.find((sa) => sa.id === selectedId);
2212
2705
  if (selectedSubAgent) {
2213
2706
  hideSubAgent(selectedSubAgent.hideKey);
@@ -2225,55 +2718,67 @@ function App({ mode }) {
2225
2718
  filterLabel
2226
2719
  });
2227
2720
  useInput((input, key) => handleInput(input, key), { isActive: isWatchMode });
2228
- 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
+ }
2229
2731
  const isPlaceholderSelected = !selectedSession || selectedId === "__cold__" || !!selectedId && selectedId.startsWith("__sub-") && selectedId.endsWith("__");
2230
2732
  const sessionDisplayName = isPlaceholderSelected ? "No session selected" : selectedSession.projectPath ? selectedSession.projectName || selectedSession.id.slice(0, 8) : selectedSession.agentId ?? selectedSession.id.slice(0, 8);
2231
- return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
2232
- migrationWarning && /* @__PURE__ */ jsx4(Box4, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "Config moved to ~/.agenthud/config.yaml" }) }),
2233
- isWatchMode && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, justifyContent: "space-between", width, children: [
2234
- /* @__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: [
2235
2736
  spinner,
2236
2737
  " AgentHUD v",
2237
2738
  getVersion()
2238
2739
  ] }),
2239
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
2740
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: statusBarItems.join(" \xB7 ") })
2240
2741
  ] }),
2241
- /* @__PURE__ */ jsx4(
2242
- SessionTreePanel,
2243
- {
2244
- sessions: sessionTree.sessions,
2245
- selectedId,
2246
- hasFocus: focus === "tree",
2247
- width,
2248
- maxRows: treeRows,
2249
- expandedIds
2250
- }
2251
- ),
2252
- /* @__PURE__ */ jsx4(Box4, { marginTop: 1, children: detailMode && detailActivity ? /* @__PURE__ */ jsx4(
2253
- DetailViewPanel,
2254
- {
2255
- activity: detailActivity,
2256
- sessionName: sessionDisplayName,
2257
- scrollOffset: detailScrollOffset,
2258
- visibleRows: viewerRows,
2259
- width
2260
- }
2261
- ) : /* @__PURE__ */ jsx4(
2262
- ActivityViewerPanel,
2263
- {
2264
- activities: mergedActivities,
2265
- sessionName: sessionDisplayName,
2266
- scrollOffset,
2267
- isLive,
2268
- newCount,
2269
- visibleRows: viewerRows,
2270
- width,
2271
- cursorLine: viewerCursorLine,
2272
- hasFocus: focus === "viewer",
2273
- spinner,
2274
- filterLabel
2275
- }
2276
- ) })
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
+ ] })
2277
2782
  ] });
2278
2783
  }
2279
2784