aoaoe 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +23 -2
- package/dist/export.d.ts +17 -0
- package/dist/export.js +132 -0
- package/dist/index.js +67 -1
- package/dist/input.d.ts +3 -0
- package/dist/input.js +22 -0
- package/dist/tui.d.ts +13 -1
- package/dist/tui.js +77 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -248,6 +248,10 @@ commands:
|
|
|
248
248
|
logs --actions show action log entries (from ~/.aoaoe/actions.log)
|
|
249
249
|
logs --grep <pattern> filter log entries by substring or regex
|
|
250
250
|
logs -n <count> number of entries to show (default: 50)
|
|
251
|
+
export export session timeline as JSON or Markdown for post-mortems
|
|
252
|
+
export --format <json|markdown> output format (default: json)
|
|
253
|
+
export --output <file> write to file (default: stdout)
|
|
254
|
+
export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
|
|
251
255
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
252
256
|
tasks show task progress (from aoaoe.tasks.json)
|
|
253
257
|
history review recent actions (from ~/.aoaoe/actions.log)
|
package/dist/config.d.ts
CHANGED
|
@@ -34,6 +34,10 @@ export declare function parseCliArgs(argv: string[]): {
|
|
|
34
34
|
logsActions: boolean;
|
|
35
35
|
logsGrep?: string;
|
|
36
36
|
logsCount?: number;
|
|
37
|
+
runExport: boolean;
|
|
38
|
+
exportFormat?: string;
|
|
39
|
+
exportOutput?: string;
|
|
40
|
+
exportLast?: string;
|
|
37
41
|
runInit: boolean;
|
|
38
42
|
initForce: boolean;
|
|
39
43
|
runTaskCli: boolean;
|
package/dist/config.js
CHANGED
|
@@ -327,7 +327,7 @@ export function parseCliArgs(argv) {
|
|
|
327
327
|
let initForce = false;
|
|
328
328
|
let runTaskCli = false;
|
|
329
329
|
let registerTitle;
|
|
330
|
-
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runInit: false, initForce: false, runTaskCli: false };
|
|
330
|
+
const defaults = { overrides, help: false, version: false, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false };
|
|
331
331
|
// check for subcommand as first non-flag arg
|
|
332
332
|
if (argv[2] === "test-context") {
|
|
333
333
|
return { ...defaults, testContext: true };
|
|
@@ -374,6 +374,23 @@ export function parseCliArgs(argv) {
|
|
|
374
374
|
}
|
|
375
375
|
return { ...defaults, runLogs: true, logsActions: actions, logsGrep: grep, logsCount: count };
|
|
376
376
|
}
|
|
377
|
+
if (argv[2] === "export") {
|
|
378
|
+
let format;
|
|
379
|
+
let output;
|
|
380
|
+
let last;
|
|
381
|
+
for (let i = 3; i < argv.length; i++) {
|
|
382
|
+
if ((argv[i] === "--format" || argv[i] === "-f") && argv[i + 1]) {
|
|
383
|
+
format = argv[++i];
|
|
384
|
+
}
|
|
385
|
+
else if ((argv[i] === "--output" || argv[i] === "-o") && argv[i + 1]) {
|
|
386
|
+
output = argv[++i];
|
|
387
|
+
}
|
|
388
|
+
else if ((argv[i] === "--last" || argv[i] === "-l") && argv[i + 1]) {
|
|
389
|
+
last = argv[++i];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { ...defaults, runExport: true, exportFormat: format, exportOutput: output, exportLast: last };
|
|
393
|
+
}
|
|
377
394
|
if (argv[2] === "init") {
|
|
378
395
|
const force = argv.includes("--force") || argv.includes("-f");
|
|
379
396
|
return { ...defaults, runInit: true, initForce: force };
|
|
@@ -471,7 +488,7 @@ export function parseCliArgs(argv) {
|
|
|
471
488
|
break;
|
|
472
489
|
}
|
|
473
490
|
}
|
|
474
|
-
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runInit: false, initForce: false, runTaskCli: false };
|
|
491
|
+
return { overrides, help, version, register: false, testContext: false, runTest: false, showTasks: false, showHistory: false, showStatus: false, showConfig: false, configValidate: false, configDiff: false, notifyTest: false, runDoctor: false, runLogs: false, logsActions: false, logsGrep: undefined, logsCount: undefined, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false };
|
|
475
492
|
}
|
|
476
493
|
export function printHelp() {
|
|
477
494
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -497,6 +514,10 @@ commands:
|
|
|
497
514
|
logs --actions show action log entries (from ~/.aoaoe/actions.log)
|
|
498
515
|
logs --grep <pattern> filter log entries by substring or regex
|
|
499
516
|
logs -n <count> number of entries to show (default: 50)
|
|
517
|
+
export export session timeline as JSON or Markdown for post-mortems
|
|
518
|
+
export --format <json|markdown> output format (default: json)
|
|
519
|
+
export --output <file> write to file (default: stdout)
|
|
520
|
+
export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
|
|
500
521
|
task manage tasks and sessions (list, start, stop, new, rm, edit)
|
|
501
522
|
tasks show task progress (from aoaoe.tasks.json)
|
|
502
523
|
history review recent actions (from ~/.aoaoe/actions.log)
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HistoryEntry } from "./tui-history.js";
|
|
2
|
+
export interface TimelineEntry {
|
|
3
|
+
ts: number;
|
|
4
|
+
source: "action" | "activity";
|
|
5
|
+
tag: string;
|
|
6
|
+
text: string;
|
|
7
|
+
success?: boolean;
|
|
8
|
+
session?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseActionLogEntries(lines: string[]): TimelineEntry[];
|
|
11
|
+
export declare function parseActivityEntries(entries: HistoryEntry[]): TimelineEntry[];
|
|
12
|
+
export declare function mergeTimeline(...sources: TimelineEntry[][]): TimelineEntry[];
|
|
13
|
+
export declare function filterByAge(entries: TimelineEntry[], maxAgeMs: number, now?: number): TimelineEntry[];
|
|
14
|
+
export declare function parseDuration(input: string): number | null;
|
|
15
|
+
export declare function formatTimelineJson(entries: TimelineEntry[]): string;
|
|
16
|
+
export declare function formatTimelineMarkdown(entries: TimelineEntry[]): string;
|
|
17
|
+
//# sourceMappingURL=export.d.ts.map
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// export.ts — pure functions for exporting session timelines as JSON or Markdown
|
|
2
|
+
// reads actions.log (JSONL) and tui-history.jsonl, merges into a unified timeline
|
|
3
|
+
import { toActionLogEntry } from "./types.js";
|
|
4
|
+
// ── parsers ─────────────────────────────────────────────────────────────────
|
|
5
|
+
// parse actions.log JSONL lines into timeline entries
|
|
6
|
+
export function parseActionLogEntries(lines) {
|
|
7
|
+
const entries = [];
|
|
8
|
+
for (const line of lines) {
|
|
9
|
+
if (!line.trim())
|
|
10
|
+
continue;
|
|
11
|
+
try {
|
|
12
|
+
const entry = toActionLogEntry(JSON.parse(line));
|
|
13
|
+
if (!entry)
|
|
14
|
+
continue;
|
|
15
|
+
// skip wait actions (noise in post-mortems)
|
|
16
|
+
if (entry.action.action === "wait")
|
|
17
|
+
continue;
|
|
18
|
+
entries.push({
|
|
19
|
+
ts: entry.timestamp,
|
|
20
|
+
source: "action",
|
|
21
|
+
tag: entry.action.action,
|
|
22
|
+
text: entry.detail || `${entry.action.action} on ${entry.action.session ?? "unknown"}`,
|
|
23
|
+
success: entry.success,
|
|
24
|
+
session: entry.action.session ?? entry.action.title,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// skip malformed lines
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return entries;
|
|
32
|
+
}
|
|
33
|
+
// convert tui-history entries into timeline entries
|
|
34
|
+
export function parseActivityEntries(entries) {
|
|
35
|
+
return entries.map((e) => ({
|
|
36
|
+
ts: e.ts,
|
|
37
|
+
source: "activity",
|
|
38
|
+
tag: e.tag,
|
|
39
|
+
text: e.text,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
// ── merge + filter ──────────────────────────────────────────────────────────
|
|
43
|
+
// merge multiple sources into a single chronological timeline
|
|
44
|
+
export function mergeTimeline(...sources) {
|
|
45
|
+
const all = sources.flat();
|
|
46
|
+
all.sort((a, b) => a.ts - b.ts);
|
|
47
|
+
return all;
|
|
48
|
+
}
|
|
49
|
+
// filter entries by time window (keep entries newer than cutoff)
|
|
50
|
+
export function filterByAge(entries, maxAgeMs, now = Date.now()) {
|
|
51
|
+
const cutoff = now - maxAgeMs;
|
|
52
|
+
return entries.filter((e) => e.ts >= cutoff);
|
|
53
|
+
}
|
|
54
|
+
// ── duration parser ─────────────────────────────────────────────────────────
|
|
55
|
+
// parse human-friendly duration strings: "1h", "6h", "24h", "2d", "7d", "30d"
|
|
56
|
+
export function parseDuration(input) {
|
|
57
|
+
const match = input.match(/^(\d+)(h|d)$/);
|
|
58
|
+
if (!match)
|
|
59
|
+
return null;
|
|
60
|
+
const val = parseInt(match[1], 10);
|
|
61
|
+
if (val <= 0)
|
|
62
|
+
return null;
|
|
63
|
+
const unit = match[2];
|
|
64
|
+
if (unit === "h")
|
|
65
|
+
return val * 60 * 60 * 1000;
|
|
66
|
+
if (unit === "d")
|
|
67
|
+
return val * 24 * 60 * 60 * 1000;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
// ── formatters ──────────────────────────────────────────────────────────────
|
|
71
|
+
// format timeline as pretty-printed JSON
|
|
72
|
+
export function formatTimelineJson(entries) {
|
|
73
|
+
const output = entries.map((e) => {
|
|
74
|
+
const obj = {
|
|
75
|
+
time: new Date(e.ts).toISOString(),
|
|
76
|
+
source: e.source,
|
|
77
|
+
tag: e.tag,
|
|
78
|
+
text: e.text,
|
|
79
|
+
};
|
|
80
|
+
if (e.success !== undefined)
|
|
81
|
+
obj.success = e.success;
|
|
82
|
+
if (e.session)
|
|
83
|
+
obj.session = e.session;
|
|
84
|
+
return obj;
|
|
85
|
+
});
|
|
86
|
+
return JSON.stringify(output, null, 2) + "\n";
|
|
87
|
+
}
|
|
88
|
+
// format timeline as a readable Markdown post-mortem document
|
|
89
|
+
export function formatTimelineMarkdown(entries) {
|
|
90
|
+
if (entries.length === 0) {
|
|
91
|
+
return "# aoaoe Session Export\n\n_No entries in the selected time range._\n";
|
|
92
|
+
}
|
|
93
|
+
const lines = [];
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const first = new Date(entries[0].ts);
|
|
96
|
+
const last = new Date(entries[entries.length - 1].ts);
|
|
97
|
+
lines.push("# aoaoe Session Export");
|
|
98
|
+
lines.push("");
|
|
99
|
+
lines.push(`**Generated**: ${now.toISOString()}`);
|
|
100
|
+
lines.push(`**Range**: ${first.toISOString()} — ${last.toISOString()}`);
|
|
101
|
+
lines.push(`**Entries**: ${entries.length}`);
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push("## Timeline");
|
|
104
|
+
lines.push("");
|
|
105
|
+
// group entries by hour
|
|
106
|
+
let currentHour = "";
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
const d = new Date(entry.ts);
|
|
109
|
+
const hour = d.toLocaleString("en-US", {
|
|
110
|
+
month: "short", day: "numeric", hour: "numeric", minute: undefined, hour12: true,
|
|
111
|
+
}).replace(/:00/, "");
|
|
112
|
+
// simpler: just use HH:00 block headers
|
|
113
|
+
const hourKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:00`;
|
|
114
|
+
if (hourKey !== currentHour) {
|
|
115
|
+
if (currentHour)
|
|
116
|
+
lines.push("");
|
|
117
|
+
lines.push(`### ${hourKey}`);
|
|
118
|
+
lines.push("");
|
|
119
|
+
currentHour = hourKey;
|
|
120
|
+
}
|
|
121
|
+
const time = `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
122
|
+
const icon = entry.source === "action"
|
|
123
|
+
? (entry.success ? "+" : "!")
|
|
124
|
+
: "·";
|
|
125
|
+
const sessionPart = entry.session ? ` → ${entry.session}` : "";
|
|
126
|
+
const text = entry.text.length > 120 ? entry.text.slice(0, 117) + "..." : entry.text;
|
|
127
|
+
lines.push(`- \`${time}\` ${icon} **${entry.tag}**${sessionPart}: ${text}`);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=export.js.map
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { isDaemonRunningFromState } from "./chat.js";
|
|
|
20
20
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
21
|
import { startHealthServer } from "./health.js";
|
|
22
22
|
import { loadTuiHistory } from "./tui-history.js";
|
|
23
|
+
import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
|
|
23
24
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
24
25
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
25
26
|
import { readFileSync, existsSync, statSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
@@ -30,7 +31,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
30
31
|
const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
|
|
31
32
|
const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
|
|
32
33
|
async function main() {
|
|
33
|
-
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
34
|
+
const { overrides, help, version, register, testContext: isTestContext, runTest, showTasks, showHistory, showStatus, showConfig, configValidate, configDiff, notifyTest, runDoctor, runLogs, logsActions, logsGrep, logsCount, runExport, exportFormat, exportOutput, exportLast, runInit, initForce, runTaskCli: isTaskCli, registerTitle } = parseCliArgs(process.argv);
|
|
34
35
|
if (help) {
|
|
35
36
|
printHelp();
|
|
36
37
|
process.exit(0);
|
|
@@ -103,6 +104,11 @@ async function main() {
|
|
|
103
104
|
await showLogs(logsActions, logsGrep, logsCount);
|
|
104
105
|
return;
|
|
105
106
|
}
|
|
107
|
+
// `aoaoe export` -- export session timeline as JSON or Markdown
|
|
108
|
+
if (runExport) {
|
|
109
|
+
await runTimelineExport(exportFormat, exportOutput, exportLast);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
106
112
|
// `aoaoe task` -- task management CLI
|
|
107
113
|
if (isTaskCli) {
|
|
108
114
|
await runTaskCli(process.argv);
|
|
@@ -229,6 +235,25 @@ async function main() {
|
|
|
229
235
|
// start interactive input listener and conversation log
|
|
230
236
|
input.start();
|
|
231
237
|
await reasonerConsole.start();
|
|
238
|
+
// wire scroll keys to TUI (PgUp/PgDn/Home/End)
|
|
239
|
+
if (tui) {
|
|
240
|
+
input.onScroll((dir) => {
|
|
241
|
+
switch (dir) {
|
|
242
|
+
case "up":
|
|
243
|
+
tui.scrollUp();
|
|
244
|
+
break;
|
|
245
|
+
case "down":
|
|
246
|
+
tui.scrollDown();
|
|
247
|
+
break;
|
|
248
|
+
case "top":
|
|
249
|
+
tui.scrollToTop();
|
|
250
|
+
break;
|
|
251
|
+
case "bottom":
|
|
252
|
+
tui.scrollToBottom();
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
232
257
|
// start TUI (alternate screen buffer) after input is ready
|
|
233
258
|
if (tui) {
|
|
234
259
|
// replay persisted history from previous runs before entering alt screen
|
|
@@ -1311,6 +1336,47 @@ async function showLogs(actions, grep, count) {
|
|
|
1311
1336
|
console.log("");
|
|
1312
1337
|
}
|
|
1313
1338
|
}
|
|
1339
|
+
// `aoaoe export` -- export session timeline as JSON or Markdown for post-mortems
|
|
1340
|
+
async function runTimelineExport(format, output, last) {
|
|
1341
|
+
const fmt = format ?? "json";
|
|
1342
|
+
if (fmt !== "json" && fmt !== "markdown" && fmt !== "md") {
|
|
1343
|
+
console.error(`error: --format must be "json" or "markdown", got "${fmt}"`);
|
|
1344
|
+
process.exit(1);
|
|
1345
|
+
}
|
|
1346
|
+
// parse time window (default 24h)
|
|
1347
|
+
const durationMs = last ? parseDuration(last) : 24 * 60 * 60 * 1000;
|
|
1348
|
+
if (durationMs === null) {
|
|
1349
|
+
console.error(`error: --last must be like "1h", "6h", "24h", "7d", got "${last}"`);
|
|
1350
|
+
process.exit(1);
|
|
1351
|
+
}
|
|
1352
|
+
// read actions.log
|
|
1353
|
+
const actionsFile = join(homedir(), ".aoaoe", "actions.log");
|
|
1354
|
+
let actionEntries = [];
|
|
1355
|
+
try {
|
|
1356
|
+
const lines = readFileSync(actionsFile, "utf-8").trim().split("\n").filter((l) => l.trim());
|
|
1357
|
+
actionEntries = parseActionLogEntries(lines);
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
// no actions.log — that's fine
|
|
1361
|
+
}
|
|
1362
|
+
// read tui-history.jsonl
|
|
1363
|
+
const historyEntries = loadTuiHistory(10_000, undefined, durationMs);
|
|
1364
|
+
const activityEntries = parseActivityEntries(historyEntries);
|
|
1365
|
+
// merge and filter
|
|
1366
|
+
let timeline = mergeTimeline(actionEntries, activityEntries);
|
|
1367
|
+
timeline = filterByAge(timeline, durationMs);
|
|
1368
|
+
// format
|
|
1369
|
+
const isMarkdown = fmt === "markdown" || fmt === "md";
|
|
1370
|
+
const content = isMarkdown ? formatTimelineMarkdown(timeline) : formatTimelineJson(timeline);
|
|
1371
|
+
// output
|
|
1372
|
+
if (output) {
|
|
1373
|
+
writeFileSync(output, content);
|
|
1374
|
+
console.log(`exported ${timeline.length} entries to ${output}`);
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
process.stdout.write(content);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1314
1380
|
// `aoaoe test` -- dynamically import and run the integration test
|
|
1315
1381
|
async function runIntegrationTest() {
|
|
1316
1382
|
const testModule = resolve(__dirname, "integration-test.js");
|
package/dist/input.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
1
2
|
export declare class InputReader {
|
|
2
3
|
private rl;
|
|
3
4
|
private queue;
|
|
4
5
|
private paused;
|
|
5
6
|
private lastEscTime;
|
|
7
|
+
private scrollHandler;
|
|
8
|
+
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
6
9
|
start(): void;
|
|
7
10
|
drain(): string[];
|
|
8
11
|
isPaused(): boolean;
|
package/dist/input.js
CHANGED
|
@@ -11,6 +11,11 @@ export class InputReader {
|
|
|
11
11
|
queue = []; // pending user messages for the reasoner
|
|
12
12
|
paused = false;
|
|
13
13
|
lastEscTime = 0;
|
|
14
|
+
scrollHandler = null;
|
|
15
|
+
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
16
|
+
onScroll(handler) {
|
|
17
|
+
this.scrollHandler = handler;
|
|
18
|
+
}
|
|
14
19
|
start() {
|
|
15
20
|
// only works if stdin is a TTY (not piped)
|
|
16
21
|
if (!process.stdin.isTTY)
|
|
@@ -39,6 +44,21 @@ export class InputReader {
|
|
|
39
44
|
else {
|
|
40
45
|
this.lastEscTime = 0;
|
|
41
46
|
}
|
|
47
|
+
// scroll key detection (PgUp, PgDn, Home, End)
|
|
48
|
+
if (this.scrollHandler) {
|
|
49
|
+
if (key?.name === "pageup" || key?.sequence === "\x1b[5~") {
|
|
50
|
+
this.scrollHandler("up");
|
|
51
|
+
}
|
|
52
|
+
else if (key?.name === "pagedown" || key?.sequence === "\x1b[6~") {
|
|
53
|
+
this.scrollHandler("down");
|
|
54
|
+
}
|
|
55
|
+
else if (key?.name === "home" || key?.sequence === "\x1b[1~") {
|
|
56
|
+
this.scrollHandler("top");
|
|
57
|
+
}
|
|
58
|
+
else if (key?.name === "end" || key?.sequence === "\x1b[4~") {
|
|
59
|
+
this.scrollHandler("bottom");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
42
62
|
});
|
|
43
63
|
// show hint on startup
|
|
44
64
|
console.error(`${DIM}type a message to talk to the AI supervisor, /help for commands, ESC ESC to interrupt${RESET}`);
|
|
@@ -105,6 +125,8 @@ ${BOLD}controls:${RESET}
|
|
|
105
125
|
/resume resume the supervisor
|
|
106
126
|
/interrupt interrupt the AI mid-thought
|
|
107
127
|
ESC ESC same as /interrupt (shortcut)
|
|
128
|
+
PgUp / PgDn scroll through activity history
|
|
129
|
+
Home / End jump to oldest / return to live
|
|
108
130
|
|
|
109
131
|
${BOLD}info:${RESET}
|
|
110
132
|
/status show daemon state
|
package/dist/tui.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare class TUI {
|
|
|
20
20
|
private activityBuffer;
|
|
21
21
|
private maxActivity;
|
|
22
22
|
private spinnerFrame;
|
|
23
|
+
private scrollOffset;
|
|
24
|
+
private newWhileScrolled;
|
|
23
25
|
private phase;
|
|
24
26
|
private pollCount;
|
|
25
27
|
private sessions;
|
|
@@ -40,6 +42,11 @@ export declare class TUI {
|
|
|
40
42
|
}): void;
|
|
41
43
|
log(tag: string, text: string): void;
|
|
42
44
|
replayHistory(entries: HistoryEntry[]): void;
|
|
45
|
+
scrollUp(lines?: number): void;
|
|
46
|
+
scrollDown(lines?: number): void;
|
|
47
|
+
scrollToTop(): void;
|
|
48
|
+
scrollToBottom(): void;
|
|
49
|
+
isScrolledBack(): boolean;
|
|
43
50
|
private updateDimensions;
|
|
44
51
|
private computeLayout;
|
|
45
52
|
private onResize;
|
|
@@ -63,5 +70,10 @@ declare function truncatePlain(str: string, max: number): string;
|
|
|
63
70
|
* Kept for backward compatibility — used by non-TUI output paths.
|
|
64
71
|
*/
|
|
65
72
|
export declare function formatSessionSentence(s: DaemonSessionState, maxCols: number): string;
|
|
66
|
-
|
|
73
|
+
declare function computeScrollSlice(bufferLen: number, visibleLines: number, scrollOffset: number): {
|
|
74
|
+
start: number;
|
|
75
|
+
end: number;
|
|
76
|
+
};
|
|
77
|
+
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
78
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator };
|
|
67
79
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -55,6 +55,8 @@ export class TUI {
|
|
|
55
55
|
activityBuffer = []; // ring buffer for activity log
|
|
56
56
|
maxActivity = 500; // max entries to keep
|
|
57
57
|
spinnerFrame = 0; // current spinner animation frame
|
|
58
|
+
scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
|
|
59
|
+
newWhileScrolled = 0; // entries added while user is scrolled back
|
|
58
60
|
// current state for repaints
|
|
59
61
|
phase = "sleeping";
|
|
60
62
|
pollCount = 0;
|
|
@@ -139,8 +141,16 @@ export class TUI {
|
|
|
139
141
|
if (this.activityBuffer.length > this.maxActivity) {
|
|
140
142
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
141
143
|
}
|
|
142
|
-
if (this.active)
|
|
143
|
-
this.
|
|
144
|
+
if (this.active) {
|
|
145
|
+
if (this.scrollOffset > 0) {
|
|
146
|
+
// user is scrolled back — don't auto-scroll, just show indicator
|
|
147
|
+
this.newWhileScrolled++;
|
|
148
|
+
this.paintSeparator();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
this.writeActivityLine(entry);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
144
154
|
// persist to disk (fire-and-forget, never blocks)
|
|
145
155
|
appendHistoryEntry({ ts: now.getTime(), time, tag, text });
|
|
146
156
|
}
|
|
@@ -155,6 +165,48 @@ export class TUI {
|
|
|
155
165
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
168
|
+
// ── Scroll navigation ────────────────────────────────────────────────────
|
|
169
|
+
scrollUp(lines) {
|
|
170
|
+
if (!this.active)
|
|
171
|
+
return;
|
|
172
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
173
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
174
|
+
const maxOffset = Math.max(0, this.activityBuffer.length - visibleLines);
|
|
175
|
+
this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
|
|
176
|
+
this.repaintActivityRegion();
|
|
177
|
+
this.paintSeparator();
|
|
178
|
+
}
|
|
179
|
+
scrollDown(lines) {
|
|
180
|
+
if (!this.active)
|
|
181
|
+
return;
|
|
182
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
183
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
184
|
+
const wasScrolled = this.scrollOffset > 0;
|
|
185
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - n);
|
|
186
|
+
if (wasScrolled && this.scrollOffset === 0)
|
|
187
|
+
this.newWhileScrolled = 0;
|
|
188
|
+
this.repaintActivityRegion();
|
|
189
|
+
this.paintSeparator();
|
|
190
|
+
}
|
|
191
|
+
scrollToTop() {
|
|
192
|
+
if (!this.active)
|
|
193
|
+
return;
|
|
194
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
195
|
+
this.scrollOffset = Math.max(0, this.activityBuffer.length - visibleLines);
|
|
196
|
+
this.repaintActivityRegion();
|
|
197
|
+
this.paintSeparator();
|
|
198
|
+
}
|
|
199
|
+
scrollToBottom() {
|
|
200
|
+
if (!this.active)
|
|
201
|
+
return;
|
|
202
|
+
this.scrollOffset = 0;
|
|
203
|
+
this.newWhileScrolled = 0;
|
|
204
|
+
this.repaintActivityRegion();
|
|
205
|
+
this.paintSeparator();
|
|
206
|
+
}
|
|
207
|
+
isScrolledBack() {
|
|
208
|
+
return this.scrollOffset > 0;
|
|
209
|
+
}
|
|
158
210
|
// ── Layout computation ──────────────────────────────────────────────────
|
|
159
211
|
updateDimensions() {
|
|
160
212
|
this.cols = process.stderr.columns || 80;
|
|
@@ -245,8 +297,14 @@ export class TUI {
|
|
|
245
297
|
process.stderr.write(RESTORE_CURSOR);
|
|
246
298
|
}
|
|
247
299
|
paintSeparator() {
|
|
248
|
-
const hints = " esc esc: interrupt /help /explain /pause ";
|
|
249
300
|
const prefix = `${BOX.h}${BOX.h} activity `;
|
|
301
|
+
let hints;
|
|
302
|
+
if (this.scrollOffset > 0) {
|
|
303
|
+
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
hints = " esc esc: interrupt /help /explain /pause ";
|
|
307
|
+
}
|
|
250
308
|
const totalLen = prefix.length + hints.length;
|
|
251
309
|
const fill = Math.max(0, this.cols - totalLen);
|
|
252
310
|
const left = Math.floor(fill / 2);
|
|
@@ -265,7 +323,8 @@ export class TUI {
|
|
|
265
323
|
}
|
|
266
324
|
repaintActivityRegion() {
|
|
267
325
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
268
|
-
const
|
|
326
|
+
const { start, end } = computeScrollSlice(this.activityBuffer.length, visibleLines, this.scrollOffset);
|
|
327
|
+
const entries = this.activityBuffer.slice(start, end);
|
|
269
328
|
for (let i = 0; i < visibleLines; i++) {
|
|
270
329
|
const row = this.scrollTop + i;
|
|
271
330
|
if (i < entries.length) {
|
|
@@ -431,6 +490,19 @@ export function formatSessionSentence(s, maxCols) {
|
|
|
431
490
|
}
|
|
432
491
|
return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
|
|
433
492
|
}
|
|
493
|
+
// ── Scroll helpers (pure, exported for testing) ─────────────────────────────
|
|
494
|
+
// compute the slice indices for the activity buffer given scroll state
|
|
495
|
+
function computeScrollSlice(bufferLen, visibleLines, scrollOffset) {
|
|
496
|
+
const end = Math.max(0, bufferLen - scrollOffset);
|
|
497
|
+
const start = Math.max(0, end - visibleLines);
|
|
498
|
+
return { start, end };
|
|
499
|
+
}
|
|
500
|
+
// format the scroll indicator text for the separator bar
|
|
501
|
+
function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
502
|
+
const position = totalEntries - offset;
|
|
503
|
+
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
504
|
+
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
505
|
+
}
|
|
434
506
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
435
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
|
|
507
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator };
|
|
436
508
|
//# sourceMappingURL=tui.js.map
|