aoaoe 0.79.0 → 0.81.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,23 @@ async function main() {
378
378
  tui.setCompact(enabled);
379
379
  tui.log("system", `compact mode: ${enabled ? "on" : "off"}`);
380
380
  });
381
+ // wire /bell toggle
382
+ input.onBell(() => {
383
+ const enabled = !tui.isBellEnabled();
384
+ tui.setBell(enabled);
385
+ tui.log("system", `bell notifications: ${enabled ? "on" : "off"}`);
386
+ });
387
+ // wire /pin toggle
388
+ input.onPin((target) => {
389
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
390
+ const ok = tui.togglePin(num ?? target);
391
+ if (ok) {
392
+ tui.log("system", `pin toggled: ${target}`);
393
+ }
394
+ else {
395
+ tui.log("system", `session not found: ${target}`);
396
+ }
397
+ });
381
398
  // wire mouse move to hover highlight on session cards (disabled in compact)
382
399
  input.onMouseMove((row, _col) => {
383
400
  if (tui.getViewMode() === "overview" && !tui.isCompact()) {
package/dist/input.d.ts CHANGED
@@ -5,6 +5,8 @@ export type SearchHandler = (pattern: string | null) => void;
5
5
  export type QuickSwitchHandler = (sessionNum: number) => void;
6
6
  export type SortHandler = (mode: string | null) => void;
7
7
  export type CompactHandler = () => void;
8
+ export type PinHandler = (target: string) => void;
9
+ export type BellHandler = () => void;
8
10
  export interface MouseEvent {
9
11
  button: number;
10
12
  col: number;
@@ -32,6 +34,8 @@ export declare class InputReader {
32
34
  private quickSwitchHandler;
33
35
  private sortHandler;
34
36
  private compactHandler;
37
+ private pinHandler;
38
+ private bellHandler;
35
39
  private mouseDataListener;
36
40
  onScroll(handler: (dir: ScrollDirection) => void): void;
37
41
  onQueueChange(handler: (count: number) => void): void;
@@ -43,6 +47,8 @@ export declare class InputReader {
43
47
  onQuickSwitch(handler: QuickSwitchHandler): void;
44
48
  onSort(handler: SortHandler): void;
45
49
  onCompact(handler: CompactHandler): void;
50
+ onPin(handler: PinHandler): void;
51
+ onBell(handler: BellHandler): void;
46
52
  private notifyQueueChange;
47
53
  start(): void;
48
54
  drain(): string[];
package/dist/input.js CHANGED
@@ -37,6 +37,8 @@ export class InputReader {
37
37
  quickSwitchHandler = null;
38
38
  sortHandler = null;
39
39
  compactHandler = null;
40
+ pinHandler = null;
41
+ bellHandler = null;
40
42
  mouseDataListener = null;
41
43
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
42
44
  onScroll(handler) {
@@ -78,6 +80,14 @@ export class InputReader {
78
80
  onCompact(handler) {
79
81
  this.compactHandler = handler;
80
82
  }
83
+ // register a callback for pin/unpin commands (/pin <target>)
84
+ onPin(handler) {
85
+ this.pinHandler = handler;
86
+ }
87
+ // register a callback for bell toggle (/bell)
88
+ onBell(handler) {
89
+ this.bellHandler = handler;
90
+ }
81
91
  notifyQueueChange() {
82
92
  this.queueChangeHandler?.(this.queue.length);
83
93
  }
@@ -257,6 +267,8 @@ ${BOLD}navigation:${RESET}
257
267
  /back return to overview from drill-down
258
268
  /sort [mode] sort sessions: status, name, activity, default (or cycle)
259
269
  /compact toggle compact mode (dense session panel)
270
+ /pin [N|name] pin/unpin a session to the top (toggle)
271
+ /bell toggle terminal bell on errors/completions
260
272
  /search <pattern> filter activity entries by substring (case-insensitive)
261
273
  /search clear active search filter
262
274
  click session click an agent card to drill down (click again to go back)
@@ -354,6 +366,29 @@ ${BOLD}other:${RESET}
354
366
  console.error(`${DIM}compact mode not available (no TUI)${RESET}`);
355
367
  }
356
368
  break;
369
+ case "/pin": {
370
+ const pinArg = line.slice("/pin".length).trim();
371
+ if (this.pinHandler) {
372
+ if (pinArg) {
373
+ this.pinHandler(pinArg);
374
+ }
375
+ else {
376
+ console.error(`${DIM}usage: /pin <N|name> — toggle pin for a session${RESET}`);
377
+ }
378
+ }
379
+ else {
380
+ console.error(`${DIM}pin not available (no TUI)${RESET}`);
381
+ }
382
+ break;
383
+ }
384
+ case "/bell":
385
+ if (this.bellHandler) {
386
+ this.bellHandler();
387
+ }
388
+ else {
389
+ console.error(`${DIM}bell not available (no TUI)${RESET}`);
390
+ }
391
+ break;
357
392
  case "/search": {
358
393
  const searchArg = line.slice("/search".length).trim();
359
394
  if (this.searchHandler) {
package/dist/tui.d.ts CHANGED
@@ -2,18 +2,24 @@ import type { DaemonSessionState, DaemonPhase } from "./types.js";
2
2
  import type { HistoryEntry } from "./tui-history.js";
3
3
  export type SortMode = "default" | "status" | "name" | "activity";
4
4
  declare const SORT_MODES: SortMode[];
5
- /** Sort sessions by mode. Returns a new array (never mutates). */
6
- declare function sortSessions(sessions: DaemonSessionState[], mode: SortMode, lastChangeAt?: Map<string, number>): DaemonSessionState[];
5
+ /** Sort sessions by mode. Pinned sessions always sort first (stable). Returns a new array (never mutates). */
6
+ declare function sortSessions(sessions: DaemonSessionState[], mode: SortMode, lastChangeAt?: Map<string, number>, pinnedIds?: Set<string>): DaemonSessionState[];
7
7
  /** Cycle to the next sort mode. */
8
8
  declare function nextSortMode(current: SortMode): SortMode;
9
+ /** Cooldown between terminal bells to avoid buzzing. */
10
+ export declare const BELL_COOLDOWN_MS = 5000;
11
+ /** Determine if an activity entry should trigger a terminal bell. High-signal events only. */
12
+ export declare function shouldBell(tag: string, text: string): boolean;
9
13
  /** Max name length in compact token. */
10
14
  declare const COMPACT_NAME_LEN = 10;
15
+ /** Pin indicator for pinned sessions. */
16
+ declare const PIN_ICON = "\u25B2";
11
17
  /**
12
18
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
13
- * Each token: "{idx}{dot}{name}" — e.g. "1Alpha" with colored dot and bold name.
19
+ * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2●Bravo" for unpinned.
14
20
  * Returns array of formatted row strings (one per display row).
15
21
  */
16
- declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number): string[];
22
+ declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>): string[];
17
23
  /** Compute how many display rows compact mode needs (minimum 1). */
18
24
  declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
19
25
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
@@ -46,6 +52,9 @@ export declare class TUI {
46
52
  private lastChangeAt;
47
53
  private prevLastActivity;
48
54
  private compactMode;
55
+ private pinnedIds;
56
+ private bellEnabled;
57
+ private lastBellAt;
49
58
  private viewMode;
50
59
  private drilldownSessionId;
51
60
  private sessionOutputs;
@@ -71,6 +80,19 @@ export declare class TUI {
71
80
  setCompact(enabled: boolean): void;
72
81
  /** Return whether compact mode is enabled. */
73
82
  isCompact(): boolean;
83
+ /**
84
+ * Toggle pin for a session (by 1-indexed number, ID, ID prefix, or title).
85
+ * Pinned sessions always sort to the top. Returns true if session found.
86
+ */
87
+ togglePin(sessionIdOrIndex: string | number): boolean;
88
+ /** Check if a session ID is pinned. */
89
+ isPinned(id: string): boolean;
90
+ /** Return count of pinned sessions. */
91
+ getPinnedCount(): number;
92
+ /** Enable or disable terminal bell notifications. */
93
+ setBell(enabled: boolean): void;
94
+ /** Return whether terminal bell is enabled. */
95
+ isBellEnabled(): boolean;
74
96
  updateState(opts: {
75
97
  phase?: DaemonPhase;
76
98
  pollCount?: number;
@@ -161,5 +183,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
161
183
  * (row = headerHeight + 2 + i for 0-indexed session i)
162
184
  */
163
185
  export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
164
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN };
186
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN, PIN_ICON };
165
187
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -26,8 +26,8 @@ const STATUS_PRIORITY = {
26
26
  error: 0, waiting: 1, working: 2, running: 2,
27
27
  idle: 3, done: 4, stopped: 5, unknown: 6,
28
28
  };
29
- /** Sort sessions by mode. Returns a new array (never mutates). */
30
- function sortSessions(sessions, mode, lastChangeAt) {
29
+ /** Sort sessions by mode. Pinned sessions always sort first (stable). Returns a new array (never mutates). */
30
+ function sortSessions(sessions, mode, lastChangeAt, pinnedIds) {
31
31
  const copy = sessions.slice();
32
32
  switch (mode) {
33
33
  case "status":
@@ -41,6 +41,10 @@ function sortSessions(sessions, mode, lastChangeAt) {
41
41
  break;
42
42
  // "default" — preserve original order
43
43
  }
44
+ // stable-sort pinned to top (preserves mode order within each group)
45
+ if (pinnedIds && pinnedIds.size > 0) {
46
+ copy.sort((a, b) => (pinnedIds.has(a.id) ? 0 : 1) - (pinnedIds.has(b.id) ? 0 : 1));
47
+ }
44
48
  return copy;
45
49
  }
46
50
  /** Cycle to the next sort mode. */
@@ -48,15 +52,28 @@ function nextSortMode(current) {
48
52
  const idx = SORT_MODES.indexOf(current);
49
53
  return SORT_MODES[(idx + 1) % SORT_MODES.length];
50
54
  }
55
+ // ── Bell notifications ──────────────────────────────────────────────────────
56
+ /** Cooldown between terminal bells to avoid buzzing. */
57
+ export const BELL_COOLDOWN_MS = 5000;
58
+ /** Determine if an activity entry should trigger a terminal bell. High-signal events only. */
59
+ export function shouldBell(tag, text) {
60
+ if (tag === "! action" || tag === "error")
61
+ return true;
62
+ if (tag === "+ action" && text.toLowerCase().includes("complete"))
63
+ return true;
64
+ return false;
65
+ }
51
66
  // ── Compact mode ────────────────────────────────────────────────────────────
52
67
  /** Max name length in compact token. */
53
68
  const COMPACT_NAME_LEN = 10;
69
+ /** Pin indicator for pinned sessions. */
70
+ const PIN_ICON = "▲";
54
71
  /**
55
72
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
56
- * Each token: "{idx}{dot}{name}" — e.g. "1Alpha" with colored dot and bold name.
73
+ * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2●Bravo" for unpinned.
57
74
  * Returns array of formatted row strings (one per display row).
58
75
  */
59
- function formatCompactRows(sessions, maxWidth) {
76
+ function formatCompactRows(sessions, maxWidth, pinnedIds) {
60
77
  if (sessions.length === 0)
61
78
  return [`${DIM}no agents connected${RESET}`];
62
79
  const tokens = [];
@@ -65,9 +82,11 @@ function formatCompactRows(sessions, maxWidth) {
65
82
  const s = sessions[i];
66
83
  const idx = String(i + 1);
67
84
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
85
+ const pinned = pinnedIds?.has(s.id) ?? false;
86
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET}` : "";
68
87
  const name = truncatePlain(s.title, COMPACT_NAME_LEN);
69
- tokens.push(`${SLATE}${idx}${RESET}${dot}${BOLD}${name}${RESET}`);
70
- widths.push(idx.length + 1 + name.length); // idx chars + dot char + name chars
88
+ tokens.push(`${SLATE}${idx}${RESET}${pin}${dot}${BOLD}${name}${RESET}`);
89
+ widths.push(idx.length + (pinned ? 1 : 0) + 1 + name.length);
71
90
  }
72
91
  const rows = [];
73
92
  let currentRow = "";
@@ -141,6 +160,9 @@ export class TUI {
141
160
  lastChangeAt = new Map(); // session ID → epoch ms of last activity change
142
161
  prevLastActivity = new Map(); // session ID → previous lastActivity string
143
162
  compactMode = false;
163
+ pinnedIds = new Set(); // pinned session IDs (always sort to top)
164
+ bellEnabled = false;
165
+ lastBellAt = 0;
144
166
  // drill-down mode: show a single session's full output
145
167
  viewMode = "overview";
146
168
  drilldownSessionId = null;
@@ -203,7 +225,7 @@ export class TUI {
203
225
  return;
204
226
  this.sortMode = mode;
205
227
  // re-sort current sessions and repaint
206
- this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt);
228
+ this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
207
229
  if (this.active) {
208
230
  this.paintSessions();
209
231
  }
@@ -226,6 +248,51 @@ export class TUI {
226
248
  isCompact() {
227
249
  return this.compactMode;
228
250
  }
251
+ /**
252
+ * Toggle pin for a session (by 1-indexed number, ID, ID prefix, or title).
253
+ * Pinned sessions always sort to the top. Returns true if session found.
254
+ */
255
+ togglePin(sessionIdOrIndex) {
256
+ let sessionId;
257
+ if (typeof sessionIdOrIndex === "number") {
258
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
259
+ }
260
+ else {
261
+ const needle = sessionIdOrIndex.toLowerCase();
262
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
263
+ sessionId = match?.id;
264
+ }
265
+ if (!sessionId)
266
+ return false;
267
+ if (this.pinnedIds.has(sessionId)) {
268
+ this.pinnedIds.delete(sessionId);
269
+ }
270
+ else {
271
+ this.pinnedIds.add(sessionId);
272
+ }
273
+ // re-sort and repaint
274
+ this.sessions = sortSessions(this.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
275
+ if (this.active) {
276
+ this.paintSessions();
277
+ }
278
+ return true;
279
+ }
280
+ /** Check if a session ID is pinned. */
281
+ isPinned(id) {
282
+ return this.pinnedIds.has(id);
283
+ }
284
+ /** Return count of pinned sessions. */
285
+ getPinnedCount() {
286
+ return this.pinnedIds.size;
287
+ }
288
+ /** Enable or disable terminal bell notifications. */
289
+ setBell(enabled) {
290
+ this.bellEnabled = enabled;
291
+ }
292
+ /** Return whether terminal bell is enabled. */
293
+ isBellEnabled() {
294
+ return this.bellEnabled;
295
+ }
229
296
  // ── State updates ───────────────────────────────────────────────────────
230
297
  updateState(opts) {
231
298
  if (opts.phase !== undefined)
@@ -251,7 +318,7 @@ export class TUI {
251
318
  if (s.lastActivity !== undefined)
252
319
  this.prevLastActivity.set(s.id, s.lastActivity);
253
320
  }
254
- const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt);
321
+ const sorted = sortSessions(opts.sessions, this.sortMode, this.lastChangeAt, this.pinnedIds);
255
322
  const sessionCountChanged = sorted.length !== this.sessions.length;
256
323
  this.sessions = sorted;
257
324
  if (sessionCountChanged) {
@@ -279,6 +346,14 @@ export class TUI {
279
346
  this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
280
347
  this.activityTimestamps = this.activityTimestamps.slice(-this.maxActivity);
281
348
  }
349
+ // terminal bell for high-signal events (with cooldown)
350
+ if (this.bellEnabled && shouldBell(tag, text)) {
351
+ const nowMs = now.getTime();
352
+ if (nowMs - this.lastBellAt >= BELL_COOLDOWN_MS) {
353
+ this.lastBellAt = nowMs;
354
+ process.stderr.write("\x07");
355
+ }
356
+ }
282
357
  if (this.active) {
283
358
  if (this.searchPattern) {
284
359
  // search active: only show new entry if it matches
@@ -596,8 +671,8 @@ export class TUI {
596
671
  process.stderr.write(moveTo(startRow + 1, 1) + CLEAR_LINE + padded);
597
672
  }
598
673
  else if (this.compactMode) {
599
- // compact: inline tokens, multiple per row
600
- const compactRows = formatCompactRows(this.sessions, innerWidth - 1);
674
+ // compact: inline tokens, multiple per row (with pin indicators)
675
+ const compactRows = formatCompactRows(this.sessions, innerWidth - 1, this.pinnedIds);
601
676
  for (let r = 0; r < compactRows.length; r++) {
602
677
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
603
678
  const padded = padBoxLine(line, this.cols);
@@ -609,7 +684,10 @@ export class TUI {
609
684
  const s = this.sessions[i];
610
685
  const isHovered = this.hoverSessionIdx === i + 1; // 1-indexed
611
686
  const bg = isHovered ? BG_HOVER : "";
612
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${formatSessionCard(s, innerWidth - 1)}`;
687
+ const pinned = this.pinnedIds.has(s.id);
688
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
689
+ const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1; // pin takes 2 extra chars
690
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
613
691
  const padded = padBoxLineHover(line, this.cols, isHovered);
614
692
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
615
693
  }
@@ -639,7 +717,10 @@ export class TUI {
639
717
  const s = this.sessions[i];
640
718
  const isHovered = this.hoverSessionIdx === idx;
641
719
  const bg = isHovered ? BG_HOVER : "";
642
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${formatSessionCard(s, innerWidth - 1)}`;
720
+ const pinned = this.pinnedIds.has(s.id);
721
+ const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
722
+ const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1;
723
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
643
724
  const padded = padBoxLineHover(line, this.cols, isHovered);
644
725
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
645
726
  }
@@ -1006,5 +1087,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
1006
1087
  return row - firstSessionRow + 1; // 1-indexed
1007
1088
  }
1008
1089
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
1009
- export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN };
1090
+ export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padBoxLineHover, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator, computeSparkline, formatSparkline, sortSessions, nextSortMode, SORT_MODES, formatCompactRows, computeCompactRowCount, COMPACT_NAME_LEN, PIN_ICON };
1010
1091
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.79.0",
3
+ "version": "0.81.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",