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 +27 -1
- package/dist/input.d.ts +6 -0
- package/dist/input.js +30 -0
- package/dist/tui.d.ts +16 -0
- package/dist/tui.js +80 -4
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
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 ?? "") ?? [];
|