aoaoe 0.66.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
@@ -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/index.js CHANGED
@@ -257,6 +257,24 @@ async function main() {
257
257
  input.onQueueChange((count) => {
258
258
  tui.updateState({ pendingCount: count });
259
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
+ });
260
278
  }
261
279
  // start TUI (alternate screen buffer) after input is ready
262
280
  if (tui) {
@@ -703,9 +721,16 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
703
721
  const sessionStates = buildSessionStates(observation);
704
722
  const taskStates = taskManager ? taskManager.tasks : undefined;
705
723
  writeState("polling", { pollCount, sessionCount, changeCount, sessions: sessionStates, tasks: taskStates });
706
- // update TUI session panel
707
- if (tui)
724
+ // update TUI session panel + drill-down outputs
725
+ if (tui) {
708
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
+ }
709
734
  const noStats = { interrupted: false, decisionsThisTick: 0, actionsOk: 0, actionsFail: 0 };
710
735
  // skip cases
711
736
  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
- // header: 1 row
221
- // sessions: top border (1) + N session rows + bottom border (1) = N+2
222
- // if no sessions, just show an empty box (2 rows: top + bottom borders)
223
- const sessBodyRows = Math.max(sessionCount, 1); // at least 1 row for "no agents"
224
- this.sessionRows = sessBodyRows + 2; // + top/bottom borders
225
- this.separatorRow = this.headerHeight + this.sessionRows + 1;
226
- // input line is the last row
227
- this.inputRow = this.rows;
228
- // scroll region: from separator+1 to rows-1 (leave room for input)
229
- this.scrollTop = this.separatorRow + 1;
230
- 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
+ }
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.paintSessions();
247
- this.paintSeparator();
248
- 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
+ }
249
320
  this.paintInputLine();
250
321
  }
251
322
  paintHeader() {
252
- const phaseText = phaseDisplay(this.phase, this.paused, this.spinnerFrame);
253
- const sessCount = `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
254
- const activeCount = this.sessions.filter((s) => s.userActive).length;
255
- const activeTag = activeCount > 0 ? ` ${SLATE}│${RESET} ${AMBER}${activeCount} user${RESET}` : "";
256
- // countdown to next tick (only in sleeping phase)
257
- let countdownTag = "";
258
- if (this.phase === "sleeping" && this.nextTickAt > 0) {
259
- const remaining = Math.max(0, Math.ceil((this.nextTickAt - Date.now()) / 1000));
260
- 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}`;
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.66.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",