aoaoe 0.81.0 → 0.83.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
@@ -378,6 +378,44 @@ async function main() {
378
378
  tui.setCompact(enabled);
379
379
  tui.log("system", `compact mode: ${enabled ? "on" : "off"}`);
380
380
  });
381
+ // wire /mark bookmark
382
+ input.onMark(() => {
383
+ const num = tui.addBookmark();
384
+ if (num > 0) {
385
+ tui.log("system", `bookmark #${num} saved`);
386
+ }
387
+ else {
388
+ tui.log("system", "nothing to bookmark (activity buffer empty)");
389
+ }
390
+ });
391
+ // wire /jump to bookmark
392
+ input.onJump((num) => {
393
+ const ok = tui.jumpToBookmark(num);
394
+ if (ok) {
395
+ tui.log("system", `jumped to bookmark #${num}`);
396
+ }
397
+ else {
398
+ tui.log("system", `bookmark #${num} not found`);
399
+ }
400
+ });
401
+ // wire /marks listing
402
+ input.onMarks(() => {
403
+ const bms = tui.getBookmarks();
404
+ if (bms.length === 0) {
405
+ tui.log("system", "no bookmarks — use /mark to save one");
406
+ }
407
+ else {
408
+ for (let i = 0; i < bms.length; i++) {
409
+ tui.log("system", ` #${i + 1}: ${bms[i].label}`);
410
+ }
411
+ }
412
+ });
413
+ // wire /focus toggle
414
+ input.onFocus(() => {
415
+ const enabled = !tui.isFocused();
416
+ tui.setFocus(enabled);
417
+ tui.log("system", `focus mode: ${enabled ? "on (pinned only)" : "off (all sessions)"}`);
418
+ });
381
419
  // wire /bell toggle
382
420
  input.onBell(() => {
383
421
  const enabled = !tui.isBellEnabled();
package/dist/input.d.ts CHANGED
@@ -7,6 +7,10 @@ export type SortHandler = (mode: string | null) => void;
7
7
  export type CompactHandler = () => void;
8
8
  export type PinHandler = (target: string) => void;
9
9
  export type BellHandler = () => void;
10
+ export type FocusHandler = () => void;
11
+ export type MarkHandler = () => void;
12
+ export type JumpHandler = (num: number) => void;
13
+ export type MarksHandler = () => void;
10
14
  export interface MouseEvent {
11
15
  button: number;
12
16
  col: number;
@@ -36,6 +40,10 @@ export declare class InputReader {
36
40
  private compactHandler;
37
41
  private pinHandler;
38
42
  private bellHandler;
43
+ private focusHandler;
44
+ private markHandler;
45
+ private jumpHandler;
46
+ private marksHandler;
39
47
  private mouseDataListener;
40
48
  onScroll(handler: (dir: ScrollDirection) => void): void;
41
49
  onQueueChange(handler: (count: number) => void): void;
@@ -49,6 +57,10 @@ export declare class InputReader {
49
57
  onCompact(handler: CompactHandler): void;
50
58
  onPin(handler: PinHandler): void;
51
59
  onBell(handler: BellHandler): void;
60
+ onFocus(handler: FocusHandler): void;
61
+ onMark(handler: MarkHandler): void;
62
+ onJump(handler: JumpHandler): void;
63
+ onMarks(handler: MarksHandler): void;
52
64
  private notifyQueueChange;
53
65
  start(): void;
54
66
  drain(): string[];
package/dist/input.js CHANGED
@@ -39,6 +39,10 @@ export class InputReader {
39
39
  compactHandler = null;
40
40
  pinHandler = null;
41
41
  bellHandler = null;
42
+ focusHandler = null;
43
+ markHandler = null;
44
+ jumpHandler = null;
45
+ marksHandler = null;
42
46
  mouseDataListener = null;
43
47
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
44
48
  onScroll(handler) {
@@ -88,6 +92,22 @@ export class InputReader {
88
92
  onBell(handler) {
89
93
  this.bellHandler = handler;
90
94
  }
95
+ // register a callback for focus mode toggle (/focus)
96
+ onFocus(handler) {
97
+ this.focusHandler = handler;
98
+ }
99
+ // register a callback for adding bookmarks (/mark)
100
+ onMark(handler) {
101
+ this.markHandler = handler;
102
+ }
103
+ // register a callback for jumping to bookmarks (/jump N)
104
+ onJump(handler) {
105
+ this.jumpHandler = handler;
106
+ }
107
+ // register a callback for listing bookmarks (/marks)
108
+ onMarks(handler) {
109
+ this.marksHandler = handler;
110
+ }
91
111
  notifyQueueChange() {
92
112
  this.queueChangeHandler?.(this.queue.length);
93
113
  }
@@ -269,6 +289,10 @@ ${BOLD}navigation:${RESET}
269
289
  /compact toggle compact mode (dense session panel)
270
290
  /pin [N|name] pin/unpin a session to the top (toggle)
271
291
  /bell toggle terminal bell on errors/completions
292
+ /focus toggle focus mode (show only pinned sessions)
293
+ /mark bookmark current activity position
294
+ /jump N jump to bookmark N
295
+ /marks list all bookmarks
272
296
  /search <pattern> filter activity entries by substring (case-insensitive)
273
297
  /search clear active search filter
274
298
  click session click an agent card to drill down (click again to go back)
@@ -389,6 +413,44 @@ ${BOLD}other:${RESET}
389
413
  console.error(`${DIM}bell not available (no TUI)${RESET}`);
390
414
  }
391
415
  break;
416
+ case "/focus":
417
+ if (this.focusHandler) {
418
+ this.focusHandler();
419
+ }
420
+ else {
421
+ console.error(`${DIM}focus not available (no TUI)${RESET}`);
422
+ }
423
+ break;
424
+ case "/mark":
425
+ if (this.markHandler) {
426
+ this.markHandler();
427
+ }
428
+ else {
429
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
430
+ }
431
+ break;
432
+ case "/jump": {
433
+ const jumpArg = line.slice("/jump".length).trim();
434
+ const jumpNum = parseInt(jumpArg, 10);
435
+ if (this.jumpHandler && !isNaN(jumpNum) && jumpNum > 0) {
436
+ this.jumpHandler(jumpNum);
437
+ }
438
+ else if (!this.jumpHandler) {
439
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
440
+ }
441
+ else {
442
+ console.error(`${DIM}usage: /jump N — jump to bookmark number N${RESET}`);
443
+ }
444
+ break;
445
+ }
446
+ case "/marks":
447
+ if (this.marksHandler) {
448
+ this.marksHandler();
449
+ }
450
+ else {
451
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
452
+ }
453
+ break;
392
454
  case "/search": {
393
455
  const searchArg = line.slice("/search".length).trim();
394
456
  if (this.searchHandler) {
package/dist/tui.d.ts CHANGED
@@ -10,6 +10,18 @@ declare function nextSortMode(current: SortMode): SortMode;
10
10
  export declare const BELL_COOLDOWN_MS = 5000;
11
11
  /** Determine if an activity entry should trigger a terminal bell. High-signal events only. */
12
12
  export declare function shouldBell(tag: string, text: string): boolean;
13
+ export interface Bookmark {
14
+ index: number;
15
+ label: string;
16
+ }
17
+ /** Max number of bookmarks. */
18
+ export declare const MAX_BOOKMARKS = 20;
19
+ /**
20
+ * Compute the scroll offset needed to show a bookmarked entry.
21
+ * Centers the entry in the visible region when possible.
22
+ * Returns 0 (live) if the entry is within the visible tail.
23
+ */
24
+ export declare function computeBookmarkOffset(bookmarkIndex: number, bufferLen: number, visibleLines: number): number;
13
25
  /** Max name length in compact token. */
14
26
  declare const COMPACT_NAME_LEN = 10;
15
27
  /** Pin indicator for pinned sessions. */
@@ -53,6 +65,8 @@ export declare class TUI {
53
65
  private prevLastActivity;
54
66
  private compactMode;
55
67
  private pinnedIds;
68
+ private focusMode;
69
+ private bookmarks;
56
70
  private bellEnabled;
57
71
  private lastBellAt;
58
72
  private viewMode;
@@ -70,7 +84,7 @@ export declare class TUI {
70
84
  start(version: string): void;
71
85
  stop(): void;
72
86
  isActive(): boolean;
73
- /** Return the current number of sessions (for mouse hit testing) */
87
+ /** Return the current number of visible sessions (for mouse hit testing) */
74
88
  getSessionCount(): number;
75
89
  /** Set the session sort mode and repaint. */
76
90
  setSortMode(mode: SortMode): void;
@@ -89,10 +103,30 @@ export declare class TUI {
89
103
  isPinned(id: string): boolean;
90
104
  /** Return count of pinned sessions. */
91
105
  getPinnedCount(): number;
106
+ /** Enable or disable focus mode. When focused, only pinned sessions are visible. */
107
+ setFocus(enabled: boolean): void;
108
+ /** Return whether focus mode is enabled. */
109
+ isFocused(): boolean;
110
+ /** Return count of visible sessions (all in normal mode, pinned-only in focus mode). */
111
+ private getVisibleCount;
92
112
  /** Enable or disable terminal bell notifications. */
93
113
  setBell(enabled: boolean): void;
94
114
  /** Return whether terminal bell is enabled. */
95
115
  isBellEnabled(): boolean;
116
+ /**
117
+ * Add a bookmark at the current activity position.
118
+ * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
119
+ */
120
+ addBookmark(): number;
121
+ /**
122
+ * Jump to a bookmark by number (1-indexed). Returns false if not found.
123
+ * Adjusts scroll offset to center the bookmarked entry.
124
+ */
125
+ jumpToBookmark(num: number): boolean;
126
+ /** Return all bookmarks (for /marks listing). */
127
+ getBookmarks(): readonly Bookmark[];
128
+ /** Return bookmark count. */
129
+ getBookmarkCount(): number;
96
130
  updateState(opts: {
97
131
  phase?: DaemonPhase;
98
132
  pollCount?: number;
package/dist/tui.js CHANGED
@@ -63,6 +63,23 @@ export function shouldBell(tag, text) {
63
63
  return true;
64
64
  return false;
65
65
  }
66
+ /** Max number of bookmarks. */
67
+ export const MAX_BOOKMARKS = 20;
68
+ /**
69
+ * Compute the scroll offset needed to show a bookmarked entry.
70
+ * Centers the entry in the visible region when possible.
71
+ * Returns 0 (live) if the entry is within the visible tail.
72
+ */
73
+ export function computeBookmarkOffset(bookmarkIndex, bufferLen, visibleLines) {
74
+ // entry position from the end of the buffer
75
+ const fromEnd = bufferLen - 1 - bookmarkIndex;
76
+ // if entry is within the visible tail, no scroll needed
77
+ if (fromEnd < visibleLines)
78
+ return 0;
79
+ // center the entry in the visible region
80
+ const half = Math.floor(visibleLines / 2);
81
+ return Math.max(0, fromEnd - half);
82
+ }
66
83
  // ── Compact mode ────────────────────────────────────────────────────────────
67
84
  /** Max name length in compact token. */
68
85
  const COMPACT_NAME_LEN = 10;
@@ -161,6 +178,8 @@ export class TUI {
161
178
  prevLastActivity = new Map(); // session ID → previous lastActivity string
162
179
  compactMode = false;
163
180
  pinnedIds = new Set(); // pinned session IDs (always sort to top)
181
+ focusMode = false; // focus mode: hide all sessions except pinned
182
+ bookmarks = []; // saved positions in activity buffer
164
183
  bellEnabled = false;
165
184
  lastBellAt = 0;
166
185
  // drill-down mode: show a single session's full output
@@ -215,9 +234,9 @@ export class TUI {
215
234
  isActive() {
216
235
  return this.active;
217
236
  }
218
- /** Return the current number of sessions (for mouse hit testing) */
237
+ /** Return the current number of visible sessions (for mouse hit testing) */
219
238
  getSessionCount() {
220
- return this.sessions.length;
239
+ return this.getVisibleCount();
221
240
  }
222
241
  /** Set the session sort mode and repaint. */
223
242
  setSortMode(mode) {
@@ -240,7 +259,7 @@ export class TUI {
240
259
  return;
241
260
  this.compactMode = enabled;
242
261
  if (this.active) {
243
- this.computeLayout(this.sessions.length);
262
+ this.computeLayout(this.getVisibleCount());
244
263
  this.paintAll();
245
264
  }
246
265
  }
@@ -285,6 +304,31 @@ export class TUI {
285
304
  getPinnedCount() {
286
305
  return this.pinnedIds.size;
287
306
  }
307
+ /** Enable or disable focus mode. When focused, only pinned sessions are visible. */
308
+ setFocus(enabled) {
309
+ if (enabled === this.focusMode)
310
+ return;
311
+ this.focusMode = enabled;
312
+ if (this.active) {
313
+ this.computeLayout(this.getVisibleCount());
314
+ this.paintAll();
315
+ }
316
+ }
317
+ /** Return whether focus mode is enabled. */
318
+ isFocused() {
319
+ return this.focusMode;
320
+ }
321
+ /** Return count of visible sessions (all in normal mode, pinned-only in focus mode). */
322
+ getVisibleCount() {
323
+ if (!this.focusMode)
324
+ return this.sessions.length;
325
+ let count = 0;
326
+ for (const s of this.sessions) {
327
+ if (this.pinnedIds.has(s.id))
328
+ count++;
329
+ }
330
+ return count;
331
+ }
288
332
  /** Enable or disable terminal bell notifications. */
289
333
  setBell(enabled) {
290
334
  this.bellEnabled = enabled;
@@ -293,6 +337,55 @@ export class TUI {
293
337
  isBellEnabled() {
294
338
  return this.bellEnabled;
295
339
  }
340
+ /**
341
+ * Add a bookmark at the current activity position.
342
+ * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
343
+ */
344
+ addBookmark() {
345
+ if (this.activityBuffer.length === 0)
346
+ return 0;
347
+ // bookmark the entry at the current view position
348
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
349
+ const { start } = computeScrollSlice(this.activityBuffer.length, visibleLines, this.scrollOffset);
350
+ const entry = this.activityBuffer[start];
351
+ if (!entry)
352
+ return 0;
353
+ const bm = { index: start, label: `${entry.time} ${entry.tag}` };
354
+ this.bookmarks.push(bm);
355
+ if (this.bookmarks.length > MAX_BOOKMARKS) {
356
+ this.bookmarks = this.bookmarks.slice(-MAX_BOOKMARKS);
357
+ }
358
+ return this.bookmarks.length;
359
+ }
360
+ /**
361
+ * Jump to a bookmark by number (1-indexed). Returns false if not found.
362
+ * Adjusts scroll offset to center the bookmarked entry.
363
+ */
364
+ jumpToBookmark(num) {
365
+ const bm = this.bookmarks[num - 1];
366
+ if (!bm)
367
+ return false;
368
+ // clamp index to current buffer
369
+ if (bm.index >= this.activityBuffer.length)
370
+ return false;
371
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
372
+ this.scrollOffset = computeBookmarkOffset(bm.index, this.activityBuffer.length, visibleLines);
373
+ if (this.scrollOffset === 0)
374
+ this.newWhileScrolled = 0;
375
+ if (this.active && this.viewMode === "overview") {
376
+ this.repaintActivityRegion();
377
+ this.paintSeparator();
378
+ }
379
+ return true;
380
+ }
381
+ /** Return all bookmarks (for /marks listing). */
382
+ getBookmarks() {
383
+ return this.bookmarks;
384
+ }
385
+ /** Return bookmark count. */
386
+ getBookmarkCount() {
387
+ return this.bookmarks.length;
388
+ }
296
389
  // ── State updates ───────────────────────────────────────────────────────
297
390
  updateState(opts) {
298
391
  if (opts.phase !== undefined)
@@ -319,10 +412,11 @@ export class TUI {
319
412
  this.prevLastActivity.set(s.id, s.lastActivity);
320
413
  }
321
414
  const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
322
- const sessionCountChanged = sorted.length !== this.sessions.length;
415
+ const prevVisibleCount = this.getVisibleCount();
323
416
  this.sessions = sorted;
324
- if (sessionCountChanged) {
325
- this.computeLayout(this.sessions.length);
417
+ const newVisibleCount = this.getVisibleCount();
418
+ if (newVisibleCount !== prevVisibleCount) {
419
+ this.computeLayout(newVisibleCount);
326
420
  this.paintAll();
327
421
  return;
328
422
  }
@@ -516,7 +610,7 @@ export class TUI {
516
610
  this.drilldownNewWhileScrolled = 0;
517
611
  this.hoverSessionIdx = null;
518
612
  if (this.active) {
519
- this.computeLayout(this.sessions.length);
613
+ this.computeLayout(this.getVisibleCount());
520
614
  this.paintAll();
521
615
  }
522
616
  return true;
@@ -531,7 +625,7 @@ export class TUI {
531
625
  this.drilldownNewWhileScrolled = 0;
532
626
  this.hoverSessionIdx = null;
533
627
  if (this.active) {
534
- this.computeLayout(this.sessions.length);
628
+ this.computeLayout(this.getVisibleCount());
535
629
  this.paintAll();
536
630
  }
537
631
  }
@@ -594,8 +688,9 @@ export class TUI {
594
688
  }
595
689
  else {
596
690
  // overview: header (1) + sessions box + separator + activity + input
691
+ const visibleSessions = this.sessions.slice(0, this.getVisibleCount());
597
692
  const sessBodyRows = this.compactMode
598
- ? computeCompactRowCount(this.sessions, this.cols - 2)
693
+ ? computeCompactRowCount(visibleSessions, this.cols - 2)
599
694
  : Math.max(sessionCount, 1);
600
695
  this.sessionRows = sessBodyRows + 2; // + top/bottom borders
601
696
  this.separatorRow = this.headerHeight + this.sessionRows + 1;
@@ -608,7 +703,7 @@ export class TUI {
608
703
  }
609
704
  }
610
705
  onResize() {
611
- this.computeLayout(this.sessions.length);
706
+ this.computeLayout(this.getVisibleCount());
612
707
  this.paintAll();
613
708
  }
614
709
  // ── Painting ────────────────────────────────────────────────────────────
@@ -636,7 +731,10 @@ export class TUI {
636
731
  }
637
732
  else {
638
733
  const phaseText = phaseDisplay(this.phase, this.paused, this.spinnerFrame);
639
- const sessCount = `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
734
+ const visCount = this.getVisibleCount();
735
+ const sessCount = this.focusMode
736
+ ? `${visCount}/${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`
737
+ : `${this.sessions.length} agent${this.sessions.length !== 1 ? "s" : ""}`;
640
738
  const activeCount = this.sessions.filter((s) => s.userActive).length;
641
739
  const activeTag = activeCount > 0 ? ` ${SLATE}│${RESET} ${AMBER}${activeCount} user${RESET}` : "";
642
740
  // countdown to next tick (only in sleeping phase)
@@ -656,23 +754,29 @@ export class TUI {
656
754
  paintSessions() {
657
755
  const startRow = this.headerHeight + 1;
658
756
  const innerWidth = this.cols - 2; // inside the box borders
659
- // top border with label (includes compact/sort mode tags)
757
+ const visibleCount = this.getVisibleCount();
758
+ const visibleSessions = this.sessions.slice(0, visibleCount); // pinned sort to top
759
+ // top border with label (includes focus/compact/sort mode tags)
760
+ const focusTag = this.focusMode ? "focus" : "";
660
761
  const sortTag = this.sortMode !== "default" ? this.sortMode : "";
661
762
  const compactTag = this.compactMode ? "compact" : "";
662
- const tags = [compactTag, sortTag].filter(Boolean).join(", ");
763
+ const tags = [focusTag, compactTag, sortTag].filter(Boolean).join(", ");
663
764
  const label = tags ? ` agents (${tags}) ` : " agents ";
664
765
  const borderAfterLabel = Math.max(0, innerWidth - label.length);
665
766
  const topBorder = `${SLATE}${BOX.rtl}${BOX.h}${RESET}${SLATE}${label}${RESET}${SLATE}${BOX.h.repeat(borderAfterLabel)}${BOX.rtr}${RESET}`;
666
767
  process.stderr.write(SAVE_CURSOR + moveTo(startRow, 1) + CLEAR_LINE + truncateAnsi(topBorder, this.cols));
667
- if (this.sessions.length === 0) {
768
+ if (visibleSessions.length === 0) {
668
769
  // empty state
669
- const empty = `${SLATE}${BOX.v}${RESET} ${DIM}no agents connected${RESET}`;
770
+ const msg = this.focusMode && this.sessions.length > 0
771
+ ? `${DIM}no pinned agents — /pin to add, /focus to exit${RESET}`
772
+ : `${DIM}no agents connected${RESET}`;
773
+ const empty = `${SLATE}${BOX.v}${RESET} ${msg}`;
670
774
  const padded = padBoxLine(empty, this.cols);
671
775
  process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
672
776
  }
673
777
  else if (this.compactMode) {
674
778
  // compact: inline tokens, multiple per row (with pin indicators)
675
- const compactRows = formatCompactRows(this.sessions, innerWidth - 1, this.pinnedIds);
779
+ const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds);
676
780
  for (let r = 0; r < compactRows.length; r++) {
677
781
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
678
782
  const padded = padBoxLine(line, this.cols);
@@ -680,8 +784,8 @@ export class TUI {
680
784
  }
681
785
  }
682
786
  else {
683
- for (let i = 0; i < this.sessions.length; i++) {
684
- const s = this.sessions[i];
787
+ for (let i = 0; i < visibleSessions.length; i++) {
788
+ const s = visibleSessions[i];
685
789
  const isHovered = this.hoverSessionIdx === i + 1; // 1-indexed
686
790
  const bg = isHovered ? BG_HOVER : "";
687
791
  const pinned = this.pinnedIds.has(s.id);
@@ -694,8 +798,8 @@ export class TUI {
694
798
  }
695
799
  // bottom border
696
800
  const bodyRows = this.compactMode
697
- ? computeCompactRowCount(this.sessions, innerWidth)
698
- : Math.max(this.sessions.length, 1);
801
+ ? computeCompactRowCount(visibleSessions, innerWidth)
802
+ : Math.max(visibleCount, 1);
699
803
  const bottomRow = startRow + 1 + bodyRows;
700
804
  const bottomBorder = `${SLATE}${BOX.rbl}${BOX.h.repeat(Math.max(0, this.cols - 2))}${BOX.rbr}${RESET}`;
701
805
  process.stderr.write(moveTo(bottomRow, 1) + CLEAR_LINE + truncateAnsi(bottomBorder, this.cols));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.81.0",
3
+ "version": "0.83.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",