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 +47 -5
- package/dist/input.d.ts +9 -0
- package/dist/input.js +64 -0
- package/dist/tui.d.ts +40 -4
- package/dist/tui.js +156 -21
- package/package.json +1 -1
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, "2
|
|
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, "2
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
496
|
-
:
|
|
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
|
-
?
|
|
520
|
-
:
|
|
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
|
|
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
|
|
794
|
-
const
|
|
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
|
|
827
|
-
const
|
|
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
|
-
//
|
|
865
|
-
|
|
866
|
-
? this.activityBuffer.filter((e) =>
|
|
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
|
|
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
|