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