aoaoe 0.70.0 → 0.72.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/dist/config.d.ts +3 -0
- package/dist/config.js +29 -2
- package/dist/index.js +23 -2
- package/dist/input.d.ts +12 -0
- package/dist/input.js +34 -0
- package/dist/replay.d.ts +43 -0
- package/dist/replay.js +174 -0
- package/dist/tui.d.ts +10 -0
- package/dist/tui.js +29 -5
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ export declare function parseCliArgs(argv: string[]): {
|
|
|
46
46
|
tailCount?: number;
|
|
47
47
|
runStats: boolean;
|
|
48
48
|
statsLast?: string;
|
|
49
|
+
runReplay: boolean;
|
|
50
|
+
replaySpeed?: number;
|
|
51
|
+
replayLast?: string;
|
|
49
52
|
registerTitle?: string;
|
|
50
53
|
};
|
|
51
54
|
export declare function printHelp(): void;
|
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, runExport: false, exportFormat: undefined, exportOutput: undefined, exportLast: undefined, runInit: false, initForce: false, runTaskCli: false, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined };
|
|
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, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
331
331
|
// check for subcommand as first non-flag arg
|
|
332
332
|
if (argv[2] === "test-context") {
|
|
333
333
|
return { ...defaults, testContext: true };
|
|
@@ -419,6 +419,24 @@ export function parseCliArgs(argv) {
|
|
|
419
419
|
}
|
|
420
420
|
return { ...defaults, runStats: true, statsLast: last };
|
|
421
421
|
}
|
|
422
|
+
if (argv[2] === "replay") {
|
|
423
|
+
let speed;
|
|
424
|
+
let last;
|
|
425
|
+
for (let i = 3; i < argv.length; i++) {
|
|
426
|
+
if ((argv[i] === "--speed" || argv[i] === "-s") && argv[i + 1]) {
|
|
427
|
+
const val = parseFloat(argv[++i]);
|
|
428
|
+
if (!isNaN(val) && val >= 0)
|
|
429
|
+
speed = val;
|
|
430
|
+
}
|
|
431
|
+
else if ((argv[i] === "--last" || argv[i] === "-l") && argv[i + 1]) {
|
|
432
|
+
last = argv[++i];
|
|
433
|
+
}
|
|
434
|
+
else if (argv[i] === "--instant") {
|
|
435
|
+
speed = 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { ...defaults, runReplay: true, replaySpeed: speed, replayLast: last };
|
|
439
|
+
}
|
|
422
440
|
if (argv[2] === "register") {
|
|
423
441
|
register = true;
|
|
424
442
|
// parse --title from remaining args
|
|
@@ -512,7 +530,7 @@ export function parseCliArgs(argv) {
|
|
|
512
530
|
break;
|
|
513
531
|
}
|
|
514
532
|
}
|
|
515
|
-
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, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined };
|
|
533
|
+
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, runTail: false, tailFollow: false, tailCount: undefined, runStats: false, statsLast: undefined, runReplay: false, replaySpeed: undefined, replayLast: undefined };
|
|
516
534
|
}
|
|
517
535
|
export function printHelp() {
|
|
518
536
|
console.log(`aoaoe - autonomous supervisor for agent-of-empires sessions
|
|
@@ -544,6 +562,10 @@ commands:
|
|
|
544
562
|
export --last <duration> time window: 1h, 6h, 24h, 7d (default: 24h)
|
|
545
563
|
stats show aggregate daemon statistics (actions, sessions, activity)
|
|
546
564
|
stats --last <duration> time window: 1h, 6h, 24h, 7d (default: all time)
|
|
565
|
+
replay play back tui-history.jsonl like a movie with simulated timing
|
|
566
|
+
replay --speed <N> playback speed: 1=realtime, 5=5x (default), 10=fast, 0=instant
|
|
567
|
+
replay --instant same as --speed 0 (no delays, dump all entries immediately)
|
|
568
|
+
replay --last <duration> only replay entries from the last 1h, 6h, 24h, 7d
|
|
547
569
|
tail live-stream daemon activity to a separate terminal
|
|
548
570
|
tail -f follow mode — keep watching for new entries (Ctrl+C to stop)
|
|
549
571
|
tail -n <N> number of entries to show (default: 50)
|
|
@@ -579,6 +601,11 @@ logs options:
|
|
|
579
601
|
--grep, -g <pattern> filter entries by substring or regex
|
|
580
602
|
-n, --count <number> number of entries to show (default: 50)
|
|
581
603
|
|
|
604
|
+
replay options:
|
|
605
|
+
--speed, -s <number> playback speed multiplier (default: 5)
|
|
606
|
+
--instant no delays, dump all entries immediately
|
|
607
|
+
--last, -l <duration> time window: 1h, 6h, 24h, 7d
|
|
608
|
+
|
|
582
609
|
tail options:
|
|
583
610
|
-f, --follow keep watching for new entries (Ctrl+C to stop)
|
|
584
611
|
-n, --count <number> number of entries to show (default: 50)
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { wakeableSleep } from "./wake.js";
|
|
|
15
15
|
import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
|
|
16
16
|
import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
|
|
17
17
|
import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
|
|
18
|
-
import { TUI } from "./tui.js";
|
|
18
|
+
import { TUI, hitTestSession } from "./tui.js";
|
|
19
19
|
import { isDaemonRunningFromState } from "./chat.js";
|
|
20
20
|
import { sendNotification, sendTestNotification } from "./notify.js";
|
|
21
21
|
import { startHealthServer } from "./health.js";
|
|
@@ -32,7 +32,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
32
32
|
const AOAOE_DIR = join(homedir(), ".aoaoe"); // watch dir for wakeable sleep
|
|
33
33
|
const INPUT_FILE = join(AOAOE_DIR, "pending-input.txt"); // file IPC from chat.ts
|
|
34
34
|
async function main() {
|
|
35
|
-
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, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, registerTitle } = parseCliArgs(process.argv);
|
|
35
|
+
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, runTail: isTail, tailFollow, tailCount, runStats: isStats, statsLast, runReplay: isReplay, replaySpeed, replayLast, registerTitle } = parseCliArgs(process.argv);
|
|
36
36
|
if (help) {
|
|
37
37
|
printHelp();
|
|
38
38
|
process.exit(0);
|
|
@@ -115,6 +115,12 @@ async function main() {
|
|
|
115
115
|
await runStatsCommand(statsLast);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
+
// `aoaoe replay` -- play back tui-history.jsonl like a movie
|
|
119
|
+
if (isReplay) {
|
|
120
|
+
const { runReplay: doReplay } = await import("./replay.js");
|
|
121
|
+
await doReplay({ speed: replaySpeed ?? 5, last: replayLast });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
118
124
|
// `aoaoe task` -- task management CLI
|
|
119
125
|
if (isTaskCli) {
|
|
120
126
|
await runTaskCli(process.argv);
|
|
@@ -287,6 +293,21 @@ async function main() {
|
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
295
|
});
|
|
296
|
+
// wire mouse clicks on session cards to drill-down
|
|
297
|
+
input.onMouseClick((row, _col) => {
|
|
298
|
+
if (tui.getViewMode() === "drilldown") {
|
|
299
|
+
// click anywhere in drilldown = back to overview
|
|
300
|
+
tui.exitDrilldown();
|
|
301
|
+
tui.log("system", "returned to overview");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const sessionIdx = hitTestSession(row, 1, tui.getSessionCount());
|
|
305
|
+
if (sessionIdx !== null) {
|
|
306
|
+
const ok = tui.enterDrilldown(sessionIdx);
|
|
307
|
+
if (ok)
|
|
308
|
+
tui.log("system", `viewing session #${sessionIdx}`);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
290
311
|
}
|
|
291
312
|
// start TUI (alternate screen buffer) after input is ready
|
|
292
313
|
if (tui) {
|
package/dist/input.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
2
2
|
export declare const INSIST_PREFIX = "__INSIST__";
|
|
3
3
|
export type ViewHandler = (target: string | null) => void;
|
|
4
|
+
export interface MouseEvent {
|
|
5
|
+
button: number;
|
|
6
|
+
col: number;
|
|
7
|
+
row: number;
|
|
8
|
+
press: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type MouseClickHandler = (row: number, col: number) => void;
|
|
11
|
+
/** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
|
|
12
|
+
export declare function parseMouseEvent(data: string): MouseEvent | null;
|
|
4
13
|
export declare class InputReader {
|
|
5
14
|
private rl;
|
|
6
15
|
private queue;
|
|
@@ -9,9 +18,12 @@ export declare class InputReader {
|
|
|
9
18
|
private scrollHandler;
|
|
10
19
|
private queueChangeHandler;
|
|
11
20
|
private viewHandler;
|
|
21
|
+
private mouseClickHandler;
|
|
22
|
+
private mouseDataListener;
|
|
12
23
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
13
24
|
onQueueChange(handler: (count: number) => void): void;
|
|
14
25
|
onView(handler: ViewHandler): void;
|
|
26
|
+
onMouseClick(handler: MouseClickHandler): void;
|
|
15
27
|
private notifyQueueChange;
|
|
16
28
|
start(): void;
|
|
17
29
|
drain(): string[];
|
package/dist/input.js
CHANGED
|
@@ -7,6 +7,20 @@ import { GREEN, DIM, YELLOW, RED, BOLD, RESET } from "./colors.js";
|
|
|
7
7
|
// ESC-ESC interrupt detection
|
|
8
8
|
const ESC_DOUBLE_TAP_MS = 500;
|
|
9
9
|
export const INSIST_PREFIX = "__INSIST__";
|
|
10
|
+
// SGR extended mouse format: \x1b[<btn;col;rowM (press) or \x1b[<btn;col;rowm (release)
|
|
11
|
+
const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
12
|
+
/** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
|
|
13
|
+
export function parseMouseEvent(data) {
|
|
14
|
+
const m = SGR_MOUSE_RE.exec(data);
|
|
15
|
+
if (!m)
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
button: parseInt(m[1], 10),
|
|
19
|
+
col: parseInt(m[2], 10),
|
|
20
|
+
row: parseInt(m[3], 10),
|
|
21
|
+
press: m[4] === "M",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
10
24
|
export class InputReader {
|
|
11
25
|
rl = null;
|
|
12
26
|
queue = []; // pending user messages for the reasoner
|
|
@@ -15,6 +29,8 @@ export class InputReader {
|
|
|
15
29
|
scrollHandler = null;
|
|
16
30
|
queueChangeHandler = null;
|
|
17
31
|
viewHandler = null;
|
|
32
|
+
mouseClickHandler = null;
|
|
33
|
+
mouseDataListener = null;
|
|
18
34
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
19
35
|
onScroll(handler) {
|
|
20
36
|
this.scrollHandler = handler;
|
|
@@ -27,6 +43,10 @@ export class InputReader {
|
|
|
27
43
|
onView(handler) {
|
|
28
44
|
this.viewHandler = handler;
|
|
29
45
|
}
|
|
46
|
+
// register a callback for mouse left-click events (row, col are 1-indexed)
|
|
47
|
+
onMouseClick(handler) {
|
|
48
|
+
this.mouseClickHandler = handler;
|
|
49
|
+
}
|
|
30
50
|
notifyQueueChange() {
|
|
31
51
|
this.queueChangeHandler?.(this.queue.length);
|
|
32
52
|
}
|
|
@@ -44,6 +64,15 @@ export class InputReader {
|
|
|
44
64
|
this.rl.on("close", () => { this.rl = null; });
|
|
45
65
|
// ESC-ESC interrupt detection (same as chat.ts)
|
|
46
66
|
emitKeypressEvents(process.stdin);
|
|
67
|
+
// intercept raw SGR mouse sequences before keypress parsing
|
|
68
|
+
this.mouseDataListener = (data) => {
|
|
69
|
+
const str = data.toString("utf8");
|
|
70
|
+
const evt = parseMouseEvent(str);
|
|
71
|
+
if (evt && evt.press && evt.button === 0 && this.mouseClickHandler) {
|
|
72
|
+
this.mouseClickHandler(evt.row, evt.col);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
process.stdin.on("data", this.mouseDataListener);
|
|
47
76
|
process.stdin.on("keypress", (_ch, key) => {
|
|
48
77
|
if (key?.name === "escape" || key?.sequence === "\x1b") {
|
|
49
78
|
const now = Date.now();
|
|
@@ -102,6 +131,10 @@ export class InputReader {
|
|
|
102
131
|
this.rl?.prompt(true);
|
|
103
132
|
}
|
|
104
133
|
stop() {
|
|
134
|
+
if (this.mouseDataListener) {
|
|
135
|
+
process.stdin.removeListener("data", this.mouseDataListener);
|
|
136
|
+
this.mouseDataListener = null;
|
|
137
|
+
}
|
|
105
138
|
this.rl?.close();
|
|
106
139
|
this.rl = null;
|
|
107
140
|
}
|
|
@@ -167,6 +200,7 @@ ${BOLD}controls:${RESET}
|
|
|
167
200
|
${BOLD}navigation:${RESET}
|
|
168
201
|
/view [N|name] drill into a session's live output (default: 1)
|
|
169
202
|
/back return to overview from drill-down
|
|
203
|
+
click session click an agent card to drill down (click again to go back)
|
|
170
204
|
PgUp / PgDn scroll through activity history
|
|
171
205
|
Home / End jump to oldest / return to live
|
|
172
206
|
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type HistoryEntry } from "./tui-history.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compute the delay between two entries, scaled by speed multiplier.
|
|
4
|
+
* Caps individual delays to prevent long waits on idle periods.
|
|
5
|
+
*/
|
|
6
|
+
export declare function computeDelay(prevTs: number, currTs: number, speed: number, maxDelayMs?: number): number;
|
|
7
|
+
/**
|
|
8
|
+
* Format a speed multiplier for display.
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatSpeed(speed: number): string;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a speed string ("2x", "10x", "0.5x", "instant") into a number.
|
|
13
|
+
* Returns 0 for instant, null for invalid.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseSpeed(input: string): number | null;
|
|
16
|
+
/**
|
|
17
|
+
* Filter entries by time window.
|
|
18
|
+
*/
|
|
19
|
+
export declare function filterByWindow(entries: HistoryEntry[], maxAgeMs?: number, now?: number): HistoryEntry[];
|
|
20
|
+
/**
|
|
21
|
+
* Build the replay header.
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatReplayHeader(entries: HistoryEntry[], speed: number, windowLabel?: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Build the replay footer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatReplayFooter(entries: HistoryEntry[]): string;
|
|
28
|
+
/**
|
|
29
|
+
* Load entries from the history file, optionally filtering by time window.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadReplayEntries(maxAgeMs?: number, filePath?: string): HistoryEntry[];
|
|
32
|
+
/**
|
|
33
|
+
* Run the replay.
|
|
34
|
+
* Prints entries with simulated delays between them.
|
|
35
|
+
* Speed 0 = instant (no delays).
|
|
36
|
+
* Ctrl+C stops playback.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runReplay(opts: {
|
|
39
|
+
speed: number;
|
|
40
|
+
last?: string;
|
|
41
|
+
filePath?: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
//# sourceMappingURL=replay.d.ts.map
|
package/dist/replay.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// replay.ts — play back tui-history.jsonl like a movie.
|
|
2
|
+
// shows what the daemon did with simulated timing or instant output.
|
|
3
|
+
// reuses formatTailEntry from tail.ts for consistent rendering.
|
|
4
|
+
// pure exported functions for testability.
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { TUI_HISTORY_FILE } from "./tui-history.js";
|
|
7
|
+
import { formatTailEntry, formatTailDate } from "./tail.js";
|
|
8
|
+
import { parseDuration } from "./export.js";
|
|
9
|
+
import { DIM, RESET, AMBER } from "./colors.js";
|
|
10
|
+
/**
|
|
11
|
+
* Compute the delay between two entries, scaled by speed multiplier.
|
|
12
|
+
* Caps individual delays to prevent long waits on idle periods.
|
|
13
|
+
*/
|
|
14
|
+
export function computeDelay(prevTs, currTs, speed, maxDelayMs = 3000) {
|
|
15
|
+
if (speed <= 0)
|
|
16
|
+
return 0;
|
|
17
|
+
const raw = currTs - prevTs;
|
|
18
|
+
if (raw <= 0)
|
|
19
|
+
return 0;
|
|
20
|
+
return Math.min(Math.round(raw / speed), maxDelayMs);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format a speed multiplier for display.
|
|
24
|
+
*/
|
|
25
|
+
export function formatSpeed(speed) {
|
|
26
|
+
if (speed <= 0)
|
|
27
|
+
return "instant";
|
|
28
|
+
if (speed === 1)
|
|
29
|
+
return "1x (realtime)";
|
|
30
|
+
if (Number.isInteger(speed))
|
|
31
|
+
return `${speed}x`;
|
|
32
|
+
return `${speed.toFixed(1)}x`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a speed string ("2x", "10x", "0.5x", "instant") into a number.
|
|
36
|
+
* Returns 0 for instant, null for invalid.
|
|
37
|
+
*/
|
|
38
|
+
export function parseSpeed(input) {
|
|
39
|
+
if (input === "instant" || input === "0")
|
|
40
|
+
return 0;
|
|
41
|
+
const match = input.match(/^(\d+(?:\.\d+)?)x?$/);
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
const val = parseFloat(match[1]);
|
|
45
|
+
if (!isFinite(val) || val < 0)
|
|
46
|
+
return null;
|
|
47
|
+
return val;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Filter entries by time window.
|
|
51
|
+
*/
|
|
52
|
+
export function filterByWindow(entries, maxAgeMs, now) {
|
|
53
|
+
if (!maxAgeMs)
|
|
54
|
+
return entries;
|
|
55
|
+
const cutoff = (now ?? Date.now()) - maxAgeMs;
|
|
56
|
+
return entries.filter((e) => e.ts >= cutoff);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the replay header.
|
|
60
|
+
*/
|
|
61
|
+
export function formatReplayHeader(entries, speed, windowLabel) {
|
|
62
|
+
if (entries.length === 0)
|
|
63
|
+
return `${DIM}no entries to replay${RESET}`;
|
|
64
|
+
const first = entries[0];
|
|
65
|
+
const last = entries[entries.length - 1];
|
|
66
|
+
const dateRange = formatTailDate(first.ts) === formatTailDate(last.ts)
|
|
67
|
+
? formatTailDate(first.ts)
|
|
68
|
+
: `${formatTailDate(first.ts)} → ${formatTailDate(last.ts)}`;
|
|
69
|
+
const span = last.ts - first.ts;
|
|
70
|
+
const spanStr = span < 60_000 ? `${Math.floor(span / 1000)}s`
|
|
71
|
+
: span < 3_600_000 ? `${Math.floor(span / 60_000)}m`
|
|
72
|
+
: `${Math.floor(span / 3_600_000)}h ${Math.floor((span % 3_600_000) / 60_000)}m`;
|
|
73
|
+
const speedStr = formatSpeed(speed);
|
|
74
|
+
const windowStr = windowLabel ? ` (${windowLabel})` : "";
|
|
75
|
+
return `${DIM}── replay: ${entries.length} entries, ${dateRange}, span ${spanStr}, ${AMBER}${speedStr}${RESET}${DIM}${windowStr} ──${RESET}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the replay footer.
|
|
79
|
+
*/
|
|
80
|
+
export function formatReplayFooter(entries) {
|
|
81
|
+
if (entries.length === 0)
|
|
82
|
+
return "";
|
|
83
|
+
return `${DIM}── replay complete: ${entries.length} entries ──${RESET}`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Sleep for a given number of milliseconds.
|
|
87
|
+
*/
|
|
88
|
+
function sleep(ms) {
|
|
89
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Load entries from the history file, optionally filtering by time window.
|
|
93
|
+
*/
|
|
94
|
+
export function loadReplayEntries(maxAgeMs, filePath = TUI_HISTORY_FILE) {
|
|
95
|
+
try {
|
|
96
|
+
if (!existsSync(filePath))
|
|
97
|
+
return [];
|
|
98
|
+
const content = readFileSync(filePath, "utf-8");
|
|
99
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(line);
|
|
104
|
+
if (isValidEntry(parsed))
|
|
105
|
+
entries.push(parsed);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// skip malformed
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return filterByWindow(entries, maxAgeMs);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run the replay.
|
|
119
|
+
* Prints entries with simulated delays between them.
|
|
120
|
+
* Speed 0 = instant (no delays).
|
|
121
|
+
* Ctrl+C stops playback.
|
|
122
|
+
*/
|
|
123
|
+
export async function runReplay(opts) {
|
|
124
|
+
const filePath = opts.filePath ?? TUI_HISTORY_FILE;
|
|
125
|
+
const maxAgeMs = opts.last ? parseDuration(opts.last) ?? undefined : undefined;
|
|
126
|
+
if (opts.last && maxAgeMs === undefined) {
|
|
127
|
+
process.stderr.write(`error: --last must be like "1h", "6h", "24h", "7d", got "${opts.last}"\n`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
const entries = loadReplayEntries(maxAgeMs, filePath);
|
|
131
|
+
const windowLabel = opts.last ?? undefined;
|
|
132
|
+
// header
|
|
133
|
+
process.stderr.write(formatReplayHeader(entries, opts.speed, windowLabel) + "\n");
|
|
134
|
+
if (entries.length === 0)
|
|
135
|
+
return;
|
|
136
|
+
// Ctrl+C cleanup
|
|
137
|
+
let stopped = false;
|
|
138
|
+
const onSignal = () => { stopped = true; };
|
|
139
|
+
process.on("SIGINT", onSignal);
|
|
140
|
+
process.on("SIGTERM", onSignal);
|
|
141
|
+
try {
|
|
142
|
+
for (let i = 0; i < entries.length; i++) {
|
|
143
|
+
if (stopped)
|
|
144
|
+
break;
|
|
145
|
+
// delay between entries
|
|
146
|
+
if (i > 0 && opts.speed > 0) {
|
|
147
|
+
const delay = computeDelay(entries[i - 1].ts, entries[i].ts, opts.speed);
|
|
148
|
+
if (delay > 0)
|
|
149
|
+
await sleep(delay);
|
|
150
|
+
}
|
|
151
|
+
if (stopped)
|
|
152
|
+
break;
|
|
153
|
+
process.stderr.write(formatTailEntry(entries[i]) + "\n");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
process.removeListener("SIGINT", onSignal);
|
|
158
|
+
process.removeListener("SIGTERM", onSignal);
|
|
159
|
+
}
|
|
160
|
+
// footer
|
|
161
|
+
if (!stopped) {
|
|
162
|
+
process.stderr.write(formatReplayFooter(entries) + "\n");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function isValidEntry(val) {
|
|
166
|
+
if (typeof val !== "object" || val === null)
|
|
167
|
+
return false;
|
|
168
|
+
const obj = val;
|
|
169
|
+
return (typeof obj.ts === "number" &&
|
|
170
|
+
typeof obj.time === "string" &&
|
|
171
|
+
typeof obj.tag === "string" &&
|
|
172
|
+
typeof obj.text === "string");
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=replay.js.map
|
package/dist/tui.d.ts
CHANGED
|
@@ -36,6 +36,8 @@ export declare class TUI {
|
|
|
36
36
|
start(version: string): void;
|
|
37
37
|
stop(): void;
|
|
38
38
|
isActive(): boolean;
|
|
39
|
+
/** Return the current number of sessions (for mouse hit testing) */
|
|
40
|
+
getSessionCount(): number;
|
|
39
41
|
updateState(opts: {
|
|
40
42
|
phase?: DaemonPhase;
|
|
41
43
|
pollCount?: number;
|
|
@@ -94,5 +96,13 @@ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scr
|
|
|
94
96
|
end: number;
|
|
95
97
|
};
|
|
96
98
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
99
|
+
/**
|
|
100
|
+
* Hit-test a mouse click row against the session panel.
|
|
101
|
+
* Returns 1-indexed session number if the click hit a session card, null otherwise.
|
|
102
|
+
*
|
|
103
|
+
* Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
|
|
104
|
+
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
105
|
+
*/
|
|
106
|
+
export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
|
|
97
107
|
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
98
108
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -12,6 +12,9 @@ const CURSOR_HIDE = `${CSI}?25l`;
|
|
|
12
12
|
const CURSOR_SHOW = `${CSI}?25h`;
|
|
13
13
|
const SAVE_CURSOR = `${ESC}7`;
|
|
14
14
|
const RESTORE_CURSOR = `${ESC}8`;
|
|
15
|
+
// mouse tracking (SGR extended mode — button events + extended coordinates)
|
|
16
|
+
const MOUSE_ON = `${CSI}?1000h${CSI}?1006h`;
|
|
17
|
+
const MOUSE_OFF = `${CSI}?1000l${CSI}?1006l`;
|
|
15
18
|
// cursor movement
|
|
16
19
|
const moveTo = (row, col) => `${CSI}${row};${col}H`;
|
|
17
20
|
const setScrollRegion = (top, bottom) => `${CSI}${top};${bottom}r`;
|
|
@@ -76,8 +79,8 @@ export class TUI {
|
|
|
76
79
|
this.active = true;
|
|
77
80
|
this.version = version;
|
|
78
81
|
this.updateDimensions();
|
|
79
|
-
// enter alternate screen, hide cursor, clear
|
|
80
|
-
process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
|
|
82
|
+
// enter alternate screen, hide cursor, clear, enable mouse
|
|
83
|
+
process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN + MOUSE_ON);
|
|
81
84
|
// handle terminal resize
|
|
82
85
|
process.stdout.on("resize", () => this.onResize());
|
|
83
86
|
// tick timer: countdown + spinner animation (~4 fps for smooth braille spin)
|
|
@@ -102,12 +105,16 @@ export class TUI {
|
|
|
102
105
|
clearInterval(this.countdownTimer);
|
|
103
106
|
this.countdownTimer = null;
|
|
104
107
|
}
|
|
105
|
-
// restore normal screen, show cursor, reset scroll region
|
|
106
|
-
process.stderr.write(resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
108
|
+
// disable mouse, restore normal screen, show cursor, reset scroll region
|
|
109
|
+
process.stderr.write(MOUSE_OFF + resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
107
110
|
}
|
|
108
111
|
isActive() {
|
|
109
112
|
return this.active;
|
|
110
113
|
}
|
|
114
|
+
/** Return the current number of sessions (for mouse hit testing) */
|
|
115
|
+
getSessionCount() {
|
|
116
|
+
return this.sessions.length;
|
|
117
|
+
}
|
|
111
118
|
// ── State updates ───────────────────────────────────────────────────────
|
|
112
119
|
updateState(opts) {
|
|
113
120
|
if (opts.phase !== undefined)
|
|
@@ -383,7 +390,7 @@ export class TUI {
|
|
|
383
390
|
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
384
391
|
}
|
|
385
392
|
else {
|
|
386
|
-
hints = " esc esc: interrupt /help
|
|
393
|
+
hints = " click agent to view esc esc: interrupt /help ";
|
|
387
394
|
}
|
|
388
395
|
const totalLen = prefix.length + hints.length;
|
|
389
396
|
const fill = Math.max(0, this.cols - totalLen);
|
|
@@ -639,6 +646,23 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
|
639
646
|
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
640
647
|
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
641
648
|
}
|
|
649
|
+
// ── Mouse hit testing (pure, exported for testing) ──────────────────────────
|
|
650
|
+
/**
|
|
651
|
+
* Hit-test a mouse click row against the session panel.
|
|
652
|
+
* Returns 1-indexed session number if the click hit a session card, null otherwise.
|
|
653
|
+
*
|
|
654
|
+
* Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
|
|
655
|
+
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
656
|
+
*/
|
|
657
|
+
export function hitTestSession(row, headerHeight, sessionCount) {
|
|
658
|
+
if (sessionCount <= 0)
|
|
659
|
+
return null;
|
|
660
|
+
const firstSessionRow = headerHeight + 2; // top border is headerHeight+1, first card is +2
|
|
661
|
+
const lastSessionRow = firstSessionRow + sessionCount - 1;
|
|
662
|
+
if (row < firstSessionRow || row > lastSessionRow)
|
|
663
|
+
return null;
|
|
664
|
+
return row - firstSessionRow + 1; // 1-indexed
|
|
665
|
+
}
|
|
642
666
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
643
667
|
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
644
668
|
//# sourceMappingURL=tui.js.map
|