aoaoe 0.72.0 → 0.74.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 +55 -13
- package/dist/input.d.ts +6 -0
- package/dist/input.js +35 -2
- package/dist/tui.d.ts +17 -1
- package/dist/tui.js +132 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -256,19 +256,36 @@ async function main() {
|
|
|
256
256
|
// wire scroll keys to TUI (PgUp/PgDn/Home/End)
|
|
257
257
|
if (tui) {
|
|
258
258
|
input.onScroll((dir) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
259
|
+
if (tui.getViewMode() === "drilldown") {
|
|
260
|
+
// PgUp/PgDn/Home/End scroll the session output in drill-down mode
|
|
261
|
+
switch (dir) {
|
|
262
|
+
case "up":
|
|
263
|
+
tui.scrollDrilldownUp();
|
|
264
|
+
break;
|
|
265
|
+
case "down":
|
|
266
|
+
tui.scrollDrilldownDown();
|
|
267
|
+
break;
|
|
268
|
+
case "bottom":
|
|
269
|
+
tui.scrollDrilldownToBottom();
|
|
270
|
+
break;
|
|
271
|
+
// "top" not wired for drilldown — could add scrollDrilldownToTop() later
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
switch (dir) {
|
|
276
|
+
case "up":
|
|
277
|
+
tui.scrollUp();
|
|
278
|
+
break;
|
|
279
|
+
case "down":
|
|
280
|
+
tui.scrollDown();
|
|
281
|
+
break;
|
|
282
|
+
case "top":
|
|
283
|
+
tui.scrollToTop();
|
|
284
|
+
break;
|
|
285
|
+
case "bottom":
|
|
286
|
+
tui.scrollToBottom();
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
272
289
|
}
|
|
273
290
|
});
|
|
274
291
|
// wire queue count changes to TUI prompt display
|
|
@@ -308,6 +325,31 @@ async function main() {
|
|
|
308
325
|
tui.log("system", `viewing session #${sessionIdx}`);
|
|
309
326
|
}
|
|
310
327
|
});
|
|
328
|
+
// wire /search command to TUI activity filter
|
|
329
|
+
input.onSearch((pattern) => {
|
|
330
|
+
tui.setSearch(pattern);
|
|
331
|
+
if (pattern) {
|
|
332
|
+
tui.log("system", `search: "${pattern}"`);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
tui.log("system", "search cleared");
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
// wire mouse wheel to scroll (3 lines per tick for smooth scrolling)
|
|
339
|
+
input.onMouseWheel((direction) => {
|
|
340
|
+
if (tui.getViewMode() === "drilldown") {
|
|
341
|
+
if (direction === "up")
|
|
342
|
+
tui.scrollDrilldownUp(3);
|
|
343
|
+
else
|
|
344
|
+
tui.scrollDrilldownDown(3);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
if (direction === "up")
|
|
348
|
+
tui.scrollUp(3);
|
|
349
|
+
else
|
|
350
|
+
tui.scrollDown(3);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
311
353
|
}
|
|
312
354
|
// start TUI (alternate screen buffer) after input is ready
|
|
313
355
|
if (tui) {
|
package/dist/input.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type ScrollDirection = "up" | "down" | "top" | "bottom";
|
|
2
2
|
export declare const INSIST_PREFIX = "__INSIST__";
|
|
3
3
|
export type ViewHandler = (target: string | null) => void;
|
|
4
|
+
export type SearchHandler = (pattern: string | null) => void;
|
|
4
5
|
export interface MouseEvent {
|
|
5
6
|
button: number;
|
|
6
7
|
col: number;
|
|
@@ -8,6 +9,7 @@ export interface MouseEvent {
|
|
|
8
9
|
press: boolean;
|
|
9
10
|
}
|
|
10
11
|
export type MouseClickHandler = (row: number, col: number) => void;
|
|
12
|
+
export type MouseWheelHandler = (direction: "up" | "down") => void;
|
|
11
13
|
/** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
|
|
12
14
|
export declare function parseMouseEvent(data: string): MouseEvent | null;
|
|
13
15
|
export declare class InputReader {
|
|
@@ -19,11 +21,15 @@ export declare class InputReader {
|
|
|
19
21
|
private queueChangeHandler;
|
|
20
22
|
private viewHandler;
|
|
21
23
|
private mouseClickHandler;
|
|
24
|
+
private mouseWheelHandler;
|
|
25
|
+
private searchHandler;
|
|
22
26
|
private mouseDataListener;
|
|
23
27
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
24
28
|
onQueueChange(handler: (count: number) => void): void;
|
|
25
29
|
onView(handler: ViewHandler): void;
|
|
26
30
|
onMouseClick(handler: MouseClickHandler): void;
|
|
31
|
+
onMouseWheel(handler: MouseWheelHandler): void;
|
|
32
|
+
onSearch(handler: SearchHandler): void;
|
|
27
33
|
private notifyQueueChange;
|
|
28
34
|
start(): void;
|
|
29
35
|
drain(): string[];
|
package/dist/input.js
CHANGED
|
@@ -30,6 +30,8 @@ export class InputReader {
|
|
|
30
30
|
queueChangeHandler = null;
|
|
31
31
|
viewHandler = null;
|
|
32
32
|
mouseClickHandler = null;
|
|
33
|
+
mouseWheelHandler = null;
|
|
34
|
+
searchHandler = null;
|
|
33
35
|
mouseDataListener = null;
|
|
34
36
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
35
37
|
onScroll(handler) {
|
|
@@ -47,6 +49,14 @@ export class InputReader {
|
|
|
47
49
|
onMouseClick(handler) {
|
|
48
50
|
this.mouseClickHandler = handler;
|
|
49
51
|
}
|
|
52
|
+
// register a callback for mouse wheel events (scroll up/down)
|
|
53
|
+
onMouseWheel(handler) {
|
|
54
|
+
this.mouseWheelHandler = handler;
|
|
55
|
+
}
|
|
56
|
+
// register a callback for search commands (/search <pattern> or /search to clear)
|
|
57
|
+
onSearch(handler) {
|
|
58
|
+
this.searchHandler = handler;
|
|
59
|
+
}
|
|
50
60
|
notifyQueueChange() {
|
|
51
61
|
this.queueChangeHandler?.(this.queue.length);
|
|
52
62
|
}
|
|
@@ -68,9 +78,19 @@ export class InputReader {
|
|
|
68
78
|
this.mouseDataListener = (data) => {
|
|
69
79
|
const str = data.toString("utf8");
|
|
70
80
|
const evt = parseMouseEvent(str);
|
|
71
|
-
if (evt
|
|
81
|
+
if (!evt)
|
|
82
|
+
return;
|
|
83
|
+
// left click press
|
|
84
|
+
if (evt.press && evt.button === 0 && this.mouseClickHandler) {
|
|
72
85
|
this.mouseClickHandler(evt.row, evt.col);
|
|
73
86
|
}
|
|
87
|
+
// mouse wheel: button 64 = scroll up, 65 = scroll down
|
|
88
|
+
if (evt.button === 64 && this.mouseWheelHandler) {
|
|
89
|
+
this.mouseWheelHandler("up");
|
|
90
|
+
}
|
|
91
|
+
else if (evt.button === 65 && this.mouseWheelHandler) {
|
|
92
|
+
this.mouseWheelHandler("down");
|
|
93
|
+
}
|
|
74
94
|
};
|
|
75
95
|
process.stdin.on("data", this.mouseDataListener);
|
|
76
96
|
process.stdin.on("keypress", (_ch, key) => {
|
|
@@ -200,8 +220,11 @@ ${BOLD}controls:${RESET}
|
|
|
200
220
|
${BOLD}navigation:${RESET}
|
|
201
221
|
/view [N|name] drill into a session's live output (default: 1)
|
|
202
222
|
/back return to overview from drill-down
|
|
223
|
+
/search <pattern> filter activity entries by substring (case-insensitive)
|
|
224
|
+
/search clear active search filter
|
|
203
225
|
click session click an agent card to drill down (click again to go back)
|
|
204
|
-
|
|
226
|
+
mouse wheel scroll activity (overview) or session output (drill-down)
|
|
227
|
+
PgUp / PgDn scroll through activity or session output
|
|
205
228
|
Home / End jump to oldest / return to live
|
|
206
229
|
|
|
207
230
|
${BOLD}info:${RESET}
|
|
@@ -276,6 +299,16 @@ ${BOLD}other:${RESET}
|
|
|
276
299
|
console.error(`${DIM}already in overview${RESET}`);
|
|
277
300
|
}
|
|
278
301
|
break;
|
|
302
|
+
case "/search": {
|
|
303
|
+
const searchArg = line.slice("/search".length).trim();
|
|
304
|
+
if (this.searchHandler) {
|
|
305
|
+
this.searchHandler(searchArg || null); // empty = clear search
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.error(`${DIM}search not available (no TUI)${RESET}`);
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
279
312
|
case "/clear":
|
|
280
313
|
process.stderr.write("\x1b[2J\x1b[H");
|
|
281
314
|
break;
|
package/dist/tui.d.ts
CHANGED
|
@@ -23,9 +23,12 @@ export declare class TUI {
|
|
|
23
23
|
private scrollOffset;
|
|
24
24
|
private newWhileScrolled;
|
|
25
25
|
private pendingCount;
|
|
26
|
+
private searchPattern;
|
|
26
27
|
private viewMode;
|
|
27
28
|
private drilldownSessionId;
|
|
28
29
|
private sessionOutputs;
|
|
30
|
+
private drilldownScrollOffset;
|
|
31
|
+
private drilldownNewWhileScrolled;
|
|
29
32
|
private phase;
|
|
30
33
|
private pollCount;
|
|
31
34
|
private sessions;
|
|
@@ -54,6 +57,10 @@ export declare class TUI {
|
|
|
54
57
|
scrollToTop(): void;
|
|
55
58
|
scrollToBottom(): void;
|
|
56
59
|
isScrolledBack(): boolean;
|
|
60
|
+
scrollDrilldownUp(lines?: number): void;
|
|
61
|
+
scrollDrilldownDown(lines?: number): void;
|
|
62
|
+
scrollDrilldownToBottom(): void;
|
|
63
|
+
isDrilldownScrolledBack(): boolean;
|
|
57
64
|
/** Store full session outputs (called each tick from main loop) */
|
|
58
65
|
setSessionOutputs(outputs: Map<string, string>): void;
|
|
59
66
|
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
@@ -64,6 +71,10 @@ export declare class TUI {
|
|
|
64
71
|
getViewMode(): "overview" | "drilldown";
|
|
65
72
|
/** Get drill-down session ID (or null) */
|
|
66
73
|
getDrilldownSessionId(): string | null;
|
|
74
|
+
/** Set or clear the search filter. Resets scroll and repaints. */
|
|
75
|
+
setSearch(pattern: string | null): void;
|
|
76
|
+
/** Get the current search pattern (or null if no active search). */
|
|
77
|
+
getSearchPattern(): string | null;
|
|
67
78
|
private updateDimensions;
|
|
68
79
|
private computeLayout;
|
|
69
80
|
private onResize;
|
|
@@ -96,6 +107,11 @@ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scr
|
|
|
96
107
|
end: number;
|
|
97
108
|
};
|
|
98
109
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
110
|
+
declare function formatDrilldownScrollIndicator(offset: number, totalLines: number, visibleLines: number, newCount: number): string;
|
|
111
|
+
/** Case-insensitive substring match against an activity entry's tag, text, and time. */
|
|
112
|
+
declare function matchesSearch(entry: ActivityEntry, pattern: string): boolean;
|
|
113
|
+
/** Format the search indicator text for the separator bar. */
|
|
114
|
+
declare function formatSearchIndicator(pattern: string, matchCount: number, totalCount: number): string;
|
|
99
115
|
/**
|
|
100
116
|
* Hit-test a mouse click row against the session panel.
|
|
101
117
|
* Returns 1-indexed session number if the click hit a session card, null otherwise.
|
|
@@ -104,5 +120,5 @@ declare function formatScrollIndicator(offset: number, totalEntries: number, vis
|
|
|
104
120
|
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
105
121
|
*/
|
|
106
122
|
export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
|
|
107
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
123
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator };
|
|
108
124
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -61,10 +61,13 @@ export class TUI {
|
|
|
61
61
|
scrollOffset = 0; // 0 = live (bottom), >0 = scrolled back N entries
|
|
62
62
|
newWhileScrolled = 0; // entries added while user is scrolled back
|
|
63
63
|
pendingCount = 0; // queued user messages awaiting next tick
|
|
64
|
+
searchPattern = null; // active search filter pattern
|
|
64
65
|
// drill-down mode: show a single session's full output
|
|
65
66
|
viewMode = "overview";
|
|
66
67
|
drilldownSessionId = null;
|
|
67
68
|
sessionOutputs = new Map(); // full output lines per session
|
|
69
|
+
drilldownScrollOffset = 0; // 0 = live (tail), >0 = scrolled back N lines
|
|
70
|
+
drilldownNewWhileScrolled = 0; // lines added while scrolled back
|
|
68
71
|
// current state for repaints
|
|
69
72
|
phase = "sleeping";
|
|
70
73
|
pollCount = 0;
|
|
@@ -156,7 +159,20 @@ export class TUI {
|
|
|
156
159
|
this.activityBuffer = this.activityBuffer.slice(-this.maxActivity);
|
|
157
160
|
}
|
|
158
161
|
if (this.active) {
|
|
159
|
-
if (this.
|
|
162
|
+
if (this.searchPattern) {
|
|
163
|
+
// search active: only show new entry if it matches
|
|
164
|
+
if (matchesSearch(entry, this.searchPattern)) {
|
|
165
|
+
if (this.scrollOffset > 0) {
|
|
166
|
+
this.newWhileScrolled++;
|
|
167
|
+
this.paintSeparator();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.writeActivityLine(entry);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// non-matching entries are silently buffered — visible when search is cleared
|
|
174
|
+
}
|
|
175
|
+
else if (this.scrollOffset > 0) {
|
|
160
176
|
// user is scrolled back — don't auto-scroll, just show indicator
|
|
161
177
|
this.newWhileScrolled++;
|
|
162
178
|
this.paintSeparator();
|
|
@@ -185,7 +201,10 @@ export class TUI {
|
|
|
185
201
|
return;
|
|
186
202
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
187
203
|
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
188
|
-
const
|
|
204
|
+
const entryCount = this.searchPattern
|
|
205
|
+
? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
|
|
206
|
+
: this.activityBuffer.length;
|
|
207
|
+
const maxOffset = Math.max(0, entryCount - visibleLines);
|
|
189
208
|
this.scrollOffset = Math.min(maxOffset, this.scrollOffset + n);
|
|
190
209
|
this.repaintActivityRegion();
|
|
191
210
|
this.paintSeparator();
|
|
@@ -206,7 +225,10 @@ export class TUI {
|
|
|
206
225
|
if (!this.active)
|
|
207
226
|
return;
|
|
208
227
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
209
|
-
|
|
228
|
+
const entryCount = this.searchPattern
|
|
229
|
+
? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern)).length
|
|
230
|
+
: this.activityBuffer.length;
|
|
231
|
+
this.scrollOffset = Math.max(0, entryCount - visibleLines);
|
|
210
232
|
this.repaintActivityRegion();
|
|
211
233
|
this.paintSeparator();
|
|
212
234
|
}
|
|
@@ -221,15 +243,58 @@ export class TUI {
|
|
|
221
243
|
isScrolledBack() {
|
|
222
244
|
return this.scrollOffset > 0;
|
|
223
245
|
}
|
|
246
|
+
// ── Drill-down scroll ─────────────────────────────────────────────────
|
|
247
|
+
scrollDrilldownUp(lines) {
|
|
248
|
+
if (!this.active || this.viewMode !== "drilldown" || !this.drilldownSessionId)
|
|
249
|
+
return;
|
|
250
|
+
const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
|
|
251
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
252
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
253
|
+
const maxOffset = Math.max(0, outputLines.length - visibleLines);
|
|
254
|
+
this.drilldownScrollOffset = Math.min(maxOffset, this.drilldownScrollOffset + n);
|
|
255
|
+
this.repaintDrilldownContent();
|
|
256
|
+
this.paintDrilldownSeparator();
|
|
257
|
+
}
|
|
258
|
+
scrollDrilldownDown(lines) {
|
|
259
|
+
if (!this.active || this.viewMode !== "drilldown")
|
|
260
|
+
return;
|
|
261
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
262
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
263
|
+
const wasScrolled = this.drilldownScrollOffset > 0;
|
|
264
|
+
this.drilldownScrollOffset = Math.max(0, this.drilldownScrollOffset - n);
|
|
265
|
+
if (wasScrolled && this.drilldownScrollOffset === 0)
|
|
266
|
+
this.drilldownNewWhileScrolled = 0;
|
|
267
|
+
this.repaintDrilldownContent();
|
|
268
|
+
this.paintDrilldownSeparator();
|
|
269
|
+
}
|
|
270
|
+
scrollDrilldownToBottom() {
|
|
271
|
+
if (!this.active || this.viewMode !== "drilldown")
|
|
272
|
+
return;
|
|
273
|
+
this.drilldownScrollOffset = 0;
|
|
274
|
+
this.drilldownNewWhileScrolled = 0;
|
|
275
|
+
this.repaintDrilldownContent();
|
|
276
|
+
this.paintDrilldownSeparator();
|
|
277
|
+
}
|
|
278
|
+
isDrilldownScrolledBack() {
|
|
279
|
+
return this.drilldownScrollOffset > 0;
|
|
280
|
+
}
|
|
224
281
|
// ── Drill-down mode ────────────────────────────────────────────────────
|
|
225
282
|
/** Store full session outputs (called each tick from main loop) */
|
|
226
283
|
setSessionOutputs(outputs) {
|
|
227
284
|
for (const [id, text] of outputs) {
|
|
228
|
-
this.sessionOutputs.
|
|
285
|
+
const prevLen = this.sessionOutputs.get(id)?.length ?? 0;
|
|
286
|
+
const lines = text.split("\n");
|
|
287
|
+
this.sessionOutputs.set(id, lines);
|
|
288
|
+
// track new lines while scrolled back in drill-down
|
|
289
|
+
if (this.viewMode === "drilldown" && this.drilldownSessionId === id && this.drilldownScrollOffset > 0) {
|
|
290
|
+
const newLines = Math.max(0, lines.length - prevLen);
|
|
291
|
+
this.drilldownNewWhileScrolled += newLines;
|
|
292
|
+
}
|
|
229
293
|
}
|
|
230
294
|
// repaint drill-down view if we're watching this session
|
|
231
295
|
if (this.active && this.viewMode === "drilldown" && this.drilldownSessionId) {
|
|
232
296
|
this.repaintDrilldownContent();
|
|
297
|
+
this.paintDrilldownSeparator();
|
|
233
298
|
}
|
|
234
299
|
}
|
|
235
300
|
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
@@ -251,6 +316,8 @@ export class TUI {
|
|
|
251
316
|
return false;
|
|
252
317
|
this.viewMode = "drilldown";
|
|
253
318
|
this.drilldownSessionId = sessionId;
|
|
319
|
+
this.drilldownScrollOffset = 0;
|
|
320
|
+
this.drilldownNewWhileScrolled = 0;
|
|
254
321
|
if (this.active) {
|
|
255
322
|
this.computeLayout(this.sessions.length);
|
|
256
323
|
this.paintAll();
|
|
@@ -263,6 +330,8 @@ export class TUI {
|
|
|
263
330
|
return;
|
|
264
331
|
this.viewMode = "overview";
|
|
265
332
|
this.drilldownSessionId = null;
|
|
333
|
+
this.drilldownScrollOffset = 0;
|
|
334
|
+
this.drilldownNewWhileScrolled = 0;
|
|
266
335
|
if (this.active) {
|
|
267
336
|
this.computeLayout(this.sessions.length);
|
|
268
337
|
this.paintAll();
|
|
@@ -276,6 +345,21 @@ export class TUI {
|
|
|
276
345
|
getDrilldownSessionId() {
|
|
277
346
|
return this.drilldownSessionId;
|
|
278
347
|
}
|
|
348
|
+
// ── Search ──────────────────────────────────────────────────────────────
|
|
349
|
+
/** Set or clear the search filter. Resets scroll and repaints. */
|
|
350
|
+
setSearch(pattern) {
|
|
351
|
+
this.searchPattern = pattern && pattern.length > 0 ? pattern : null;
|
|
352
|
+
this.scrollOffset = 0;
|
|
353
|
+
this.newWhileScrolled = 0;
|
|
354
|
+
if (this.active && this.viewMode === "overview") {
|
|
355
|
+
this.repaintActivityRegion();
|
|
356
|
+
this.paintSeparator();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Get the current search pattern (or null if no active search). */
|
|
360
|
+
getSearchPattern() {
|
|
361
|
+
return this.searchPattern;
|
|
362
|
+
}
|
|
279
363
|
// ── Layout computation ──────────────────────────────────────────────────
|
|
280
364
|
updateDimensions() {
|
|
281
365
|
this.cols = process.stderr.columns || 80;
|
|
@@ -386,7 +470,11 @@ export class TUI {
|
|
|
386
470
|
paintSeparator() {
|
|
387
471
|
const prefix = `${BOX.h}${BOX.h} activity `;
|
|
388
472
|
let hints;
|
|
389
|
-
if (this.
|
|
473
|
+
if (this.searchPattern) {
|
|
474
|
+
const filtered = this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern));
|
|
475
|
+
hints = formatSearchIndicator(this.searchPattern, filtered.length, this.activityBuffer.length);
|
|
476
|
+
}
|
|
477
|
+
else if (this.scrollOffset > 0) {
|
|
390
478
|
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
391
479
|
}
|
|
392
480
|
else {
|
|
@@ -410,8 +498,12 @@ export class TUI {
|
|
|
410
498
|
}
|
|
411
499
|
repaintActivityRegion() {
|
|
412
500
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
413
|
-
|
|
414
|
-
const
|
|
501
|
+
// when search is active, filter entries first, then paginate
|
|
502
|
+
const source = this.searchPattern
|
|
503
|
+
? this.activityBuffer.filter((e) => matchesSearch(e, this.searchPattern))
|
|
504
|
+
: this.activityBuffer;
|
|
505
|
+
const { start, end } = computeScrollSlice(source.length, visibleLines, this.scrollOffset);
|
|
506
|
+
const entries = source.slice(start, end);
|
|
415
507
|
for (let i = 0; i < visibleLines; i++) {
|
|
416
508
|
const row = this.scrollTop + i;
|
|
417
509
|
if (i < entries.length) {
|
|
@@ -428,7 +520,15 @@ export class TUI {
|
|
|
428
520
|
const session = this.sessions.find((s) => s.id === this.drilldownSessionId);
|
|
429
521
|
const title = session ? session.title : this.drilldownSessionId ?? "?";
|
|
430
522
|
const prefix = `${BOX.h}${BOX.h} ${title} `;
|
|
431
|
-
|
|
523
|
+
let hints;
|
|
524
|
+
if (this.drilldownScrollOffset > 0) {
|
|
525
|
+
const outputLines = this.sessionOutputs.get(this.drilldownSessionId ?? "") ?? [];
|
|
526
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
527
|
+
hints = formatDrilldownScrollIndicator(this.drilldownScrollOffset, outputLines.length, visibleLines, this.drilldownNewWhileScrolled);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
hints = " click or /back: overview scroll: navigate /view N: switch ";
|
|
531
|
+
}
|
|
432
532
|
const totalLen = prefix.length + hints.length;
|
|
433
533
|
const fill = Math.max(0, this.cols - totalLen);
|
|
434
534
|
const left = Math.floor(fill / 2);
|
|
@@ -441,9 +541,9 @@ export class TUI {
|
|
|
441
541
|
return;
|
|
442
542
|
const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
|
|
443
543
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
444
|
-
//
|
|
445
|
-
const
|
|
446
|
-
const visible = outputLines.slice(
|
|
544
|
+
// use scroll offset: 0 = tail (live), >0 = scrolled back
|
|
545
|
+
const { start, end } = computeScrollSlice(outputLines.length, visibleLines, this.drilldownScrollOffset);
|
|
546
|
+
const visible = outputLines.slice(start, end);
|
|
447
547
|
for (let i = 0; i < visibleLines; i++) {
|
|
448
548
|
const row = this.scrollTop + i;
|
|
449
549
|
if (i < visible.length) {
|
|
@@ -646,6 +746,26 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
|
646
746
|
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
647
747
|
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
648
748
|
}
|
|
749
|
+
// format the scroll indicator for drill-down separator bar
|
|
750
|
+
function formatDrilldownScrollIndicator(offset, totalLines, visibleLines, newCount) {
|
|
751
|
+
const position = totalLines - offset;
|
|
752
|
+
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
753
|
+
return ` ↑ ${offset} lines │ ${position}/${totalLines} │ scroll: navigate End=live${newTag} `;
|
|
754
|
+
}
|
|
755
|
+
// ── Search helpers (pure, exported for testing) ─────────────────────────────
|
|
756
|
+
/** Case-insensitive substring match against an activity entry's tag, text, and time. */
|
|
757
|
+
function matchesSearch(entry, pattern) {
|
|
758
|
+
if (!pattern)
|
|
759
|
+
return true;
|
|
760
|
+
const lower = pattern.toLowerCase();
|
|
761
|
+
return (entry.tag.toLowerCase().includes(lower) ||
|
|
762
|
+
entry.text.toLowerCase().includes(lower) ||
|
|
763
|
+
entry.time.toLowerCase().includes(lower));
|
|
764
|
+
}
|
|
765
|
+
/** Format the search indicator text for the separator bar. */
|
|
766
|
+
function formatSearchIndicator(pattern, matchCount, totalCount) {
|
|
767
|
+
return ` search: "${pattern}" │ ${matchCount} of ${totalCount} │ /search: clear `;
|
|
768
|
+
}
|
|
649
769
|
// ── Mouse hit testing (pure, exported for testing) ──────────────────────────
|
|
650
770
|
/**
|
|
651
771
|
* Hit-test a mouse click row against the session panel.
|
|
@@ -664,5 +784,5 @@ export function hitTestSession(row, headerHeight, sessionCount) {
|
|
|
664
784
|
return row - firstSessionRow + 1; // 1-indexed
|
|
665
785
|
}
|
|
666
786
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
667
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
787
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader, matchesSearch, formatSearchIndicator };
|
|
668
788
|
//# sourceMappingURL=tui.js.map
|