aoaoe 0.85.0 → 0.87.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
@@ -444,6 +444,26 @@ async function main() {
444
444
  tui.log("system", `session not found: ${target}`);
445
445
  }
446
446
  });
447
+ // wire /unmute-all
448
+ input.onUnmuteAll(() => {
449
+ const count = tui.unmuteAll();
450
+ if (count > 0) {
451
+ tui.log("system", `unmuted ${count} session${count === 1 ? "" : "s"}`);
452
+ }
453
+ else {
454
+ tui.log("system", "no sessions are muted");
455
+ }
456
+ });
457
+ // wire /filter tag
458
+ input.onTagFilter((tag) => {
459
+ tui.setTagFilter(tag);
460
+ if (tag) {
461
+ tui.log("system", `filter: ${tag}`);
462
+ }
463
+ else {
464
+ tui.log("system", "filter cleared");
465
+ }
466
+ });
447
467
  // wire /note set/clear
448
468
  input.onNote((target, text) => {
449
469
  const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
package/dist/input.d.ts CHANGED
@@ -12,6 +12,8 @@ export type MarkHandler = () => void;
12
12
  export type JumpHandler = (num: number) => void;
13
13
  export type MarksHandler = () => void;
14
14
  export type MuteHandler = (target: string) => void;
15
+ export type UnmuteAllHandler = () => void;
16
+ export type TagFilterHandler = (tag: string | null) => void;
15
17
  export type NoteHandler = (target: string, text: string) => void;
16
18
  export type NotesHandler = () => void;
17
19
  export interface MouseEvent {
@@ -48,6 +50,8 @@ export declare class InputReader {
48
50
  private jumpHandler;
49
51
  private marksHandler;
50
52
  private muteHandler;
53
+ private unmuteAllHandler;
54
+ private tagFilterHandler;
51
55
  private noteHandler;
52
56
  private notesHandler;
53
57
  private mouseDataListener;
@@ -68,6 +72,8 @@ export declare class InputReader {
68
72
  onJump(handler: JumpHandler): void;
69
73
  onMarks(handler: MarksHandler): void;
70
74
  onMute(handler: MuteHandler): void;
75
+ onUnmuteAll(handler: UnmuteAllHandler): void;
76
+ onTagFilter(handler: TagFilterHandler): void;
71
77
  onNote(handler: NoteHandler): void;
72
78
  onNotes(handler: NotesHandler): void;
73
79
  private notifyQueueChange;
package/dist/input.js CHANGED
@@ -44,6 +44,8 @@ export class InputReader {
44
44
  jumpHandler = null;
45
45
  marksHandler = null;
46
46
  muteHandler = null;
47
+ unmuteAllHandler = null;
48
+ tagFilterHandler = null;
47
49
  noteHandler = null;
48
50
  notesHandler = null;
49
51
  mouseDataListener = null;
@@ -115,6 +117,14 @@ export class InputReader {
115
117
  onMute(handler) {
116
118
  this.muteHandler = handler;
117
119
  }
120
+ // register a callback for unmuting all sessions (/unmute-all)
121
+ onUnmuteAll(handler) {
122
+ this.unmuteAllHandler = handler;
123
+ }
124
+ // register a callback for tag filter commands (/filter <tag>)
125
+ onTagFilter(handler) {
126
+ this.tagFilterHandler = handler;
127
+ }
118
128
  // register a callback for note commands (/note <target> <text>)
119
129
  onNote(handler) {
120
130
  this.noteHandler = handler;
@@ -306,6 +316,8 @@ ${BOLD}navigation:${RESET}
306
316
  /bell toggle terminal bell on errors/completions
307
317
  /focus toggle focus mode (show only pinned sessions)
308
318
  /mute [N|name] mute/unmute a session's activity entries (toggle)
319
+ /unmute-all unmute all sessions at once
320
+ /filter [tag] filter activity by tag (error, system, etc. — no arg = clear)
309
321
  /note N|name text attach a note to a session (no text = clear)
310
322
  /notes list all session notes
311
323
  /mark bookmark current activity position
@@ -454,6 +466,24 @@ ${BOLD}other:${RESET}
454
466
  }
455
467
  break;
456
468
  }
469
+ case "/unmute-all":
470
+ if (this.unmuteAllHandler) {
471
+ this.unmuteAllHandler();
472
+ }
473
+ else {
474
+ console.error(`${DIM}unmute-all not available (no TUI)${RESET}`);
475
+ }
476
+ break;
477
+ case "/filter": {
478
+ const filterArg = line.slice("/filter".length).trim();
479
+ if (this.tagFilterHandler) {
480
+ this.tagFilterHandler(filterArg || null);
481
+ }
482
+ else {
483
+ console.error(`${DIM}filter not available (no TUI)${RESET}`);
484
+ }
485
+ break;
486
+ }
457
487
  case "/note": {
458
488
  const noteArg = line.slice("/note".length).trim();
459
489
  if (this.noteHandler) {
package/dist/tui.d.ts CHANGED
@@ -51,6 +51,12 @@ export declare const MAX_NOTE_LEN = 80;
51
51
  export declare function truncateNote(text: string): string;
52
52
  /** Determine if an activity entry should be hidden due to muting. */
53
53
  export declare function shouldMuteEntry(entry: ActivityEntry, mutedIds: Set<string>): boolean;
54
+ /** Format a suppressed entry count badge for muted sessions. Returns empty string for 0. */
55
+ export declare function formatMuteBadge(count: number): string;
56
+ /** Check if an activity entry matches a tag filter (case-insensitive exact match on tag). */
57
+ export declare function matchesTagFilter(entry: ActivityEntry, tag: string): boolean;
58
+ /** Format the tag filter indicator text for the separator bar. */
59
+ export declare function formatTagFilterIndicator(tag: string, matchCount: number, totalCount: number): string;
54
60
  export declare class TUI {
55
61
  private active;
56
62
  private countdownTimer;
@@ -69,6 +75,7 @@ export declare class TUI {
69
75
  private newWhileScrolled;
70
76
  private pendingCount;
71
77
  private searchPattern;
78
+ private filterTag;
72
79
  private hoverSessionIdx;
73
80
  private activityTimestamps;
74
81
  private sortMode;
@@ -81,6 +88,7 @@ export declare class TUI {
81
88
  private bellEnabled;
82
89
  private lastBellAt;
83
90
  private mutedIds;
91
+ private mutedEntryCounts;
84
92
  private sessionNotes;
85
93
  private viewMode;
86
94
  private drilldownSessionId;
@@ -132,10 +140,14 @@ export declare class TUI {
132
140
  * Returns true if session found.
133
141
  */
134
142
  toggleMute(sessionIdOrIndex: string | number): boolean;
143
+ /** Unmute all sessions at once. Returns count of sessions unmuted. */
144
+ unmuteAll(): number;
135
145
  /** Check if a session ID is muted. */
136
146
  isMuted(id: string): boolean;
137
147
  /** Return count of muted sessions. */
138
148
  getMutedCount(): number;
149
+ /** Return count of suppressed entries for a muted session (0 if not muted). */
150
+ getMutedEntryCount(id: string): number;
139
151
  /**
140
152
  * Set a note on a session (by 1-indexed number, ID, ID prefix, or title).
141
153
  * Returns true if session found. Pass empty text to clear.
@@ -197,6 +209,10 @@ export declare class TUI {
197
209
  setSearch(pattern: string | null): void;
198
210
  /** Get the current search pattern (or null if no active search). */
199
211
  getSearchPattern(): string | null;
212
+ /** Set or clear the tag filter. Resets scroll and repaints. */
213
+ setTagFilter(tag: string | null): void;
214
+ /** Get the current tag filter (or null if none active). */
215
+ getTagFilter(): string | null;
200
216
  /** Set the hovered session index (1-indexed) or null to clear. Only repaints the affected cards. */
201
217
  setHoverSession(idx: number | null): void;
202
218
  /** Get the current hovered session index (1-indexed, null if none). */
package/dist/tui.js CHANGED
@@ -174,6 +174,23 @@ export function shouldMuteEntry(entry, mutedIds) {
174
174
  return false;
175
175
  return mutedIds.has(entry.sessionId);
176
176
  }
177
+ /** Format a suppressed entry count badge for muted sessions. Returns empty string for 0. */
178
+ export function formatMuteBadge(count) {
179
+ if (count <= 0)
180
+ return "";
181
+ const label = count > 999 ? "999+" : String(count);
182
+ return `${DIM}(${label})${RESET}`;
183
+ }
184
+ /** Check if an activity entry matches a tag filter (case-insensitive exact match on tag). */
185
+ export function matchesTagFilter(entry, tag) {
186
+ if (!tag)
187
+ return true;
188
+ return entry.tag.toLowerCase() === tag.toLowerCase();
189
+ }
190
+ /** Format the tag filter indicator text for the separator bar. */
191
+ export function formatTagFilterIndicator(tag, matchCount, totalCount) {
192
+ return `${SLATE}filter:${RESET} ${AMBER}${tag}${RESET} ${DIM}(${matchCount}/${totalCount})${RESET}`;
193
+ }
177
194
  // ── TUI class ───────────────────────────────────────────────────────────────
178
195
  export class TUI {
179
196
  active = false;
@@ -193,6 +210,7 @@ export class TUI {
193
210
  newWhileScrolled = 0; // entries added while user is scrolled back
194
211
  pendingCount = 0; // queued user messages awaiting next tick
195
212
  searchPattern = null; // active search filter pattern
213
+ filterTag = null; // active tag filter (exact match on entry.tag)
196
214
  hoverSessionIdx = null; // 1-indexed session under mouse cursor (null = none)
197
215
  activityTimestamps = []; // epoch ms of each log() call for sparkline
198
216
  sortMode = "default";
@@ -205,6 +223,7 @@ export class TUI {
205
223
  bellEnabled = false;
206
224
  lastBellAt = 0;
207
225
  mutedIds = new Set(); // muted session IDs (activity entries hidden)
226
+ mutedEntryCounts = new Map(); // session ID → suppressed entry count since mute
208
227
  sessionNotes = new Map(); // session ID → note text
209
228
  // drill-down mode: show a single session's full output
210
229
  viewMode = "overview";
@@ -380,9 +399,11 @@ export class TUI {
380
399
  return false;
381
400
  if (this.mutedIds.has(sessionId)) {
382
401
  this.mutedIds.delete(sessionId);
402
+ this.mutedEntryCounts.delete(sessionId);
383
403
  }
384
404
  else {
385
405
  this.mutedIds.add(sessionId);
406
+ this.mutedEntryCounts.set(sessionId, 0);
386
407
  }
387
408
  // repaint sessions (mute icon) and activity (filter changes)
388
409
  if (this.active) {
@@ -391,6 +412,19 @@ export class TUI {
391
412
  }
392
413
  return true;
393
414
  }
415
+ /** Unmute all sessions at once. Returns count of sessions unmuted. */
416
+ unmuteAll() {
417
+ const count = this.mutedIds.size;
418
+ if (count === 0)
419
+ return 0;
420
+ this.mutedIds.clear();
421
+ this.mutedEntryCounts.clear();
422
+ if (this.active) {
423
+ this.paintSessions();
424
+ this.repaintActivityRegion();
425
+ }
426
+ return count;
427
+ }
394
428
  /** Check if a session ID is muted. */
395
429
  isMuted(id) {
396
430
  return this.mutedIds.has(id);
@@ -399,6 +433,10 @@ export class TUI {
399
433
  getMutedCount() {
400
434
  return this.mutedIds.size;
401
435
  }
436
+ /** Return count of suppressed entries for a muted session (0 if not muted). */
437
+ getMutedEntryCount(id) {
438
+ return this.mutedEntryCounts.get(id) ?? 0;
439
+ }
402
440
  /**
403
441
  * Set a note on a session (by 1-indexed number, ID, ID prefix, or title).
404
442
  * Returns true if session found. Pass empty text to clear.
@@ -557,11 +595,18 @@ export class TUI {
557
595
  process.stderr.write("\x07");
558
596
  }
559
597
  }
598
+ // track suppressed entry counts regardless of active state (for badge accuracy)
599
+ if (shouldMuteEntry(entry, this.mutedIds) && entry.sessionId) {
600
+ this.mutedEntryCounts.set(entry.sessionId, (this.mutedEntryCounts.get(entry.sessionId) ?? 0) + 1);
601
+ }
560
602
  if (this.active) {
561
603
  // muted entries are still buffered + persisted but hidden from display
562
604
  if (shouldMuteEntry(entry, this.mutedIds)) {
563
605
  // silently skip display — entry is in buffer for scroll-back if unmuted later
564
606
  }
607
+ else if (this.filterTag && !matchesTagFilter(entry, this.filterTag)) {
608
+ // tag filter active: silently skip non-matching entries
609
+ }
565
610
  else if (this.searchPattern) {
566
611
  // search active: only show new entry if it matches
567
612
  if (matchesSearch(entry, this.searchPattern)) {
@@ -607,6 +652,8 @@ export class TUI {
607
652
  let filtered = this.mutedIds.size > 0
608
653
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
609
654
  : this.activityBuffer;
655
+ if (this.filterTag)
656
+ filtered = filtered.filter((e) => matchesTagFilter(e, this.filterTag));
610
657
  const entryCount = this.searchPattern
611
658
  ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
612
659
  : filtered.length;
@@ -634,6 +681,8 @@ export class TUI {
634
681
  let filtered = this.mutedIds.size > 0
635
682
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
636
683
  : this.activityBuffer;
684
+ if (this.filterTag)
685
+ filtered = filtered.filter((e) => matchesTagFilter(e, this.filterTag));
637
686
  const entryCount = this.searchPattern
638
687
  ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
639
688
  : filtered.length;
@@ -771,6 +820,21 @@ export class TUI {
771
820
  getSearchPattern() {
772
821
  return this.searchPattern;
773
822
  }
823
+ // ── Tag filter ─────────────────────────────────────────────────────────
824
+ /** Set or clear the tag filter. Resets scroll and repaints. */
825
+ setTagFilter(tag) {
826
+ this.filterTag = tag && tag.length > 0 ? tag : null;
827
+ this.scrollOffset = 0;
828
+ this.newWhileScrolled = 0;
829
+ if (this.active && this.viewMode === "overview") {
830
+ this.repaintActivityRegion();
831
+ this.paintSeparator();
832
+ }
833
+ }
834
+ /** Get the current tag filter (or null if none active). */
835
+ getTagFilter() {
836
+ return this.filterTag;
837
+ }
774
838
  // ── Hover ───────────────────────────────────────────────────────────────
775
839
  /** Set the hovered session index (1-indexed) or null to clear. Only repaints the affected cards. */
776
840
  setHoverSession(idx) {
@@ -911,12 +975,16 @@ export class TUI {
911
975
  const pinned = this.pinnedIds.has(s.id);
912
976
  const muted = this.mutedIds.has(s.id);
913
977
  const noted = this.sessionNotes.has(s.id);
978
+ const muteBadge = muted ? formatMuteBadge(this.mutedEntryCounts.get(s.id) ?? 0) : "";
979
+ const muteBadgeWidth = muted ? String(Math.min(this.mutedEntryCounts.get(s.id) ?? 0, 9999)).length + 2 : 0; // "(N)" visible chars, 0 when count is 0
980
+ const actualBadgeWidth = (this.mutedEntryCounts.get(s.id) ?? 0) > 0 ? muteBadgeWidth + 1 : 0; // +1 for trailing space
914
981
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
915
982
  const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
916
983
  const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
917
- const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0);
984
+ const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
985
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth;
918
986
  const cardWidth = innerWidth - 1 - iconsWidth;
919
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${note}${formatSessionCard(s, cardWidth)}`;
987
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${formatSessionCard(s, cardWidth)}`;
920
988
  const padded = padBoxLineHover(line, this.cols, isHovered);
921
989
  process.stderr.write(moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded);
922
990
  }
@@ -949,19 +1017,31 @@ export class TUI {
949
1017
  const pinned = this.pinnedIds.has(s.id);
950
1018
  const muted = this.mutedIds.has(s.id);
951
1019
  const noted = this.sessionNotes.has(s.id);
1020
+ const muteBadge = muted ? formatMuteBadge(this.mutedEntryCounts.get(s.id) ?? 0) : "";
1021
+ const actualBadgeWidth = (this.mutedEntryCounts.get(s.id) ?? 0) > 0
1022
+ ? String(Math.min(this.mutedEntryCounts.get(s.id) ?? 0, 9999)).length + 3 : 0; // "(N) " visible chars
952
1023
  const pin = pinned ? `${AMBER}${PIN_ICON}${RESET} ` : "";
953
1024
  const mute = muted ? `${DIM}${MUTE_ICON}${RESET} ` : "";
954
1025
  const note = noted ? `${TEAL}${NOTE_ICON}${RESET} ` : "";
955
- const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0);
1026
+ const badgeSuffix = muteBadge ? `${muteBadge} ` : "";
1027
+ const iconsWidth = (pinned ? 2 : 0) + (muted ? 2 : 0) + (noted ? 2 : 0) + actualBadgeWidth;
956
1028
  const cardWidth = innerWidth - 1 - iconsWidth;
957
- const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${note}${formatSessionCard(s, cardWidth)}`;
1029
+ const line = `${bg}${SLATE}${BOX.v}${RESET}${bg} ${pin}${mute}${badgeSuffix}${note}${formatSessionCard(s, cardWidth)}`;
958
1030
  const padded = padBoxLineHover(line, this.cols, isHovered);
959
1031
  process.stderr.write(SAVE_CURSOR + moveTo(startRow + 1 + i, 1) + CLEAR_LINE + padded + RESTORE_CURSOR);
960
1032
  }
961
1033
  paintSeparator() {
962
1034
  const prefix = `${BOX.h}${BOX.h} activity `;
963
1035
  let hints;
964
- if (this.searchPattern) {
1036
+ if (this.filterTag) {
1037
+ // tag filter takes precedence in the separator display
1038
+ let source = this.mutedIds.size > 0
1039
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
1040
+ : this.activityBuffer;
1041
+ const matchCount = source.filter((e) => matchesTagFilter(e, this.filterTag)).length;
1042
+ hints = formatTagFilterIndicator(this.filterTag, matchCount, source.length);
1043
+ }
1044
+ else if (this.searchPattern) {
965
1045
  const filtered = this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern));
966
1046
  hints = formatSearchIndicator(this.searchPattern, filtered.length, this.activityBuffer.length);
967
1047
  }
@@ -991,10 +1071,12 @@ export class TUI {
991
1071
  }
992
1072
  repaintActivityRegion() {
993
1073
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
994
- // filter: muted entries first, then search on top
1074
+ // filter pipeline: muted tag search
995
1075
  let source = this.mutedIds.size > 0
996
1076
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
997
1077
  : this.activityBuffer;
1078
+ if (this.filterTag)
1079
+ source = source.filter((e) => matchesTagFilter(e, this.filterTag));
998
1080
  if (this.searchPattern) {
999
1081
  source = source.filter((e) => matchesSearch(e, this.searchPattern));
1000
1082
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.85.0",
3
+ "version": "0.87.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",