aoaoe 0.64.0 → 0.66.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,7 @@ 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)
595
596
  /status request daemon status
596
597
  /dashboard request full dashboard output
597
598
  /pause pause the daemon
@@ -600,6 +601,7 @@ interactive commands (while daemon is running):
600
601
  /verbose toggle verbose logging
601
602
  /clear clear the screen
602
603
  ESC ESC interrupt the current reasoner (shortcut)
603
- (anything) send a message to the AI it reads your input next cycle`);
604
+ !message insist shortcutsame as /insist message
605
+ (anything) send a message to the AI — queued for next cycle`);
604
606
  }
605
607
  //# 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";
@@ -235,6 +235,29 @@ async function main() {
235
235
  // start interactive input listener and conversation log
236
236
  input.start();
237
237
  await reasonerConsole.start();
238
+ // wire scroll keys to TUI (PgUp/PgDn/Home/End)
239
+ if (tui) {
240
+ input.onScroll((dir) => {
241
+ switch (dir) {
242
+ case "up":
243
+ tui.scrollUp();
244
+ break;
245
+ case "down":
246
+ tui.scrollDown();
247
+ break;
248
+ case "top":
249
+ tui.scrollToTop();
250
+ break;
251
+ case "bottom":
252
+ tui.scrollToBottom();
253
+ break;
254
+ }
255
+ });
256
+ // wire queue count changes to TUI prompt display
257
+ input.onQueueChange((count) => {
258
+ tui.updateState({ pendingCount: count });
259
+ });
260
+ }
238
261
  // start TUI (alternate screen buffer) after input is ready
239
262
  if (tui) {
240
263
  // replay persisted history from previous runs before entering alt screen
@@ -374,6 +397,17 @@ async function main() {
374
397
  const allMessages = [...stdinMessages, ...consoleMessages];
375
398
  // classify into commands vs. real user messages
376
399
  const { commands, userMessages } = classifyMessages(allMessages);
400
+ // strip insist prefix from priority messages and log them distinctly
401
+ for (let i = 0; i < userMessages.length; i++) {
402
+ if (isInsistMessage(userMessages[i])) {
403
+ const raw = stripInsistPrefix(userMessages[i]);
404
+ userMessages[i] = raw;
405
+ if (tui)
406
+ tui.log("you", `! ${raw}`);
407
+ else
408
+ log(`[insist] ${raw}`);
409
+ }
410
+ }
377
411
  // auto-explain on first tick: inject an explain prompt so the AI introduces itself
378
412
  if (autoExplainPending && pollCount === 1) {
379
413
  const autoExplainPrompt = "This is your first observation. Please briefly introduce what you see: " +
package/dist/input.d.ts CHANGED
@@ -1,8 +1,15 @@
1
+ export type ScrollDirection = "up" | "down" | "top" | "bottom";
2
+ export declare const INSIST_PREFIX = "__INSIST__";
1
3
  export declare class InputReader {
2
4
  private rl;
3
5
  private queue;
4
6
  private paused;
5
7
  private lastEscTime;
8
+ private scrollHandler;
9
+ private queueChangeHandler;
10
+ onScroll(handler: (dir: ScrollDirection) => void): void;
11
+ onQueueChange(handler: (count: number) => void): void;
12
+ private notifyQueueChange;
6
13
  start(): void;
7
14
  drain(): string[];
8
15
  isPaused(): boolean;
@@ -11,6 +18,7 @@ export declare class InputReader {
11
18
  prompt(): void;
12
19
  stop(): void;
13
20
  private handleEscInterrupt;
21
+ private handleInsist;
14
22
  private handleLine;
15
23
  private handleCommand;
16
24
  }
package/dist/input.js CHANGED
@@ -6,11 +6,25 @@ 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;
15
+ scrollHandler = null;
16
+ queueChangeHandler = null;
17
+ // register a callback for scroll key events (PgUp/PgDn/Home/End)
18
+ onScroll(handler) {
19
+ this.scrollHandler = handler;
20
+ }
21
+ // register a callback for queue size changes (for TUI pending count display)
22
+ onQueueChange(handler) {
23
+ this.queueChangeHandler = handler;
24
+ }
25
+ notifyQueueChange() {
26
+ this.queueChangeHandler?.(this.queue.length);
27
+ }
14
28
  start() {
15
29
  // only works if stdin is a TTY (not piped)
16
30
  if (!process.stdin.isTTY)
@@ -39,6 +53,21 @@ export class InputReader {
39
53
  else {
40
54
  this.lastEscTime = 0;
41
55
  }
56
+ // scroll key detection (PgUp, PgDn, Home, End)
57
+ if (this.scrollHandler) {
58
+ if (key?.name === "pageup" || key?.sequence === "\x1b[5~") {
59
+ this.scrollHandler("up");
60
+ }
61
+ else if (key?.name === "pagedown" || key?.sequence === "\x1b[6~") {
62
+ this.scrollHandler("down");
63
+ }
64
+ else if (key?.name === "home" || key?.sequence === "\x1b[1~") {
65
+ this.scrollHandler("top");
66
+ }
67
+ else if (key?.name === "end" || key?.sequence === "\x1b[4~") {
68
+ this.scrollHandler("bottom");
69
+ }
70
+ }
42
71
  });
43
72
  // show hint on startup
44
73
  console.error(`${DIM}type a message to talk to the AI supervisor, /help for commands, ESC ESC to interrupt${RESET}`);
@@ -47,6 +76,8 @@ export class InputReader {
47
76
  // drain all pending user messages (called each tick)
48
77
  drain() {
49
78
  const msgs = this.queue.splice(0);
79
+ if (msgs.length > 0)
80
+ this.notifyQueueChange();
50
81
  return msgs;
51
82
  }
52
83
  isPaused() {
@@ -59,6 +90,7 @@ export class InputReader {
59
90
  // inject a message directly into the queue (used after interrupt to feed text into next tick)
60
91
  inject(msg) {
61
92
  this.queue.push(msg);
93
+ this.notifyQueueChange();
62
94
  }
63
95
  // re-show the prompt (called after daemon prints output)
64
96
  prompt() {
@@ -71,10 +103,18 @@ export class InputReader {
71
103
  handleEscInterrupt() {
72
104
  requestInterrupt();
73
105
  this.queue.push("__CMD_INTERRUPT__");
106
+ this.notifyQueueChange();
74
107
  console.error(`\n${RED}${BOLD}>>> interrupting reasoner <<<${RESET}`);
75
108
  console.error(`${YELLOW}type your message now -- it will be sent before the next cycle${RESET}`);
76
109
  this.rl?.prompt(true);
77
110
  }
111
+ handleInsist(msg) {
112
+ requestInterrupt();
113
+ this.queue.push("__CMD_INTERRUPT__");
114
+ this.queue.push(`${INSIST_PREFIX}${msg}`);
115
+ this.notifyQueueChange();
116
+ console.error(`${RED}${BOLD}!${RESET} ${GREEN}insist${RESET} ${DIM}— interrupting + delivering your message immediately${RESET}`);
117
+ }
78
118
  handleLine(line) {
79
119
  if (!line) {
80
120
  this.rl?.prompt();
@@ -86,9 +126,20 @@ export class InputReader {
86
126
  this.rl?.prompt();
87
127
  return;
88
128
  }
129
+ // ! prefix = insist mode: interrupt + priority message
130
+ if (line.startsWith("!") && line.length > 1) {
131
+ const msg = line.slice(1).trim();
132
+ if (msg) {
133
+ this.handleInsist(msg);
134
+ this.rl?.prompt();
135
+ return;
136
+ }
137
+ }
89
138
  // queue as a user message for the reasoner
90
139
  this.queue.push(line);
91
- console.error(`${GREEN}Got it!${RESET} ${DIM}The AI will read your message on the next cycle.${RESET}`);
140
+ this.notifyQueueChange();
141
+ const pending = this.queue.filter(m => !m.startsWith("__CMD_")).length;
142
+ console.error(`${GREEN}queued${RESET} ${DIM}(${pending} pending) — will be read next cycle${RESET}`);
92
143
  this.rl?.prompt();
93
144
  }
94
145
  handleCommand(line) {
@@ -97,7 +148,9 @@ export class InputReader {
97
148
  case "/help":
98
149
  console.error(`
99
150
  ${BOLD}talking to the AI:${RESET}
100
- just type send a message to the AI supervisor
151
+ just type send a message queued for next cycle
152
+ !message insist — interrupt + deliver message immediately
153
+ /insist <msg> same as !message
101
154
  /explain ask the AI to explain what's happening right now
102
155
 
103
156
  ${BOLD}controls:${RESET}
@@ -105,6 +158,8 @@ ${BOLD}controls:${RESET}
105
158
  /resume resume the supervisor
106
159
  /interrupt interrupt the AI mid-thought
107
160
  ESC ESC same as /interrupt (shortcut)
161
+ PgUp / PgDn scroll through activity history
162
+ Home / End jump to oldest / return to live
108
163
 
109
164
  ${BOLD}info:${RESET}
110
165
  /status show daemon state
@@ -141,6 +196,16 @@ ${BOLD}other:${RESET}
141
196
  case "/interrupt":
142
197
  this.handleEscInterrupt();
143
198
  break;
199
+ case "/insist": {
200
+ const insistMsg = line.slice("/insist".length).trim();
201
+ if (insistMsg) {
202
+ this.handleInsist(insistMsg);
203
+ }
204
+ else {
205
+ console.error(`${DIM}usage: /insist <message> — interrupts and delivers your message immediately${RESET}`);
206
+ }
207
+ break;
208
+ }
144
209
  case "/tasks":
145
210
  this.queue.push("__CMD_TASK__list");
146
211
  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
@@ -20,6 +20,9 @@ export declare class TUI {
20
20
  private activityBuffer;
21
21
  private maxActivity;
22
22
  private spinnerFrame;
23
+ private scrollOffset;
24
+ private newWhileScrolled;
25
+ private pendingCount;
23
26
  private phase;
24
27
  private pollCount;
25
28
  private sessions;
@@ -37,9 +40,15 @@ export declare class TUI {
37
40
  paused?: boolean;
38
41
  reasonerName?: string;
39
42
  nextTickAt?: number;
43
+ pendingCount?: number;
40
44
  }): void;
41
45
  log(tag: string, text: string): void;
42
46
  replayHistory(entries: HistoryEntry[]): void;
47
+ scrollUp(lines?: number): void;
48
+ scrollDown(lines?: number): void;
49
+ scrollToTop(): void;
50
+ scrollToBottom(): void;
51
+ isScrolledBack(): boolean;
43
52
  private updateDimensions;
44
53
  private computeLayout;
45
54
  private onResize;
@@ -63,5 +72,11 @@ declare function truncatePlain(str: string, max: number): string;
63
72
  * Kept for backward compatibility — used by non-TUI output paths.
64
73
  */
65
74
  export declare function formatSessionSentence(s: DaemonSessionState, maxCols: number): string;
66
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
75
+ declare function formatPrompt(phase: DaemonPhase, paused: boolean, pendingCount: number): string;
76
+ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scrollOffset: number): {
77
+ start: number;
78
+ end: number;
79
+ };
80
+ 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 };
67
82
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -55,6 +55,9 @@ export class TUI {
55
55
  activityBuffer = []; // ring buffer for activity log
56
56
  maxActivity = 500; // max entries to keep
57
57
  spinnerFrame = 0; // current spinner animation frame
58
+ scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
59
+ newWhileScrolled = 0; // entries added while user is scrolled back
60
+ pendingCount = 0; // queued user messages awaiting next tick
58
61
  // current state for repaints
59
62
  phase = "sleeping";
60
63
  pollCount = 0;
@@ -113,6 +116,8 @@ export class TUI {
113
116
  this.reasonerName = opts.reasonerName;
114
117
  if (opts.nextTickAt !== undefined)
115
118
  this.nextTickAt = opts.nextTickAt;
119
+ if (opts.pendingCount !== undefined)
120
+ this.pendingCount = opts.pendingCount;
116
121
  if (opts.sessions !== undefined) {
117
122
  const sessionCountChanged = opts.sessions.length !== this.sessions.length;
118
123
  this.sessions = opts.sessions;
@@ -139,8 +144,16 @@ export class TUI {
139
144
  if (this.activityBuffer.length > this.maxActivity) {
140
145
  this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
141
146
  }
142
- if (this.active)
143
- this.writeActivityLine(entry);
147
+ if (this.active) {
148
+ if (this.scrollOffset > 0) {
149
+ // user is scrolled back — don't auto-scroll, just show indicator
150
+ this.newWhileScrolled++;
151
+ this.paintSeparator();
152
+ }
153
+ else {
154
+ this.writeActivityLine(entry);
155
+ }
156
+ }
144
157
  // persist to disk (fire-and-forget, never blocks)
145
158
  appendHistoryEntry({ ts: now.getTime(), time, tag, text });
146
159
  }
@@ -155,6 +168,48 @@ export class TUI {
155
168
  this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
156
169
  }
157
170
  }
171
+ // ── Scroll navigation ────────────────────────────────────────────────────
172
+ scrollUp(lines) {
173
+ if (!this.active)
174
+ return;
175
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
176
+ const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
177
+ const maxOffset = Math.max(0, this.activityBuffer.length - visibleLines);
178
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
179
+ this.repaintActivityRegion();
180
+ this.paintSeparator();
181
+ }
182
+ scrollDown(lines) {
183
+ if (!this.active)
184
+ return;
185
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
186
+ const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
187
+ const wasScrolled = this.scrollOffset > 0;
188
+ this.scrollOffset = Math.max(0, this.scrollOffset - n);
189
+ if (wasScrolled && this.scrollOffset === 0)
190
+ this.newWhileScrolled = 0;
191
+ this.repaintActivityRegion();
192
+ this.paintSeparator();
193
+ }
194
+ scrollToTop() {
195
+ if (!this.active)
196
+ return;
197
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
198
+ this.scrollOffset = Math.max(0, this.activityBuffer.length - visibleLines);
199
+ this.repaintActivityRegion();
200
+ this.paintSeparator();
201
+ }
202
+ scrollToBottom() {
203
+ if (!this.active)
204
+ return;
205
+ this.scrollOffset = 0;
206
+ this.newWhileScrolled = 0;
207
+ this.repaintActivityRegion();
208
+ this.paintSeparator();
209
+ }
210
+ isScrolledBack() {
211
+ return this.scrollOffset > 0;
212
+ }
158
213
  // ── Layout computation ──────────────────────────────────────────────────
159
214
  updateDimensions() {
160
215
  this.cols = process.stderr.columns || 80;
@@ -245,8 +300,14 @@ export class TUI {
245
300
  process.stderr.write(RESTORE_CURSOR);
246
301
  }
247
302
  paintSeparator() {
248
- const hints = " esc esc: interrupt /help /explain /pause ";
249
303
  const prefix = `${BOX.h}${BOX.h} activity `;
304
+ let hints;
305
+ if (this.scrollOffset > 0) {
306
+ hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
307
+ }
308
+ else {
309
+ hints = " esc esc: interrupt /help /explain /pause ";
310
+ }
250
311
  const totalLen = prefix.length + hints.length;
251
312
  const fill = Math.max(0, this.cols - totalLen);
252
313
  const left = Math.floor(fill / 2);
@@ -265,7 +326,8 @@ export class TUI {
265
326
  }
266
327
  repaintActivityRegion() {
267
328
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
268
- const entries = this.activityBuffer.slice(-visibleLines);
329
+ const { start, end } = computeScrollSlice(this.activityBuffer.length, visibleLines, this.scrollOffset);
330
+ const entries = this.activityBuffer.slice(start, end);
269
331
  for (let i = 0; i < visibleLines; i++) {
270
332
  const row = this.scrollTop + i;
271
333
  if (i < entries.length) {
@@ -278,12 +340,7 @@ export class TUI {
278
340
  }
279
341
  }
280
342
  paintInputLine() {
281
- // phase-aware prompt styling
282
- const prompt = this.paused
283
- ? `${AMBER}${BOLD}paused >${RESET} `
284
- : this.phase === "reasoning"
285
- ? `${SKY}thinking >${RESET} `
286
- : `${LIME}>${RESET} `;
343
+ const prompt = formatPrompt(this.phase, this.paused, this.pendingCount);
287
344
  process.stderr.write(SAVE_CURSOR +
288
345
  moveTo(this.inputRow, 1) + CLEAR_LINE + prompt +
289
346
  RESTORE_CURSOR);
@@ -431,6 +488,29 @@ export function formatSessionSentence(s, maxCols) {
431
488
  }
432
489
  return truncateAnsi(`${dot} ${BOLD}${name}${RESET} ${tool} ${SLATE}—${RESET} ${statusDesc}`, maxCols);
433
490
  }
491
+ // ── Prompt helpers (pure, exported for testing) ─────────────────────────────
492
+ // format the input prompt based on phase, pause state, and pending queue count
493
+ function formatPrompt(phase, paused, pendingCount) {
494
+ const queueTag = pendingCount > 0 ? `${AMBER}${pendingCount} queued${RESET} ` : "";
495
+ if (paused)
496
+ return `${queueTag}${AMBER}${BOLD}paused >${RESET} `;
497
+ if (phase === "reasoning")
498
+ return `${queueTag}${SKY}thinking >${RESET} `;
499
+ return `${queueTag}${LIME}>${RESET} `;
500
+ }
501
+ // ── Scroll helpers (pure, exported for testing) ─────────────────────────────
502
+ // compute the slice indices for the activity buffer given scroll state
503
+ function computeScrollSlice(bufferLen, visibleLines, scrollOffset) {
504
+ const end = Math.max(0, bufferLen - scrollOffset);
505
+ const start = Math.max(0, end - visibleLines);
506
+ return { start, end };
507
+ }
508
+ // format the scroll indicator text for the separator bar
509
+ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
510
+ const position = totalEntries - offset;
511
+ const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
512
+ return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
513
+ }
434
514
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
435
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay };
515
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt };
436
516
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.64.0",
3
+ "version": "0.66.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",