aoaoe 0.86.0 → 0.88.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
@@ -15,7 +15,7 @@ import { wakeableSleep } from "./wake.js";
15
15
  import { classifyMessages, formatUserMessages, buildReceipts, shouldSkipSleep, hasPendingFile, isInsistMessage, stripInsistPrefix } from "./message.js";
16
16
  import { TaskManager, loadTaskDefinitions, loadTaskState, formatTaskTable } from "./task-manager.js";
17
17
  import { runTaskCli, handleTaskSlashCommand } from "./task-cli.js";
18
- import { TUI, hitTestSession, nextSortMode, SORT_MODES } from "./tui.js";
18
+ import { TUI, hitTestSession, nextSortMode, SORT_MODES, formatUptime } from "./tui.js";
19
19
  import { isDaemonRunningFromState } from "./chat.js";
20
20
  import { sendNotification, sendTestNotification } from "./notify.js";
21
21
  import { startHealthServer } from "./health.js";
@@ -454,6 +454,32 @@ async function main() {
454
454
  tui.log("system", "no sessions are muted");
455
455
  }
456
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
+ });
467
+ // wire /uptime listing
468
+ input.onUptime(() => {
469
+ const firstSeen = tui.getAllFirstSeen();
470
+ const sessions = tui.getSessions();
471
+ if (sessions.length === 0) {
472
+ tui.log("system", "no sessions — uptime not available");
473
+ }
474
+ else {
475
+ const now = Date.now();
476
+ for (const s of sessions) {
477
+ const start = firstSeen.get(s.id);
478
+ const up = start !== undefined ? formatUptime(now - start) : "unknown";
479
+ tui.log("system", ` ${s.title}: ${up}`);
480
+ }
481
+ }
482
+ });
457
483
  // wire /note set/clear
458
484
  input.onNote((target, text) => {
459
485
  const num = /^\d+$/.test(target) ? parseInt(target, 10) : undefined;
package/dist/input.d.ts CHANGED
@@ -13,6 +13,8 @@ export type JumpHandler = (num: number) => void;
13
13
  export type MarksHandler = () => void;
14
14
  export type MuteHandler = (target: string) => void;
15
15
  export type UnmuteAllHandler = () => void;
16
+ export type TagFilterHandler = (tag: string | null) => void;
17
+ export type UptimeHandler = () => void;
16
18
  export type NoteHandler = (target: string, text: string) => void;
17
19
  export type NotesHandler = () => void;
18
20
  export interface MouseEvent {
@@ -50,6 +52,8 @@ export declare class InputReader {
50
52
  private marksHandler;
51
53
  private muteHandler;
52
54
  private unmuteAllHandler;
55
+ private tagFilterHandler;
56
+ private uptimeHandler;
53
57
  private noteHandler;
54
58
  private notesHandler;
55
59
  private mouseDataListener;
@@ -71,6 +75,8 @@ export declare class InputReader {
71
75
  onMarks(handler: MarksHandler): void;
72
76
  onMute(handler: MuteHandler): void;
73
77
  onUnmuteAll(handler: UnmuteAllHandler): void;
78
+ onTagFilter(handler: TagFilterHandler): void;
79
+ onUptime(handler: UptimeHandler): void;
74
80
  onNote(handler: NoteHandler): void;
75
81
  onNotes(handler: NotesHandler): void;
76
82
  private notifyQueueChange;
package/dist/input.js CHANGED
@@ -45,6 +45,8 @@ export class InputReader {
45
45
  marksHandler = null;
46
46
  muteHandler = null;
47
47
  unmuteAllHandler = null;
48
+ tagFilterHandler = null;
49
+ uptimeHandler = null;
48
50
  noteHandler = null;
49
51
  notesHandler = null;
50
52
  mouseDataListener = null;
@@ -120,6 +122,14 @@ export class InputReader {
120
122
  onUnmuteAll(handler) {
121
123
  this.unmuteAllHandler = handler;
122
124
  }
125
+ // register a callback for tag filter commands (/filter <tag>)
126
+ onTagFilter(handler) {
127
+ this.tagFilterHandler = handler;
128
+ }
129
+ // register a callback for uptime listing (/uptime)
130
+ onUptime(handler) {
131
+ this.uptimeHandler = handler;
132
+ }
123
133
  // register a callback for note commands (/note <target> <text>)
124
134
  onNote(handler) {
125
135
  this.noteHandler = handler;
@@ -312,6 +322,8 @@ ${BOLD}navigation:${RESET}
312
322
  /focus toggle focus mode (show only pinned sessions)
313
323
  /mute [N|name] mute/unmute a session's activity entries (toggle)
314
324
  /unmute-all unmute all sessions at once
325
+ /filter [tag] filter activity by tag (error, system, etc. — no arg = clear)
326
+ /uptime show session uptimes (time since first observed)
315
327
  /note N|name text attach a note to a session (no text = clear)
316
328
  /notes list all session notes
317
329
  /mark bookmark current activity position
@@ -468,6 +480,24 @@ ${BOLD}other:${RESET}
468
480
  console.error(`${DIM}unmute-all not available (no TUI)${RESET}`);
469
481
  }
470
482
  break;
483
+ case "/filter": {
484
+ const filterArg = line.slice("/filter".length).trim();
485
+ if (this.tagFilterHandler) {
486
+ this.tagFilterHandler(filterArg || null);
487
+ }
488
+ else {
489
+ console.error(`${DIM}filter not available (no TUI)${RESET}`);
490
+ }
491
+ break;
492
+ }
493
+ case "/uptime":
494
+ if (this.uptimeHandler) {
495
+ this.uptimeHandler();
496
+ }
497
+ else {
498
+ console.error(`${DIM}uptime not available (no TUI)${RESET}`);
499
+ }
500
+ break;
471
501
  case "/note": {
472
502
  const noteArg = line.slice("/note".length).trim();
473
503
  if (this.noteHandler) {
package/dist/tui.d.ts CHANGED
@@ -53,6 +53,12 @@ export declare function truncateNote(text: string): string;
53
53
  export declare function shouldMuteEntry(entry: ActivityEntry, mutedIds: Set<string>): boolean;
54
54
  /** Format a suppressed entry count badge for muted sessions. Returns empty string for 0. */
55
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;
60
+ /** Format milliseconds as human-readable uptime: "2h 15m", "45m", "3d 2h", "< 1m". */
61
+ export declare function formatUptime(ms: number): string;
56
62
  export declare class TUI {
57
63
  private active;
58
64
  private countdownTimer;
@@ -71,6 +77,7 @@ export declare class TUI {
71
77
  private newWhileScrolled;
72
78
  private pendingCount;
73
79
  private searchPattern;
80
+ private filterTag;
74
81
  private hoverSessionIdx;
75
82
  private activityTimestamps;
76
83
  private sortMode;
@@ -85,6 +92,7 @@ export declare class TUI {
85
92
  private mutedIds;
86
93
  private mutedEntryCounts;
87
94
  private sessionNotes;
95
+ private sessionFirstSeen;
88
96
  private viewMode;
89
97
  private drilldownSessionId;
90
98
  private sessionOutputs;
@@ -156,6 +164,10 @@ export declare class TUI {
156
164
  getAllNotes(): ReadonlyMap<string, string>;
157
165
  /** Return the current sessions (read-only, for resolving IDs to titles in the UI). */
158
166
  getSessions(): readonly DaemonSessionState[];
167
+ /** Return the uptime in ms for a session (0 if not tracked). */
168
+ getUptime(id: string): number;
169
+ /** Return all session first-seen timestamps (for /uptime listing). */
170
+ getAllFirstSeen(): ReadonlyMap<string, number>;
159
171
  /**
160
172
  * Add a bookmark at the current activity position.
161
173
  * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
@@ -204,6 +216,10 @@ export declare class TUI {
204
216
  setSearch(pattern: string | null): void;
205
217
  /** Get the current search pattern (or null if no active search). */
206
218
  getSearchPattern(): string | null;
219
+ /** Set or clear the tag filter. Resets scroll and repaints. */
220
+ setTagFilter(tag: string | null): void;
221
+ /** Get the current tag filter (or null if none active). */
222
+ getTagFilter(): string | null;
207
223
  /** Set the hovered session index (1-indexed) or null to clear. Only repaints the affected cards. */
208
224
  setHoverSession(idx: number | null): void;
209
225
  /** Get the current hovered session index (1-indexed, null if none). */
package/dist/tui.js CHANGED
@@ -181,6 +181,33 @@ export function formatMuteBadge(count) {
181
181
  const label = count > 999 ? "999+" : String(count);
182
182
  return `${DIM}(${label})${RESET}`;
183
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
+ }
194
+ // ── Uptime ───────────────────────────────────────────────────────────────────
195
+ /** Format milliseconds as human-readable uptime: "2h 15m", "45m", "3d 2h", "< 1m". */
196
+ export function formatUptime(ms) {
197
+ if (ms < 0)
198
+ return "< 1m";
199
+ const totalMin = Math.floor(ms / 60_000);
200
+ if (totalMin < 1)
201
+ return "< 1m";
202
+ const days = Math.floor(totalMin / 1440);
203
+ const hours = Math.floor((totalMin % 1440) / 60);
204
+ const minutes = totalMin % 60;
205
+ if (days > 0)
206
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
207
+ if (hours > 0)
208
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
209
+ return `${minutes}m`;
210
+ }
184
211
  // ── TUI class ───────────────────────────────────────────────────────────────
185
212
  export class TUI {
186
213
  active = false;
@@ -200,6 +227,7 @@ export class TUI {
200
227
  newWhileScrolled = 0; // entries added while user is scrolled back
201
228
  pendingCount = 0; // queued user messages awaiting next tick
202
229
  searchPattern = null; // active search filter pattern
230
+ filterTag = null; // active tag filter (exact match on entry.tag)
203
231
  hoverSessionIdx = null; // 1-indexed session under mouse cursor (null = none)
204
232
  activityTimestamps = []; // epoch ms of each log() call for sparkline
205
233
  sortMode = "default";
@@ -214,6 +242,7 @@ export class TUI {
214
242
  mutedIds = new Set(); // muted session IDs (activity entries hidden)
215
243
  mutedEntryCounts = new Map(); // session ID → suppressed entry count since mute
216
244
  sessionNotes = new Map(); // session ID → note text
245
+ sessionFirstSeen = new Map(); // session ID → epoch ms when first observed
217
246
  // drill-down mode: show a single session's full output
218
247
  viewMode = "overview";
219
248
  drilldownSessionId = null;
@@ -472,6 +501,17 @@ export class TUI {
472
501
  getSessions() {
473
502
  return this.sessions;
474
503
  }
504
+ /** Return the uptime in ms for a session (0 if not tracked). */
505
+ getUptime(id) {
506
+ const firstSeen = this.sessionFirstSeen.get(id);
507
+ if (firstSeen === undefined)
508
+ return 0;
509
+ return Date.now() - firstSeen;
510
+ }
511
+ /** Return all session first-seen timestamps (for /uptime listing). */
512
+ getAllFirstSeen() {
513
+ return this.sessionFirstSeen;
514
+ }
475
515
  /**
476
516
  * Add a bookmark at the current activity position.
477
517
  * Returns the bookmark number (1-indexed) or 0 if buffer is empty.
@@ -536,9 +576,11 @@ export class TUI {
536
576
  if (opts.pendingCount !== undefined)
537
577
  this.pendingCount = opts.pendingCount;
538
578
  if (opts.sessions !== undefined) {
539
- // track activity changes for sort-by-activity
579
+ // track activity changes for sort-by-activity + first-seen for uptime
540
580
  const now = Date.now();
541
581
  for (const s of opts.sessions) {
582
+ if (!this.sessionFirstSeen.has(s.id))
583
+ this.sessionFirstSeen.set(s.id, now);
542
584
  const prev = this.prevLastActivity.get(s.id);
543
585
  if (s.lastActivity !== undefined && s.lastActivity !== prev) {
544
586
  this.lastChangeAt.set(s.id, now);
@@ -593,6 +635,9 @@ export class TUI {
593
635
  if (shouldMuteEntry(entry, this.mutedIds)) {
594
636
  // silently skip display — entry is in buffer for scroll-back if unmuted later
595
637
  }
638
+ else if (this.filterTag && !matchesTagFilter(entry, this.filterTag)) {
639
+ // tag filter active: silently skip non-matching entries
640
+ }
596
641
  else if (this.searchPattern) {
597
642
  // search active: only show new entry if it matches
598
643
  if (matchesSearch(entry, this.searchPattern)) {
@@ -638,6 +683,8 @@ export class TUI {
638
683
  let filtered = this.mutedIds.size > 0
639
684
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
640
685
  : this.activityBuffer;
686
+ if (this.filterTag)
687
+ filtered = filtered.filter((e) => matchesTagFilter(e, this.filterTag));
641
688
  const entryCount = this.searchPattern
642
689
  ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
643
690
  : filtered.length;
@@ -665,6 +712,8 @@ export class TUI {
665
712
  let filtered = this.mutedIds.size > 0
666
713
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
667
714
  : this.activityBuffer;
715
+ if (this.filterTag)
716
+ filtered = filtered.filter((e) => matchesTagFilter(e, this.filterTag));
668
717
  const entryCount = this.searchPattern
669
718
  ? filtered.filter((e) => matchesSearch(e, this.searchPattern)).length
670
719
  : filtered.length;
@@ -802,6 +851,21 @@ export class TUI {
802
851
  getSearchPattern() {
803
852
  return this.searchPattern;
804
853
  }
854
+ // ── Tag filter ─────────────────────────────────────────────────────────
855
+ /** Set or clear the tag filter. Resets scroll and repaints. */
856
+ setTagFilter(tag) {
857
+ this.filterTag = tag && tag.length > 0 ? tag : null;
858
+ this.scrollOffset = 0;
859
+ this.newWhileScrolled = 0;
860
+ if (this.active && this.viewMode === "overview") {
861
+ this.repaintActivityRegion();
862
+ this.paintSeparator();
863
+ }
864
+ }
865
+ /** Get the current tag filter (or null if none active). */
866
+ getTagFilter() {
867
+ return this.filterTag;
868
+ }
805
869
  // ── Hover ───────────────────────────────────────────────────────────────
806
870
  /** Set the hovered session index (1-indexed) or null to clear. Only repaints the affected cards. */
807
871
  setHoverSession(idx) {
@@ -1000,7 +1064,15 @@ export class TUI {
1000
1064
  paintSeparator() {
1001
1065
  const prefix = `${BOX.h}${BOX.h} activity `;
1002
1066
  let hints;
1003
- if (this.searchPattern) {
1067
+ if (this.filterTag) {
1068
+ // tag filter takes precedence in the separator display
1069
+ let source = this.mutedIds.size > 0
1070
+ ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
1071
+ : this.activityBuffer;
1072
+ const matchCount = source.filter((e) => matchesTagFilter(e, this.filterTag)).length;
1073
+ hints = formatTagFilterIndicator(this.filterTag, matchCount, source.length);
1074
+ }
1075
+ else if (this.searchPattern) {
1004
1076
  const filtered = this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern));
1005
1077
  hints = formatSearchIndicator(this.searchPattern, filtered.length, this.activityBuffer.length);
1006
1078
  }
@@ -1030,10 +1102,12 @@ export class TUI {
1030
1102
  }
1031
1103
  repaintActivityRegion() {
1032
1104
  const visibleLines = this.scrollBottom - this.scrollTop + 1;
1033
- // filter: muted entries first, then search on top
1105
+ // filter pipeline: muted tag search
1034
1106
  let source = this.mutedIds.size > 0
1035
1107
  ? this.activityBuffer.filter((e) => !shouldMuteEntry(e, this.mutedIds))
1036
1108
  : this.activityBuffer;
1109
+ if (this.filterTag)
1110
+ source = source.filter((e) => matchesTagFilter(e, this.filterTag));
1037
1111
  if (this.searchPattern) {
1038
1112
  source = source.filter((e) => matchesSearch(e, this.searchPattern));
1039
1113
  }
@@ -1056,7 +1130,9 @@ export class TUI {
1056
1130
  const title = session ? session.title : this.drilldownSessionId ?? "?";
1057
1131
  const noteText = this.drilldownSessionId ? this.sessionNotes.get(this.drilldownSessionId) : undefined;
1058
1132
  const noteSuffix = noteText ? `"${noteText}" ` : "";
1059
- const prefix = `${BOX.h}${BOX.h} ${title} ${noteSuffix}`;
1133
+ const firstSeen = this.drilldownSessionId ? this.sessionFirstSeen.get(this.drilldownSessionId) : undefined;
1134
+ const uptimeSuffix = firstSeen !== undefined ? `${DIM}${formatUptime(Date.now() - firstSeen)}${RESET} ` : "";
1135
+ const prefix = `${BOX.h}${BOX.h} ${title} ${uptimeSuffix}${noteSuffix}`;
1060
1136
  let hints;
1061
1137
  if (this.drilldownScrollOffset > 0) {
1062
1138
  const outputLines = this.sessionOutputs.get(this.drilldownSessionId ?? "") ?? [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aoaoe",
3
- "version": "0.86.0",
3
+ "version": "0.88.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",