aoaoe 0.71.0 → 0.73.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 +61 -14
- package/dist/input.d.ts +15 -0
- package/dist/input.js +51 -1
- package/dist/tui.d.ts +18 -1
- package/dist/tui.js +98 -11
- 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 } from "./tui.js";
|
|
18
|
+
import { TUI, hitTestSession } 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";
|
|
@@ -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
|
|
@@ -293,6 +310,36 @@ async function main() {
|
|
|
293
310
|
}
|
|
294
311
|
}
|
|
295
312
|
});
|
|
313
|
+
// wire mouse clicks on session cards to drill-down
|
|
314
|
+
input.onMouseClick((row, _col) => {
|
|
315
|
+
if (tui.getViewMode() === "drilldown") {
|
|
316
|
+
// click anywhere in drilldown = back to overview
|
|
317
|
+
tui.exitDrilldown();
|
|
318
|
+
tui.log("system", "returned to overview");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const sessionIdx = hitTestSession(row, 1, tui.getSessionCount());
|
|
322
|
+
if (sessionIdx !== null) {
|
|
323
|
+
const ok = tui.enterDrilldown(sessionIdx);
|
|
324
|
+
if (ok)
|
|
325
|
+
tui.log("system", `viewing session #${sessionIdx}`);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
// wire mouse wheel to scroll (3 lines per tick for smooth scrolling)
|
|
329
|
+
input.onMouseWheel((direction) => {
|
|
330
|
+
if (tui.getViewMode() === "drilldown") {
|
|
331
|
+
if (direction === "up")
|
|
332
|
+
tui.scrollDrilldownUp(3);
|
|
333
|
+
else
|
|
334
|
+
tui.scrollDrilldownDown(3);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
if (direction === "up")
|
|
338
|
+
tui.scrollUp(3);
|
|
339
|
+
else
|
|
340
|
+
tui.scrollDown(3);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
296
343
|
}
|
|
297
344
|
// start TUI (alternate screen buffer) after input is ready
|
|
298
345
|
if (tui) {
|
package/dist/input.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
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 interface MouseEvent {
|
|
5
|
+
button: number;
|
|
6
|
+
col: number;
|
|
7
|
+
row: number;
|
|
8
|
+
press: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type MouseClickHandler = (row: number, col: number) => void;
|
|
11
|
+
export type MouseWheelHandler = (direction: "up" | "down") => void;
|
|
12
|
+
/** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
|
|
13
|
+
export declare function parseMouseEvent(data: string): MouseEvent | null;
|
|
4
14
|
export declare class InputReader {
|
|
5
15
|
private rl;
|
|
6
16
|
private queue;
|
|
@@ -9,9 +19,14 @@ export declare class InputReader {
|
|
|
9
19
|
private scrollHandler;
|
|
10
20
|
private queueChangeHandler;
|
|
11
21
|
private viewHandler;
|
|
22
|
+
private mouseClickHandler;
|
|
23
|
+
private mouseWheelHandler;
|
|
24
|
+
private mouseDataListener;
|
|
12
25
|
onScroll(handler: (dir: ScrollDirection) => void): void;
|
|
13
26
|
onQueueChange(handler: (count: number) => void): void;
|
|
14
27
|
onView(handler: ViewHandler): void;
|
|
28
|
+
onMouseClick(handler: MouseClickHandler): void;
|
|
29
|
+
onMouseWheel(handler: MouseWheelHandler): void;
|
|
15
30
|
private notifyQueueChange;
|
|
16
31
|
start(): void;
|
|
17
32
|
drain(): string[];
|
package/dist/input.js
CHANGED
|
@@ -7,6 +7,20 @@ import { GREEN, DIM, YELLOW, RED, BOLD, RESET } from "./colors.js";
|
|
|
7
7
|
// ESC-ESC interrupt detection
|
|
8
8
|
const ESC_DOUBLE_TAP_MS = 500;
|
|
9
9
|
export const INSIST_PREFIX = "__INSIST__";
|
|
10
|
+
// SGR extended mouse format: \x1b[<btn;col;rowM (press) or \x1b[<btn;col;rowm (release)
|
|
11
|
+
const SGR_MOUSE_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
12
|
+
/** Parse an SGR extended mouse event from raw terminal data. Returns null if not a mouse event. */
|
|
13
|
+
export function parseMouseEvent(data) {
|
|
14
|
+
const m = SGR_MOUSE_RE.exec(data);
|
|
15
|
+
if (!m)
|
|
16
|
+
return null;
|
|
17
|
+
return {
|
|
18
|
+
button: parseInt(m[1], 10),
|
|
19
|
+
col: parseInt(m[2], 10),
|
|
20
|
+
row: parseInt(m[3], 10),
|
|
21
|
+
press: m[4] === "M",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
10
24
|
export class InputReader {
|
|
11
25
|
rl = null;
|
|
12
26
|
queue = []; // pending user messages for the reasoner
|
|
@@ -15,6 +29,9 @@ export class InputReader {
|
|
|
15
29
|
scrollHandler = null;
|
|
16
30
|
queueChangeHandler = null;
|
|
17
31
|
viewHandler = null;
|
|
32
|
+
mouseClickHandler = null;
|
|
33
|
+
mouseWheelHandler = null;
|
|
34
|
+
mouseDataListener = null;
|
|
18
35
|
// register a callback for scroll key events (PgUp/PgDn/Home/End)
|
|
19
36
|
onScroll(handler) {
|
|
20
37
|
this.scrollHandler = handler;
|
|
@@ -27,6 +44,14 @@ export class InputReader {
|
|
|
27
44
|
onView(handler) {
|
|
28
45
|
this.viewHandler = handler;
|
|
29
46
|
}
|
|
47
|
+
// register a callback for mouse left-click events (row, col are 1-indexed)
|
|
48
|
+
onMouseClick(handler) {
|
|
49
|
+
this.mouseClickHandler = handler;
|
|
50
|
+
}
|
|
51
|
+
// register a callback for mouse wheel events (scroll up/down)
|
|
52
|
+
onMouseWheel(handler) {
|
|
53
|
+
this.mouseWheelHandler = handler;
|
|
54
|
+
}
|
|
30
55
|
notifyQueueChange() {
|
|
31
56
|
this.queueChangeHandler?.(this.queue.length);
|
|
32
57
|
}
|
|
@@ -44,6 +69,25 @@ export class InputReader {
|
|
|
44
69
|
this.rl.on("close", () => { this.rl = null; });
|
|
45
70
|
// ESC-ESC interrupt detection (same as chat.ts)
|
|
46
71
|
emitKeypressEvents(process.stdin);
|
|
72
|
+
// intercept raw SGR mouse sequences before keypress parsing
|
|
73
|
+
this.mouseDataListener = (data) => {
|
|
74
|
+
const str = data.toString("utf8");
|
|
75
|
+
const evt = parseMouseEvent(str);
|
|
76
|
+
if (!evt)
|
|
77
|
+
return;
|
|
78
|
+
// left click press
|
|
79
|
+
if (evt.press && evt.button === 0 && this.mouseClickHandler) {
|
|
80
|
+
this.mouseClickHandler(evt.row, evt.col);
|
|
81
|
+
}
|
|
82
|
+
// mouse wheel: button 64 = scroll up, 65 = scroll down
|
|
83
|
+
if (evt.button === 64 && this.mouseWheelHandler) {
|
|
84
|
+
this.mouseWheelHandler("up");
|
|
85
|
+
}
|
|
86
|
+
else if (evt.button === 65 && this.mouseWheelHandler) {
|
|
87
|
+
this.mouseWheelHandler("down");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
process.stdin.on("data", this.mouseDataListener);
|
|
47
91
|
process.stdin.on("keypress", (_ch, key) => {
|
|
48
92
|
if (key?.name === "escape" || key?.sequence === "\x1b") {
|
|
49
93
|
const now = Date.now();
|
|
@@ -102,6 +146,10 @@ export class InputReader {
|
|
|
102
146
|
this.rl?.prompt(true);
|
|
103
147
|
}
|
|
104
148
|
stop() {
|
|
149
|
+
if (this.mouseDataListener) {
|
|
150
|
+
process.stdin.removeListener("data", this.mouseDataListener);
|
|
151
|
+
this.mouseDataListener = null;
|
|
152
|
+
}
|
|
105
153
|
this.rl?.close();
|
|
106
154
|
this.rl = null;
|
|
107
155
|
}
|
|
@@ -167,7 +215,9 @@ ${BOLD}controls:${RESET}
|
|
|
167
215
|
${BOLD}navigation:${RESET}
|
|
168
216
|
/view [N|name] drill into a session's live output (default: 1)
|
|
169
217
|
/back return to overview from drill-down
|
|
170
|
-
|
|
218
|
+
click session click an agent card to drill down (click again to go back)
|
|
219
|
+
mouse wheel scroll activity (overview) or session output (drill-down)
|
|
220
|
+
PgUp / PgDn scroll through activity or session output
|
|
171
221
|
Home / End jump to oldest / return to live
|
|
172
222
|
|
|
173
223
|
${BOLD}info:${RESET}
|
package/dist/tui.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export declare class TUI {
|
|
|
26
26
|
private viewMode;
|
|
27
27
|
private drilldownSessionId;
|
|
28
28
|
private sessionOutputs;
|
|
29
|
+
private drilldownScrollOffset;
|
|
30
|
+
private drilldownNewWhileScrolled;
|
|
29
31
|
private phase;
|
|
30
32
|
private pollCount;
|
|
31
33
|
private sessions;
|
|
@@ -36,6 +38,8 @@ export declare class TUI {
|
|
|
36
38
|
start(version: string): void;
|
|
37
39
|
stop(): void;
|
|
38
40
|
isActive(): boolean;
|
|
41
|
+
/** Return the current number of sessions (for mouse hit testing) */
|
|
42
|
+
getSessionCount(): number;
|
|
39
43
|
updateState(opts: {
|
|
40
44
|
phase?: DaemonPhase;
|
|
41
45
|
pollCount?: number;
|
|
@@ -52,6 +56,10 @@ export declare class TUI {
|
|
|
52
56
|
scrollToTop(): void;
|
|
53
57
|
scrollToBottom(): void;
|
|
54
58
|
isScrolledBack(): boolean;
|
|
59
|
+
scrollDrilldownUp(lines?: number): void;
|
|
60
|
+
scrollDrilldownDown(lines?: number): void;
|
|
61
|
+
scrollDrilldownToBottom(): void;
|
|
62
|
+
isDrilldownScrolledBack(): boolean;
|
|
55
63
|
/** Store full session outputs (called each tick from main loop) */
|
|
56
64
|
setSessionOutputs(outputs: Map<string, string>): void;
|
|
57
65
|
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
@@ -94,5 +102,14 @@ declare function computeScrollSlice(bufferLen: number, visibleLines: number, scr
|
|
|
94
102
|
end: number;
|
|
95
103
|
};
|
|
96
104
|
declare function formatScrollIndicator(offset: number, totalEntries: number, visibleLines: number, newCount: number): string;
|
|
97
|
-
|
|
105
|
+
declare function formatDrilldownScrollIndicator(offset: number, totalLines: number, visibleLines: number, newCount: number): string;
|
|
106
|
+
/**
|
|
107
|
+
* Hit-test a mouse click row against the session panel.
|
|
108
|
+
* Returns 1-indexed session number if the click hit a session card, null otherwise.
|
|
109
|
+
*
|
|
110
|
+
* Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
|
|
111
|
+
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
112
|
+
*/
|
|
113
|
+
export declare function hitTestSession(row: number, headerHeight: number, sessionCount: number): number | null;
|
|
114
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
98
115
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/tui.js
CHANGED
|
@@ -12,6 +12,9 @@ const CURSOR_HIDE = `${CSI}?25l`;
|
|
|
12
12
|
const CURSOR_SHOW = `${CSI}?25h`;
|
|
13
13
|
const SAVE_CURSOR = `${ESC}7`;
|
|
14
14
|
const RESTORE_CURSOR = `${ESC}8`;
|
|
15
|
+
// mouse tracking (SGR extended mode — button events + extended coordinates)
|
|
16
|
+
const MOUSE_ON = `${CSI}?1000h${CSI}?1006h`;
|
|
17
|
+
const MOUSE_OFF = `${CSI}?1000l${CSI}?1006l`;
|
|
15
18
|
// cursor movement
|
|
16
19
|
const moveTo = (row, col) => `${CSI}${row};${col}H`;
|
|
17
20
|
const setScrollRegion = (top, bottom) => `${CSI}${top};${bottom}r`;
|
|
@@ -62,6 +65,8 @@ export class TUI {
|
|
|
62
65
|
viewMode = "overview";
|
|
63
66
|
drilldownSessionId = null;
|
|
64
67
|
sessionOutputs = new Map(); // full output lines per session
|
|
68
|
+
drilldownScrollOffset = 0; // 0 = live (tail), >0 = scrolled back N lines
|
|
69
|
+
drilldownNewWhileScrolled = 0; // lines added while scrolled back
|
|
65
70
|
// current state for repaints
|
|
66
71
|
phase = "sleeping";
|
|
67
72
|
pollCount = 0;
|
|
@@ -76,8 +81,8 @@ export class TUI {
|
|
|
76
81
|
this.active = true;
|
|
77
82
|
this.version = version;
|
|
78
83
|
this.updateDimensions();
|
|
79
|
-
// enter alternate screen, hide cursor, clear
|
|
80
|
-
process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
|
|
84
|
+
// enter alternate screen, hide cursor, clear, enable mouse
|
|
85
|
+
process.stderr.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN + MOUSE_ON);
|
|
81
86
|
// handle terminal resize
|
|
82
87
|
process.stdout.on("resize", () => this.onResize());
|
|
83
88
|
// tick timer: countdown + spinner animation (~4 fps for smooth braille spin)
|
|
@@ -102,12 +107,16 @@ export class TUI {
|
|
|
102
107
|
clearInterval(this.countdownTimer);
|
|
103
108
|
this.countdownTimer = null;
|
|
104
109
|
}
|
|
105
|
-
// restore normal screen, show cursor, reset scroll region
|
|
106
|
-
process.stderr.write(resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
110
|
+
// disable mouse, restore normal screen, show cursor, reset scroll region
|
|
111
|
+
process.stderr.write(MOUSE_OFF + resetScrollRegion() + CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
107
112
|
}
|
|
108
113
|
isActive() {
|
|
109
114
|
return this.active;
|
|
110
115
|
}
|
|
116
|
+
/** Return the current number of sessions (for mouse hit testing) */
|
|
117
|
+
getSessionCount() {
|
|
118
|
+
return this.sessions.length;
|
|
119
|
+
}
|
|
111
120
|
// ── State updates ───────────────────────────────────────────────────────
|
|
112
121
|
updateState(opts) {
|
|
113
122
|
if (opts.phase !== undefined)
|
|
@@ -214,15 +223,58 @@ export class TUI {
|
|
|
214
223
|
isScrolledBack() {
|
|
215
224
|
return this.scrollOffset > 0;
|
|
216
225
|
}
|
|
226
|
+
// ── Drill-down scroll ─────────────────────────────────────────────────
|
|
227
|
+
scrollDrilldownUp(lines) {
|
|
228
|
+
if (!this.active || this.viewMode !== "drilldown" || !this.drilldownSessionId)
|
|
229
|
+
return;
|
|
230
|
+
const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
|
|
231
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
232
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
233
|
+
const maxOffset = Math.max(0, outputLines.length - visibleLines);
|
|
234
|
+
this.drilldownScrollOffset = Math.min(maxOffset, this.drilldownScrollOffset + n);
|
|
235
|
+
this.repaintDrilldownContent();
|
|
236
|
+
this.paintDrilldownSeparator();
|
|
237
|
+
}
|
|
238
|
+
scrollDrilldownDown(lines) {
|
|
239
|
+
if (!this.active || this.viewMode !== "drilldown")
|
|
240
|
+
return;
|
|
241
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
242
|
+
const n = lines ?? Math.max(1, Math.floor(visibleLines / 2));
|
|
243
|
+
const wasScrolled = this.drilldownScrollOffset > 0;
|
|
244
|
+
this.drilldownScrollOffset = Math.max(0, this.drilldownScrollOffset - n);
|
|
245
|
+
if (wasScrolled && this.drilldownScrollOffset === 0)
|
|
246
|
+
this.drilldownNewWhileScrolled = 0;
|
|
247
|
+
this.repaintDrilldownContent();
|
|
248
|
+
this.paintDrilldownSeparator();
|
|
249
|
+
}
|
|
250
|
+
scrollDrilldownToBottom() {
|
|
251
|
+
if (!this.active || this.viewMode !== "drilldown")
|
|
252
|
+
return;
|
|
253
|
+
this.drilldownScrollOffset = 0;
|
|
254
|
+
this.drilldownNewWhileScrolled = 0;
|
|
255
|
+
this.repaintDrilldownContent();
|
|
256
|
+
this.paintDrilldownSeparator();
|
|
257
|
+
}
|
|
258
|
+
isDrilldownScrolledBack() {
|
|
259
|
+
return this.drilldownScrollOffset > 0;
|
|
260
|
+
}
|
|
217
261
|
// ── Drill-down mode ────────────────────────────────────────────────────
|
|
218
262
|
/** Store full session outputs (called each tick from main loop) */
|
|
219
263
|
setSessionOutputs(outputs) {
|
|
220
264
|
for (const [id, text] of outputs) {
|
|
221
|
-
this.sessionOutputs.
|
|
265
|
+
const prevLen = this.sessionOutputs.get(id)?.length ?? 0;
|
|
266
|
+
const lines = text.split("\n");
|
|
267
|
+
this.sessionOutputs.set(id, lines);
|
|
268
|
+
// track new lines while scrolled back in drill-down
|
|
269
|
+
if (this.viewMode === "drilldown" && this.drilldownSessionId === id && this.drilldownScrollOffset > 0) {
|
|
270
|
+
const newLines = Math.max(0, lines.length - prevLen);
|
|
271
|
+
this.drilldownNewWhileScrolled += newLines;
|
|
272
|
+
}
|
|
222
273
|
}
|
|
223
274
|
// repaint drill-down view if we're watching this session
|
|
224
275
|
if (this.active && this.viewMode === "drilldown" && this.drilldownSessionId) {
|
|
225
276
|
this.repaintDrilldownContent();
|
|
277
|
+
this.paintDrilldownSeparator();
|
|
226
278
|
}
|
|
227
279
|
}
|
|
228
280
|
/** Enter drill-down view for a session. Returns false if session not found. */
|
|
@@ -244,6 +296,8 @@ export class TUI {
|
|
|
244
296
|
return false;
|
|
245
297
|
this.viewMode = "drilldown";
|
|
246
298
|
this.drilldownSessionId = sessionId;
|
|
299
|
+
this.drilldownScrollOffset = 0;
|
|
300
|
+
this.drilldownNewWhileScrolled = 0;
|
|
247
301
|
if (this.active) {
|
|
248
302
|
this.computeLayout(this.sessions.length);
|
|
249
303
|
this.paintAll();
|
|
@@ -256,6 +310,8 @@ export class TUI {
|
|
|
256
310
|
return;
|
|
257
311
|
this.viewMode = "overview";
|
|
258
312
|
this.drilldownSessionId = null;
|
|
313
|
+
this.drilldownScrollOffset = 0;
|
|
314
|
+
this.drilldownNewWhileScrolled = 0;
|
|
259
315
|
if (this.active) {
|
|
260
316
|
this.computeLayout(this.sessions.length);
|
|
261
317
|
this.paintAll();
|
|
@@ -383,7 +439,7 @@ export class TUI {
|
|
|
383
439
|
hints = formatScrollIndicator(this.scrollOffset, this.activityBuffer.length, this.scrollBottom - this.scrollTop + 1, this.newWhileScrolled);
|
|
384
440
|
}
|
|
385
441
|
else {
|
|
386
|
-
hints = " esc esc: interrupt /help
|
|
442
|
+
hints = " click agent to view esc esc: interrupt /help ";
|
|
387
443
|
}
|
|
388
444
|
const totalLen = prefix.length + hints.length;
|
|
389
445
|
const fill = Math.max(0, this.cols - totalLen);
|
|
@@ -421,7 +477,15 @@ export class TUI {
|
|
|
421
477
|
const session = this.sessions.find((s) => s.id === this.drilldownSessionId);
|
|
422
478
|
const title = session ? session.title : this.drilldownSessionId ?? "?";
|
|
423
479
|
const prefix = `${BOX.h}${BOX.h} ${title} `;
|
|
424
|
-
|
|
480
|
+
let hints;
|
|
481
|
+
if (this.drilldownScrollOffset > 0) {
|
|
482
|
+
const outputLines = this.sessionOutputs.get(this.drilldownSessionId ?? "") ?? [];
|
|
483
|
+
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
484
|
+
hints = formatDrilldownScrollIndicator(this.drilldownScrollOffset, outputLines.length, visibleLines, this.drilldownNewWhileScrolled);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
hints = " click or /back: overview scroll: navigate /view N: switch ";
|
|
488
|
+
}
|
|
425
489
|
const totalLen = prefix.length + hints.length;
|
|
426
490
|
const fill = Math.max(0, this.cols - totalLen);
|
|
427
491
|
const left = Math.floor(fill / 2);
|
|
@@ -434,9 +498,9 @@ export class TUI {
|
|
|
434
498
|
return;
|
|
435
499
|
const outputLines = this.sessionOutputs.get(this.drilldownSessionId) ?? [];
|
|
436
500
|
const visibleLines = this.scrollBottom - this.scrollTop + 1;
|
|
437
|
-
//
|
|
438
|
-
const
|
|
439
|
-
const visible = outputLines.slice(
|
|
501
|
+
// use scroll offset: 0 = tail (live), >0 = scrolled back
|
|
502
|
+
const { start, end } = computeScrollSlice(outputLines.length, visibleLines, this.drilldownScrollOffset);
|
|
503
|
+
const visible = outputLines.slice(start, end);
|
|
440
504
|
for (let i = 0; i < visibleLines; i++) {
|
|
441
505
|
const row = this.scrollTop + i;
|
|
442
506
|
if (i < visible.length) {
|
|
@@ -639,6 +703,29 @@ function formatScrollIndicator(offset, totalEntries, visibleLines, newCount) {
|
|
|
639
703
|
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
640
704
|
return ` ↑ ${offset} older │ ${position}/${totalEntries} │ PgUp/PgDn End=live${newTag} `;
|
|
641
705
|
}
|
|
706
|
+
// format the scroll indicator for drill-down separator bar
|
|
707
|
+
function formatDrilldownScrollIndicator(offset, totalLines, visibleLines, newCount) {
|
|
708
|
+
const position = totalLines - offset;
|
|
709
|
+
const newTag = newCount > 0 ? ` ${newCount} new ↓` : "";
|
|
710
|
+
return ` ↑ ${offset} lines │ ${position}/${totalLines} │ scroll: navigate End=live${newTag} `;
|
|
711
|
+
}
|
|
712
|
+
// ── Mouse hit testing (pure, exported for testing) ──────────────────────────
|
|
713
|
+
/**
|
|
714
|
+
* Hit-test a mouse click row against the session panel.
|
|
715
|
+
* Returns 1-indexed session number if the click hit a session card, null otherwise.
|
|
716
|
+
*
|
|
717
|
+
* Session cards occupy rows: headerHeight + 2 through headerHeight + 1 + sessionCount
|
|
718
|
+
* (row = headerHeight + 2 + i for 0-indexed session i)
|
|
719
|
+
*/
|
|
720
|
+
export function hitTestSession(row, headerHeight, sessionCount) {
|
|
721
|
+
if (sessionCount <= 0)
|
|
722
|
+
return null;
|
|
723
|
+
const firstSessionRow = headerHeight + 2; // top border is headerHeight+1, first card is +2
|
|
724
|
+
const lastSessionRow = firstSessionRow + sessionCount - 1;
|
|
725
|
+
if (row < firstSessionRow || row > lastSessionRow)
|
|
726
|
+
return null;
|
|
727
|
+
return row - firstSessionRow + 1; // 1-indexed
|
|
728
|
+
}
|
|
642
729
|
// ── Exported pure helpers (for testing) ─────────────────────────────────────
|
|
643
|
-
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
730
|
+
export { formatActivity, formatSessionCard, truncateAnsi, truncatePlain, padBoxLine, padToWidth, stripAnsiForLen, phaseDisplay, computeScrollSlice, formatScrollIndicator, formatDrilldownScrollIndicator, formatPrompt, formatDrilldownHeader };
|
|
644
731
|
//# sourceMappingURL=tui.js.map
|