aoaoe 0.66.0 → 0.68.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-watcher.d.ts +44 -0
- package/dist/config-watcher.js +159 -0
- package/dist/config.js +4 -0
- package/dist/executor.d.ts +2 -0
- package/dist/executor.js +4 -0
- package/dist/index.js +60 -3
- package/dist/input.d.ts +3 -0
- package/dist/input.js +27 -0
- package/dist/tui.d.ts +17 -1
- package/dist/tui.js +155 -27
- package/package.json +1 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AoaoeConfig } from "./types.js";
|
|
2
|
+
/** Describes a single config field change */
|
|
3
|
+
export interface ConfigChange {
|
|
4
|
+
field: string;
|
|
5
|
+
oldValue: unknown;
|
|
6
|
+
newValue: unknown;
|
|
7
|
+
applied: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Merge hot-reloadable fields from fresh config into the current config.
|
|
11
|
+
* Returns: { config: updated config, changes: list of what changed }
|
|
12
|
+
* Non-reloadable fields are left unchanged but reported as changes.
|
|
13
|
+
*/
|
|
14
|
+
export declare function mergeHotReload(current: AoaoeConfig, fresh: AoaoeConfig): {
|
|
15
|
+
config: AoaoeConfig;
|
|
16
|
+
changes: ConfigChange[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Format a config change for display in the TUI.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatConfigChange(change: ConfigChange): string;
|
|
22
|
+
export type ConfigChangeCallback = (changes: ConfigChange[], newConfig: AoaoeConfig) => void;
|
|
23
|
+
/**
|
|
24
|
+
* ConfigWatcher — watches the config file and calls back when it changes.
|
|
25
|
+
* Debounces rapid changes (editors do save-rename-write sequences).
|
|
26
|
+
*/
|
|
27
|
+
export declare class ConfigWatcher {
|
|
28
|
+
private watcher;
|
|
29
|
+
private debounceTimer;
|
|
30
|
+
private configPath;
|
|
31
|
+
private callback;
|
|
32
|
+
private currentConfig;
|
|
33
|
+
private debounceMs;
|
|
34
|
+
constructor(config: AoaoeConfig, debounceMs?: number);
|
|
35
|
+
/** Start watching the config file. Returns the watched path or null. */
|
|
36
|
+
start(callback: ConfigChangeCallback): string | null;
|
|
37
|
+
/** Stop watching */
|
|
38
|
+
stop(): void;
|
|
39
|
+
/** Get the current config (may have been hot-reloaded) */
|
|
40
|
+
getConfig(): AoaoeConfig;
|
|
41
|
+
private scheduleReload;
|
|
42
|
+
private doReload;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=config-watcher.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// config-watcher.ts — watch config file for changes and hot-reload safe fields.
|
|
2
|
+
// pure mergeHotReload function + ConfigWatcher class using fs.watch.
|
|
3
|
+
import { watch } from "node:fs";
|
|
4
|
+
import { loadConfig, findConfigFile, validateConfig } from "./config.js";
|
|
5
|
+
/**
|
|
6
|
+
* Fields that are safe to hot-reload without restarting the daemon.
|
|
7
|
+
* Fields NOT in this list require a daemon restart to take effect.
|
|
8
|
+
*
|
|
9
|
+
* Unsafe (require restart): reasoner, opencode.port, healthPort, dryRun, observe, confirm
|
|
10
|
+
*/
|
|
11
|
+
const HOT_RELOAD_FIELDS = new Set([
|
|
12
|
+
"pollIntervalMs",
|
|
13
|
+
"sessionDirs",
|
|
14
|
+
"protectedSessions",
|
|
15
|
+
"contextFiles",
|
|
16
|
+
"verbose",
|
|
17
|
+
"captureLinesCount",
|
|
18
|
+
"tuiHistoryRetentionDays",
|
|
19
|
+
]);
|
|
20
|
+
/** Nested objects where all sub-fields are safe to hot-reload */
|
|
21
|
+
const HOT_RELOAD_OBJECTS = new Set([
|
|
22
|
+
"policies",
|
|
23
|
+
"notifications",
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Merge hot-reloadable fields from fresh config into the current config.
|
|
27
|
+
* Returns: { config: updated config, changes: list of what changed }
|
|
28
|
+
* Non-reloadable fields are left unchanged but reported as changes.
|
|
29
|
+
*/
|
|
30
|
+
export function mergeHotReload(current, fresh) {
|
|
31
|
+
const changes = [];
|
|
32
|
+
const merged = { ...current };
|
|
33
|
+
// check top-level scalar/array fields
|
|
34
|
+
for (const field of HOT_RELOAD_FIELDS) {
|
|
35
|
+
const key = field;
|
|
36
|
+
const curVal = JSON.stringify(current[key]);
|
|
37
|
+
const freshVal = JSON.stringify(fresh[key]);
|
|
38
|
+
if (curVal !== freshVal) {
|
|
39
|
+
changes.push({ field, oldValue: current[key], newValue: fresh[key], applied: true });
|
|
40
|
+
merged[key] = fresh[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// check nested objects (policies, notifications)
|
|
44
|
+
for (const objField of HOT_RELOAD_OBJECTS) {
|
|
45
|
+
const key = objField;
|
|
46
|
+
const curVal = JSON.stringify(current[key]);
|
|
47
|
+
const freshVal = JSON.stringify(fresh[key]);
|
|
48
|
+
if (curVal !== freshVal) {
|
|
49
|
+
changes.push({ field: objField, oldValue: current[key], newValue: fresh[key], applied: true });
|
|
50
|
+
merged[key] = fresh[key];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// detect non-reloadable changes (warn user)
|
|
54
|
+
const unsafeFields = [
|
|
55
|
+
"reasoner", "dryRun", "observe", "confirm", "healthPort",
|
|
56
|
+
];
|
|
57
|
+
for (const key of unsafeFields) {
|
|
58
|
+
const curVal = JSON.stringify(current[key]);
|
|
59
|
+
const freshVal = JSON.stringify(fresh[key]);
|
|
60
|
+
if (curVal !== freshVal) {
|
|
61
|
+
changes.push({ field: key, oldValue: current[key], newValue: fresh[key], applied: false });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// check nested non-reloadable (opencode.port)
|
|
65
|
+
if (current.opencode?.port !== fresh.opencode?.port) {
|
|
66
|
+
changes.push({ field: "opencode.port", oldValue: current.opencode?.port, newValue: fresh.opencode?.port, applied: false });
|
|
67
|
+
}
|
|
68
|
+
return { config: merged, changes };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Format a config change for display in the TUI.
|
|
72
|
+
*/
|
|
73
|
+
export function formatConfigChange(change) {
|
|
74
|
+
const status = change.applied ? "applied" : "requires restart";
|
|
75
|
+
const newVal = typeof change.newValue === "object"
|
|
76
|
+
? JSON.stringify(change.newValue)
|
|
77
|
+
: String(change.newValue);
|
|
78
|
+
const truncated = newVal.length > 60 ? newVal.slice(0, 57) + "..." : newVal;
|
|
79
|
+
return `${change.field}: ${truncated} (${status})`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* ConfigWatcher — watches the config file and calls back when it changes.
|
|
83
|
+
* Debounces rapid changes (editors do save-rename-write sequences).
|
|
84
|
+
*/
|
|
85
|
+
export class ConfigWatcher {
|
|
86
|
+
watcher = null;
|
|
87
|
+
debounceTimer = null;
|
|
88
|
+
configPath = null;
|
|
89
|
+
callback = null;
|
|
90
|
+
currentConfig;
|
|
91
|
+
debounceMs;
|
|
92
|
+
constructor(config, debounceMs = 500) {
|
|
93
|
+
this.currentConfig = config;
|
|
94
|
+
this.debounceMs = debounceMs;
|
|
95
|
+
}
|
|
96
|
+
/** Start watching the config file. Returns the watched path or null. */
|
|
97
|
+
start(callback) {
|
|
98
|
+
this.callback = callback;
|
|
99
|
+
this.configPath = findConfigFile();
|
|
100
|
+
if (!this.configPath)
|
|
101
|
+
return null;
|
|
102
|
+
try {
|
|
103
|
+
this.watcher = watch(this.configPath, { persistent: false }, () => {
|
|
104
|
+
this.scheduleReload();
|
|
105
|
+
});
|
|
106
|
+
this.watcher.on("error", () => {
|
|
107
|
+
// config file may be renamed during save — try to re-watch
|
|
108
|
+
this.stop();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// watch failed — degrade gracefully
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return this.configPath;
|
|
116
|
+
}
|
|
117
|
+
/** Stop watching */
|
|
118
|
+
stop() {
|
|
119
|
+
if (this.debounceTimer) {
|
|
120
|
+
clearTimeout(this.debounceTimer);
|
|
121
|
+
this.debounceTimer = null;
|
|
122
|
+
}
|
|
123
|
+
if (this.watcher) {
|
|
124
|
+
try {
|
|
125
|
+
this.watcher.close();
|
|
126
|
+
}
|
|
127
|
+
catch { }
|
|
128
|
+
this.watcher = null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Get the current config (may have been hot-reloaded) */
|
|
132
|
+
getConfig() {
|
|
133
|
+
return this.currentConfig;
|
|
134
|
+
}
|
|
135
|
+
scheduleReload() {
|
|
136
|
+
if (this.debounceTimer)
|
|
137
|
+
clearTimeout(this.debounceTimer);
|
|
138
|
+
this.debounceTimer = setTimeout(() => {
|
|
139
|
+
this.debounceTimer = null;
|
|
140
|
+
this.doReload();
|
|
141
|
+
}, this.debounceMs);
|
|
142
|
+
}
|
|
143
|
+
doReload() {
|
|
144
|
+
try {
|
|
145
|
+
const fresh = loadConfig();
|
|
146
|
+
validateConfig(fresh);
|
|
147
|
+
const { config, changes } = mergeHotReload(this.currentConfig, fresh);
|
|
148
|
+
if (changes.length > 0) {
|
|
149
|
+
this.currentConfig = config;
|
|
150
|
+
this.callback?.(changes, config);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// invalid config — ignore silently (keep running with current config)
|
|
155
|
+
console.error(`[config-watcher] reload failed: ${err}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=config-watcher.js.map
|
package/dist/config.js
CHANGED
|
@@ -593,6 +593,8 @@ interactive commands (while daemon is running):
|
|
|
593
593
|
/help show available commands
|
|
594
594
|
/explain ask the AI to explain what's happening in plain English
|
|
595
595
|
/insist <msg> interrupt + deliver message immediately (skip queue)
|
|
596
|
+
/view [N|name] drill into a session's live output (default: 1)
|
|
597
|
+
/back return to overview from drill-down
|
|
596
598
|
/status request daemon status
|
|
597
599
|
/dashboard request full dashboard output
|
|
598
600
|
/pause pause the daemon
|
|
@@ -600,6 +602,8 @@ interactive commands (while daemon is running):
|
|
|
600
602
|
/interrupt interrupt the current reasoner call
|
|
601
603
|
/verbose toggle verbose logging
|
|
602
604
|
/clear clear the screen
|
|
605
|
+
PgUp / PgDn scroll through activity history
|
|
606
|
+
Home / End jump to oldest / return to live
|
|
603
607
|
ESC ESC interrupt the current reasoner (shortcut)
|
|
604
608
|
!message insist shortcut — same as /insist message
|
|
605
609
|
(anything) send a message to the AI — queued for next cycle`);
|
package/dist/executor.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare class Executor {
|
|
|
10
10
|
private taskManager?;
|
|
11
11
|
constructor(config: AoaoeConfig);
|
|
12
12
|
setTaskManager(tm: TaskManager): void;
|
|
13
|
+
/** Hot-reload: update the config reference (picks up new protectedSessions, policies, etc.) */
|
|
14
|
+
updateConfig(newConfig: AoaoeConfig): void;
|
|
13
15
|
execute(actions: Action[], snapshots: SessionSnapshot[]): Promise<ActionLogEntry[]>;
|
|
14
16
|
private executeOne;
|
|
15
17
|
private sendInput;
|
package/dist/executor.js
CHANGED
|
@@ -32,6 +32,10 @@ export class Executor {
|
|
|
32
32
|
setTaskManager(tm) {
|
|
33
33
|
this.taskManager = tm;
|
|
34
34
|
}
|
|
35
|
+
/** Hot-reload: update the config reference (picks up new protectedSessions, policies, etc.) */
|
|
36
|
+
updateConfig(newConfig) {
|
|
37
|
+
this.config = newConfig;
|
|
38
|
+
}
|
|
35
39
|
async execute(actions, snapshots) {
|
|
36
40
|
const results = [];
|
|
37
41
|
for (const action of actions) {
|
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 { ConfigWatcher, formatConfigChange } from "./config-watcher.js";
|
|
23
24
|
import { parseActionLogEntries, parseActivityEntries, mergeTimeline, filterByAge, parseDuration, formatTimelineJson, formatTimelineMarkdown } from "./export.js";
|
|
24
25
|
import { actionSession, actionDetail, toActionLogEntry } from "./types.js";
|
|
25
26
|
import { YELLOW, GREEN, DIM, BOLD, RED, RESET } from "./colors.js";
|
|
@@ -137,7 +138,7 @@ async function main() {
|
|
|
137
138
|
}
|
|
138
139
|
const configResult = loadConfig(overrides);
|
|
139
140
|
const configPath = configResult._configPath;
|
|
140
|
-
|
|
141
|
+
let config = configResult; // strip _configPath from type for downstream (let: hot-reloaded)
|
|
141
142
|
// acquire daemon lock — prevent two daemons from running simultaneously
|
|
142
143
|
const lock = acquireLock();
|
|
143
144
|
if (!lock.acquired) {
|
|
@@ -257,6 +258,24 @@ async function main() {
|
|
|
257
258
|
input.onQueueChange((count) => {
|
|
258
259
|
tui.updateState({ pendingCount: count });
|
|
259
260
|
});
|
|
261
|
+
// wire /view and /back commands to TUI drill-down
|
|
262
|
+
input.onView((target) => {
|
|
263
|
+
if (target === null) {
|
|
264
|
+
tui.exitDrilldown();
|
|
265
|
+
tui.log("system", "returned to overview");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// try number first, then name/id
|
|
269
|
+
const num = parseInt(target, 10);
|
|
270
|
+
const ok = !isNaN(num) ? tui.enterDrilldown(num) : tui.enterDrilldown(target);
|
|
271
|
+
if (ok) {
|
|
272
|
+
tui.log("system", `viewing session: ${target}`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
tui.log("system", `session not found: ${target}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
260
279
|
}
|
|
261
280
|
// start TUI (alternate screen buffer) after input is ready
|
|
262
281
|
if (tui) {
|
|
@@ -355,6 +374,7 @@ async function main() {
|
|
|
355
374
|
console.error(` mode: dry-run (no execution)`);
|
|
356
375
|
console.error("");
|
|
357
376
|
log("shutting down...");
|
|
377
|
+
configWatcher.stop();
|
|
358
378
|
if (healthServer)
|
|
359
379
|
healthServer.close();
|
|
360
380
|
// notify: daemon stopped (fire-and-forget, don't block shutdown)
|
|
@@ -382,6 +402,36 @@ async function main() {
|
|
|
382
402
|
else {
|
|
383
403
|
log("entering main loop (Ctrl+C to stop)\n");
|
|
384
404
|
}
|
|
405
|
+
// ── config hot-reload watcher ──────────────────────────────────────────────
|
|
406
|
+
const configWatcher = new ConfigWatcher(config);
|
|
407
|
+
const watchedPath = configWatcher.start((changes, newConfig) => {
|
|
408
|
+
config = newConfig;
|
|
409
|
+
if (executor)
|
|
410
|
+
executor.updateConfig(newConfig);
|
|
411
|
+
const applied = changes.filter((c) => c.applied);
|
|
412
|
+
const needsRestart = changes.filter((c) => !c.applied);
|
|
413
|
+
for (const c of applied) {
|
|
414
|
+
const msg = `config: ${formatConfigChange(c)}`;
|
|
415
|
+
if (tui)
|
|
416
|
+
tui.log("system", msg);
|
|
417
|
+
else
|
|
418
|
+
log(msg);
|
|
419
|
+
}
|
|
420
|
+
for (const c of needsRestart) {
|
|
421
|
+
const msg = `config: ${c.field} changed but requires restart`;
|
|
422
|
+
if (tui)
|
|
423
|
+
tui.log("system", msg);
|
|
424
|
+
else
|
|
425
|
+
log(msg);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
if (watchedPath) {
|
|
429
|
+
const msg = `watching config: ${watchedPath}`;
|
|
430
|
+
if (tui)
|
|
431
|
+
tui.log("system", msg);
|
|
432
|
+
else
|
|
433
|
+
log(msg);
|
|
434
|
+
}
|
|
385
435
|
// notify: daemon started
|
|
386
436
|
sendNotification(config, { event: "daemon_started", timestamp: Date.now(), detail: `reasoner: ${config.reasoner}` });
|
|
387
437
|
// clear any stale interrupt from a previous run
|
|
@@ -703,9 +753,16 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
703
753
|
const sessionStates = buildSessionStates(observation);
|
|
704
754
|
const taskStates = taskManager ? taskManager.tasks : undefined;
|
|
705
755
|
writeState("polling", { pollCount, sessionCount, changeCount, sessions: sessionStates, tasks: taskStates });
|
|
706
|
-
// update TUI session panel
|
|
707
|
-
if (tui)
|
|
756
|
+
// update TUI session panel + drill-down outputs
|
|
757
|
+
if (tui) {
|
|
708
758
|
tui.updateState({ phase: "polling", pollCount, sessions: sessionStates });
|
|
759
|
+
// pass full session outputs for drill-down view
|
|
760
|
+
const outputs = new Map();
|
|
761
|
+
for (const snap of observation.sessions) {
|
|
762
|
+
outputs.set(snap.session.id, snap.output);
|
|
763
|
+
}
|
|
764
|
+
tui.setSessionOutputs(outputs);
|
|
765
|
+
}
|
|
709
766
|
const noStats = { interrupted: false, decisionsThisTick: 0, actionsOk: 0, actionsFail: 0 };
|
|
710
767
|
// skip cases
|
|
711
768
|
if (skippedReason === "no sessions") {
|
package/dist/input.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
2
2
|
export declare const INSIST_PREFIX = "__INSIST__";
|
|
3
|
+
export type ViewHandler = (target: string | null) => void;
|
|
3
4
|
export declare class InputReader {
|
|
4
5
|
private rl;
|
|
5
6
|
private queue;
|
|
@@ -7,8 +8,10 @@ export declare class InputReader {
|
|
|
7
8
|
private lastEscTime;
|
|
8
9
|
private scrollHandler;
|
|
9
10
|
private queueChangeHandler;
|
|
11
|
+
private viewHandler;
|
|
10
12
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
11
13
|
onQueueChange(handler: (count: number) => void): void;
|
|
14
|
+
onView(handler: ViewHandler): void;
|
|
12
15
|
private notifyQueueChange;
|
|
13
16
|
start(): void;
|
|
14
17
|
drain(): string[];
|
package/dist/input.js
CHANGED
|
@@ -14,6 +14,7 @@ export class InputReader {
|
|
|
14
14
|
lastEscTime = 0;
|
|
15
15
|
scrollHandler = null;
|
|
16
16
|
queueChangeHandler = null;
|
|
17
|
+
viewHandler = null;
|
|
17
18
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
18
19
|
onScroll(handler) {
|
|
19
20
|
this.scrollHandler = handler;
|
|
@@ -22,6 +23,10 @@ export class InputReader {
|
|
|
22
23
|
onQueueChange(handler) {
|
|
23
24
|
this.queueChangeHandler = handler;
|
|
24
25
|
}
|
|
26
|
+
// register a callback for view commands (/view, /back)
|
|
27
|
+
onView(handler) {
|
|
28
|
+
this.viewHandler = handler;
|
|
29
|
+
}
|
|
25
30
|
notifyQueueChange() {
|
|
26
31
|
this.queueChangeHandler?.(this.queue.length);
|
|
27
32
|
}
|
|
@@ -158,6 +163,10 @@ ${BOLD}controls:${RESET}
|
|
|
158
163
|
/resume resume the supervisor
|
|
159
164
|
/interrupt interrupt the AI mid-thought
|
|
160
165
|
ESC ESC same as /interrupt (shortcut)
|
|
166
|
+
|
|
167
|
+
${BOLD}navigation:${RESET}
|
|
168
|
+
/view [N|name] drill into a session's live output (default: 1)
|
|
169
|
+
/back return to overview from drill-down
|
|
161
170
|
PgUp / PgDn scroll through activity history
|
|
162
171
|
Home / End jump to oldest / return to live
|
|
163
172
|
|
|
@@ -215,6 +224,24 @@ ${BOLD}other:${RESET}
|
|
|
215
224
|
this.queue.push(`__CMD_TASK__${taskArgs}`);
|
|
216
225
|
break;
|
|
217
226
|
}
|
|
227
|
+
case "/view": {
|
|
228
|
+
const viewArg = line.slice("/view".length).trim();
|
|
229
|
+
if (this.viewHandler) {
|
|
230
|
+
this.viewHandler(viewArg || "1"); // default to session 1
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
console.error(`${DIM}drill-down not available (no TUI)${RESET}`);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case "/back":
|
|
238
|
+
if (this.viewHandler) {
|
|
239
|
+
this.viewHandler(null); // null = back to overview
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
console.error(`${DIM}already in overview${RESET}`);
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
218
245
|
case "/clear":
|
|
219
246
|
process.stderr.write("\x1b[2J\x1b[H");
|
|
220
247
|
break;
|
package/dist/tui.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export declare class TUI {
|
|
|
23
23
|
private scrollOffset;
|
|
24
24
|
private newWhileScrolled;
|
|
25
25
|
private pendingCount;
|
|
26
|
+
private viewMode;
|
|
27
|
+
private drilldownSessionId;
|
|
28
|
+
private sessionOutputs;
|
|
26
29
|
private phase;
|
|
27
30
|
private pollCount;
|
|
28
31
|
private sessions;
|
|
@@ -49,6 +52,16 @@ export declare class TUI {
|
|
|
49
52
|
scrollToTop(): void;
|
|
50
53
|
scrollToBottom(): void;
|
|
51
54
|
isScrolledBack(): boolean;
|
|
55
|
+
/** Store full session outputs (called each tick from main loop) */
|
|
56
|
+
setSessionOutputs(outputs: Map<string, string>): void;
|
|
57
|
+
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
58
|
+
enterDrilldown(sessionIdOrIndex: string | number): boolean;
|
|
59
|
+
/** Exit drill-down, return to overview */
|
|
60
|
+
exitDrilldown(): void;
|
|
61
|
+
/** Get current view mode */
|
|
62
|
+
getViewMode(): "overview" | "drilldown";
|
|
63
|
+
/** Get drill-down session ID (or null) */
|
|
64
|
+
getDrilldownSessionId(): string | null;
|
|
52
65
|
private updateDimensions;
|
|
53
66
|
private computeLayout;
|
|
54
67
|
private onResize;
|
|
@@ -58,6 +71,8 @@ export declare class TUI {
|
|
|
58
71
|
private paintSeparator;
|
|
59
72
|
private writeActivityLine;
|
|
60
73
|
private repaintActivityRegion;
|
|
74
|
+
private paintDrilldownSeparator;
|
|
75
|
+
private repaintDrilldownContent;
|
|
61
76
|
private paintInputLine;
|
|
62
77
|
}
|
|
63
78
|
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number): string;
|
|
@@ -72,11 +87,12 @@ declare function truncatePlain(str: string, max: number): string;
|
|
|
72
87
|
* Kept for backward compatibility — used by non-TUI output paths.
|
|
73
88
|
*/
|
|
74
89
|
export declare function formatSessionSentence(s: DaemonSessionState, maxCols: number): string;
|
|
90
|
+
declare function formatDrilldownHeader(sessionId: string, sessions: DaemonSessionState[], phase: DaemonPhase, paused: boolean, spinnerFrame: number, _cols: number): string;
|
|
75
91
|
declare function formatPrompt(phase: DaemonPhase, paused: boolean, pendingCount: number): string;
|
|
76
92
|
declare function computeScrollSlice(bufferLen: number, visibleLines: number, scrollOffset: number): {
|
|
77
93
|
start: number;
|
|
78
94
|
end: number;
|
|
79
95
|
};
|
|
80
96
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
81
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt };
|
|
97
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
82
98
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -58,6 +58,10 @@ export class TUI {
|
|
|
58
58
|
scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
|
|
59
59
|
newWhileScrolled = 0; // entries added while user is scrolled back
|
|
60
60
|
pendingCount = 0; // queued user messages awaiting next tick
|
|
61
|
+
// drill-down mode: show a single session's full output
|
|
62
|
+
viewMode = "overview";
|
|
63
|
+
drilldownSessionId = null;
|
|
64
|
+
sessionOutputs = new Map(); // full output lines per session
|
|
61
65
|
// current state for repaints
|
|
62
66
|
phase = "sleeping";
|
|
63
67
|
pollCount = 0;
|
|
@@ -210,6 +214,61 @@ export class TUI {
|
|
|
210
214
|
isScrolledBack() {
|
|
211
215
|
return this.scrollOffset > 0;
|
|
212
216
|
}
|
|
217
|
+
// ── Drill-down mode ────────────────────────────────────────────────────
|
|
218
|
+
/** Store full session outputs (called each tick from main loop) */
|
|
219
|
+
setSessionOutputs(outputs) {
|
|
220
|
+
for (const [id, text] of outputs) {
|
|
221
|
+
this.sessionOutputs.set(id, text.split("\n"));
|
|
222
|
+
}
|
|
223
|
+
// repaint drill-down view if we're watching this session
|
|
224
|
+
if (this.active && this.viewMode === "drilldown" && this.drilldownSessionId) {
|
|
225
|
+
this.repaintDrilldownContent();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
229
|
+
enterDrilldown(sessionIdOrIndex) {
|
|
230
|
+
let sessionId;
|
|
231
|
+
if (typeof sessionIdOrIndex === "number") {
|
|
232
|
+
const idx = sessionIdOrIndex - 1; // 1-indexed for user
|
|
233
|
+
if (idx >= 0 && idx < this.sessions.length) {
|
|
234
|
+
sessionId = this.sessions[idx].id;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// match by id prefix or title (case-insensitive)
|
|
239
|
+
const needle = sessionIdOrIndex.toLowerCase();
|
|
240
|
+
const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
|
|
241
|
+
sessionId = match?.id;
|
|
242
|
+
}
|
|
243
|
+
if (!sessionId)
|
|
244
|
+
return false;
|
|
245
|
+
this.viewMode = "drilldown";
|
|
246
|
+
this.drilldownSessionId = sessionId;
|
|
247
|
+
if (this.active) {
|
|
248
|
+
this.computeLayout(this.sessions.length);
|
|
249
|
+
this.paintAll();
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
/** Exit drill-down, return to overview */
|
|
254
|
+
exitDrilldown() {
|
|
255
|
+
if (this.viewMode === "overview")
|
|
256
|
+
return;
|
|
257
|
+
this.viewMode = "overview";
|
|
258
|
+
this.drilldownSessionId = null;
|
|
259
|
+
if (this.active) {
|
|
260
|
+
this.computeLayout(this.sessions.length);
|
|
261
|
+
this.paintAll();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/** Get current view mode */
|
|
265
|
+
getViewMode() {
|
|
266
|
+
return this.viewMode;
|
|
267
|
+
}
|
|
268
|
+
/** Get drill-down session ID (or null) */
|
|
269
|
+
getDrilldownSessionId() {
|
|
270
|
+
return this.drilldownSessionId;
|
|
271
|
+
}
|
|
213
272
|
// ── Layout computation ──────────────────────────────────────────────────
|
|
214
273
|
updateDimensions() {
|
|
215
274
|
this.cols = process.stderr.columns || 80;
|
|
@@ -217,17 +276,23 @@ export class TUI {
|
|
|
217
276
|
}
|
|
218
277
|
computeLayout(sessionCount) {
|
|
219
278
|
this.updateDimensions();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
279
|
+
if (this.viewMode === "drilldown") {
|
|
280
|
+
// drilldown: header (1) + separator (1) + content + input (1)
|
|
281
|
+
this.sessionRows = 0;
|
|
282
|
+
this.separatorRow = this.headerHeight + 1;
|
|
283
|
+
this.inputRow = this.rows;
|
|
284
|
+
this.scrollTop = this.separatorRow + 1;
|
|
285
|
+
this.scrollBottom = this.rows - 1;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// overview: header (1) + sessions box + separator + activity + input
|
|
289
|
+
const sessBodyRows = Math.max(sessionCount, 1);
|
|
290
|
+
this.sessionRows = sessBodyRows + 2; // + top/bottom borders
|
|
291
|
+
this.separatorRow = this.headerHeight + this.sessionRows + 1;
|
|
292
|
+
this.inputRow = this.rows;
|
|
293
|
+
this.scrollTop = this.separatorRow + 1;
|
|
294
|
+
this.scrollBottom = this.rows - 1;
|
|
295
|
+
}
|
|
231
296
|
if (this.active) {
|
|
232
297
|
process.stderr.write(setScrollRegion(this.scrollTop, this.scrollBottom));
|
|
233
298
|
}
|
|
@@ -243,25 +308,37 @@ export class TUI {
|
|
|
243
308
|
process.stderr.write(CLEAR_SCREEN);
|
|
244
309
|
process.stderr.write(setScrollRegion(this.scrollTop, this.scrollBottom));
|
|
245
310
|
this.paintHeader();
|
|
246
|
-
this.
|
|
247
|
-
|
|
248
|
-
|
|
311
|
+
if (this.viewMode === "drilldown") {
|
|
312
|
+
this.paintDrilldownSeparator();
|
|
313
|
+
this.repaintDrilldownContent();
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.paintSessions();
|
|
317
|
+
this.paintSeparator();
|
|
318
|
+
this.repaintActivityRegion();
|
|
319
|
+
}
|
|
249
320
|
this.paintInputLine();
|
|
250
321
|
}
|
|
251
322
|
paintHeader() {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
323
|
+
let line;
|
|
324
|
+
if (this.viewMode === "drilldown" && this.drilldownSessionId) {
|
|
325
|
+
line = formatDrilldownHeader(this.drilldownSessionId, this.sessions, this.phase, this.paused, this.spinnerFrame, this.cols);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const phaseText = phaseDisplay(this.phase, this.paused, this.spinnerFrame);
|
|
329
|
+
const sessCount = `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
|
|
330
|
+
const activeCount = this.sessions.filter((s) => s.userActive).length;
|
|
331
|
+
const activeTag = activeCount > 0 ? ` ${SLATE}│${RESET} ${AMBER}${activeCount} user${RESET}` : "";
|
|
332
|
+
// countdown to next tick (only in sleeping phase)
|
|
333
|
+
let countdownTag = "";
|
|
334
|
+
if (this.phase === "sleeping" && this.nextTickAt > 0) {
|
|
335
|
+
const remaining = Math.max(0, Math.ceil((this.nextTickAt - Date.now()) / 1000));
|
|
336
|
+
countdownTag = ` ${SLATE}│${RESET} ${SLATE}${remaining}s${RESET}`;
|
|
337
|
+
}
|
|
338
|
+
// reasoner badge
|
|
339
|
+
const reasonerTag = this.reasonerName ? ` ${SLATE}│${RESET} ${TEAL}${this.reasonerName}${RESET}` : "";
|
|
340
|
+
line = ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}${this.version}${RESET} ${SLATE}│${RESET} #${this.pollCount} ${SLATE}│${RESET} ${sessCount} ${SLATE}│${RESET} ${phaseText}${activeTag}${countdownTag}${reasonerTag}`;
|
|
261
341
|
}
|
|
262
|
-
// reasoner badge
|
|
263
|
-
const reasonerTag = this.reasonerName ? ` ${SLATE}│${RESET} ${TEAL}${this.reasonerName}${RESET}` : "";
|
|
264
|
-
const line = ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}${this.version}${RESET} ${SLATE}│${RESET} #${this.pollCount} ${SLATE}│${RESET} ${sessCount} ${SLATE}│${RESET} ${phaseText}${activeTag}${countdownTag}${reasonerTag}`;
|
|
265
342
|
process.stderr.write(SAVE_CURSOR +
|
|
266
343
|
moveTo(1, 1) + CLEAR_LINE + BG_DARK + WHITE + truncateAnsi(line, this.cols) + padToWidth(line, this.cols) + RESET +
|
|
267
344
|
RESTORE_CURSOR);
|
|
@@ -339,6 +416,38 @@ export class TUI {
|
|
|
339
416
|
}
|
|
340
417
|
}
|
|
341
418
|
}
|
|
419
|
+
// ── Drill-down rendering ──────────────────────────────────────────────
|
|
420
|
+
paintDrilldownSeparator() {
|
|
421
|
+
const session = this.sessions.find((s) => s.id === this.drilldownSessionId);
|
|
422
|
+
const title = session ? session.title : this.drilldownSessionId ?? "?";
|
|
423
|
+
const prefix = `${BOX.h}${BOX.h} ${title} `;
|
|
424
|
+
const hints = " /back: overview /view N: switch session ";
|
|
425
|
+
const totalLen = prefix.length + hints.length;
|
|
426
|
+
const fill = Math.max(0, this.cols - totalLen);
|
|
427
|
+
const left = Math.floor(fill / 2);
|
|
428
|
+
const right = Math.ceil(fill / 2);
|
|
429
|
+
const line = `${SLATE}${prefix}${BOX.h.repeat(left)}${DIM}${hints}${RESET}${SLATE}${BOX.h.repeat(right)}${RESET}`;
|
|
430
|
+
process.stderr.write(SAVE_CURSOR + moveTo(this.separatorRow, 1) + CLEAR_LINE + truncateAnsi(line, this.cols) + RESTORE_CURSOR);
|
|
431
|
+
}
|
|
432
|
+
repaintDrilldownContent() {
|
|
433
|
+
if (!this.drilldownSessionId)
|
|
434
|
+
return;
|
|
435
|
+
const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
|
|
436
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
437
|
+
// show the last N lines (tail view, like following output)
|
|
438
|
+
const startIdx = Math.max(0, outputLines.length - visibleLines);
|
|
439
|
+
const visible = outputLines.slice(startIdx);
|
|
440
|
+
for (let i = 0; i < visibleLines; i++) {
|
|
441
|
+
const row = this.scrollTop + i;
|
|
442
|
+
if (i < visible.length) {
|
|
443
|
+
const line = ` ${visible[i]}`;
|
|
444
|
+
process.stderr.write(moveTo(row, 1) + CLEAR_LINE + truncateAnsi(line, this.cols));
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
process.stderr.write(moveTo(row, 1) + CLEAR_LINE);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
342
451
|
paintInputLine() {
|
|
343
452
|
const prompt = formatPrompt(this.phase, this.paused, this.pendingCount);
|
|
344
453
|
process.stderr.write(SAVE_CURSOR +
|
|
@@ -488,6 +597,25 @@ export function formatSessionSentence(s, maxCols) {
|
|
|
488
597
|
}
|
|
489
598
|
return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
|
|
490
599
|
}
|
|
600
|
+
// ── Drill-down helpers (pure, exported for testing) ─────────────────────────
|
|
601
|
+
// format the header line for drill-down view
|
|
602
|
+
function formatDrilldownHeader(sessionId, sessions, phase, paused, spinnerFrame, _cols) {
|
|
603
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
604
|
+
const phaseText = phaseDisplay(phase, paused, spinnerFrame);
|
|
605
|
+
if (!session) {
|
|
606
|
+
return ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}│${RESET} ${DIM}session not found${RESET} ${SLATE}│${RESET} ${phaseText}`;
|
|
607
|
+
}
|
|
608
|
+
const dot = STATUS_DOT[session.status] ?? `${AMBER}${DOT.filled}${RESET}`;
|
|
609
|
+
const name = `${BOLD}${session.title}${RESET}`;
|
|
610
|
+
const toolBadge = `${SLATE}${session.tool}${RESET}`;
|
|
611
|
+
const statusText = session.status === "working" || session.status === "running"
|
|
612
|
+
? `${LIME}${session.status}${RESET}`
|
|
613
|
+
: session.status === "error"
|
|
614
|
+
? `${ROSE}error${RESET}`
|
|
615
|
+
: `${SLATE}${session.status}${RESET}`;
|
|
616
|
+
const taskTag = session.currentTask ? ` ${SLATE}│${RESET} ${DIM}${truncatePlain(session.currentTask, 40)}${RESET}` : "";
|
|
617
|
+
return ` ${dot} ${name} ${toolBadge} ${SLATE}│${RESET} ${statusText}${taskTag} ${SLATE}│${RESET} ${phaseText}`;
|
|
618
|
+
}
|
|
491
619
|
// ── Prompt helpers (pure, exported for testing) ─────────────────────────────
|
|
492
620
|
// format the input prompt based on phase, pause state, and pending queue count
|
|
493
621
|
function formatPrompt(phase, paused, pendingCount) {
|
|
@@ -512,5 +640,5 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
|
512
640
|
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
513
641
|
}
|
|
514
642
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
515
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt };
|
|
643
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
516
644
|
//# sourceMappingURL=tui.js.map
|