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 +48 -5
- package/dist/input.d.ts +12 -0
- package/dist/input.js +69 -0
- package/dist/tui.d.ts +47 -4
- package/dist/tui.js +157 -20
- package/package.json +1 -1
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, "2
|
|
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, "2
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
429
|
-
:
|
|
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
|
-
?
|
|
453
|
-
:
|
|
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
|
|
727
|
-
const
|
|
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
|
|
760
|
-
const
|
|
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
|
-
//
|
|
798
|
-
|
|
799
|
-
? this.activityBuffer.filter((e) =>
|
|
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
|