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 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
- (anything) send a message to the AI it reads your input next cycle`);
608
+ !message insist shortcutsame 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
- console.error(`${GREEN}Got it!${RESET} ${DIM}The AI will read your message on the next cycle.${RESET}`);
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 to the AI supervisor
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
- // header: 1 row
218
- // sessions: top border (1) + N session rows + bottom border (1) = N+2
219
- // if no sessions, just show an empty box (2 rows: top + bottom borders)
220
- const sessBodyRows = Math.max(sessionCount, 1); // at least 1 row for "no agents"
221
- this.sessionRows = sessBodyRows + 2; // + top/bottom borders
222
- this.separatorRow = this.headerHeight + this.sessionRows + 1;
223
- // input line is the last row
224
- this.inputRow = this.rows;
225
- // scroll region: from separator+1 to rows-1 (leave room for input)
226
- this.scrollTop = this.separatorRow + 1;
227
- this.scrollBottom = this.rows - 1;
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.paintSessions();
244
- this.paintSeparator();
245
- this.repaintActivityRegion();
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
- const phaseText = phaseDisplay(this.phase, this.paused, this.spinnerFrame);
250
- const sessCount = `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
251
- const activeCount = this.sessions.filter((s) => s.userActive).length;
252
- const activeTag = activeCount > 0 ? ` ${SLATE}│${RESET} ${AMBER}${activeCount} user${RESET}` : "";
253
- // countdown to next tick (only in sleeping phase)
254
- let countdownTag = "";
255
- if (this.phase === "sleeping" && this.nextTickAt > 0) {
256
- const remaining = Math.max(0, Math.ceil((this.nextTickAt - Date.now()) / 1000));
257
- countdownTag = ` ${SLATE}│${RESET} ${SLATE}${remaining}s${RESET}`;
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
- // phase-aware prompt styling
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.65.0",
3
+ "version": "0.67.0",
4
4
  "description": "Autonomous supervisor for agent-of-empires sessions using OpenCode or Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",