aoaoe 0.82.0 → 0.84.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,38 @@ 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
+ });
381
413
  // wire /focus toggle
382
414
  input.onFocus(() => {
383
415
  const enabled = !tui.isFocused();
@@ -401,6 +433,17 @@ async function main() {
401
433
  tui.log("system", `session not found: ${target}`);
402
434
  }
403
435
  });
436
+ // wire /mute toggle
437
+ input.onMute((target) => {
438
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
439
+ const ok = tui.toggleMute(num ?? target);
440
+ if (ok) {
441
+ tui.log("system", `mute toggled: ${target}`);
442
+ }
443
+ else {
444
+ tui.log("system", `session not found: ${target}`);
445
+ }
446
+ });
404
447
  // wire mouse move to hover highlight on session cards (disabled in compact)
405
448
  input.onMouseMove((row, _col) => {
406
449
  if (tui.getViewMode() === "overview" && !tui.isCompact()) {
@@ -958,17 +1001,17 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
958
1001
  }));
959
1002
  const narration = narrateObservation(sessionInfos, changedTitles);
960
1003
  tui.log("observation", narration + (userMessage ? " +your message" : ""));
961
- // event highlights — call attention to important events
1004
+ // event highlights — call attention to important events (with sessionId for mute filtering)
962
1005
  for (const snap of observation.sessions) {
963
1006
  const s = snap.session;
964
1007
  if (s.status === "error" && changedTitles.has(s.title)) {
965
- tui.log("! action", `${s.title} hit an error! The AI will investigate.`);
1008
+ tui.log("! action", `${s.title} hit an error! The AI will investigate.`, s.id);
966
1009
  }
967
1010
  if (s.status === "done" && changedTitles.has(s.title)) {
968
- tui.log("+ action", `${s.title} finished its task!`);
1011
+ tui.log("+ action", `${s.title} finished its task!`, s.id);
969
1012
  }
970
1013
  if (snap.userActive) {
971
- tui.log("status", `You're working in ${s.title} — the AI won't interfere.`);
1014
+ tui.log("status", `You're working in ${s.title} — the AI won't interfere.`, s.id);
972
1015
  }
973
1016
  }
974
1017
  }
@@ -1048,7 +1091,7 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
1048
1091
  ? `${plainEnglish} — ${friendlyError(entry.detail)}`
1049
1092
  : plainEnglish;
1050
1093
  if (tui) {
1051
- tui.log(tag, displayText);
1094
+ tui.log(tag, displayText, sessionId);
1052
1095
  }
1053
1096
  else {
1054
1097
  const icon = entry.success ? "+" : "!";
package/dist/input.d.ts CHANGED
@@ -8,6 +8,10 @@ export type CompactHandler = () => void;
8
8
  export type PinHandler = (target: string) => void;
9
9
  export type BellHandler = () => void;
10
10
  export type FocusHandler = () => void;
11
+ export type MarkHandler = () => void;
12
+ export type JumpHandler = (num: number) => void;
13
+ export type MarksHandler = () => void;
14
+ export type MuteHandler = (target: string) => void;
11
15
  export interface MouseEvent {
12
16
  button: number;
13
17
  col: number;
@@ -38,6 +42,10 @@ export declare class InputReader {
38
42
  private pinHandler;
39
43
  private bellHandler;
40
44
  private focusHandler;
45
+ private markHandler;
46
+ private jumpHandler;
47
+ private marksHandler;
48
+ private muteHandler;
41
49
  private mouseDataListener;
42
50
  onScroll(handler: (dir: ScrollDirection) => void): void;
43
51
  onQueueChange(handler: (count: number) => void): void;
@@ -52,6 +60,10 @@ export declare class InputReader {
52
60
  onPin(handler: PinHandler): void;
53
61
  onBell(handler: BellHandler): void;
54
62
  onFocus(handler: FocusHandler): void;
63
+ onMark(handler: MarkHandler): void;
64
+ onJump(handler: JumpHandler): void;
65
+ onMarks(handler: MarksHandler): void;
66
+ onMute(handler: MuteHandler): void;
55
67
  private notifyQueueChange;
56
68
  start(): void;
57
69
  drain(): string[];
package/dist/input.js CHANGED
@@ -40,6 +40,10 @@ export class InputReader {
40
40
  pinHandler = null;
41
41
  bellHandler = null;
42
42
  focusHandler = null;
43
+ markHandler = null;
44
+ jumpHandler = null;
45
+ marksHandler = null;
46
+ muteHandler = null;
43
47
  mouseDataListener = null;
44
48
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
45
49
  onScroll(handler) {
@@ -93,6 +97,22 @@ export class InputReader {
93
97
  onFocus(handler) {
94
98
  this.focusHandler = handler;
95
99
  }
100
+ // register a callback for adding bookmarks (/mark)
101
+ onMark(handler) {
102
+ this.markHandler = handler;
103
+ }
104
+ // register a callback for jumping to bookmarks (/jump N)
105
+ onJump(handler) {
106
+ this.jumpHandler = handler;
107
+ }
108
+ // register a callback for listing bookmarks (/marks)
109
+ onMarks(handler) {
110
+ this.marksHandler = handler;
111
+ }
112
+ // register a callback for mute/unmute commands (/mute <target>)
113
+ onMute(handler) {
114
+ this.muteHandler = handler;
115
+ }
96
116
  notifyQueueChange() {
97
117
  this.queueChangeHandler?.(this.queue.length);
98
118
  }
@@ -275,6 +295,10 @@ ${BOLD}navigation:${RESET}
275
295
  /pin [N|name] pin/unpin a session to the top (toggle)
276
296
  /bell toggle terminal bell on errors/completions
277
297
  /focus toggle focus mode (show only pinned sessions)
298
+ /mute [N|name] mute/unmute a session's activity entries (toggle)
299
+ /mark bookmark current activity position
300
+ /jump N jump to bookmark N
301
+ /marks list all bookmarks
278
302
  /search <pattern> filter activity entries by substring (case-insensitive)
279
303
  /search clear active search filter
280
304
  click session click an agent card to drill down (click again to go back)
@@ -403,6 +427,51 @@ ${BOLD}other:${RESET}
403
427
  console.error(`${DIM}focus not available (no TUI)${RESET}`);
404
428
  }
405
429
  break;
430
+ case "/mute": {
431
+ const muteArg = line.slice("/mute".length).trim();
432
+ if (this.muteHandler) {
433
+ if (muteArg) {
434
+ this.muteHandler(muteArg);
435
+ }
436
+ else {
437
+ console.error(`${DIM}usage: /mute <N|name> — toggle mute for a session${RESET}`);
438
+ }
439
+ }
440
+ else {
441
+ console.error(`${DIM}mute not available (no TUI)${RESET}`);
442
+ }
443
+ break;
444
+ }
445
+ case "/mark":
446
+ if (this.markHandler) {
447
+ this.markHandler();
448
+ }
449
+ else {
450
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
451
+ }
452
+ break;
453
+ case "/jump": {
454
+ const jumpArg = line.slice("/jump".length).trim();
455
+ const jumpNum = parseInt(jumpArg, 10);
456
+ if (this.jumpHandler && !isNaN(jumpNum) && jumpNum > 0) {
457
+ this.jumpHandler(jumpNum);
458
+ }
459
+ else if (!this.jumpHandler) {
460
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
461
+ }
462
+ else {
463
+ console.error(`${DIM}usage: /jump N — jump to bookmark number N${RESET}`);
464
+ }
465
+ break;
466
+ }
467
+ case "/marks":
468
+ if (this.marksHandler) {
469
+ this.marksHandler();
470
+ }
471
+ else {
472
+ console.error(`${DIM}bookmarks not available (no TUI)${RESET}`);
473
+ }
474
+ break;
406
475
  case "/search": {
407
476
  const searchArg = line.slice("/search".length).trim();
408
477
  if (this.searchHandler) {
package/dist/tui.d.ts CHANGED
@@ -10,16 +10,28 @@ 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. */
16
28
  declare const PIN_ICON = "\u25B2";
17
29
  /**
18
30
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
19
- * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2Bravo" for unpinned.
31
+ * Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
20
32
  * Returns array of formatted row strings (one per display row).
21
33
  */
22
- declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>): string[];
34
+ declare function formatCompactRows(sessions: DaemonSessionState[], maxWidth: number, pinnedIds?: Set<string>, mutedIds?: Set<string>): string[];
23
35
  /** Compute how many display rows compact mode needs (minimum 1). */
24
36
  declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
25
37
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
@@ -27,7 +39,12 @@ export interface ActivityEntry {
27
39
  time: string;
28
40
  tag: string;
29
41
  text: string;
42
+ sessionId?: string;
30
43
  }
44
+ /** Mute indicator for muted sessions (shown dim beside session card). */
45
+ declare const MUTE_ICON = "\u25CC";
46
+ /** Determine if an activity entry should be hidden due to muting. */
47
+ export declare function shouldMuteEntry(entry: ActivityEntry, mutedIds: Set<string>): boolean;
31
48
  export declare class TUI {
32
49
  private active;
33
50
  private countdownTimer;
@@ -54,8 +71,10 @@ export declare class TUI {
54
71
  private compactMode;
55
72
  private pinnedIds;
56
73
  private focusMode;
74
+ private bookmarks;
57
75
  private bellEnabled;
58
76
  private lastBellAt;
77
+ private mutedIds;
59
78
  private viewMode;
60
79
  private drilldownSessionId;
61
80
  private sessionOutputs;
@@ -100,6 +119,30 @@ export declare class TUI {
100
119
  setBell(enabled: boolean): void;
101
120
  /** Return whether terminal bell is enabled. */
102
121
  isBellEnabled(): boolean;
122
+ /**
123
+ * Toggle mute for a session (by 1-indexed number, ID, ID prefix, or title).
124
+ * Muted sessions' activity entries are hidden from the log (still buffered + persisted).
125
+ * Returns true if session found.
126
+ */
127
+ toggleMute(sessionIdOrIndex: string | number): boolean;
128
+ /** Check if a session ID is muted. */
129
+ isMuted(id: string): boolean;
130
+ /** Return count of muted sessions. */
131
+ getMutedCount(): number;
132
+ /**
133
+ * Add a bookmark at the current activity position.
134
+ * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
135
+ */
136
+ addBookmark(): number;
137
+ /**
138
+ * Jump to a bookmark by number (1-indexed). Returns false if not found.
139
+ * Adjusts scroll offset to center the bookmarked entry.
140
+ */
141
+ jumpToBookmark(num: number): boolean;
142
+ /** Return all bookmarks (for /marks listing). */
143
+ getBookmarks(): readonly Bookmark[];
144
+ /** Return bookmark count. */
145
+ getBookmarkCount(): number;
103
146
  updateState(opts: {
104
147
  phase?: DaemonPhase;
105
148
  pollCount?: number;
@@ -109,7 +152,7 @@ export declare class TUI {
109
152
  nextTickAt?: number;
110
153
  pendingCount?: number;
111
154
  }): void;
112
- log(tag: string, text: string): void;
155
+ log(tag: string, text: string, sessionId?: string): void;
113
156
  replayHistory(entries: HistoryEntry[]): void;
114
157
  scrollUp(lines?: number): void;
115
158
  scrollDown(lines?: number): void;
@@ -190,5 +233,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
190
233
  * (row = headerHeight + 2 + i for 0-indexed session i)
191
234
  */
192
235
  export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
193
- 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 };
236
+ 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, MUTE_ICON };
194
237
  //# sourceMappingURL=tui.d.ts.map
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;
@@ -70,10 +87,10 @@ const COMPACT_NAME_LEN = 10;
70
87
  const PIN_ICON = "▲";
71
88
  /**
72
89
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
73
- * Each token: "{idx}{pin?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2Bravo" for unpinned.
90
+ * Each token: "{idx}{pin?}{mute?}{dot}{name}" — e.g. "1▲●Alpha" for pinned, "2◌●Bravo" for muted.
74
91
  * Returns array of formatted row strings (one per display row).
75
92
  */
76
- function formatCompactRows(sessions, maxWidth, pinnedIds) {
93
+ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds) {
77
94
  if (sessions.length === 0)
78
95
  return [`${DIM}no agents connected${RESET}`];
79
96
  const tokens = [];
@@ -83,10 +100,12 @@ function formatCompactRows(sessions, maxWidth, pinnedIds) {
83
100
  const idx = String(i + 1);
84
101
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
85
102
  const pinned = pinnedIds?.has(s.id) ?? false;
103
+ const muted = mutedIds?.has(s.id) ?? false;
86
104
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET}` : "";
105
+ const muteIcon = muted ? `${DIM}${MUTE_ICON}${RESET}` : "";
87
106
  const name = truncatePlain(s.title, COMPACT_NAME_LEN);
88
- tokens.push(`${SLATE}${idx}${RESET}${pin}${dot}${BOLD}${name}${RESET}`);
89
- widths.push(idx.length + (pinned ? 1 : 0) + 1 + name.length);
107
+ tokens.push(`${SLATE}${idx}${RESET}${pin}${muteIcon}${dot}${BOLD}${name}${RESET}`);
108
+ widths.push(idx.length + (pinned ? 1 : 0) + (muted ? 1 : 0) + 1 + name.length);
90
109
  }
91
110
  const rows = [];
92
111
  let currentRow = "";
@@ -135,6 +154,15 @@ function phaseDisplay(phase, paused, spinnerFrame) {
135
154
  default: return `${SLATE}${phase}${RESET}`;
136
155
  }
137
156
  }
157
+ // ── Mute helpers ──────────────────────────────────────────────────────────────
158
+ /** Mute indicator for muted sessions (shown dim beside session card). */
159
+ const MUTE_ICON = "◌";
160
+ /** Determine if an activity entry should be hidden due to muting. */
161
+ export function shouldMuteEntry(entry, mutedIds) {
162
+ if (!entry.sessionId)
163
+ return false;
164
+ return mutedIds.has(entry.sessionId);
165
+ }
138
166
  // ── TUI class ───────────────────────────────────────────────────────────────
139
167
  export class TUI {
140
168
  active = false;
@@ -162,8 +190,10 @@ export class TUI {
162
190
  compactMode = false;
163
191
  pinnedIds = new Set(); // pinned session IDs (always sort to top)
164
192
  focusMode = false; // focus mode: hide all sessions except pinned
193
+ bookmarks = []; // saved positions in activity buffer
165
194
  bellEnabled = false;
166
195
  lastBellAt = 0;
196
+ mutedIds = new Set(); // muted session IDs (activity entries hidden)
167
197
  // drill-down mode: show a single session's full output
168
198
  viewMode = "overview";
169
199
  drilldownSessionId = null;
@@ -319,6 +349,93 @@ export class TUI {
319
349
  isBellEnabled() {
320
350
  return this.bellEnabled;
321
351
  }
352
+ /**
353
+ * Toggle mute for a session (by 1-indexed number, ID, ID prefix, or title).
354
+ * Muted sessions' activity entries are hidden from the log (still buffered + persisted).
355
+ * Returns true if session found.
356
+ */
357
+ toggleMute(sessionIdOrIndex) {
358
+ let sessionId;
359
+ if (typeof sessionIdOrIndex === "number") {
360
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
361
+ }
362
+ else {
363
+ const needle = sessionIdOrIndex.toLowerCase();
364
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
365
+ sessionId = match?.id;
366
+ }
367
+ if (!sessionId)
368
+ return false;
369
+ if (this.mutedIds.has(sessionId)) {
370
+ this.mutedIds.delete(sessionId);
371
+ }
372
+ else {
373
+ this.mutedIds.add(sessionId);
374
+ }
375
+ // repaint sessions (mute icon) and activity (filter changes)
376
+ if (this.active) {
377
+ this.paintSessions();
378
+ this.repaintActivityRegion();
379
+ }
380
+ return true;
381
+ }
382
+ /** Check if a session ID is muted. */
383
+ isMuted(id) {
384
+ return this.mutedIds.has(id);
385
+ }
386
+ /** Return count of muted sessions. */
387
+ getMutedCount() {
388
+ return this.mutedIds.size;
389
+ }
390
+ /**
391
+ * Add a bookmark at the current activity position.
392
+ * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
393
+ */
394
+ addBookmark() {
395
+ if (this.activityBuffer.length === 0)
396
+ return 0;
397
+ // bookmark the entry at the current view position
398
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
399
+ const { start } = computeScrollSlice(this.activityBuffer.length, visibleLines, this.scrollOffset);
400
+ const entry = this.activityBuffer[start];
401
+ if (!entry)
402
+ return 0;
403
+ const bm = { index: start, label: `${entry.time} ${entry.tag}` };
404
+ this.bookmarks.push(bm);
405
+ if (this.bookmarks.length > MAX_BOOKMARKS) {
406
+ this.bookmarks = this.bookmarks.slice(-MAX_BOOKMARKS);
407
+ }
408
+ return this.bookmarks.length;
409
+ }
410
+ /**
411
+ * Jump to a bookmark by number (1-indexed). Returns false if not found.
412
+ * Adjusts scroll offset to center the bookmarked entry.
413
+ */
414
+ jumpToBookmark(num) {
415
+ const bm = this.bookmarks[num - 1];
416
+ if (!bm)
417
+ return false;
418
+ // clamp index to current buffer
419
+ if (bm.index >= this.activityBuffer.length)
420
+ return false;
421
+ const visibleLines = this.scrollBottom - this.scrollTop + 1;
422
+ this.scrollOffset = computeBookmarkOffset(bm.index, this.activityBuffer.length, visibleLines);
423
+ if (this.scrollOffset === 0)
424
+ this.newWhileScrolled = 0;
425
+ if (this.active && this.viewMode === "overview") {
426
+ this.repaintActivityRegion();
427
+ this.paintSeparator();
428
+ }
429
+ return true;
430
+ }
431
+ /** Return all bookmarks (for /marks listing). */
432
+ getBookmarks() {
433
+ return this.bookmarks;
434
+ }
435
+ /** Return bookmark count. */
436
+ getBookmarkCount() {
437
+ return this.bookmarks.length;
438
+ }
322
439
  // ── State updates ───────────────────────────────────────────────────────
323
440
  updateState(opts) {
324
441
  if (opts.phase !== undefined)
@@ -363,10 +480,11 @@ export class TUI {
363
480
  }
364
481
  // ── Activity log ────────────────────────────────────────────────────────
365
482
  // push a new activity entry — this is the primary way to show output
366
- log(tag, text) {
483
+ // sessionId optionally ties the entry to a specific session (for mute filtering)
484
+ log(tag, text, sessionId) {
367
485
  const now = new Date();
368
486
  const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
369
- const entry = { time, tag, text };
487
+ const entry = { time, tag, text, ...(sessionId ? { sessionId } : {}) };
370
488
  this.activityBuffer.push(entry);
371
489
  this.activityTimestamps.push(now.getTime());
372
490
  if (this.activityBuffer.length > this.maxActivity) {
@@ -382,7 +500,11 @@ export class TUI {
382
500
  }
383
501
  }
384
502
  if (this.active) {
385
- if (this.searchPattern) {
503
+ // muted entries are still buffered + persisted but hidden from display
504
+ if (shouldMuteEntry(entry, this.mutedIds)) {
505
+ // silently skip display — entry is in buffer for scroll-back if unmuted later
506
+ }
507
+ else if (this.searchPattern) {
386
508
  // search active: only show new entry if it matches
387
509
  if (matchesSearch(entry, this.searchPattern)) {
388
510
  if (this.scrollOffset > 0) {
@@ -424,9 +546,12 @@ export class TUI {
424
546
  return;
425
547
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
426
548
  const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
549
+ let filtered = this.mutedIds.size > 0
550
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
551
+ : this.activityBuffer;
427
552
  const entryCount = this.searchPattern
428
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
429
- : this.activityBuffer.length;
553
+ ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
554
+ : filtered.length;
430
555
  const maxOffset = Math.max(0, entryCount - visibleLines);
431
556
  this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
432
557
  this.repaintActivityRegion();
@@ -448,9 +573,12 @@ export class TUI {
448
573
  if (!this.active)
449
574
  return;
450
575
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
576
+ let filtered = this.mutedIds.size > 0
577
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
578
+ : this.activityBuffer;
451
579
  const entryCount = this.searchPattern
452
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
453
- : this.activityBuffer.length;
580
+ ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
581
+ : filtered.length;
454
582
  this.scrollOffset = Math.max(0, entryCount - visibleLines);
455
583
  this.repaintActivityRegion();
456
584
  this.paintSeparator();
@@ -709,7 +837,7 @@ export class TUI {
709
837
  }
710
838
  else if (this.compactMode) {
711
839
  // compact: inline tokens, multiple per row (with pin indicators)
712
- const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds);
840
+ const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds, this.mutedIds);
713
841
  for (let r = 0; r < compactRows.length; r++) {
714
842
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
715
843
  const padded = padBoxLine(line, this.cols);
@@ -722,9 +850,12 @@ export class TUI {
722
850
  const isHovered = this.hoverSessionIdx === i + 1; // 1-indexed
723
851
  const bg = isHovered ? BG_HOVER : "";
724
852
  const pinned = this.pinnedIds.has(s.id);
853
+ const muted = this.mutedIds.has(s.id);
725
854
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
726
- const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1; // pin takes 2 extra chars
727
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
855
+ const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
856
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0); // each icon + space = 2 chars
857
+ const cardWidth = innerWidth - 1 - iconsWidth;
858
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${formatSessionCard(s, cardWidth)}`;
728
859
  const padded = padBoxLineHover(line, this.cols, isHovered);
729
860
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
730
861
  }
@@ -755,9 +886,12 @@ export class TUI {
755
886
  const isHovered = this.hoverSessionIdx === idx;
756
887
  const bg = isHovered ? BG_HOVER : "";
757
888
  const pinned = this.pinnedIds.has(s.id);
889
+ const muted = this.mutedIds.has(s.id);
758
890
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
759
- const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1;
760
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
891
+ const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
892
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0);
893
+ const cardWidth = innerWidth - 1 - iconsWidth;
894
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${formatSessionCard(s, cardWidth)}`;
761
895
  const padded = padBoxLineHover(line, this.cols, isHovered);
762
896
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
763
897
  }
@@ -794,10 +928,13 @@ export class TUI {
794
928
  }
795
929
  repaintActivityRegion() {
796
930
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
797
- // when search is active, filter entries first, then paginate
798
- const source = this.searchPattern
799
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern))
931
+ // filter: muted entries first, then search on top
932
+ let source = this.mutedIds.size > 0
933
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
800
934
  : this.activityBuffer;
935
+ if (this.searchPattern) {
936
+ source = source.filter((e) => matchesSearch(e, this.searchPattern));
937
+ }
801
938
  const { start, end } = computeScrollSlice(source.length, visibleLines, this.scrollOffset);
802
939
  const entries = source.slice(start, end);
803
940
  for (let i = 0; i < visibleLines; i++) {
@@ -1124,5 +1261,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
1124
1261
  return row - firstSessionRow + 1; // 1-indexed
1125
1262
  }
1126
1263
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
1127
- 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 };
1264
+ 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, MUTE_ICON };
1128
1265
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.82.0",
3
+ "version": "0.84.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",