aoaoe 0.83.0 → 0.85.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
@@ -433,6 +433,48 @@ async function main() {
433
433
  tui.log("system", `session not found: ${target}`);
434
434
  }
435
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
+ });
447
+ // wire /note set/clear
448
+ input.onNote((target, text) => {
449
+ const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
450
+ const ok = tui.setNote(num ?? target, text);
451
+ if (ok) {
452
+ if (text) {
453
+ tui.log("system", `note set for ${target}: "${text}"`);
454
+ }
455
+ else {
456
+ tui.log("system", `note cleared for ${target}`);
457
+ }
458
+ }
459
+ else {
460
+ tui.log("system", `session not found: ${target}`);
461
+ }
462
+ });
463
+ // wire /notes listing
464
+ input.onNotes(() => {
465
+ const notes = tui.getAllNotes();
466
+ if (notes.size === 0) {
467
+ tui.log("system", "no notes — use /note <N|name> <text> to add one");
468
+ }
469
+ else {
470
+ const sessions = tui.getSessions();
471
+ for (const [id, text] of notes) {
472
+ const session = sessions.find((s) => s.id === id);
473
+ const label = session ? session.title : id.slice(0, 8);
474
+ tui.log("system", ` ${label}: "${text}"`);
475
+ }
476
+ }
477
+ });
436
478
  // wire mouse move to hover highlight on session cards (disabled in compact)
437
479
  input.onMouseMove((row, _col) => {
438
480
  if (tui.getViewMode() === "overview" && !tui.isCompact()) {
@@ -990,17 +1032,17 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
990
1032
  }));
991
1033
  const narration = narrateObservation(sessionInfos, changedTitles);
992
1034
  tui.log("observation", narration + (userMessage ? " +your message" : ""));
993
- // event highlights — call attention to important events
1035
+ // event highlights — call attention to important events (with sessionId for mute filtering)
994
1036
  for (const snap of observation.sessions) {
995
1037
  const s = snap.session;
996
1038
  if (s.status === "error" && changedTitles.has(s.title)) {
997
- tui.log("! action", `${s.title} hit an error! The AI will investigate.`);
1039
+ tui.log("! action", `${s.title} hit an error! The AI will investigate.`, s.id);
998
1040
  }
999
1041
  if (s.status === "done" && changedTitles.has(s.title)) {
1000
- tui.log("+ action", `${s.title} finished its task!`);
1042
+ tui.log("+ action", `${s.title} finished its task!`, s.id);
1001
1043
  }
1002
1044
  if (snap.userActive) {
1003
- tui.log("status", `You're working in ${s.title} — the AI won't interfere.`);
1045
+ tui.log("status", `You're working in ${s.title} — the AI won't interfere.`, s.id);
1004
1046
  }
1005
1047
  }
1006
1048
  }
@@ -1080,7 +1122,7 @@ async function daemonTick(config, poller, reasoner, executor, reasonerConsole, p
1080
1122
  ? `${plainEnglish} — ${friendlyError(entry.detail)}`
1081
1123
  : plainEnglish;
1082
1124
  if (tui) {
1083
- tui.log(tag, displayText);
1125
+ tui.log(tag, displayText, sessionId);
1084
1126
  }
1085
1127
  else {
1086
1128
  const icon = entry.success ? "+" : "!";
package/dist/input.d.ts CHANGED
@@ -11,6 +11,9 @@ export type FocusHandler = () => void;
11
11
  export type MarkHandler = () => void;
12
12
  export type JumpHandler = (num: number) => void;
13
13
  export type MarksHandler = () => void;
14
+ export type MuteHandler = (target: string) => void;
15
+ export type NoteHandler = (target: string, text: string) => void;
16
+ export type NotesHandler = () => void;
14
17
  export interface MouseEvent {
15
18
  button: number;
16
19
  col: number;
@@ -44,6 +47,9 @@ export declare class InputReader {
44
47
  private markHandler;
45
48
  private jumpHandler;
46
49
  private marksHandler;
50
+ private muteHandler;
51
+ private noteHandler;
52
+ private notesHandler;
47
53
  private mouseDataListener;
48
54
  onScroll(handler: (dir: ScrollDirection) => void): void;
49
55
  onQueueChange(handler: (count: number) => void): void;
@@ -61,6 +67,9 @@ export declare class InputReader {
61
67
  onMark(handler: MarkHandler): void;
62
68
  onJump(handler: JumpHandler): void;
63
69
  onMarks(handler: MarksHandler): void;
70
+ onMute(handler: MuteHandler): void;
71
+ onNote(handler: NoteHandler): void;
72
+ onNotes(handler: NotesHandler): void;
64
73
  private notifyQueueChange;
65
74
  start(): void;
66
75
  drain(): string[];
package/dist/input.js CHANGED
@@ -43,6 +43,9 @@ export class InputReader {
43
43
  markHandler = null;
44
44
  jumpHandler = null;
45
45
  marksHandler = null;
46
+ muteHandler = null;
47
+ noteHandler = null;
48
+ notesHandler = null;
46
49
  mouseDataListener = null;
47
50
  // register a callback for scroll key events (PgUp/PgDn/Home/End)
48
51
  onScroll(handler) {
@@ -108,6 +111,18 @@ export class InputReader {
108
111
  onMarks(handler) {
109
112
  this.marksHandler = handler;
110
113
  }
114
+ // register a callback for mute/unmute commands (/mute <target>)
115
+ onMute(handler) {
116
+ this.muteHandler = handler;
117
+ }
118
+ // register a callback for note commands (/note <target> <text>)
119
+ onNote(handler) {
120
+ this.noteHandler = handler;
121
+ }
122
+ // register a callback for listing notes (/notes)
123
+ onNotes(handler) {
124
+ this.notesHandler = handler;
125
+ }
111
126
  notifyQueueChange() {
112
127
  this.queueChangeHandler?.(this.queue.length);
113
128
  }
@@ -290,6 +305,9 @@ ${BOLD}navigation:${RESET}
290
305
  /pin [N|name] pin/unpin a session to the top (toggle)
291
306
  /bell toggle terminal bell on errors/completions
292
307
  /focus toggle focus mode (show only pinned sessions)
308
+ /mute [N|name] mute/unmute a session's activity entries (toggle)
309
+ /note N|name text attach a note to a session (no text = clear)
310
+ /notes list all session notes
293
311
  /mark bookmark current activity position
294
312
  /jump N jump to bookmark N
295
313
  /marks list all bookmarks
@@ -421,6 +439,52 @@ ${BOLD}other:${RESET}
421
439
  console.error(`${DIM}focus not available (no TUI)${RESET}`);
422
440
  }
423
441
  break;
442
+ case "/mute": {
443
+ const muteArg = line.slice("/mute".length).trim();
444
+ if (this.muteHandler) {
445
+ if (muteArg) {
446
+ this.muteHandler(muteArg);
447
+ }
448
+ else {
449
+ console.error(`${DIM}usage: /mute <N|name> — toggle mute for a session${RESET}`);
450
+ }
451
+ }
452
+ else {
453
+ console.error(`${DIM}mute not available (no TUI)${RESET}`);
454
+ }
455
+ break;
456
+ }
457
+ case "/note": {
458
+ const noteArg = line.slice("/note".length).trim();
459
+ if (this.noteHandler) {
460
+ // split: first word is target, rest is note text
461
+ const spaceIdx = noteArg.indexOf(" ");
462
+ if (spaceIdx > 0) {
463
+ const target = noteArg.slice(0, spaceIdx);
464
+ const text = noteArg.slice(spaceIdx + 1).trim();
465
+ this.noteHandler(target, text);
466
+ }
467
+ else if (noteArg) {
468
+ // target only, no text — clear note
469
+ this.noteHandler(noteArg, "");
470
+ }
471
+ else {
472
+ console.error(`${DIM}usage: /note <N|name> <text> — set note, or /note <N|name> — clear${RESET}`);
473
+ }
474
+ }
475
+ else {
476
+ console.error(`${DIM}notes not available (no TUI)${RESET}`);
477
+ }
478
+ break;
479
+ }
480
+ case "/notes":
481
+ if (this.notesHandler) {
482
+ this.notesHandler();
483
+ }
484
+ else {
485
+ console.error(`${DIM}notes not available (no TUI)${RESET}`);
486
+ }
487
+ break;
424
488
  case "/mark":
425
489
  if (this.markHandler) {
426
490
  this.markHandler();
package/dist/tui.d.ts CHANGED
@@ -28,10 +28,10 @@ declare const COMPACT_NAME_LEN = 10;
28
28
  declare const PIN_ICON = "\u25B2";
29
29
  /**
30
30
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
31
- * 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.
32
32
  * Returns array of formatted row strings (one per display row).
33
33
  */
34
- 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>, noteIds?: Set<string>): string[];
35
35
  /** Compute how many display rows compact mode needs (minimum 1). */
36
36
  declare function computeCompactRowCount(sessions: DaemonSessionState[], maxWidth: number): number;
37
37
  declare function phaseDisplay(phase: DaemonPhase, paused: boolean, spinnerFrame: number): string;
@@ -39,7 +39,18 @@ export interface ActivityEntry {
39
39
  time: string;
40
40
  tag: string;
41
41
  text: string;
42
+ sessionId?: string;
42
43
  }
44
+ /** Mute indicator for muted sessions (shown dim beside session card). */
45
+ declare const MUTE_ICON = "\u25CC";
46
+ /** Note indicator for sessions with notes. */
47
+ declare const NOTE_ICON = "\u270E";
48
+ /** Max length for a session note (visible chars). */
49
+ export declare const MAX_NOTE_LEN = 80;
50
+ /** Truncate a note to the max length. */
51
+ export declare function truncateNote(text: string): string;
52
+ /** Determine if an activity entry should be hidden due to muting. */
53
+ export declare function shouldMuteEntry(entry: ActivityEntry, mutedIds: Set<string>): boolean;
43
54
  export declare class TUI {
44
55
  private active;
45
56
  private countdownTimer;
@@ -69,6 +80,8 @@ export declare class TUI {
69
80
  private bookmarks;
70
81
  private bellEnabled;
71
82
  private lastBellAt;
83
+ private mutedIds;
84
+ private sessionNotes;
72
85
  private viewMode;
73
86
  private drilldownSessionId;
74
87
  private sessionOutputs;
@@ -113,6 +126,29 @@ export declare class TUI {
113
126
  setBell(enabled: boolean): void;
114
127
  /** Return whether terminal bell is enabled. */
115
128
  isBellEnabled(): boolean;
129
+ /**
130
+ * Toggle mute for a session (by 1-indexed number, ID, ID prefix, or title).
131
+ * Muted sessions' activity entries are hidden from the log (still buffered + persisted).
132
+ * Returns true if session found.
133
+ */
134
+ toggleMute(sessionIdOrIndex: string | number): boolean;
135
+ /** Check if a session ID is muted. */
136
+ isMuted(id: string): boolean;
137
+ /** Return count of muted sessions. */
138
+ getMutedCount(): number;
139
+ /**
140
+ * Set a note on a session (by 1-indexed number, ID, ID prefix, or title).
141
+ * Returns true if session found. Pass empty text to clear.
142
+ */
143
+ setNote(sessionIdOrIndex: string | number, text: string): boolean;
144
+ /** Get the note for a session ID (or undefined if none). */
145
+ getNote(id: string): string | undefined;
146
+ /** Return count of sessions with notes. */
147
+ getNoteCount(): number;
148
+ /** Return all session notes (for /notes listing). */
149
+ getAllNotes(): ReadonlyMap<string, string>;
150
+ /** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
151
+ getSessions(): readonly DaemonSessionState[];
116
152
  /**
117
153
  * Add a bookmark at the current activity position.
118
154
  * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
@@ -136,7 +172,7 @@ export declare class TUI {
136
172
  nextTickAt?: number;
137
173
  pendingCount?: number;
138
174
  }): void;
139
- log(tag: string, text: string): void;
175
+ log(tag: string, text: string, sessionId?: string): void;
140
176
  replayHistory(entries: HistoryEntry[]): void;
141
177
  scrollUp(lines?: number): void;
142
178
  scrollDown(lines?: number): void;
@@ -217,5 +253,5 @@ declare function formatSearchIndicator(pattern: string, matchCount: number, tota
217
253
  * (row = headerHeight + 2 + i for 0-indexed session i)
218
254
  */
219
255
  export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
220
- 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 };
256
+ 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, NOTE_ICON };
221
257
  //# sourceMappingURL=tui.d.ts.map
package/dist/tui.js CHANGED
@@ -87,10 +87,10 @@ const COMPACT_NAME_LEN = 10;
87
87
  const PIN_ICON = "▲";
88
88
  /**
89
89
  * Format sessions as inline compact tokens, wrapped to fit maxWidth.
90
- * 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.
91
91
  * Returns array of formatted row strings (one per display row).
92
92
  */
93
- function formatCompactRows(sessions, maxWidth, pinnedIds) {
93
+ function formatCompactRows(sessions, maxWidth, pinnedIds, mutedIds, noteIds) {
94
94
  if (sessions.length === 0)
95
95
  return [`${DIM}no agents connected${RESET}`];
96
96
  const tokens = [];
@@ -100,10 +100,14 @@ function formatCompactRows(sessions, maxWidth, pinnedIds) {
100
100
  const idx = String(i + 1);
101
101
  const dot = STATUS_DOT[s.status] ?? `${AMBER}${DOT.filled}${RESET}`;
102
102
  const pinned = pinnedIds?.has(s.id) ?? false;
103
+ const muted = mutedIds?.has(s.id) ?? false;
104
+ const noted = noteIds?.has(s.id) ?? false;
103
105
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET}` : "";
106
+ const muteIcon = muted ? `${DIM}${MUTE_ICON}${RESET}` : "";
107
+ const noteIcon = noted ? `${TEAL}${NOTE_ICON}${RESET}` : "";
104
108
  const name = truncatePlain(s.title, COMPACT_NAME_LEN);
105
- tokens.push(`${SLATE}${idx}${RESET}${pin}${dot}${BOLD}${name}${RESET}`);
106
- widths.push(idx.length + (pinned ? 1 : 0) + 1 + name.length);
109
+ tokens.push(`${SLATE}${idx}${RESET}${pin}${muteIcon}${noteIcon}${dot}${BOLD}${name}${RESET}`);
110
+ widths.push(idx.length + (pinned ? 1 : 0) + (muted ? 1 : 0) + (noted ? 1 : 0) + 1 + name.length);
107
111
  }
108
112
  const rows = [];
109
113
  let currentRow = "";
@@ -152,6 +156,24 @@ function phaseDisplay(phase, paused, spinnerFrame) {
152
156
  default: return `${SLATE}${phase}${RESET}`;
153
157
  }
154
158
  }
159
+ // ── Mute helpers ──────────────────────────────────────────────────────────────
160
+ /** Mute indicator for muted sessions (shown dim beside session card). */
161
+ const MUTE_ICON = "◌";
162
+ // ── Notes ─────────────────────────────────────────────────────────────────────
163
+ /** Note indicator for sessions with notes. */
164
+ const NOTE_ICON = "✎";
165
+ /** Max length for a session note (visible chars). */
166
+ export const MAX_NOTE_LEN = 80;
167
+ /** Truncate a note to the max length. */
168
+ export function truncateNote(text) {
169
+ return text.length > MAX_NOTE_LEN ? text.slice(0, MAX_NOTE_LEN - 2) + ".." : text;
170
+ }
171
+ /** Determine if an activity entry should be hidden due to muting. */
172
+ export function shouldMuteEntry(entry, mutedIds) {
173
+ if (!entry.sessionId)
174
+ return false;
175
+ return mutedIds.has(entry.sessionId);
176
+ }
155
177
  // ── TUI class ───────────────────────────────────────────────────────────────
156
178
  export class TUI {
157
179
  active = false;
@@ -182,6 +204,8 @@ export class TUI {
182
204
  bookmarks = []; // saved positions in activity buffer
183
205
  bellEnabled = false;
184
206
  lastBellAt = 0;
207
+ mutedIds = new Set(); // muted session IDs (activity entries hidden)
208
+ sessionNotes = new Map(); // session ID → note text
185
209
  // drill-down mode: show a single session's full output
186
210
  viewMode = "overview";
187
211
  drilldownSessionId = null;
@@ -337,6 +361,90 @@ export class TUI {
337
361
  isBellEnabled() {
338
362
  return this.bellEnabled;
339
363
  }
364
+ /**
365
+ * Toggle mute for a session (by 1-indexed number, ID, ID prefix, or title).
366
+ * Muted sessions' activity entries are hidden from the log (still buffered + persisted).
367
+ * Returns true if session found.
368
+ */
369
+ toggleMute(sessionIdOrIndex) {
370
+ let sessionId;
371
+ if (typeof sessionIdOrIndex === "number") {
372
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
373
+ }
374
+ else {
375
+ const needle = sessionIdOrIndex.toLowerCase();
376
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
377
+ sessionId = match?.id;
378
+ }
379
+ if (!sessionId)
380
+ return false;
381
+ if (this.mutedIds.has(sessionId)) {
382
+ this.mutedIds.delete(sessionId);
383
+ }
384
+ else {
385
+ this.mutedIds.add(sessionId);
386
+ }
387
+ // repaint sessions (mute icon) and activity (filter changes)
388
+ if (this.active) {
389
+ this.paintSessions();
390
+ this.repaintActivityRegion();
391
+ }
392
+ return true;
393
+ }
394
+ /** Check if a session ID is muted. */
395
+ isMuted(id) {
396
+ return this.mutedIds.has(id);
397
+ }
398
+ /** Return count of muted sessions. */
399
+ getMutedCount() {
400
+ return this.mutedIds.size;
401
+ }
402
+ /**
403
+ * Set a note on a session (by 1-indexed number, ID, ID prefix, or title).
404
+ * Returns true if session found. Pass empty text to clear.
405
+ */
406
+ setNote(sessionIdOrIndex, text) {
407
+ let sessionId;
408
+ if (typeof sessionIdOrIndex === "number") {
409
+ sessionId = this.sessions[sessionIdOrIndex - 1]?.id;
410
+ }
411
+ else {
412
+ const needle = sessionIdOrIndex.toLowerCase();
413
+ const match = this.sessions.find((s) => s.id === sessionIdOrIndex || s.id.startsWith(needle) || s.title.toLowerCase() === needle);
414
+ sessionId = match?.id;
415
+ }
416
+ if (!sessionId)
417
+ return false;
418
+ if (text.trim() === "") {
419
+ this.sessionNotes.delete(sessionId);
420
+ }
421
+ else {
422
+ this.sessionNotes.set(sessionId, truncateNote(text.trim()));
423
+ }
424
+ if (this.active) {
425
+ this.paintSessions();
426
+ if (this.viewMode === "drilldown" && this.drilldownSessionId === sessionId) {
427
+ this.paintDrilldownSeparator();
428
+ }
429
+ }
430
+ return true;
431
+ }
432
+ /** Get the note for a session ID (or undefined if none). */
433
+ getNote(id) {
434
+ return this.sessionNotes.get(id);
435
+ }
436
+ /** Return count of sessions with notes. */
437
+ getNoteCount() {
438
+ return this.sessionNotes.size;
439
+ }
440
+ /** Return all session notes (for /notes listing). */
441
+ getAllNotes() {
442
+ return this.sessionNotes;
443
+ }
444
+ /** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
445
+ getSessions() {
446
+ return this.sessions;
447
+ }
340
448
  /**
341
449
  * Add a bookmark at the current activity position.
342
450
  * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
@@ -430,10 +538,11 @@ export class TUI {
430
538
  }
431
539
  // ── Activity log ────────────────────────────────────────────────────────
432
540
  // push a new activity entry — this is the primary way to show output
433
- log(tag, text) {
541
+ // sessionId optionally ties the entry to a specific session (for mute filtering)
542
+ log(tag, text, sessionId) {
434
543
  const now = new Date();
435
544
  const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
436
- const entry = { time, tag, text };
545
+ const entry = { time, tag, text, ...(sessionId ? { sessionId } : {}) };
437
546
  this.activityBuffer.push(entry);
438
547
  this.activityTimestamps.push(now.getTime());
439
548
  if (this.activityBuffer.length > this.maxActivity) {
@@ -449,7 +558,11 @@ export class TUI {
449
558
  }
450
559
  }
451
560
  if (this.active) {
452
- if (this.searchPattern) {
561
+ // muted entries are still buffered + persisted but hidden from display
562
+ if (shouldMuteEntry(entry, this.mutedIds)) {
563
+ // silently skip display — entry is in buffer for scroll-back if unmuted later
564
+ }
565
+ else if (this.searchPattern) {
453
566
  // search active: only show new entry if it matches
454
567
  if (matchesSearch(entry, this.searchPattern)) {
455
568
  if (this.scrollOffset > 0) {
@@ -491,9 +604,12 @@ export class TUI {
491
604
  return;
492
605
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
493
606
  const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
607
+ let filtered = this.mutedIds.size > 0
608
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
609
+ : this.activityBuffer;
494
610
  const entryCount = this.searchPattern
495
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
496
- : this.activityBuffer.length;
611
+ ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
612
+ : filtered.length;
497
613
  const maxOffset = Math.max(0, entryCount - visibleLines);
498
614
  this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
499
615
  this.repaintActivityRegion();
@@ -515,9 +631,12 @@ export class TUI {
515
631
  if (!this.active)
516
632
  return;
517
633
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
634
+ let filtered = this.mutedIds.size > 0
635
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
636
+ : this.activityBuffer;
518
637
  const entryCount = this.searchPattern
519
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
520
- : this.activityBuffer.length;
638
+ ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
639
+ : filtered.length;
521
640
  this.scrollOffset = Math.max(0, entryCount - visibleLines);
522
641
  this.repaintActivityRegion();
523
642
  this.paintSeparator();
@@ -776,7 +895,8 @@ export class TUI {
776
895
  }
777
896
  else if (this.compactMode) {
778
897
  // compact: inline tokens, multiple per row (with pin indicators)
779
- const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds);
898
+ const noteIdSet = new Set(this.sessionNotes.keys());
899
+ const compactRows = formatCompactRows(visibleSessions, innerWidth - 1, this.pinnedIds, this.mutedIds, noteIdSet);
780
900
  for (let r = 0; r < compactRows.length; r++) {
781
901
  const line = `${SLATE}${BOX.v}${RESET} ${compactRows[r]}`;
782
902
  const padded = padBoxLine(line, this.cols);
@@ -789,9 +909,14 @@ export class TUI {
789
909
  const isHovered = this.hoverSessionIdx === i + 1; // 1-indexed
790
910
  const bg = isHovered ? BG_HOVER : "";
791
911
  const pinned = this.pinnedIds.has(s.id);
912
+ const muted = this.mutedIds.has(s.id);
913
+ const noted = this.sessionNotes.has(s.id);
792
914
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
793
- const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1; // pin takes 2 extra chars
794
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
915
+ const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
916
+ const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
917
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0);
918
+ const cardWidth = innerWidth - 1 - iconsWidth;
919
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${note}${formatSessionCard(s, cardWidth)}`;
795
920
  const padded = padBoxLineHover(line, this.cols, isHovered);
796
921
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
797
922
  }
@@ -822,9 +947,14 @@ export class TUI {
822
947
  const isHovered = this.hoverSessionIdx === idx;
823
948
  const bg = isHovered ? BG_HOVER : "";
824
949
  const pinned = this.pinnedIds.has(s.id);
950
+ const muted = this.mutedIds.has(s.id);
951
+ const noted = this.sessionNotes.has(s.id);
825
952
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
826
- const cardWidth = pinned ? innerWidth - 3 : innerWidth - 1;
827
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${formatSessionCard(s, cardWidth)}`;
953
+ const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
954
+ const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
955
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0);
956
+ const cardWidth = innerWidth - 1 - iconsWidth;
957
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${note}${formatSessionCard(s, cardWidth)}`;
828
958
  const padded = padBoxLineHover(line, this.cols, isHovered);
829
959
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
830
960
  }
@@ -861,10 +991,13 @@ export class TUI {
861
991
  }
862
992
  repaintActivityRegion() {
863
993
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
864
- // when search is active, filter entries first, then paginate
865
- const source = this.searchPattern
866
- ? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern))
994
+ // filter: muted entries first, then search on top
995
+ let source = this.mutedIds.size > 0
996
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
867
997
  : this.activityBuffer;
998
+ if (this.searchPattern) {
999
+ source = source.filter((e) => matchesSearch(e, this.searchPattern));
1000
+ }
868
1001
  const { start, end } = computeScrollSlice(source.length, visibleLines, this.scrollOffset);
869
1002
  const entries = source.slice(start, end);
870
1003
  for (let i = 0; i < visibleLines; i++) {
@@ -882,7 +1015,9 @@ export class TUI {
882
1015
  paintDrilldownSeparator() {
883
1016
  const session = this.sessions.find((s) => s.id === this.drilldownSessionId);
884
1017
  const title = session ? session.title : this.drilldownSessionId ?? "?";
885
- const prefix = `${BOX.h}${BOX.h} ${title} `;
1018
+ const noteText = this.drilldownSessionId ? this.sessionNotes.get(this.drilldownSessionId) : undefined;
1019
+ const noteSuffix = noteText ? `"${noteText}" ` : "";
1020
+ const prefix = `${BOX.h}${BOX.h} ${title} ${noteSuffix}`;
886
1021
  let hints;
887
1022
  if (this.drilldownScrollOffset > 0) {
888
1023
  const outputLines = this.sessionOutputs.get(this.drilldownSessionId ?? "") ?? [];
@@ -1191,5 +1326,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
1191
1326
  return row - firstSessionRow + 1; // 1-indexed
1192
1327
  }
1193
1328
  // ── Exported pure helpers (for testing) ─────────────────────────────────────
1194
- 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 };
1329
+ 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, NOTE_ICON };
1195
1330
  //# sourceMappingURL=tui.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.83.0",
3
+ "version": "0.85.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",