aoaoe 0.65.0 → 0.67.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.js +7 -1
- package/dist/index.js +43 -3
- package/dist/input.d.ts +8 -0
- package/dist/input.js +72 -2
- package/dist/message.d.ts +13 -0
- package/dist/message.js +17 -0
- package/dist/tui.d.ts +20 -1
- package/dist/tui.js +169 -33
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -592,6 +592,9 @@ example config:
|
|
|
592
592
|
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
|
+
/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
|
|
595
598
|
/status request daemon status
|
|
596
599
|
/dashboard request full dashboard output
|
|
597
600
|
/pause pause the daemon
|
|
@@ -599,7 +602,10 @@ interactive commands (while daemon is running):
|
|
|
599
602
|
/interrupt interrupt the current reasoner call
|
|
600
603
|
/verbose toggle verbose logging
|
|
601
604
|
/clear clear the screen
|
|
605
|
+
PgUp / PgDn scroll through activity history
|
|
606
|
+
Home / End jump to oldest / return to live
|
|
602
607
|
ESC ESC interrupt the current reasoner (shortcut)
|
|
603
|
-
|
|
608
|
+
!message insist shortcut — same as /insist message
|
|
609
|
+
(anything) send a message to the AI — queued for next cycle`);
|
|
604
610
|
}
|
|
605
611
|
//# sourceMappingURL=config.js.map
|
package/dist/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadGlobalContext, resolveProjectDirWithSource, discoverContextFiles, l
|
|
|
12
12
|
import { tick as loopTick } from "./loop.js";
|
|
13
13
|
import { exec as shellExec } from "./shell.js";
|
|
14
14
|
import { wakeableSleep } from "./wake.js";
|
|
15
|
-
import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile } from "./message.js";
|
|
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
18
|
import { TUI } from "./tui.js";
|
|
@@ -253,6 +253,28 @@ async function main() {
|
|
|
253
253
|
break;
|
|
254
254
|
}
|
|
255
255
|
});
|
|
256
|
+
// wire queue count changes to TUI prompt display
|
|
257
|
+
input.onQueueChange((count) => {
|
|
258
|
+
tui.updateState({ pendingCount: count });
|
|
259
|
+
});
|
|
260
|
+
// wire /view and /back commands to TUI drill-down
|
|
261
|
+
input.onView((target) => {
|
|
262
|
+
if (target === null) {
|
|
263
|
+
tui.exitDrilldown();
|
|
264
|
+
tui.log("system", "returned to overview");
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// try number first, then name/id
|
|
268
|
+
const num = parseInt(target, 10);
|
|
269
|
+
const ok = !isNaN(num) ? tui.enterDrilldown(num) : tui.enterDrilldown(target);
|
|
270
|
+
if (ok) {
|
|
271
|
+
tui.log("system", `viewing session: ${target}`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
tui.log("system", `session not found: ${target}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
});
|
|
256
278
|
}
|
|
257
279
|
// start TUI (alternate screen buffer) after input is ready
|
|
258
280
|
if (tui) {
|
|
@@ -393,6 +415,17 @@ async function main() {
|
|
|
393
415
|
const allMessages = [...stdinMessages, ...consoleMessages];
|
|
394
416
|
// classify into commands vs. real user messages
|
|
395
417
|
const { commands, userMessages } = classifyMessages(allMessages);
|
|
418
|
+
// strip insist prefix from priority messages and log them distinctly
|
|
419
|
+
for (let i = 0; i < userMessages.length; i++) {
|
|
420
|
+
if (isInsistMessage(userMessages[i])) {
|
|
421
|
+
const raw = stripInsistPrefix(userMessages[i]);
|
|
422
|
+
userMessages[i] = raw;
|
|
423
|
+
if (tui)
|
|
424
|
+
tui.log("you", `! ${raw}`);
|
|
425
|
+
else
|
|
426
|
+
log(`[insist] ${raw}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
396
429
|
// auto-explain on first tick: inject an explain prompt so the AI introduces itself
|
|
397
430
|
if (autoExplainPending && pollCount === 1) {
|
|
398
431
|
const autoExplainPrompt = "This is your first observation. Please briefly introduce what you see: " +
|
|
@@ -688,9 +721,16 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
|
|
|
688
721
|
const sessionStates = buildSessionStates(observation);
|
|
689
722
|
const taskStates = taskManager ? taskManager.tasks : undefined;
|
|
690
723
|
writeState("polling", { pollCount, sessionCount, changeCount, sessions: sessionStates, tasks: taskStates });
|
|
691
|
-
// update TUI session panel
|
|
692
|
-
if (tui)
|
|
724
|
+
// update TUI session panel + drill-down outputs
|
|
725
|
+
if (tui) {
|
|
693
726
|
tui.updateState({ phase: "polling", pollCount, sessions: sessionStates });
|
|
727
|
+
// pass full session outputs for drill-down view
|
|
728
|
+
const outputs = new Map();
|
|
729
|
+
for (const snap of observation.sessions) {
|
|
730
|
+
outputs.set(snap.session.id, snap.output);
|
|
731
|
+
}
|
|
732
|
+
tui.setSessionOutputs(outputs);
|
|
733
|
+
}
|
|
694
734
|
const noStats = { interrupted: false, decisionsThisTick: 0, actionsOk: 0, actionsFail: 0 };
|
|
695
735
|
// skip cases
|
|
696
736
|
if (skippedReason === "no sessions") {
|
package/dist/input.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
2
|
+
export declare const INSIST_PREFIX = "__INSIST__";
|
|
3
|
+
export type ViewHandler = (target: string | null) => void;
|
|
2
4
|
export declare class InputReader {
|
|
3
5
|
private rl;
|
|
4
6
|
private queue;
|
|
5
7
|
private paused;
|
|
6
8
|
private lastEscTime;
|
|
7
9
|
private scrollHandler;
|
|
10
|
+
private queueChangeHandler;
|
|
11
|
+
private viewHandler;
|
|
8
12
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
13
|
+
onQueueChange(handler: (count: number) => void): void;
|
|
14
|
+
onView(handler: ViewHandler): void;
|
|
15
|
+
private notifyQueueChange;
|
|
9
16
|
start(): void;
|
|
10
17
|
drain(): string[];
|
|
11
18
|
isPaused(): boolean;
|
|
@@ -14,6 +21,7 @@ export declare class InputReader {
|
|
|
14
21
|
prompt(): void;
|
|
15
22
|
stop(): void;
|
|
16
23
|
private handleEscInterrupt;
|
|
24
|
+
private handleInsist;
|
|
17
25
|
private handleLine;
|
|
18
26
|
private handleCommand;
|
|
19
27
|
}
|
package/dist/input.js
CHANGED
|
@@ -6,16 +6,30 @@ import { requestInterrupt } from "./daemon-state.js";
|
|
|
6
6
|
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
|
+
export const INSIST_PREFIX = "__INSIST__";
|
|
9
10
|
export class InputReader {
|
|
10
11
|
rl = null;
|
|
11
12
|
queue = []; // pending user messages for the reasoner
|
|
12
13
|
paused = false;
|
|
13
14
|
lastEscTime = 0;
|
|
14
15
|
scrollHandler = null;
|
|
16
|
+
queueChangeHandler = null;
|
|
17
|
+
viewHandler = null;
|
|
15
18
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
16
19
|
onScroll(handler) {
|
|
17
20
|
this.scrollHandler = handler;
|
|
18
21
|
}
|
|
22
|
+
// register a callback for queue size changes (for TUI pending count display)
|
|
23
|
+
onQueueChange(handler) {
|
|
24
|
+
this.queueChangeHandler = handler;
|
|
25
|
+
}
|
|
26
|
+
// register a callback for view commands (/view, /back)
|
|
27
|
+
onView(handler) {
|
|
28
|
+
this.viewHandler = handler;
|
|
29
|
+
}
|
|
30
|
+
notifyQueueChange() {
|
|
31
|
+
this.queueChangeHandler?.(this.queue.length);
|
|
32
|
+
}
|
|
19
33
|
start() {
|
|
20
34
|
// only works if stdin is a TTY (not piped)
|
|
21
35
|
if (!process.stdin.isTTY)
|
|
@@ -67,6 +81,8 @@ export class InputReader {
|
|
|
67
81
|
// drain all pending user messages (called each tick)
|
|
68
82
|
drain() {
|
|
69
83
|
const msgs = this.queue.splice(0);
|
|
84
|
+
if (msgs.length > 0)
|
|
85
|
+
this.notifyQueueChange();
|
|
70
86
|
return msgs;
|
|
71
87
|
}
|
|
72
88
|
isPaused() {
|
|
@@ -79,6 +95,7 @@ export class InputReader {
|
|
|
79
95
|
// inject a message directly into the queue (used after interrupt to feed text into next tick)
|
|
80
96
|
inject(msg) {
|
|
81
97
|
this.queue.push(msg);
|
|
98
|
+
this.notifyQueueChange();
|
|
82
99
|
}
|
|
83
100
|
// re-show the prompt (called after daemon prints output)
|
|
84
101
|
prompt() {
|
|
@@ -91,10 +108,18 @@ export class InputReader {
|
|
|
91
108
|
handleEscInterrupt() {
|
|
92
109
|
requestInterrupt();
|
|
93
110
|
this.queue.push("__CMD_INTERRUPT__");
|
|
111
|
+
this.notifyQueueChange();
|
|
94
112
|
console.error(`\n${RED}${BOLD}>>> interrupting reasoner <<<${RESET}`);
|
|
95
113
|
console.error(`${YELLOW}type your message now -- it will be sent before the next cycle${RESET}`);
|
|
96
114
|
this.rl?.prompt(true);
|
|
97
115
|
}
|
|
116
|
+
handleInsist(msg) {
|
|
117
|
+
requestInterrupt();
|
|
118
|
+
this.queue.push("__CMD_INTERRUPT__");
|
|
119
|
+
this.queue.push(`${INSIST_PREFIX}${msg}`);
|
|
120
|
+
this.notifyQueueChange();
|
|
121
|
+
console.error(`${RED}${BOLD}!${RESET} ${GREEN}insist${RESET} ${DIM}— interrupting + delivering your message immediately${RESET}`);
|
|
122
|
+
}
|
|
98
123
|
handleLine(line) {
|
|
99
124
|
if (!line) {
|
|
100
125
|
this.rl?.prompt();
|
|
@@ -106,9 +131,20 @@ export class InputReader {
|
|
|
106
131
|
this.rl?.prompt();
|
|
107
132
|
return;
|
|
108
133
|
}
|
|
134
|
+
// ! prefix = insist mode: interrupt + priority message
|
|
135
|
+
if (line.startsWith("!") && line.length > 1) {
|
|
136
|
+
const msg = line.slice(1).trim();
|
|
137
|
+
if (msg) {
|
|
138
|
+
this.handleInsist(msg);
|
|
139
|
+
this.rl?.prompt();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
109
143
|
// queue as a user message for the reasoner
|
|
110
144
|
this.queue.push(line);
|
|
111
|
-
|
|
145
|
+
this.notifyQueueChange();
|
|
146
|
+
const pending = this.queue.filter(m => !m.startsWith("__CMD_")).length;
|
|
147
|
+
console.error(`${GREEN}queued${RESET} ${DIM}(${pending} pending) — will be read next cycle${RESET}`);
|
|
112
148
|
this.rl?.prompt();
|
|
113
149
|
}
|
|
114
150
|
handleCommand(line) {
|
|
@@ -117,7 +153,9 @@ export class InputReader {
|
|
|
117
153
|
case "/help":
|
|
118
154
|
console.error(`
|
|
119
155
|
${BOLD}talking to the AI:${RESET}
|
|
120
|
-
just type send a message
|
|
156
|
+
just type send a message — queued for next cycle
|
|
157
|
+
!message insist — interrupt + deliver message immediately
|
|
158
|
+
/insist <msg> same as !message
|
|
121
159
|
/explain ask the AI to explain what's happening right now
|
|
122
160
|
|
|
123
161
|
${BOLD}controls:${RESET}
|
|
@@ -125,6 +163,10 @@ ${BOLD}controls:${RESET}
|
|
|
125
163
|
/resume resume the supervisor
|
|
126
164
|
/interrupt interrupt the AI mid-thought
|
|
127
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
|
|
128
170
|
PgUp / PgDn scroll through activity history
|
|
129
171
|
Home / End jump to oldest / return to live
|
|
130
172
|
|
|
@@ -163,6 +205,16 @@ ${BOLD}other:${RESET}
|
|
|
163
205
|
case "/interrupt":
|
|
164
206
|
this.handleEscInterrupt();
|
|
165
207
|
break;
|
|
208
|
+
case "/insist": {
|
|
209
|
+
const insistMsg = line.slice("/insist".length).trim();
|
|
210
|
+
if (insistMsg) {
|
|
211
|
+
this.handleInsist(insistMsg);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
console.error(`${DIM}usage: /insist <message> — interrupts and delivers your message immediately${RESET}`);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
166
218
|
case "/tasks":
|
|
167
219
|
this.queue.push("__CMD_TASK__list");
|
|
168
220
|
break;
|
|
@@ -172,6 +224,24 @@ ${BOLD}other:${RESET}
|
|
|
172
224
|
this.queue.push(`__CMD_TASK__${taskArgs}`);
|
|
173
225
|
break;
|
|
174
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;
|
|
175
245
|
case "/clear":
|
|
176
246
|
process.stderr.write("\x1b[2J\x1b[H");
|
|
177
247
|
break;
|
package/dist/message.d.ts
CHANGED
|
@@ -36,4 +36,17 @@ export declare function shouldSkipSleep(state: {
|
|
|
36
36
|
* Lightweight stat-only check — does not read or drain the file.
|
|
37
37
|
*/
|
|
38
38
|
export declare function hasPendingFile(filePath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Insist prefix marker — messages prefixed with this bypass the normal queue
|
|
41
|
+
* and trigger an immediate interrupt + delivery.
|
|
42
|
+
*/
|
|
43
|
+
export declare const INSIST_PREFIX = "__INSIST__";
|
|
44
|
+
/**
|
|
45
|
+
* Check if a message is an insist (priority) message.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isInsistMessage(msg: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Strip the insist prefix from a message, returning the raw user text.
|
|
50
|
+
*/
|
|
51
|
+
export declare function stripInsistPrefix(msg: string): string;
|
|
39
52
|
//# sourceMappingURL=message.d.ts.map
|
package/dist/message.js
CHANGED
|
@@ -73,6 +73,23 @@ export function hasPendingFile(filePath) {
|
|
|
73
73
|
return false;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Insist prefix marker — messages prefixed with this bypass the normal queue
|
|
78
|
+
* and trigger an immediate interrupt + delivery.
|
|
79
|
+
*/
|
|
80
|
+
export const INSIST_PREFIX = "__INSIST__";
|
|
81
|
+
/**
|
|
82
|
+
* Check if a message is an insist (priority) message.
|
|
83
|
+
*/
|
|
84
|
+
export function isInsistMessage(msg) {
|
|
85
|
+
return msg.startsWith(INSIST_PREFIX);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Strip the insist prefix from a message, returning the raw user text.
|
|
89
|
+
*/
|
|
90
|
+
export function stripInsistPrefix(msg) {
|
|
91
|
+
return msg.startsWith(INSIST_PREFIX) ? msg.slice(INSIST_PREFIX.length) : msg;
|
|
92
|
+
}
|
|
76
93
|
function truncate(s, max) {
|
|
77
94
|
return s.length <= max ? s : s.slice(0, max - 3) + "...";
|
|
78
95
|
}
|
package/dist/tui.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export declare class TUI {
|
|
|
22
22
|
private spinnerFrame;
|
|
23
23
|
private scrollOffset;
|
|
24
24
|
private newWhileScrolled;
|
|
25
|
+
private pendingCount;
|
|
26
|
+
private viewMode;
|
|
27
|
+
private drilldownSessionId;
|
|
28
|
+
private sessionOutputs;
|
|
25
29
|
private phase;
|
|
26
30
|
private pollCount;
|
|
27
31
|
private sessions;
|
|
@@ -39,6 +43,7 @@ export declare class TUI {
|
|
|
39
43
|
paused?: boolean;
|
|
40
44
|
reasonerName?: string;
|
|
41
45
|
nextTickAt?: number;
|
|
46
|
+
pendingCount?: number;
|
|
42
47
|
}): void;
|
|
43
48
|
log(tag: string, text: string): void;
|
|
44
49
|
replayHistory(entries: HistoryEntry[]): void;
|
|
@@ -47,6 +52,16 @@ export declare class TUI {
|
|
|
47
52
|
scrollToTop(): void;
|
|
48
53
|
scrollToBottom(): void;
|
|
49
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;
|
|
50
65
|
private updateDimensions;
|
|
51
66
|
private computeLayout;
|
|
52
67
|
private onResize;
|
|
@@ -56,6 +71,8 @@ export declare class TUI {
|
|
|
56
71
|
private paintSeparator;
|
|
57
72
|
private writeActivityLine;
|
|
58
73
|
private repaintActivityRegion;
|
|
74
|
+
private paintDrilldownSeparator;
|
|
75
|
+
private repaintDrilldownContent;
|
|
59
76
|
private paintInputLine;
|
|
60
77
|
}
|
|
61
78
|
declare function formatSessionCard(s: DaemonSessionState, maxWidth: number): string;
|
|
@@ -70,10 +87,12 @@ declare function truncatePlain(str: string, max: number): string;
|
|
|
70
87
|
* Kept for backward compatibility — used by non-TUI output paths.
|
|
71
88
|
*/
|
|
72
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;
|
|
91
|
+
declare function formatPrompt(phase: DaemonPhase, paused: boolean, pendingCount: number): string;
|
|
73
92
|
declare function computeScrollSlice(bufferLen: number, visibleLines: number, scrollOffset: number): {
|
|
74
93
|
start: number;
|
|
75
94
|
end: number;
|
|
76
95
|
};
|
|
77
96
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
78
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator };
|
|
97
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
79
98
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -57,6 +57,11 @@ export class TUI {
|
|
|
57
57
|
spinnerFrame = 0; // current spinner animation frame
|
|
58
58
|
scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
|
|
59
59
|
newWhileScrolled = 0; // entries added while user is scrolled back
|
|
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
|
|
60
65
|
// current state for repaints
|
|
61
66
|
phase = "sleeping";
|
|
62
67
|
pollCount = 0;
|
|
@@ -115,6 +120,8 @@ export class TUI {
|
|
|
115
120
|
this.reasonerName = opts.reasonerName;
|
|
116
121
|
if (opts.nextTickAt !== undefined)
|
|
117
122
|
this.nextTickAt = opts.nextTickAt;
|
|
123
|
+
if (opts.pendingCount !== undefined)
|
|
124
|
+
this.pendingCount = opts.pendingCount;
|
|
118
125
|
if (opts.sessions !== undefined) {
|
|
119
126
|
const sessionCountChanged = opts.sessions.length !== this.sessions.length;
|
|
120
127
|
this.sessions = opts.sessions;
|
|
@@ -207,6 +214,61 @@ export class TUI {
|
|
|
207
214
|
isScrolledBack() {
|
|
208
215
|
return this.scrollOffset > 0;
|
|
209
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
|
+
}
|
|
210
272
|
// ── Layout computation ──────────────────────────────────────────────────
|
|
211
273
|
updateDimensions() {
|
|
212
274
|
this.cols = process.stderr.columns || 80;
|
|
@@ -214,17 +276,23 @@ export class TUI {
|
|
|
214
276
|
}
|
|
215
277
|
computeLayout(sessionCount) {
|
|
216
278
|
this.updateDimensions();
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
}
|
|
228
296
|
if (this.active) {
|
|
229
297
|
process.stderr.write(setScrollRegion(this.scrollTop, this.scrollBottom));
|
|
230
298
|
}
|
|
@@ -240,25 +308,37 @@ export class TUI {
|
|
|
240
308
|
process.stderr.write(CLEAR_SCREEN);
|
|
241
309
|
process.stderr.write(setScrollRegion(this.scrollTop, this.scrollBottom));
|
|
242
310
|
this.paintHeader();
|
|
243
|
-
this.
|
|
244
|
-
|
|
245
|
-
|
|
311
|
+
if (this.viewMode === "drilldown") {
|
|
312
|
+
this.paintDrilldownSeparator();
|
|
313
|
+
this.repaintDrilldownContent();
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
this.paintSessions();
|
|
317
|
+
this.paintSeparator();
|
|
318
|
+
this.repaintActivityRegion();
|
|
319
|
+
}
|
|
246
320
|
this.paintInputLine();
|
|
247
321
|
}
|
|
248
322
|
paintHeader() {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
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}`;
|
|
258
341
|
}
|
|
259
|
-
// reasoner badge
|
|
260
|
-
const reasonerTag = this.reasonerName ? ` ${SLATE}│${RESET} ${TEAL}${this.reasonerName}${RESET}` : "";
|
|
261
|
-
const line = ` ${INDIGO}${BOLD}aoaoe${RESET} ${SLATE}${this.version}${RESET} ${SLATE}│${RESET} #${this.pollCount} ${SLATE}│${RESET} ${sessCount} ${SLATE}│${RESET} ${phaseText}${activeTag}${countdownTag}${reasonerTag}`;
|
|
262
342
|
process.stderr.write(SAVE_CURSOR +
|
|
263
343
|
moveTo(1, 1) + CLEAR_LINE + BG_DARK + WHITE + truncateAnsi(line, this.cols) + padToWidth(line, this.cols) + RESET +
|
|
264
344
|
RESTORE_CURSOR);
|
|
@@ -336,13 +416,40 @@ export class TUI {
|
|
|
336
416
|
}
|
|
337
417
|
}
|
|
338
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
|
+
}
|
|
339
451
|
paintInputLine() {
|
|
340
|
-
|
|
341
|
-
const prompt = this.paused
|
|
342
|
-
? `${AMBER}${BOLD}paused >${RESET} `
|
|
343
|
-
: this.phase === "reasoning"
|
|
344
|
-
? `${SKY}thinking >${RESET} `
|
|
345
|
-
: `${LIME}>${RESET} `;
|
|
452
|
+
const prompt = formatPrompt(this.phase, this.paused, this.pendingCount);
|
|
346
453
|
process.stderr.write(SAVE_CURSOR +
|
|
347
454
|
moveTo(this.inputRow, 1) + CLEAR_LINE + prompt +
|
|
348
455
|
RESTORE_CURSOR);
|
|
@@ -490,6 +597,35 @@ export function formatSessionSentence(s, maxCols) {
|
|
|
490
597
|
}
|
|
491
598
|
return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
|
|
492
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
|
+
}
|
|
619
|
+
// ── Prompt helpers (pure, exported for testing) ─────────────────────────────
|
|
620
|
+
// format the input prompt based on phase, pause state, and pending queue count
|
|
621
|
+
function formatPrompt(phase, paused, pendingCount) {
|
|
622
|
+
const queueTag = pendingCount > 0 ? `${AMBER}${pendingCount} queued${RESET} ` : "";
|
|
623
|
+
if (paused)
|
|
624
|
+
return `${queueTag}${AMBER}${BOLD}paused >${RESET} `;
|
|
625
|
+
if (phase === "reasoning")
|
|
626
|
+
return `${queueTag}${SKY}thinking >${RESET} `;
|
|
627
|
+
return `${queueTag}${LIME}>${RESET} `;
|
|
628
|
+
}
|
|
493
629
|
// ── Scroll helpers (pure, exported for testing) ─────────────────────────────
|
|
494
630
|
// compute the slice indices for the activity buffer given scroll state
|
|
495
631
|
function computeScrollSlice(bufferLen, visibleLines, scrollOffset) {
|
|
@@ -504,5 +640,5 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
|
504
640
|
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
505
641
|
}
|
|
506
642
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
507
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator };
|
|
643
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
508
644
|
//# sourceMappingURL=tui.js.map
|