aoaoe 0.71.0 → 0.73.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/index.js CHANGED
@@ -15,7 +15,7 @@ import { wakeableSleep } from "./wake.js";
15
15
  import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
16
16
  import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
17
17
  import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
18
- import { TUI } from "./tui.js";
18
+ import { TUI, hitTestSession } from "./tui.js";
19
19
  import { isDaemonRunningFromState } from "./chat.js";
20
20
  import { sendNotification, sendTestNotification } from "./notify.js";
21
21
  import { startHealthServer } from "./health.js";
@@ -256,19 +256,36 @@ async function main() {
256
256
  // wire scroll keys to TUI (PgUp/PgDn/Home/End)
257
257
  if (tui) {
258
258
  input.onScroll((dir) => {
259
- switch (dir) {
260
- case "up":
261
- tui.scrollUp();
262
- break;
263
- case "down":
264
- tui.scrollDown();
265
- break;
266
- case "top":
267
- tui.scrollToTop();
268
- break;
269
- case "bottom":
270
- tui.scrollToBottom();
271
- break;
259
+ if (tui.getViewMode() === "drilldown") {
260
+ // PgUp/PgDn/Home/End scroll the session output in drill-down mode
261
+ switch (dir) {
262
+ case "up":
263
+ tui.scrollDrilldownUp();
264
+ break;
265
+ case "down":
266
+ tui.scrollDrilldownDown();
267
+ break;
268
+ case "bottom":
269
+ tui.scrollDrilldownToBottom();
270
+ break;
271
+ // "top" not wired for drilldown — could add scrollDrilldownToTop() later
272
+ }
273
+ }
274
+ else {
275
+ switch (dir) {
276
+ case "up":
277
+ tui.scrollUp();
278
+ break;
279
+ case "down":
280
+ tui.scrollDown();
281
+ break;
282
+ case "top":
283
+ tui.scrollToTop();
284
+ break;
285
+ case "bottom":
286
+ tui.scrollToBottom();
287
+ break;
288
+ }
272
289
  }
273
290
  });
274
291
  // wire queue count changes to TUI prompt display
@@ -293,6 +310,36 @@ async function main() {
293
310
  }
294
311
  }
295
312
  });
313
+ // wire mouse clicks on session cards to drill-down
314
+ input.onMouseClick((row, _col) => {
315
+ if (tui.getViewMode() === "drilldown") {
316
+ // click anywhere in drilldown = back to overview
317
+ tui.exitDrilldown();
318
+ tui.log("system", "returned to overview");
319
+ return;
320
+ }
321
+ const sessionIdx = hitTestSession(row, 1, tui.getSessionCount());
322
+ if (sessionIdx !== null) {
323
+ const ok = tui.enterDrilldown(sessionIdx);
324
+ if (ok)
325
+ tui.log("system", `viewing session #${sessionIdx}`);
326
+ }
327
+ });
328
+ // wire mouse wheel to scroll (3 lines per tick for smooth scrolling)
329
+ input.onMouseWheel((direction) => {
330
+ if (tui.getViewMode() === "drilldown") {
331
+ if (direction === "up")
332
+ tui.scrollDrilldownUp(3);
333
+ else
334
+ tui.scrollDrilldownDown(3);
335
+ }
336
+ else {
337
+ if (direction === "up")
338
+ tui.scrollUp(3);
339
+ else
340
+ tui.scrollDown(3);
341
+ }
342
+ });
296
343
  }
297
344
  // start TUI (alternate screen buffer) after input is ready
298
345
  if (tui) {
package/dist/input.d.ts CHANGED
@@ -1,6 +1,16 @@
1
1
  export type ScrollDirection = "up" | "down" | "top" | "bottom";
2
2
  export declare const INSIST_PREFIX = "__INSIST__";
3
3
  export type ViewHandler = (target: string | null) => void;
4
+ export interface MouseEvent {
5
+ button: number;
6
+ col: number;
7
+ row: number;
8
+ press: boolean;
9
+ }
10
+ export type MouseClickHandler = (row: number, col: number) => void;
11
+ export type MouseWheelHandler = (direction: "up" | "down") => void;
12
+ /** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
13
+ export declare function parseMouseEvent(data: string): MouseEvent | null;
4
14
  export declare class InputReader {
5
15
  private rl;
6
16
  private queue;
@@ -9,9 +19,14 @@ export declare class InputReader {
9
19
  private scrollHandler;
10
20
  private queueChangeHandler;
11
21
  private viewHandler;
22
+ private mouseClickHandler;
23
+ private mouseWheelHandler;
24
+ private mouseDataListener;
12
25
  onScroll(handler: (dir: ScrollDirection) => void): void;
13
26
  onQueueChange(handler: (count: number) => void): void;
14
27
  onView(handler: ViewHandler): void;
28
+ onMouseClick(handler: MouseClickHandler): void;
29
+ onMouseWheel(handler: MouseWheelHandler): void;
15
30
  private notifyQueueChange;
16
31
  start(): void;
17
32
  drain(): string[];
package/dist/input.js CHANGED
@@ -7,6 +7,20 @@ import { GREEN, DIM, YELLOW, RED, BOLD, RESET } from "./colors.js";
7
7
  // ESC-ESC interrupt detection
8
8
  const ESC_DOUBLE_TAP_MS = 500;
9
9
  export const INSIST_PREFIX = "__INSIST__";
10
+ // SGR extended mouse format: \x1b[<btn;col;rowM (press) or \x1b[<btn;col;rowm (release)
11
+ const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
12
+ /** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
13
+ export function parseMouseEvent(data) {
14
+ const m = SGR_MOUSE_RE.exec(data);
15
+ if (!m)
16
+ return null;
17
+ return {
18
+ button: parseInt(m[1], 10),
19
+ col: parseInt(m[2], 10),
20
+ row: parseInt(m[3], 10),
21
+ press: m[4] === "M",
22
+ };
23
+ }
10
24
  export class InputReader {
11
25
  rl = null;
12
26
  queue = []; // pending user messages for the reasoner
@@ -15,6 +29,9 @@ export class InputReader {
15
29
  scrollHandler = null;
16
30
  queueChangeHandler = null;
17
31
  viewHandler = null;
32
+ mouseClickHandler = null;
33
+ mouseWheelHandler = null;
34
+ mouseDataListener = null;
18
35
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
19
36
  onScroll(handler) {
20
37
  this.scrollHandler = handler;
@@ -27,6 +44,14 @@ export class InputReader {
27
44
  onView(handler) {
28
45
  this.viewHandler = handler;
29
46
  }
47
+ // register a callback for mouse left-click events (row, col are 1-indexed)
48
+ onMouseClick(handler) {
49
+ this.mouseClickHandler = handler;
50
+ }
51
+ // register a callback for mouse wheel events (scroll up/down)
52
+ onMouseWheel(handler) {
53
+ this.mouseWheelHandler = handler;
54
+ }
30
55
  notifyQueueChange() {
31
56
  this.queueChangeHandler?.(this.queue.length);
32
57
  }
@@ -44,6 +69,25 @@ export class InputReader {
44
69
  this.rl.on("close", () => { this.rl = null; });
45
70
  // ESC-ESC interrupt detection (same as chat.ts)
46
71
  emitKeypressEvents(process.stdin);
72
+ // intercept raw SGR mouse sequences before keypress parsing
73
+ this.mouseDataListener = (data) => {
74
+ const str = data.toString("utf8");
75
+ const evt = parseMouseEvent(str);
76
+ if (!evt)
77
+ return;
78
+ // left click press
79
+ if (evt.press && evt.button === 0 && this.mouseClickHandler) {
80
+ this.mouseClickHandler(evt.row, evt.col);
81
+ }
82
+ // mouse wheel: button 64 = scroll up, 65 = scroll down
83
+ if (evt.button === 64 && this.mouseWheelHandler) {
84
+ this.mouseWheelHandler("up");
85
+ }
86
+ else if (evt.button === 65 && this.mouseWheelHandler) {
87
+ this.mouseWheelHandler("down");
88
+ }
89
+ };
90
+ process.stdin.on("data", this.mouseDataListener);
47
91
  process.stdin.on("keypress", (_ch, key) => {
48
92
  if (key?.name === "escape" || key?.sequence === "\x1b") {
49
93
  const now = Date.now();
@@ -102,6 +146,10 @@ export class InputReader {
102
146
  this.rl?.prompt(true);
103
147
  }
104
148
  stop() {
149
+ if (this.mouseDataListener) {
150
+ process.stdin.removeListener("data", this.mouseDataListener);
151
+ this.mouseDataListener = null;
152
+ }
105
153
  this.rl?.close();
106
154
  this.rl = null;
107
155
  }
@@ -167,7 +215,9 @@ ${BOLD}controls:${RESET}
167
215
  ${BOLD}navigation:${RESET}
168
216
  /view [N|name] drill into a session's live output (default: 1)
169
217
  /back return to overview from drill-down
170
- PgUp / PgDn scroll through activity history
218
+ click session click an agent card to drill down (click again to go back)
219
+ mouse wheel scroll activity (overview) or session output (drill-down)
220
+ PgUp / PgDn scroll through activity or session output
171
221
  Home / End jump to oldest / return to live
172
222
 
173
223
  ${BOLD}info:${RESET}
package/dist/tui.d.ts CHANGED
@@ -26,6 +26,8 @@ export declare class TUI {
26
26
  private viewMode;
27
27
  private drilldownSessionId;
28
28
  private sessionOutputs;
29
+ private drilldownScrollOffset;
30
+ private drilldownNewWhileScrolled;
29
31
  private phase;
30
32
  private pollCount;
31
33
  private sessions;
@@ -36,6 +38,8 @@ export declare class TUI {
36
38
  start(version: string): void;
37
39
  stop(): void;
38
40
  isActive(): boolean;
41
+ /** Return the current number of sessions (for mouse hit testing) */
42
+ getSessionCount(): number;
39
43
  updateState(opts: {
40
44
  phase?: DaemonPhase;
41
45
  pollCount?: number;
@@ -52,6 +56,10 @@ export declare class TUI {
52
56
  scrollToTop(): void;
53
57
  scrollToBottom(): void;
54
58
  isScrolledBack(): boolean;
59
+ scrollDrilldownUp(lines?: number): void;
60
+ scrollDrilldownDown(lines?: number): void;
61
+ scrollDrilldownToBottom(): void;
62
+ isDrilldownScrolledBack(): boolean;
55
63
  /** Store full session outputs (called each tick from main loop) */
56
64
  setSessionOutputs(outputs: Map<string, string>): void;
57
65
  /** Enter drill-down view for a session. Returns false if session not found. */
@@ -94,5 +102,14 @@ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scr
94
102
  end: number;
95
103
  };
96
104
  declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
97
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
105
+ declare function formatDrilldownScrollIndicator(offset: number, totalLines: number, visibleLines: number, newCount: number): string;
106
+ /**
107
+ * Hit-test a mouse click row against the session panel.
108
+ * Returns 1-indexed session number if the click hit a session card, null otherwise.
109
+ *
110
+ * Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
111
+ * (row = headerHeight + 2 + i for 0-indexed session i)
112
+ */
113
+ export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
114
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader };
98
115
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -12,6 +12,9 @@ const CURSOR_HIDE = `${CSI}?25l`;
12
12
  const CURSOR_SHOW = `${CSI}?25h`;
13
13
  const SAVE_CURSOR = `${ESC}7`;
14
14
  const RESTORE_CURSOR = `${ESC}8`;
15
+ // mouse tracking (SGR extended mode — button events + extended coordinates)
16
+ const MOUSE_ON = `${CSI}?1000h${CSI}?1006h`;
17
+ const MOUSE_OFF = `${CSI}?1000l${CSI}?1006l`;
15
18
  // cursor movement
16
19
  const moveTo = (row, col) => `${CSI}${row};${col}H`;
17
20
  const setScrollRegion = (top, bottom) => `${CSI}${top};${bottom}r`;
@@ -62,6 +65,8 @@ export class TUI {
62
65
  viewMode = "overview";
63
66
  drilldownSessionId = null;
64
67
  sessionOutputs = new Map(); // full output lines per session
68
+ drilldownScrollOffset = 0; // 0 = live (tail), >0 = scrolled back N lines
69
+ drilldownNewWhileScrolled = 0; // lines added while scrolled back
65
70
  // current state for repaints
66
71
  phase = "sleeping";
67
72
  pollCount = 0;
@@ -76,8 +81,8 @@ export class TUI {
76
81
  this.active = true;
77
82
  this.version = version;
78
83
  this.updateDimensions();
79
- // enter alternate screen, hide cursor, clear
80
- process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
84
+ // enter alternate screen, hide cursor, clear, enable mouse
85
+ process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN + MOUSE_ON);
81
86
  // handle terminal resize
82
87
  process.stdout.on("resize", () => this.onResize());
83
88
  // tick timer: countdown + spinner animation (~4 fps for smooth braille spin)
@@ -102,12 +107,16 @@ export class TUI {
102
107
  clearInterval(this.countdownTimer);
103
108
  this.countdownTimer = null;
104
109
  }
105
- // restore normal screen, show cursor, reset scroll region
106
- process.stderr.write(resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
110
+ // disable mouse, restore normal screen, show cursor, reset scroll region
111
+ process.stderr.write(MOUSE_OFF + resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
107
112
  }
108
113
  isActive() {
109
114
  return this.active;
110
115
  }
116
+ /** Return the current number of sessions (for mouse hit testing) */
117
+ getSessionCount() {
118
+ return this.sessions.length;
119
+ }
111
120
  // ── State updates ───────────────────────────────────────────────────────
112
121
  updateState(opts) {
113
122
  if (opts.phase !== undefined)
@@ -214,15 +223,58 @@ export class TUI {
214
223
  isScrolledBack() {
215
224
  return this.scrollOffset > 0;
216
225
  }
226
+ // ── Drill-down scroll ─────────────────────────────────────────────────
227
+ scrollDrilldownUp(lines) {
228
+ if (!this.active || this.viewMode !== "drilldown" || !this.drilldownSessionId)
229
+ return;
230
+ const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
231
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
232
+ const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
233
+ const maxOffset = Math.max(0, outputLines.length - visibleLines);
234
+ this.drilldownScrollOffset = Math.min(maxOffset, this.drilldownScrollOffset + n);
235
+ this.repaintDrilldownContent();
236
+ this.paintDrilldownSeparator();
237
+ }
238
+ scrollDrilldownDown(lines) {
239
+ if (!this.active || this.viewMode !== "drilldown")
240
+ return;
241
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
242
+ const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
243
+ const wasScrolled = this.drilldownScrollOffset > 0;
244
+ this.drilldownScrollOffset = Math.max(0, this.drilldownScrollOffset - n);
245
+ if (wasScrolled && this.drilldownScrollOffset === 0)
246
+ this.drilldownNewWhileScrolled = 0;
247
+ this.repaintDrilldownContent();
248
+ this.paintDrilldownSeparator();
249
+ }
250
+ scrollDrilldownToBottom() {
251
+ if (!this.active || this.viewMode !== "drilldown")
252
+ return;
253
+ this.drilldownScrollOffset = 0;
254
+ this.drilldownNewWhileScrolled = 0;
255
+ this.repaintDrilldownContent();
256
+ this.paintDrilldownSeparator();
257
+ }
258
+ isDrilldownScrolledBack() {
259
+ return this.drilldownScrollOffset > 0;
260
+ }
217
261
  // ── Drill-down mode ────────────────────────────────────────────────────
218
262
  /** Store full session outputs (called each tick from main loop) */
219
263
  setSessionOutputs(outputs) {
220
264
  for (const [id, text] of outputs) {
221
- this.sessionOutputs.set(id, text.split("\n"));
265
+ const prevLen = this.sessionOutputs.get(id)?.length ?? 0;
266
+ const lines = text.split("\n");
267
+ this.sessionOutputs.set(id, lines);
268
+ // track new lines while scrolled back in drill-down
269
+ if (this.viewMode === "drilldown" && this.drilldownSessionId === id && this.drilldownScrollOffset > 0) {
270
+ const newLines = Math.max(0, lines.length - prevLen);
271
+ this.drilldownNewWhileScrolled += newLines;
272
+ }
222
273
  }
223
274
  // repaint drill-down view if we're watching this session
224
275
  if (this.active && this.viewMode === "drilldown" && this.drilldownSessionId) {
225
276
  this.repaintDrilldownContent();
277
+ this.paintDrilldownSeparator();
226
278
  }
227
279
  }
228
280
  /** Enter drill-down view for a session. Returns false if session not found. */
@@ -244,6 +296,8 @@ export class TUI {
244
296
  return false;
245
297
  this.viewMode = "drilldown";
246
298
  this.drilldownSessionId = sessionId;
299
+ this.drilldownScrollOffset = 0;
300
+ this.drilldownNewWhileScrolled = 0;
247
301
  if (this.active) {
248
302
  this.computeLayout(this.sessions.length);
249
303
  this.paintAll();
@@ -256,6 +310,8 @@ export class TUI {
256
310
  return;
257
311
  this.viewMode = "overview";
258
312
  this.drilldownSessionId = null;
313
+ this.drilldownScrollOffset = 0;
314
+ this.drilldownNewWhileScrolled = 0;
259
315
  if (this.active) {
260
316
  this.computeLayout(this.sessions.length);
261
317
  this.paintAll();
@@ -383,7 +439,7 @@ export class TUI {
383
439
  hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
384
440
  }
385
441
  else {
386
- hints = " esc esc: interrupt /help /explain /pause ";
442
+ hints = " click agent to view esc esc: interrupt /help ";
387
443
  }
388
444
  const totalLen = prefix.length + hints.length;
389
445
  const fill = Math.max(0, this.cols - totalLen);
@@ -421,7 +477,15 @@ export class TUI {
421
477
  const session = this.sessions.find((s) => s.id === this.drilldownSessionId);
422
478
  const title = session ? session.title : this.drilldownSessionId ?? "?";
423
479
  const prefix = `${BOX.h}${BOX.h} ${title} `;
424
- const hints = " /back: overview /view N: switch session ";
480
+ let hints;
481
+ if (this.drilldownScrollOffset > 0) {
482
+ const outputLines = this.sessionOutputs.get(this.drilldownSessionId ?? "") ?? [];
483
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
484
+ hints = formatDrilldownScrollIndicator(this.drilldownScrollOffset, outputLines.length, visibleLines, this.drilldownNewWhileScrolled);
485
+ }
486
+ else {
487
+ hints = " click or /back: overview scroll: navigate /view N: switch ";
488
+ }
425
489
  const totalLen = prefix.length + hints.length;
426
490
  const fill = Math.max(0, this.cols - totalLen);
427
491
  const left = Math.floor(fill / 2);
@@ -434,9 +498,9 @@ export class TUI {
434
498
  return;
435
499
  const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
436
500
  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);
501
+ // use scroll offset: 0 = tail (live), >0 = scrolled back
502
+ const { start, end } = computeScrollSlice(outputLines.length, visibleLines, this.drilldownScrollOffset);
503
+ const visible = outputLines.slice(start, end);
440
504
  for (let i = 0; i < visibleLines; i++) {
441
505
  const row = this.scrollTop + i;
442
506
  if (i < visible.length) {
@@ -639,6 +703,29 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
639
703
  const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
640
704
  return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
641
705
  }
706
+ // format the scroll indicator for drill-down separator bar
707
+ function formatDrilldownScrollIndicator(offset, totalLines, visibleLines, newCount) {
708
+ const position = totalLines - offset;
709
+ const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
710
+ return ` ↑ ${offset} lines │ ${position}/${totalLines} │ scroll: navigate End=live${newTag} `;
711
+ }
712
+ // ── Mouse hit testing (pure, exported for testing) ──────────────────────────
713
+ /**
714
+ * Hit-test a mouse click row against the session panel.
715
+ * Returns 1-indexed session number if the click hit a session card, null otherwise.
716
+ *
717
+ * Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
718
+ * (row = headerHeight + 2 + i for 0-indexed session i)
719
+ */
720
+ export function hitTestSession(row, headerHeight, sessionCount) {
721
+ if (sessionCount <= 0)
722
+ return null;
723
+ const firstSessionRow = headerHeight + 2; // top border is headerHeight+1, first card is +2
724
+ const lastSessionRow = firstSessionRow + sessionCount - 1;
725
+ if (row < firstSessionRow || row > lastSessionRow)
726
+ return null;
727
+ return row - firstSessionRow + 1; // 1-indexed
728
+ }
642
729
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
643
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
730
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader };
644
731
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.71.0",
3
+ "version": "0.73.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",