agenthud 0.9.2 → 0.9.4
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 +64 -34
- package/dist/index.js +1 -1
- package/dist/{main-6SGKXL7E.js → main-26QL33AJ.js} +472 -179
- package/package.json +1 -1
|
@@ -53,6 +53,7 @@ var KNOWN_SUMMARY_FLAGS = /* @__PURE__ */ new Set([
|
|
|
53
53
|
"--to",
|
|
54
54
|
"--prompt",
|
|
55
55
|
"--force",
|
|
56
|
+
"--model",
|
|
56
57
|
"-y",
|
|
57
58
|
"--yes"
|
|
58
59
|
]);
|
|
@@ -89,6 +90,7 @@ Commands:
|
|
|
89
90
|
--to YYYY-MM-DD Date range: end date (use with --from)
|
|
90
91
|
--prompt TEXT Override prompt for this run (daily only)
|
|
91
92
|
--force Regenerate even if cached
|
|
93
|
+
--model NAME Pass --model to claude (e.g. "sonnet", "haiku", or a full model ID)
|
|
92
94
|
-y, --yes Skip confirmation prompts for new daily summaries
|
|
93
95
|
|
|
94
96
|
Environment:
|
|
@@ -172,10 +174,18 @@ function parseArgs(args) {
|
|
|
172
174
|
const includeIdx = rest.indexOf("--include");
|
|
173
175
|
if (includeIdx !== -1) {
|
|
174
176
|
const includeStr = rest[includeIdx + 1];
|
|
175
|
-
if (includeStr
|
|
177
|
+
if (!includeStr) {
|
|
178
|
+
reportError = "Invalid --include: missing value.";
|
|
179
|
+
} else if (includeStr === "all") {
|
|
176
180
|
reportInclude = ALL_TYPES;
|
|
177
|
-
} else
|
|
178
|
-
|
|
181
|
+
} else {
|
|
182
|
+
const tokens = includeStr.split(",").map((s) => s.trim()).filter(Boolean);
|
|
183
|
+
const unknown = tokens.filter((t) => !ALL_TYPES.includes(t));
|
|
184
|
+
if (unknown.length > 0) {
|
|
185
|
+
reportError = `Unknown --include type${unknown.length > 1 ? "s" : ""}: ${unknown.map((u) => `"${u}"`).join(", ")}. Valid types: ${ALL_TYPES.join(", ")} (or "all").`;
|
|
186
|
+
} else {
|
|
187
|
+
reportInclude = tokens;
|
|
188
|
+
}
|
|
179
189
|
}
|
|
180
190
|
}
|
|
181
191
|
let reportFormat = "markdown";
|
|
@@ -220,13 +230,15 @@ function parseArgs(args) {
|
|
|
220
230
|
let summaryPrompt;
|
|
221
231
|
let summaryForce = false;
|
|
222
232
|
let summaryAssumeYes = false;
|
|
233
|
+
let summaryModel;
|
|
223
234
|
let summaryError;
|
|
224
235
|
const FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
225
236
|
"--date",
|
|
226
237
|
"--last",
|
|
227
238
|
"--from",
|
|
228
239
|
"--to",
|
|
229
|
-
"--prompt"
|
|
240
|
+
"--prompt",
|
|
241
|
+
"--model"
|
|
230
242
|
]);
|
|
231
243
|
for (let i = 0; i < rest.length; i++) {
|
|
232
244
|
const arg = rest[i];
|
|
@@ -318,6 +330,15 @@ function parseArgs(args) {
|
|
|
318
330
|
summaryPrompt = val;
|
|
319
331
|
}
|
|
320
332
|
}
|
|
333
|
+
const modelIdx = rest.indexOf("--model");
|
|
334
|
+
if (modelIdx !== -1) {
|
|
335
|
+
const val = rest[modelIdx + 1];
|
|
336
|
+
if (!val) {
|
|
337
|
+
summaryError = "Invalid --model: missing value (e.g. --model sonnet).";
|
|
338
|
+
} else {
|
|
339
|
+
summaryModel = val;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
321
342
|
if (rest.includes("--force")) summaryForce = true;
|
|
322
343
|
if (rest.includes("-y") || rest.includes("--yes")) summaryAssumeYes = true;
|
|
323
344
|
return {
|
|
@@ -328,6 +349,7 @@ function parseArgs(args) {
|
|
|
328
349
|
summaryPrompt,
|
|
329
350
|
summaryForce,
|
|
330
351
|
summaryAssumeYes,
|
|
352
|
+
summaryModel,
|
|
331
353
|
summaryError
|
|
332
354
|
};
|
|
333
355
|
}
|
|
@@ -348,6 +370,44 @@ function parseArgs(args) {
|
|
|
348
370
|
return { mode: "watch" };
|
|
349
371
|
}
|
|
350
372
|
|
|
373
|
+
// src/utils/altScreen.ts
|
|
374
|
+
var ENTER = "\x1B[?1049h";
|
|
375
|
+
var LEAVE = "\x1B[?1049l";
|
|
376
|
+
var entered = false;
|
|
377
|
+
var left = false;
|
|
378
|
+
function enterAltScreen() {
|
|
379
|
+
if (entered) return;
|
|
380
|
+
entered = true;
|
|
381
|
+
process.stdout.write(ENTER);
|
|
382
|
+
}
|
|
383
|
+
function leaveAltScreen() {
|
|
384
|
+
if (left || !entered) return;
|
|
385
|
+
left = true;
|
|
386
|
+
process.stdout.write(LEAVE);
|
|
387
|
+
}
|
|
388
|
+
var hooksInstalled = false;
|
|
389
|
+
function installAltScreenCleanup() {
|
|
390
|
+
if (hooksInstalled) return;
|
|
391
|
+
hooksInstalled = true;
|
|
392
|
+
process.on("exit", () => {
|
|
393
|
+
leaveAltScreen();
|
|
394
|
+
});
|
|
395
|
+
process.on("SIGINT", () => {
|
|
396
|
+
leaveAltScreen();
|
|
397
|
+
process.exit(130);
|
|
398
|
+
});
|
|
399
|
+
process.on("SIGTERM", () => {
|
|
400
|
+
leaveAltScreen();
|
|
401
|
+
process.exit(143);
|
|
402
|
+
});
|
|
403
|
+
process.on("uncaughtException", (err) => {
|
|
404
|
+
leaveAltScreen();
|
|
405
|
+
setImmediate(() => {
|
|
406
|
+
throw err;
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
351
411
|
// src/config/globalConfig.ts
|
|
352
412
|
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
353
413
|
import { homedir } from "os";
|
|
@@ -359,9 +419,16 @@ var DEFAULT_GLOBAL_CONFIG = {
|
|
|
359
419
|
refreshIntervalMs: 2e3,
|
|
360
420
|
hiddenSessions: [],
|
|
361
421
|
hiddenSubAgents: [],
|
|
362
|
-
|
|
422
|
+
// [] means "show all"; conversation preset bundles assistant + user;
|
|
423
|
+
// commits-only preset filters down to git activity.
|
|
424
|
+
filterPresets: [[], ["response", "user"], ["commit"]],
|
|
363
425
|
hiddenProjects: []
|
|
364
426
|
};
|
|
427
|
+
var ALL_PRESET_KEYWORDS = /* @__PURE__ */ new Set(["all", "*", "any"]);
|
|
428
|
+
function normalizePreset(tokens) {
|
|
429
|
+
if (tokens.some((t) => ALL_PRESET_KEYWORDS.has(t.toLowerCase()))) return [];
|
|
430
|
+
return tokens;
|
|
431
|
+
}
|
|
365
432
|
function parseInterval(value) {
|
|
366
433
|
const match = value.match(/^(\d+)(s|m)$/);
|
|
367
434
|
if (!match) return null;
|
|
@@ -381,10 +448,11 @@ function writeDefaultConfig() {
|
|
|
381
448
|
refreshInterval: 2s
|
|
382
449
|
|
|
383
450
|
# Activity filter presets (cycle with 'f' key in viewer)
|
|
384
|
-
# Each list is one preset
|
|
451
|
+
# Each list is one preset. Use "all" (or "*") to show everything.
|
|
452
|
+
# Types: response, user, bash, edit, thinking, read, glob, commit
|
|
385
453
|
filterPresets:
|
|
386
|
-
- []
|
|
387
|
-
- ["response"]
|
|
454
|
+
- ["all"]
|
|
455
|
+
- ["response", "user"]
|
|
388
456
|
- ["commit"]
|
|
389
457
|
`;
|
|
390
458
|
try {
|
|
@@ -430,9 +498,12 @@ function loadGlobalConfig() {
|
|
|
430
498
|
if (ms !== null) config.refreshIntervalMs = ms;
|
|
431
499
|
}
|
|
432
500
|
if (Array.isArray(configRaw.filterPresets)) {
|
|
433
|
-
const presets = configRaw.filterPresets.filter(Array.isArray).map(
|
|
434
|
-
|
|
435
|
-
|
|
501
|
+
const presets = configRaw.filterPresets.filter(Array.isArray).map((p) => {
|
|
502
|
+
const tokens = p.filter(
|
|
503
|
+
(t) => typeof t === "string"
|
|
504
|
+
);
|
|
505
|
+
return normalizePreset(tokens);
|
|
506
|
+
});
|
|
436
507
|
if (presets.length > 0) config.filterPresets = presets;
|
|
437
508
|
}
|
|
438
509
|
const legacyHidden = {};
|
|
@@ -572,7 +643,7 @@ function getCommitDetail(projectPath, hash) {
|
|
|
572
643
|
if (!projectPath) return null;
|
|
573
644
|
try {
|
|
574
645
|
return execSync(
|
|
575
|
-
`git --git-dir="${projectPath}/.git" show --stat --no-color ${hash}`,
|
|
646
|
+
`git --git-dir="${projectPath}/.git" show --stat --patch --no-color ${hash}`,
|
|
576
647
|
{ encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
|
|
577
648
|
).trim();
|
|
578
649
|
} catch {
|
|
@@ -931,6 +1002,21 @@ function createBottomLine(panelWidth = DEFAULT_PANEL_WIDTH) {
|
|
|
931
1002
|
return BOX.bl + BOX.h.repeat(getInnerWidth(panelWidth)) + BOX.br;
|
|
932
1003
|
}
|
|
933
1004
|
var SEPARATOR = "\u2500".repeat(CONTENT_WIDTH);
|
|
1005
|
+
function truncateByWidth(text, maxWidth) {
|
|
1006
|
+
if (maxWidth <= 0) return "";
|
|
1007
|
+
if (getDisplayWidth(text) <= maxWidth) return text;
|
|
1008
|
+
if (maxWidth === 1) return "\u2026";
|
|
1009
|
+
const ellipsisWidth = 1;
|
|
1010
|
+
let acc = "";
|
|
1011
|
+
let used = 0;
|
|
1012
|
+
for (const ch of text) {
|
|
1013
|
+
const w = getDisplayWidth(ch);
|
|
1014
|
+
if (used + w + ellipsisWidth > maxWidth) break;
|
|
1015
|
+
acc += ch;
|
|
1016
|
+
used += w;
|
|
1017
|
+
}
|
|
1018
|
+
return `${acc}\u2026`;
|
|
1019
|
+
}
|
|
934
1020
|
var widthCache = /* @__PURE__ */ new Map();
|
|
935
1021
|
function getDisplayWidth(s) {
|
|
936
1022
|
const cached = widthCache.get(s);
|
|
@@ -965,13 +1051,19 @@ function getSessionStatus(mtimeMs) {
|
|
|
965
1051
|
}
|
|
966
1052
|
return "cold";
|
|
967
1053
|
}
|
|
1054
|
+
var MAX_TITLE_LEN = 300;
|
|
1055
|
+
function capWithEllipsis(s, max = MAX_TITLE_LEN) {
|
|
1056
|
+
const trimmed = s.trim();
|
|
1057
|
+
if (trimmed.length <= max) return trimmed;
|
|
1058
|
+
return `${trimmed.slice(0, max - 1)}\u2026`;
|
|
1059
|
+
}
|
|
968
1060
|
function extractTaskDescription(content) {
|
|
969
1061
|
const headerMatch = content.match(/##\s*(Task\s+\d+[:\s].+)/m);
|
|
970
|
-
if (headerMatch) return headerMatch[1]
|
|
1062
|
+
if (headerMatch) return capWithEllipsis(headerMatch[1]);
|
|
971
1063
|
const thisTaskMatch = content.match(/\*\*This Task[^:]+:\*\*\s*(.+)/);
|
|
972
|
-
if (thisTaskMatch) return thisTaskMatch[1]
|
|
1064
|
+
if (thisTaskMatch) return capWithEllipsis(thisTaskMatch[1]);
|
|
973
1065
|
const firstLine = content.split("\n").find((l) => l.trim());
|
|
974
|
-
return (firstLine ?? "")
|
|
1066
|
+
return capWithEllipsis(firstLine ?? "");
|
|
975
1067
|
}
|
|
976
1068
|
function readSubAgentInfo(filePath) {
|
|
977
1069
|
if (!existsSync3(filePath)) return { agentId: null, taskDescription: null };
|
|
@@ -1054,8 +1146,7 @@ function readFirstUserPrompt(filePath) {
|
|
|
1054
1146
|
if (!text || isSystemNoise(text)) continue;
|
|
1055
1147
|
const firstLine = text.split("\n").find((l) => l.trim()) ?? "";
|
|
1056
1148
|
if (!firstLine || isSystemNoise(firstLine)) continue;
|
|
1057
|
-
|
|
1058
|
-
return trimmed.length > 80 ? trimmed.slice(0, 80) : trimmed;
|
|
1149
|
+
return capWithEllipsis(firstLine);
|
|
1059
1150
|
}
|
|
1060
1151
|
return null;
|
|
1061
1152
|
}
|
|
@@ -1322,16 +1413,18 @@ function formatUsage(u) {
|
|
|
1322
1413
|
}
|
|
1323
1414
|
function spawnClaude(opts) {
|
|
1324
1415
|
return new Promise((resolve2) => {
|
|
1416
|
+
const args = [
|
|
1417
|
+
"-p",
|
|
1418
|
+
"--no-session-persistence",
|
|
1419
|
+
"--output-format",
|
|
1420
|
+
"stream-json",
|
|
1421
|
+
"--verbose"
|
|
1422
|
+
];
|
|
1423
|
+
if (opts.model) args.push("--model", opts.model);
|
|
1424
|
+
args.push(opts.prompt);
|
|
1325
1425
|
const proc = spawn(
|
|
1326
1426
|
"claude",
|
|
1327
|
-
|
|
1328
|
-
"-p",
|
|
1329
|
-
"--no-session-persistence",
|
|
1330
|
-
"--output-format",
|
|
1331
|
-
"stream-json",
|
|
1332
|
-
"--verbose",
|
|
1333
|
-
opts.prompt
|
|
1334
|
-
],
|
|
1427
|
+
args,
|
|
1335
1428
|
{
|
|
1336
1429
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1337
1430
|
cwd: agenthudHomeDir()
|
|
@@ -1444,6 +1537,7 @@ function spawnClaude(opts) {
|
|
|
1444
1537
|
proc.stdin.end(opts.stdin);
|
|
1445
1538
|
});
|
|
1446
1539
|
}
|
|
1540
|
+
var REPORT_TOKEN_WARN_THRESHOLD = 3e5;
|
|
1447
1541
|
async function generateDailySummary(opts) {
|
|
1448
1542
|
ensureUserPromptFile("daily");
|
|
1449
1543
|
const isToday = isSameLocalDay2(opts.date, opts.today);
|
|
@@ -1487,6 +1581,8 @@ async function generateDailySummary(opts) {
|
|
|
1487
1581
|
detailLimit: 0,
|
|
1488
1582
|
withGit: true
|
|
1489
1583
|
});
|
|
1584
|
+
const reportBytes = Buffer.byteLength(reportMarkdown, "utf-8");
|
|
1585
|
+
const estimatedTokens = Math.ceil(reportBytes / 4);
|
|
1490
1586
|
if (opts.announce) {
|
|
1491
1587
|
const reportLines = reportMarkdown.split("\n");
|
|
1492
1588
|
const sessionCount = reportLines.filter((l) => l.startsWith("## ")).length;
|
|
@@ -1496,11 +1592,9 @@ async function generateDailySummary(opts) {
|
|
|
1496
1592
|
const commitCount = reportLines.filter(
|
|
1497
1593
|
(l) => /^\[\d{2}:\d{2}\] ◆/.test(l)
|
|
1498
1594
|
).length;
|
|
1499
|
-
const sizeKb = (
|
|
1500
|
-
1
|
|
1501
|
-
);
|
|
1595
|
+
const sizeKb = (reportBytes / 1024).toFixed(1);
|
|
1502
1596
|
process.stderr.write(
|
|
1503
|
-
`agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB)
|
|
1597
|
+
`agenthud: input: ${sessionCount} sessions, ${activityCount} activities, ${commitCount} commits (${reportLines.length} lines, ${sizeKb}KB \u2248 ${estimatedTokens.toLocaleString()} tokens)
|
|
1504
1598
|
`
|
|
1505
1599
|
);
|
|
1506
1600
|
}
|
|
@@ -1516,6 +1610,29 @@ async function generateDailySummary(opts) {
|
|
|
1516
1610
|
};
|
|
1517
1611
|
}
|
|
1518
1612
|
}
|
|
1613
|
+
if (estimatedTokens > REPORT_TOKEN_WARN_THRESHOLD) {
|
|
1614
|
+
const sizeMb = (reportBytes / (1024 * 1024)).toFixed(1);
|
|
1615
|
+
process.stderr.write(
|
|
1616
|
+
`agenthud: \u26A0 report is large (~${estimatedTokens.toLocaleString()} tokens, ${sizeMb}MB). Cost will be high; very long reports may exceed context.
|
|
1617
|
+
`
|
|
1618
|
+
);
|
|
1619
|
+
if (!opts.assumeYes) {
|
|
1620
|
+
const proceed = await ask("Send anyway? [Y/n] ", true);
|
|
1621
|
+
if (!proceed) {
|
|
1622
|
+
process.stderr.write(
|
|
1623
|
+
`agenthud: ${dateLabel} \u2014 aborted (report too large).
|
|
1624
|
+
`
|
|
1625
|
+
);
|
|
1626
|
+
return {
|
|
1627
|
+
code: 0,
|
|
1628
|
+
markdown: "",
|
|
1629
|
+
fromCache: false,
|
|
1630
|
+
skipped: true,
|
|
1631
|
+
usage: null
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1519
1636
|
if (opts.announce) {
|
|
1520
1637
|
process.stderr.write(
|
|
1521
1638
|
`agenthud: sending to claude (this may take a minute)...
|
|
@@ -1528,7 +1645,8 @@ async function generateDailySummary(opts) {
|
|
|
1528
1645
|
prompt,
|
|
1529
1646
|
stdin: reportMarkdown,
|
|
1530
1647
|
cachePath: cached,
|
|
1531
|
-
streamToStdout: opts.streamToStdout
|
|
1648
|
+
streamToStdout: opts.streamToStdout,
|
|
1649
|
+
model: opts.model
|
|
1532
1650
|
});
|
|
1533
1651
|
if (opts.announce && result.code === 0) {
|
|
1534
1652
|
process.stderr.write("\n");
|
|
@@ -1554,7 +1672,8 @@ async function runSummary(options2) {
|
|
|
1554
1672
|
force: options2.force,
|
|
1555
1673
|
promptOverride: options2.prompt,
|
|
1556
1674
|
streamToStdout: true,
|
|
1557
|
-
announce: true
|
|
1675
|
+
announce: true,
|
|
1676
|
+
model: options2.model
|
|
1558
1677
|
});
|
|
1559
1678
|
return res.code;
|
|
1560
1679
|
}
|
|
@@ -1617,7 +1736,9 @@ agenthud: --- ${label} ---
|
|
|
1617
1736
|
force: false,
|
|
1618
1737
|
streamToStdout: false,
|
|
1619
1738
|
announce: true,
|
|
1620
|
-
confirmBeforeSpawn: confirmer
|
|
1739
|
+
confirmBeforeSpawn: confirmer,
|
|
1740
|
+
assumeYes: options2.assumeYes,
|
|
1741
|
+
model: options2.model
|
|
1621
1742
|
});
|
|
1622
1743
|
if (res.skipped) {
|
|
1623
1744
|
process.stderr.write(`agenthud: ${label} \u2014 skipped by user.
|
|
@@ -1662,7 +1783,8 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1662
1783
|
prompt: metaPrompt,
|
|
1663
1784
|
stdin: metaInput,
|
|
1664
1785
|
cachePath: rangeCache,
|
|
1665
|
-
streamToStdout: true
|
|
1786
|
+
streamToStdout: true,
|
|
1787
|
+
model: options2.model
|
|
1666
1788
|
});
|
|
1667
1789
|
if (metaResult.code !== 0) {
|
|
1668
1790
|
return metaResult.code;
|
|
@@ -1680,7 +1802,7 @@ agenthud: combining ${dailyMarkdowns.length} daily summaries into range summary.
|
|
|
1680
1802
|
// src/ui/App.tsx
|
|
1681
1803
|
import { existsSync as existsSync5, watch } from "fs";
|
|
1682
1804
|
import { Box as Box5, Text as Text5, useApp, useInput, useStdout } from "ink";
|
|
1683
|
-
import { useCallback, useEffect as
|
|
1805
|
+
import { useCallback, useEffect as useEffect3, useMemo, useRef, useState as useState3 } from "react";
|
|
1684
1806
|
|
|
1685
1807
|
// src/ui/ActivityViewerPanel.tsx
|
|
1686
1808
|
import { Box, Text } from "ink";
|
|
@@ -1740,6 +1862,8 @@ function ActivityViewerPanel({
|
|
|
1740
1862
|
isLive,
|
|
1741
1863
|
newCount,
|
|
1742
1864
|
visibleRows,
|
|
1865
|
+
trailingBlankRows = 0,
|
|
1866
|
+
liveIndicatorPosition = null,
|
|
1743
1867
|
width,
|
|
1744
1868
|
cursorLine,
|
|
1745
1869
|
hasFocus,
|
|
@@ -1753,18 +1877,18 @@ function ActivityViewerPanel({
|
|
|
1753
1877
|
if (isLive) {
|
|
1754
1878
|
titleSuffix = `[LIVE ${spinner || "\u25BC"}${filterSuffix}]`;
|
|
1755
1879
|
} else {
|
|
1756
|
-
const badge = newCount > 0 ? ` +${newCount}\
|
|
1757
|
-
titleSuffix = `[PAUSED \
|
|
1880
|
+
const badge = newCount > 0 ? ` +${newCount}\u2193` : "";
|
|
1881
|
+
titleSuffix = `[PAUSED \u2191${scrollOffset}${badge}${filterSuffix}]`;
|
|
1758
1882
|
}
|
|
1759
1883
|
let visibleActivities;
|
|
1760
1884
|
if (activities.length === 0) {
|
|
1761
1885
|
visibleActivities = [];
|
|
1762
1886
|
} else if (isLive) {
|
|
1763
|
-
visibleActivities = activities.slice(-visibleRows)
|
|
1887
|
+
visibleActivities = activities.slice(-visibleRows);
|
|
1764
1888
|
} else {
|
|
1765
1889
|
const end = Math.max(0, activities.length - scrollOffset);
|
|
1766
1890
|
const start = Math.max(0, end - visibleRows);
|
|
1767
|
-
visibleActivities = activities.slice(start, end)
|
|
1891
|
+
visibleActivities = activities.slice(start, end);
|
|
1768
1892
|
}
|
|
1769
1893
|
const now = /* @__PURE__ */ new Date();
|
|
1770
1894
|
const lines = [];
|
|
@@ -1782,10 +1906,11 @@ function ActivityViewerPanel({
|
|
|
1782
1906
|
);
|
|
1783
1907
|
} else {
|
|
1784
1908
|
const effectiveCursor = Math.min(cursorLine, visibleActivities.length - 1);
|
|
1909
|
+
const cursorIndexInSlice = visibleActivities.length - 1 - effectiveCursor;
|
|
1785
1910
|
for (let i = 0; i < visibleActivities.length; i++) {
|
|
1786
1911
|
const activity = visibleActivities[i];
|
|
1787
1912
|
const style = getActivityStyle(activity);
|
|
1788
|
-
const isCursor = hasFocus && i ===
|
|
1913
|
+
const isCursor = hasFocus && i === cursorIndexInSlice;
|
|
1789
1914
|
const time = formatActivityTime(activity.timestamp, now);
|
|
1790
1915
|
const timestamp = `[${time}] `;
|
|
1791
1916
|
const timestampWidth = timestamp.length;
|
|
@@ -1837,28 +1962,110 @@ function ActivityViewerPanel({
|
|
|
1837
1962
|
}
|
|
1838
1963
|
}
|
|
1839
1964
|
const emptyRow = `${BOX.v}${" ".repeat(contentWidth + 1)}${BOX.v}`;
|
|
1840
|
-
|
|
1841
|
-
|
|
1965
|
+
const padCount = Math.max(0, visibleRows - lines.length);
|
|
1966
|
+
const padded = [];
|
|
1967
|
+
for (let i = 0; i < padCount; i++) {
|
|
1968
|
+
padded.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `pad-${i}`));
|
|
1969
|
+
}
|
|
1970
|
+
const hasContent = visibleActivities.length > 0;
|
|
1971
|
+
const trailing = [];
|
|
1972
|
+
for (let i = 0; i < trailingBlankRows; i++) {
|
|
1973
|
+
if (i === 0 && isLive && liveIndicatorPosition != null && hasContent) {
|
|
1974
|
+
const pos = Math.max(0, liveIndicatorPosition);
|
|
1975
|
+
const arrow = "\u203A";
|
|
1976
|
+
const safePos = Math.min(pos, Math.max(0, contentWidth - 1));
|
|
1977
|
+
const padAfter = Math.max(0, contentWidth - safePos - 1);
|
|
1978
|
+
trailing.push(
|
|
1979
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
1980
|
+
BOX.v,
|
|
1981
|
+
" ",
|
|
1982
|
+
" ".repeat(safePos),
|
|
1983
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", dimColor: true, children: arrow }),
|
|
1984
|
+
" ".repeat(padAfter),
|
|
1985
|
+
BOX.v
|
|
1986
|
+
] }, `trail-${i}`)
|
|
1987
|
+
);
|
|
1988
|
+
} else {
|
|
1989
|
+
trailing.push(/* @__PURE__ */ jsx(Text, { children: emptyRow }, `trail-${i}`));
|
|
1990
|
+
}
|
|
1842
1991
|
}
|
|
1992
|
+
const finalLines = [...padded, ...lines, ...trailing];
|
|
1843
1993
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
1844
1994
|
/* @__PURE__ */ jsx(Text, { color: isLive ? void 0 : "yellow", children: createTitleLine(sessionName, titleSuffix, width) }),
|
|
1845
|
-
|
|
1995
|
+
finalLines,
|
|
1846
1996
|
/* @__PURE__ */ jsx(Text, { children: createBottomLine(width) })
|
|
1847
1997
|
] });
|
|
1848
1998
|
}
|
|
1849
1999
|
|
|
1850
2000
|
// src/ui/DetailViewPanel.tsx
|
|
1851
2001
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
2002
|
+
|
|
2003
|
+
// src/ui/lineColoring.ts
|
|
2004
|
+
var DIFF_META_PREFIXES = [
|
|
2005
|
+
"diff --git",
|
|
2006
|
+
"index ",
|
|
2007
|
+
"commit ",
|
|
2008
|
+
"Author:",
|
|
2009
|
+
"Date:",
|
|
2010
|
+
"Merge:"
|
|
2011
|
+
];
|
|
2012
|
+
function classifyDiffLines(lines) {
|
|
2013
|
+
return lines.map((line) => {
|
|
2014
|
+
if (line.startsWith("+++") || line.startsWith("---")) return "diff-meta";
|
|
2015
|
+
if (DIFF_META_PREFIXES.some((p) => line.startsWith(p))) return "diff-meta";
|
|
2016
|
+
if (line.startsWith("@@")) return "diff-hunk";
|
|
2017
|
+
if (line.startsWith("+")) return "diff-add";
|
|
2018
|
+
if (line.startsWith("-")) return "diff-remove";
|
|
2019
|
+
return "prose";
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
function classifyCodeFences(lines) {
|
|
2023
|
+
const out = [];
|
|
2024
|
+
let inCode = false;
|
|
2025
|
+
for (const line of lines) {
|
|
2026
|
+
if (/^\s*```/.test(line)) {
|
|
2027
|
+
out.push("code-fence");
|
|
2028
|
+
inCode = !inCode;
|
|
2029
|
+
} else {
|
|
2030
|
+
out.push(inCode ? "code" : "prose");
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return out;
|
|
2034
|
+
}
|
|
2035
|
+
function getLineStyle(category) {
|
|
2036
|
+
switch (category) {
|
|
2037
|
+
case "diff-add":
|
|
2038
|
+
return { color: "green" };
|
|
2039
|
+
case "diff-remove":
|
|
2040
|
+
return { color: "red" };
|
|
2041
|
+
case "diff-hunk":
|
|
2042
|
+
return { color: "cyan" };
|
|
2043
|
+
case "diff-meta":
|
|
2044
|
+
return { dimColor: true };
|
|
2045
|
+
case "code-fence":
|
|
2046
|
+
return { color: "cyan", dimColor: true };
|
|
2047
|
+
case "code":
|
|
2048
|
+
return { color: "cyan" };
|
|
2049
|
+
case "prose":
|
|
2050
|
+
return {};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// src/ui/DetailViewPanel.tsx
|
|
1852
2055
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1853
|
-
function
|
|
1854
|
-
if (!text) return ["(empty)"];
|
|
1855
|
-
const
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2056
|
+
function wrapClassified(text, maxWidth, classifier) {
|
|
2057
|
+
if (!text) return [{ text: "(empty)", category: "prose" }];
|
|
2058
|
+
const sourceLines = text.split("\n");
|
|
2059
|
+
const categories = classifier(sourceLines);
|
|
2060
|
+
const out = [];
|
|
2061
|
+
for (let i = 0; i < sourceLines.length; i++) {
|
|
2062
|
+
const line = sourceLines[i];
|
|
2063
|
+
const cat = categories[i] ?? "prose";
|
|
2064
|
+
if (!line) {
|
|
2065
|
+
out.push({ text: "", category: cat });
|
|
1859
2066
|
continue;
|
|
1860
2067
|
}
|
|
1861
|
-
const words =
|
|
2068
|
+
const words = line.split(" ");
|
|
1862
2069
|
let current = "";
|
|
1863
2070
|
for (const word of words) {
|
|
1864
2071
|
if (!current) {
|
|
@@ -1866,13 +2073,13 @@ function wrapText(text, maxWidth) {
|
|
|
1866
2073
|
} else if (getDisplayWidth(`${current} ${word}`) <= maxWidth) {
|
|
1867
2074
|
current += ` ${word}`;
|
|
1868
2075
|
} else {
|
|
1869
|
-
|
|
2076
|
+
out.push({ text: current, category: cat });
|
|
1870
2077
|
current = word;
|
|
1871
2078
|
}
|
|
1872
2079
|
}
|
|
1873
|
-
if (current)
|
|
2080
|
+
if (current) out.push({ text: current, category: cat });
|
|
1874
2081
|
}
|
|
1875
|
-
return
|
|
2082
|
+
return out.length > 0 ? out : [{ text: "(empty)", category: "prose" }];
|
|
1876
2083
|
}
|
|
1877
2084
|
function DetailViewPanel({
|
|
1878
2085
|
activity,
|
|
@@ -1882,7 +2089,8 @@ function DetailViewPanel({
|
|
|
1882
2089
|
}) {
|
|
1883
2090
|
const innerWidth = getInnerWidth(width);
|
|
1884
2091
|
const contentWidth = innerWidth - 1;
|
|
1885
|
-
const
|
|
2092
|
+
const classifier = activity.type === "commit" ? classifyDiffLines : classifyCodeFences;
|
|
2093
|
+
const allLines = wrapClassified(activity.detail, contentWidth, classifier);
|
|
1886
2094
|
const totalLines = allLines.length;
|
|
1887
2095
|
const clampedOffset = Math.min(
|
|
1888
2096
|
scrollOffset,
|
|
@@ -1906,13 +2114,14 @@ function DetailViewPanel({
|
|
|
1906
2114
|
const titleRight = `${dashes}${scrollPart}${BOX.tr}`;
|
|
1907
2115
|
const contentRows = [];
|
|
1908
2116
|
for (let i = 0; i < visibleRows; i++) {
|
|
1909
|
-
const
|
|
1910
|
-
const padding = Math.max(0, contentWidth - getDisplayWidth(
|
|
2117
|
+
const entry = visibleSlice[i] ?? { text: "", category: "prose" };
|
|
2118
|
+
const padding = Math.max(0, contentWidth - getDisplayWidth(entry.text));
|
|
2119
|
+
const lineStyle = getLineStyle(entry.category);
|
|
1911
2120
|
contentRows.push(
|
|
1912
2121
|
/* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1913
2122
|
BOX.v,
|
|
1914
2123
|
" ",
|
|
1915
|
-
|
|
2124
|
+
/* @__PURE__ */ jsx2(Text2, { color: lineStyle.color, dimColor: lineStyle.dimColor, children: entry.text }),
|
|
1916
2125
|
" ".repeat(padding),
|
|
1917
2126
|
BOX.v
|
|
1918
2127
|
] }, i)
|
|
@@ -1956,8 +2165,8 @@ var SECTIONS = [
|
|
|
1956
2165
|
["\u2191 \u2193 / k j", "Scroll one line"],
|
|
1957
2166
|
["PgUp/Dn, Ctrl+B/F", "Scroll one page"],
|
|
1958
2167
|
["Ctrl+U / Ctrl+D", "Scroll half page"],
|
|
1959
|
-
["g", "Jump to
|
|
1960
|
-
["G", "Jump to
|
|
2168
|
+
["g", "Jump to top (oldest)"],
|
|
2169
|
+
["G", "Jump to live (newest, bottom)"],
|
|
1961
2170
|
["\u21B5", "Open detail view for selected activity"],
|
|
1962
2171
|
["f", "Cycle filter preset (set in config.yaml)"],
|
|
1963
2172
|
["Tab", "Switch focus to project tree"]
|
|
@@ -2234,8 +2443,8 @@ function useHotkeys({
|
|
|
2234
2443
|
"Tab: projects",
|
|
2235
2444
|
"\u2191\u2193/jk: scroll",
|
|
2236
2445
|
"PgUp/Dn: page",
|
|
2237
|
-
"g:
|
|
2238
|
-
"G:
|
|
2446
|
+
"g: oldest",
|
|
2447
|
+
"G: live",
|
|
2239
2448
|
"\u21B5: detail",
|
|
2240
2449
|
`f: ${filterLabel}`,
|
|
2241
2450
|
"?: help",
|
|
@@ -2244,12 +2453,29 @@ function useHotkeys({
|
|
|
2244
2453
|
return { handleInput, statusBarItems };
|
|
2245
2454
|
}
|
|
2246
2455
|
|
|
2247
|
-
// src/ui/hooks/
|
|
2456
|
+
// src/ui/hooks/useSlide.ts
|
|
2248
2457
|
import { useEffect, useState } from "react";
|
|
2249
|
-
|
|
2250
|
-
function useSpinner(active, intervalMs = 100) {
|
|
2458
|
+
function useSlide(active, positions, intervalMs = 180, resetKey) {
|
|
2251
2459
|
const [index, setIndex] = useState(0);
|
|
2460
|
+
useEffect(() => {
|
|
2461
|
+
setIndex(0);
|
|
2462
|
+
}, [resetKey]);
|
|
2252
2463
|
useEffect(() => {
|
|
2464
|
+
if (!active) return;
|
|
2465
|
+
const timer = setInterval(() => {
|
|
2466
|
+
setIndex((i) => (i + 1) % positions);
|
|
2467
|
+
}, intervalMs);
|
|
2468
|
+
return () => clearInterval(timer);
|
|
2469
|
+
}, [active, positions, intervalMs]);
|
|
2470
|
+
return index;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
// src/ui/hooks/useSpinner.ts
|
|
2474
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
2475
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2476
|
+
function useSpinner(active, intervalMs = 100) {
|
|
2477
|
+
const [index, setIndex] = useState2(0);
|
|
2478
|
+
useEffect2(() => {
|
|
2253
2479
|
if (!active) return;
|
|
2254
2480
|
const timer = setInterval(() => {
|
|
2255
2481
|
setIndex((i) => (i + 1) % FRAMES.length);
|
|
@@ -2263,12 +2489,20 @@ function useSpinner(active, intervalMs = 100) {
|
|
|
2263
2489
|
import { homedir as homedir4 } from "os";
|
|
2264
2490
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
2265
2491
|
import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2266
|
-
function formatElapsed(lastModifiedMs) {
|
|
2267
|
-
const elapsed =
|
|
2492
|
+
function formatElapsed(lastModifiedMs, now = Date.now()) {
|
|
2493
|
+
const elapsed = Math.max(0, now - lastModifiedMs);
|
|
2268
2494
|
const seconds = Math.floor(elapsed / 1e3);
|
|
2269
2495
|
const minutes = Math.floor(seconds / 60);
|
|
2270
2496
|
const hours = Math.floor(minutes / 60);
|
|
2271
|
-
|
|
2497
|
+
const days = Math.floor(hours / 24);
|
|
2498
|
+
const weeks = Math.floor(days / 7);
|
|
2499
|
+
const months = Math.floor(days / 30);
|
|
2500
|
+
const years = Math.floor(days / 365);
|
|
2501
|
+
if (years >= 1) return `${years}y`;
|
|
2502
|
+
if (months >= 1) return `${months}mo`;
|
|
2503
|
+
if (weeks >= 1) return `${weeks}w`;
|
|
2504
|
+
if (days >= 1) return `${days}d`;
|
|
2505
|
+
if (hours > 0) return `${hours}h`;
|
|
2272
2506
|
if (minutes > 0) return `${minutes}m`;
|
|
2273
2507
|
if (seconds > 0) return `${seconds}s`;
|
|
2274
2508
|
return "<1s";
|
|
@@ -2290,11 +2524,6 @@ function formatProjectPath(projectPath) {
|
|
|
2290
2524
|
const raw = projectPath.startsWith(home) ? `~${projectPath.slice(home.length)}` : projectPath;
|
|
2291
2525
|
return raw;
|
|
2292
2526
|
}
|
|
2293
|
-
function truncatePath(path, maxWidth) {
|
|
2294
|
-
if (getDisplayWidth(path) <= maxWidth) return path;
|
|
2295
|
-
if (maxWidth < 4) return "";
|
|
2296
|
-
return `...${path.slice(-(maxWidth - 3))}`;
|
|
2297
|
-
}
|
|
2298
2527
|
function SessionRow({
|
|
2299
2528
|
session,
|
|
2300
2529
|
isSelected,
|
|
@@ -2316,36 +2545,40 @@ function SessionRow({
|
|
|
2316
2545
|
const leftCoreBase = `${prefix}${rawName}${shortIdDisplay} ${badge}`;
|
|
2317
2546
|
const leftCoreWidth = getDisplayWidth(leftCoreBase);
|
|
2318
2547
|
const rightWidth = getDisplayWidth(rightSide);
|
|
2319
|
-
const
|
|
2548
|
+
const RIGHT_GAP = 3;
|
|
2549
|
+
const middleAvailable = contentWidth - leftCoreWidth - 1 - rightWidth - RIGHT_GAP;
|
|
2320
2550
|
let middleText = "";
|
|
2321
|
-
if (middleAvailable >
|
|
2551
|
+
if (middleAvailable > 1) {
|
|
2322
2552
|
const raw = isParent ? session.firstUserPrompt ?? "" : session.taskDescription ?? "";
|
|
2323
2553
|
if (raw) {
|
|
2324
|
-
const
|
|
2554
|
+
const flat = raw.replace(/[\r\n\t]+/g, " ").trim();
|
|
2555
|
+
const truncated = truncateByWidth(flat, middleAvailable);
|
|
2325
2556
|
if (truncated) middleText = truncated;
|
|
2326
2557
|
}
|
|
2327
2558
|
}
|
|
2328
2559
|
const middleSection = middleText ? ` ${middleText}` : "";
|
|
2329
2560
|
const gapWidth = Math.max(
|
|
2330
|
-
|
|
2561
|
+
RIGHT_GAP,
|
|
2331
2562
|
contentWidth - leftCoreWidth - getDisplayWidth(middleSection) - rightWidth
|
|
2332
2563
|
);
|
|
2333
2564
|
const gap = " ".repeat(gapWidth);
|
|
2334
2565
|
const fullLine = leftCoreBase + middleSection + gap + rightSide;
|
|
2335
2566
|
const linePadding = Math.max(0, contentWidth - getDisplayWidth(fullLine));
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2567
|
+
const focused = isSelected && hasFocus;
|
|
2568
|
+
const muted = isSelected && !hasFocus;
|
|
2569
|
+
const showBg = focused || muted;
|
|
2570
|
+
const shouldDim = isNonInteractive || muted;
|
|
2338
2571
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2339
2572
|
BOX.v,
|
|
2340
2573
|
" ",
|
|
2341
2574
|
/* @__PURE__ */ jsxs4(
|
|
2342
2575
|
Text4,
|
|
2343
2576
|
{
|
|
2344
|
-
backgroundColor:
|
|
2345
|
-
bold:
|
|
2346
|
-
dimColor: shouldDim && !
|
|
2577
|
+
backgroundColor: showBg ? "blue" : void 0,
|
|
2578
|
+
bold: focused,
|
|
2579
|
+
dimColor: shouldDim && !focused,
|
|
2347
2580
|
children: [
|
|
2348
|
-
/* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !
|
|
2581
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: shouldDim && !focused, children: prefix }),
|
|
2349
2582
|
/* @__PURE__ */ jsx4(Text4, { bold: !shouldDim, children: rawName }),
|
|
2350
2583
|
shortIdDisplay ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: shortIdDisplay }) : null,
|
|
2351
2584
|
/* @__PURE__ */ jsx4(Text4, { children: " " }),
|
|
@@ -2442,23 +2675,47 @@ function ProjectRow({
|
|
|
2442
2675
|
}) {
|
|
2443
2676
|
const nameText = `> ${project.name}`;
|
|
2444
2677
|
const pathText = project.projectPath ? formatProjectPath(project.projectPath) : "";
|
|
2678
|
+
const latestMtime = project.sessions.reduce(
|
|
2679
|
+
(acc, s) => Math.max(acc, s.lastModifiedMs),
|
|
2680
|
+
0
|
|
2681
|
+
);
|
|
2682
|
+
const elapsed = latestMtime > 0 ? formatElapsed(latestMtime) : "";
|
|
2445
2683
|
const nameWidth = getDisplayWidth(nameText);
|
|
2446
2684
|
const pathWidth = pathText ? getDisplayWidth(pathText) : 0;
|
|
2447
|
-
const
|
|
2448
|
-
const
|
|
2685
|
+
const elapsedWidth = elapsed ? getDisplayWidth(elapsed) : 0;
|
|
2686
|
+
const middleGap = pathText ? 2 : 0;
|
|
2687
|
+
const leftWidth = nameWidth + middleGap + pathWidth;
|
|
2688
|
+
const PROJECT_RIGHT_GAP = 3;
|
|
2689
|
+
const rightGap = Math.max(
|
|
2690
|
+
PROJECT_RIGHT_GAP,
|
|
2691
|
+
contentWidth - leftWidth - elapsedWidth
|
|
2692
|
+
);
|
|
2693
|
+
const totalWidth = leftWidth + rightGap + elapsedWidth;
|
|
2449
2694
|
const padding = Math.max(0, contentWidth - totalWidth);
|
|
2450
|
-
const
|
|
2695
|
+
const focused = isSelected && hasFocus;
|
|
2696
|
+
const muted = isSelected && !hasFocus;
|
|
2697
|
+
const showBg = focused || muted;
|
|
2451
2698
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2452
2699
|
BOX.v,
|
|
2453
2700
|
" ",
|
|
2454
|
-
/* @__PURE__ */ jsxs4(
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
"
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2701
|
+
/* @__PURE__ */ jsxs4(
|
|
2702
|
+
Text4,
|
|
2703
|
+
{
|
|
2704
|
+
backgroundColor: showBg ? "blue" : void 0,
|
|
2705
|
+
bold: !showBg,
|
|
2706
|
+
dimColor: muted,
|
|
2707
|
+
children: [
|
|
2708
|
+
nameText,
|
|
2709
|
+
pathText ? /* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
2710
|
+
" ",
|
|
2711
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: pathText })
|
|
2712
|
+
] }) : null,
|
|
2713
|
+
" ".repeat(rightGap),
|
|
2714
|
+
elapsed ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: elapsed }) : null,
|
|
2715
|
+
" ".repeat(padding)
|
|
2716
|
+
]
|
|
2717
|
+
}
|
|
2718
|
+
),
|
|
2462
2719
|
BOX.v
|
|
2463
2720
|
] });
|
|
2464
2721
|
}
|
|
@@ -2478,11 +2735,12 @@ function SubagentSummaryRow({
|
|
|
2478
2735
|
0,
|
|
2479
2736
|
contentWidth - getDisplayWidth(text) - getDisplayWidth(hint)
|
|
2480
2737
|
);
|
|
2481
|
-
const
|
|
2738
|
+
const focused = isSelected && hasFocus;
|
|
2739
|
+
const muted = isSelected && !hasFocus;
|
|
2482
2740
|
return /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
2483
2741
|
BOX.v,
|
|
2484
2742
|
" ",
|
|
2485
|
-
/* @__PURE__ */ jsxs4(Text4, { dimColor: !
|
|
2743
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: !focused, inverse: focused || muted, children: [
|
|
2486
2744
|
text,
|
|
2487
2745
|
" ".repeat(padding),
|
|
2488
2746
|
hint
|
|
@@ -2504,13 +2762,14 @@ function ColdProjectsSummaryRow({
|
|
|
2504
2762
|
const dashCount = Math.max(0, innerWidth - 1 - labelWidth - hintWidth);
|
|
2505
2763
|
const dashes = BOX.h.repeat(dashCount);
|
|
2506
2764
|
const line = `${BOX.ml}${BOX.h}${label}${dashes}${hint}${BOX.mr}`;
|
|
2507
|
-
const
|
|
2765
|
+
const focused = isSelected && hasFocus;
|
|
2766
|
+
const muted = isSelected && !hasFocus;
|
|
2508
2767
|
return /* @__PURE__ */ jsx4(
|
|
2509
2768
|
Text4,
|
|
2510
2769
|
{
|
|
2511
|
-
backgroundColor:
|
|
2512
|
-
bold:
|
|
2513
|
-
dimColor: !
|
|
2770
|
+
backgroundColor: focused || muted ? "blue" : void 0,
|
|
2771
|
+
bold: focused,
|
|
2772
|
+
dimColor: !focused,
|
|
2514
2773
|
children: line
|
|
2515
2774
|
}
|
|
2516
2775
|
);
|
|
@@ -2712,14 +2971,14 @@ function getSelectedActivity(acts, live, scrollOff, rows, cursorLine) {
|
|
|
2712
2971
|
if (acts.length === 0) return null;
|
|
2713
2972
|
let visible;
|
|
2714
2973
|
if (live) {
|
|
2715
|
-
visible = acts.slice(-rows)
|
|
2974
|
+
visible = acts.slice(-rows);
|
|
2716
2975
|
} else {
|
|
2717
2976
|
const end = Math.max(0, acts.length - scrollOff);
|
|
2718
2977
|
const start = Math.max(0, end - rows);
|
|
2719
|
-
visible = acts.slice(start, end)
|
|
2978
|
+
visible = acts.slice(start, end);
|
|
2720
2979
|
}
|
|
2721
2980
|
const effectiveCursor = Math.min(cursorLine, visible.length - 1);
|
|
2722
|
-
return visible[effectiveCursor] ?? null;
|
|
2981
|
+
return visible[visible.length - 1 - effectiveCursor] ?? null;
|
|
2723
2982
|
}
|
|
2724
2983
|
function App({ mode }) {
|
|
2725
2984
|
const { exit } = useApp();
|
|
@@ -2727,47 +2986,47 @@ function App({ mode }) {
|
|
|
2727
2986
|
const isWatchMode = mode === "watch";
|
|
2728
2987
|
const config = useMemo(() => loadGlobalConfig(), []);
|
|
2729
2988
|
const migrationWarning = useMemo(() => hasProjectLevelConfig(), []);
|
|
2730
|
-
const [sessionTree, setSessionTree] =
|
|
2989
|
+
const [sessionTree, setSessionTree] = useState3(
|
|
2731
2990
|
() => discoverSessions(config)
|
|
2732
2991
|
);
|
|
2733
|
-
const [selectedId, setSelectedId] =
|
|
2992
|
+
const [selectedId, setSelectedId] = useState3(() => {
|
|
2734
2993
|
const firstProject = sessionTree.projects[0];
|
|
2735
2994
|
if (firstProject) return `__proj-${firstProject.name}__`;
|
|
2736
2995
|
return null;
|
|
2737
2996
|
});
|
|
2738
|
-
const [focus, setFocus] =
|
|
2739
|
-
const [scrollOffset, setScrollOffset] =
|
|
2740
|
-
const [isLive, setIsLive] =
|
|
2741
|
-
const [activities, setActivities] =
|
|
2742
|
-
const [gitActivities, setGitActivities] =
|
|
2743
|
-
const [newCount, setNewCount] =
|
|
2744
|
-
const [expandedIds, setExpandedIds] =
|
|
2745
|
-
const [viewerCursorLine, setViewerCursorLine] =
|
|
2746
|
-
const [detailMode, setDetailMode] =
|
|
2747
|
-
const [detailActivity, setDetailActivity] =
|
|
2997
|
+
const [focus, setFocus] = useState3("tree");
|
|
2998
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
2999
|
+
const [isLive, setIsLive] = useState3(true);
|
|
3000
|
+
const [activities, setActivities] = useState3([]);
|
|
3001
|
+
const [gitActivities, setGitActivities] = useState3([]);
|
|
3002
|
+
const [newCount, setNewCount] = useState3(0);
|
|
3003
|
+
const [expandedIds, setExpandedIds] = useState3(/* @__PURE__ */ new Set());
|
|
3004
|
+
const [viewerCursorLine, setViewerCursorLine] = useState3(0);
|
|
3005
|
+
const [detailMode, setDetailMode] = useState3(false);
|
|
3006
|
+
const [detailActivity, setDetailActivity] = useState3(
|
|
2748
3007
|
null
|
|
2749
3008
|
);
|
|
2750
|
-
const [detailScrollOffset, setDetailScrollOffset] =
|
|
2751
|
-
const [filterIndex, setFilterIndex] =
|
|
2752
|
-
const [helpMode, setHelpMode] =
|
|
2753
|
-
const [helpScroll, setHelpScroll] =
|
|
3009
|
+
const [detailScrollOffset, setDetailScrollOffset] = useState3(0);
|
|
3010
|
+
const [filterIndex, setFilterIndex] = useState3(0);
|
|
3011
|
+
const [helpMode, setHelpMode] = useState3(false);
|
|
3012
|
+
const [helpScroll, setHelpScroll] = useState3(0);
|
|
2754
3013
|
const helpTotalLinesRef = useRef(0);
|
|
2755
3014
|
const allFlat = useMemo(
|
|
2756
3015
|
() => flattenSessions2(sessionTree, expandedIds),
|
|
2757
3016
|
[sessionTree, expandedIds]
|
|
2758
3017
|
);
|
|
2759
3018
|
const allFlatRef = useRef(allFlat);
|
|
2760
|
-
|
|
3019
|
+
useEffect3(() => {
|
|
2761
3020
|
allFlatRef.current = allFlat;
|
|
2762
3021
|
}, [allFlat]);
|
|
2763
3022
|
const activitiesLengthRef = useRef(0);
|
|
2764
3023
|
const activitiesRef = useRef(activities);
|
|
2765
|
-
|
|
3024
|
+
useEffect3(() => {
|
|
2766
3025
|
activitiesLengthRef.current = activities.length;
|
|
2767
3026
|
activitiesRef.current = activities;
|
|
2768
3027
|
}, [activities]);
|
|
2769
3028
|
const lastLoadedFileRef = useRef(null);
|
|
2770
|
-
|
|
3029
|
+
useEffect3(() => {
|
|
2771
3030
|
let node = allFlatRef.current.find((s) => s.id === selectedId);
|
|
2772
3031
|
if (node && selectedId?.startsWith("__proj-") && selectedId.endsWith("__")) {
|
|
2773
3032
|
const projectName = selectedId.slice(7, -2);
|
|
@@ -2795,12 +3054,12 @@ function App({ mode }) {
|
|
|
2795
3054
|
if (fileChanged) setGitActivities([]);
|
|
2796
3055
|
}
|
|
2797
3056
|
}, [selectedId, sessionTree]);
|
|
2798
|
-
|
|
3057
|
+
useEffect3(() => {
|
|
2799
3058
|
setScrollOffset(0);
|
|
2800
3059
|
setIsLive(true);
|
|
2801
3060
|
setViewerCursorLine(0);
|
|
2802
3061
|
}, [filterIndex]);
|
|
2803
|
-
|
|
3062
|
+
useEffect3(() => {
|
|
2804
3063
|
if (!isWatchMode) return;
|
|
2805
3064
|
const node = allFlatRef.current.find((s) => s.id === selectedId);
|
|
2806
3065
|
if (!node?.projectPath) return;
|
|
@@ -2855,10 +3114,10 @@ function App({ mode }) {
|
|
|
2855
3114
|
}
|
|
2856
3115
|
}, [selectedId, isLive, expandedIds]);
|
|
2857
3116
|
const refreshRef = useRef(refresh);
|
|
2858
|
-
|
|
3117
|
+
useEffect3(() => {
|
|
2859
3118
|
refreshRef.current = refresh;
|
|
2860
3119
|
}, [refresh]);
|
|
2861
|
-
|
|
3120
|
+
useEffect3(() => {
|
|
2862
3121
|
if (!isWatchMode) return;
|
|
2863
3122
|
const projectsDir = getProjectsDir();
|
|
2864
3123
|
const usePolling = process.platform === "linux" || !existsSync5(projectsDir);
|
|
@@ -2912,8 +3171,21 @@ function App({ mode }) {
|
|
|
2912
3171
|
const maxTreeRows = Math.floor(height * (1 - VIEWER_HEIGHT_FRACTION));
|
|
2913
3172
|
const naturalTreeRows = allFlat.length;
|
|
2914
3173
|
const treeRows = Math.max(1, Math.min(naturalTreeRows, maxTreeRows));
|
|
2915
|
-
const
|
|
3174
|
+
const VIEWER_BREATHING_ROWS = 1;
|
|
3175
|
+
const viewerRows = Math.max(
|
|
3176
|
+
5,
|
|
3177
|
+
height - 7 - treeRows - VIEWER_BREATHING_ROWS
|
|
3178
|
+
);
|
|
2916
3179
|
const spinner = useSpinner(isWatchMode);
|
|
3180
|
+
const viewerIndicatorWidth = Math.max(1, width - 3);
|
|
3181
|
+
const liveIndicatorPosition = useSlide(
|
|
3182
|
+
isWatchMode,
|
|
3183
|
+
viewerIndicatorWidth,
|
|
3184
|
+
180,
|
|
3185
|
+
// Reset to 0 whenever the viewer's subject changes so each new
|
|
3186
|
+
// session/sub-agent restarts the arrow from the left.
|
|
3187
|
+
selectedId
|
|
3188
|
+
);
|
|
2917
3189
|
const helpViewportRows = Math.max(1, height - 3);
|
|
2918
3190
|
const helpScrollStep = (delta) => {
|
|
2919
3191
|
const max = Math.max(0, helpTotalLinesRef.current - helpViewportRows);
|
|
@@ -2930,11 +3202,30 @@ function App({ mode }) {
|
|
|
2930
3202
|
onHelpScroll: helpScrollStep,
|
|
2931
3203
|
onHelpScrollToTop: () => setHelpScroll(0),
|
|
2932
3204
|
onSwitchFocus: () => setFocus((f) => f === "tree" ? "viewer" : "tree"),
|
|
3205
|
+
// cursorLine = "entries back from the newest" (0 = newest = bottom row).
|
|
3206
|
+
// Up arrow moves visually upward = older direction = cursorLine++.
|
|
3207
|
+
// Down arrow moves visually downward = newer direction = cursorLine--.
|
|
2933
3208
|
onScrollUp: () => {
|
|
2934
3209
|
if (focus === "tree") {
|
|
2935
3210
|
if (selectedIndex === -1) return;
|
|
2936
3211
|
const prev = Math.max(0, selectedIndex - 1);
|
|
2937
3212
|
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
3213
|
+
} else {
|
|
3214
|
+
if (viewerCursorLine < viewerRows - 1) {
|
|
3215
|
+
setViewerCursorLine((c) => c + 1);
|
|
3216
|
+
} else {
|
|
3217
|
+
setIsLive(false);
|
|
3218
|
+
setScrollOffset(
|
|
3219
|
+
(o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
},
|
|
3224
|
+
onScrollDown: () => {
|
|
3225
|
+
if (focus === "tree") {
|
|
3226
|
+
if (selectedIndex === -1) return;
|
|
3227
|
+
const next = Math.min(allFlat.length - 1, selectedIndex + 1);
|
|
3228
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
2938
3229
|
} else {
|
|
2939
3230
|
if (viewerCursorLine > 0) {
|
|
2940
3231
|
setViewerCursorLine((c) => c - 1);
|
|
@@ -2950,26 +3241,25 @@ function App({ mode }) {
|
|
|
2950
3241
|
}
|
|
2951
3242
|
}
|
|
2952
3243
|
},
|
|
2953
|
-
|
|
3244
|
+
// PgUp/PgDn semantics flip to match the bottom-feed layout:
|
|
3245
|
+
// PgUp = visually up = older direction = scrollOffset++
|
|
3246
|
+
// PgDn = visually down = newer direction = scrollOffset--
|
|
3247
|
+
onScrollPageUp: () => {
|
|
2954
3248
|
if (focus === "tree") {
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3249
|
+
const prev = Math.max(0, selectedIndex - 5);
|
|
3250
|
+
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
2958
3251
|
} else {
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
(o) => Math.min(o + 1, Math.max(0, activities.length - viewerRows))
|
|
2965
|
-
);
|
|
2966
|
-
}
|
|
3252
|
+
setViewerCursorLine(0);
|
|
3253
|
+
setIsLive(false);
|
|
3254
|
+
setScrollOffset(
|
|
3255
|
+
(o) => Math.min(o + viewerRows, Math.max(0, activities.length - viewerRows))
|
|
3256
|
+
);
|
|
2967
3257
|
}
|
|
2968
3258
|
},
|
|
2969
|
-
|
|
3259
|
+
onScrollPageDown: () => {
|
|
2970
3260
|
if (focus === "tree") {
|
|
2971
|
-
const
|
|
2972
|
-
setSelectedId(allFlat[
|
|
3261
|
+
const next = Math.min(allFlat.length - 1, selectedIndex + 5);
|
|
3262
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
2973
3263
|
} else {
|
|
2974
3264
|
setViewerCursorLine(0);
|
|
2975
3265
|
setScrollOffset((o) => {
|
|
@@ -2982,22 +3272,28 @@ function App({ mode }) {
|
|
|
2982
3272
|
});
|
|
2983
3273
|
}
|
|
2984
3274
|
},
|
|
2985
|
-
|
|
3275
|
+
onScrollHalfPageUp: () => {
|
|
2986
3276
|
if (focus === "tree") {
|
|
2987
|
-
const
|
|
2988
|
-
setSelectedId(allFlat[
|
|
3277
|
+
const prev = Math.max(0, selectedIndex - Math.ceil(5 / 2));
|
|
3278
|
+
setSelectedId(allFlat[prev]?.id ?? selectedId);
|
|
2989
3279
|
} else {
|
|
2990
3280
|
setViewerCursorLine(0);
|
|
2991
3281
|
setIsLive(false);
|
|
2992
3282
|
setScrollOffset(
|
|
2993
|
-
(o) => Math.min(
|
|
3283
|
+
(o) => Math.min(
|
|
3284
|
+
o + Math.floor(viewerRows / 2),
|
|
3285
|
+
Math.max(0, activities.length - viewerRows)
|
|
3286
|
+
)
|
|
2994
3287
|
);
|
|
2995
3288
|
}
|
|
2996
3289
|
},
|
|
2997
|
-
|
|
3290
|
+
onScrollHalfPageDown: () => {
|
|
2998
3291
|
if (focus === "tree") {
|
|
2999
|
-
const
|
|
3000
|
-
|
|
3292
|
+
const next = Math.min(
|
|
3293
|
+
allFlat.length - 1,
|
|
3294
|
+
selectedIndex + Math.ceil(5 / 2)
|
|
3295
|
+
);
|
|
3296
|
+
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3001
3297
|
} else {
|
|
3002
3298
|
setViewerCursorLine(0);
|
|
3003
3299
|
setScrollOffset((o) => {
|
|
@@ -3010,35 +3306,17 @@ function App({ mode }) {
|
|
|
3010
3306
|
});
|
|
3011
3307
|
}
|
|
3012
3308
|
},
|
|
3013
|
-
onScrollHalfPageDown: () => {
|
|
3014
|
-
if (focus === "tree") {
|
|
3015
|
-
const next = Math.min(
|
|
3016
|
-
allFlat.length - 1,
|
|
3017
|
-
selectedIndex + Math.ceil(5 / 2)
|
|
3018
|
-
);
|
|
3019
|
-
setSelectedId(allFlat[next]?.id ?? selectedId);
|
|
3020
|
-
} else {
|
|
3021
|
-
setViewerCursorLine(0);
|
|
3022
|
-
setIsLive(false);
|
|
3023
|
-
setScrollOffset(
|
|
3024
|
-
(o) => Math.min(
|
|
3025
|
-
o + Math.floor(viewerRows / 2),
|
|
3026
|
-
Math.max(0, activities.length - viewerRows)
|
|
3027
|
-
)
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
},
|
|
3031
3309
|
onScrollTop: () => {
|
|
3310
|
+
setViewerCursorLine(Math.max(0, viewerRows - 1));
|
|
3311
|
+
setIsLive(false);
|
|
3312
|
+
setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
|
|
3313
|
+
},
|
|
3314
|
+
onScrollBottom: () => {
|
|
3032
3315
|
setViewerCursorLine(0);
|
|
3033
3316
|
setIsLive(true);
|
|
3034
3317
|
setScrollOffset(0);
|
|
3035
3318
|
setNewCount(0);
|
|
3036
3319
|
},
|
|
3037
|
-
onScrollBottom: () => {
|
|
3038
|
-
setViewerCursorLine(0);
|
|
3039
|
-
setIsLive(false);
|
|
3040
|
-
setScrollOffset(Math.max(0, mergedActivities.length - viewerRows));
|
|
3041
|
-
},
|
|
3042
3320
|
onDetailClose: () => {
|
|
3043
3321
|
setDetailMode(false);
|
|
3044
3322
|
},
|
|
@@ -3130,15 +3408,23 @@ function App({ mode }) {
|
|
|
3130
3408
|
const toggleKey = isCold ? `__expanded-session-${selectedId}` : `__collapsed-session-${selectedId}`;
|
|
3131
3409
|
setExpandedIds((prev) => {
|
|
3132
3410
|
const next = new Set(prev);
|
|
3133
|
-
if (
|
|
3134
|
-
next.
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3411
|
+
if (isCold) {
|
|
3412
|
+
if (next.has(toggleKey)) {
|
|
3413
|
+
next.delete(toggleKey);
|
|
3414
|
+
next.delete(selectedId);
|
|
3415
|
+
} else {
|
|
3416
|
+
next.add(toggleKey);
|
|
3139
3417
|
const firstSub = selectedSessionObj.subAgents[0];
|
|
3140
3418
|
if (firstSub) setSelectedId(firstSub.id);
|
|
3141
3419
|
}
|
|
3420
|
+
} else {
|
|
3421
|
+
if (next.has(toggleKey)) {
|
|
3422
|
+
next.delete(toggleKey);
|
|
3423
|
+
} else {
|
|
3424
|
+
next.add(toggleKey);
|
|
3425
|
+
next.delete(selectedId);
|
|
3426
|
+
setSelectedId(selectedId);
|
|
3427
|
+
}
|
|
3142
3428
|
}
|
|
3143
3429
|
return next;
|
|
3144
3430
|
});
|
|
@@ -3273,6 +3559,8 @@ function App({ mode }) {
|
|
|
3273
3559
|
isLive,
|
|
3274
3560
|
newCount,
|
|
3275
3561
|
visibleRows: viewerRows,
|
|
3562
|
+
trailingBlankRows: VIEWER_BREATHING_ROWS,
|
|
3563
|
+
liveIndicatorPosition,
|
|
3276
3564
|
width,
|
|
3277
3565
|
cursorLine: viewerCursorLine,
|
|
3278
3566
|
hasFocus: focus === "viewer",
|
|
@@ -3356,7 +3644,8 @@ if (options.mode === "summary") {
|
|
|
3356
3644
|
to: options.summaryTo,
|
|
3357
3645
|
today,
|
|
3358
3646
|
force: options.summaryForce ?? false,
|
|
3359
|
-
assumeYes: options.summaryAssumeYes ?? false
|
|
3647
|
+
assumeYes: options.summaryAssumeYes ?? false,
|
|
3648
|
+
model: options.summaryModel
|
|
3360
3649
|
});
|
|
3361
3650
|
process.exit(exitCode2);
|
|
3362
3651
|
}
|
|
@@ -3364,11 +3653,15 @@ if (options.mode === "summary") {
|
|
|
3364
3653
|
date: options.summaryDate,
|
|
3365
3654
|
prompt: options.summaryPrompt,
|
|
3366
3655
|
force: options.summaryForce ?? false,
|
|
3367
|
-
today
|
|
3656
|
+
today,
|
|
3657
|
+
model: options.summaryModel
|
|
3368
3658
|
});
|
|
3369
3659
|
process.exit(exitCode);
|
|
3370
3660
|
}
|
|
3371
3661
|
if (options.mode === "watch") {
|
|
3372
|
-
|
|
3662
|
+
installAltScreenCleanup();
|
|
3663
|
+
enterAltScreen();
|
|
3664
|
+
} else {
|
|
3665
|
+
if (options.mode === "once") clearScreen();
|
|
3373
3666
|
}
|
|
3374
3667
|
render(React.createElement(App, { mode: options.mode }));
|