@yusukeshib/pi-babysit 0.6.26 → 0.6.27
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/README.md +12 -54
- package/index.ts +80 -702
- package/package.json +1 -3
- package/log-source.ts +0 -232
- package/log-viewer.ts +0 -496
package/log-viewer.ts
DELETED
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
Key,
|
|
4
|
-
Markdown,
|
|
5
|
-
ScrollView,
|
|
6
|
-
Text,
|
|
7
|
-
matchesKey,
|
|
8
|
-
truncateToWidth,
|
|
9
|
-
visibleWidth,
|
|
10
|
-
type Component,
|
|
11
|
-
type TuiMouseEvent,
|
|
12
|
-
} from "@earendil-works/pi-tui";
|
|
13
|
-
|
|
14
|
-
import { FileLogSource, type LogPage } from "./log-source";
|
|
15
|
-
|
|
16
|
-
export interface LogViewerOptions {
|
|
17
|
-
getHeight: () => number;
|
|
18
|
-
requestRender: () => void;
|
|
19
|
-
getTheme: () => Theme;
|
|
20
|
-
onClose: () => void;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Only numeric SGR escapes are allowed; never forward cursor, hyperlink or image controls. */
|
|
24
|
-
function sanitize(text: string, sgr = false): string {
|
|
25
|
-
return text
|
|
26
|
-
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\|$)/g, "")
|
|
27
|
-
.replace(/\x1b[P_^X][\s\S]*?(?:\x1b\\|$)/g, "")
|
|
28
|
-
.replace(/\r\n/g, "\n")
|
|
29
|
-
.replace(/\r/g, "\n")
|
|
30
|
-
.replace(/\t/g, " ")
|
|
31
|
-
.replace(
|
|
32
|
-
/\x1b\[[0-?]*[ -/]*[@-~]|\x1b[^\[]?|[\x00-\x08\x0b-\x1f\x7f-\x9f]/g,
|
|
33
|
-
(s) => (sgr && /^\x1b\[[\d;]*m$/.test(s) ? s : ""),
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
type Block = { kind: "assistant" | "tool" | "result" | "error"; text: string; timestamp?: number };
|
|
37
|
-
function contentText(content: unknown): string {
|
|
38
|
-
if (typeof content === "string") return content;
|
|
39
|
-
return Array.isArray(content)
|
|
40
|
-
? content
|
|
41
|
-
.filter((c) => c?.type === "text")
|
|
42
|
-
.map((c) => c.text ?? "")
|
|
43
|
-
.join("")
|
|
44
|
-
: "";
|
|
45
|
-
}
|
|
46
|
-
function agentBlocks(raw: string, timestamps?: (number | undefined)[]): Block[] {
|
|
47
|
-
const blocks: Block[] = [];
|
|
48
|
-
let current: Block | undefined;
|
|
49
|
-
const toolResults = new Map<string, Block>();
|
|
50
|
-
const assistant = (timestamp: number | undefined) => {
|
|
51
|
-
current ??= (blocks.push({ kind: "assistant", text: "", timestamp }), blocks[blocks.length - 1]);
|
|
52
|
-
current.timestamp ??= timestamp;
|
|
53
|
-
return current;
|
|
54
|
-
};
|
|
55
|
-
for (const [index, line] of raw.split(/\r\n|\r|\n/).entries()) {
|
|
56
|
-
const timestamp = timestamps?.[index];
|
|
57
|
-
let e: any;
|
|
58
|
-
try {
|
|
59
|
-
e = JSON.parse(line);
|
|
60
|
-
} catch {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (!e || typeof e !== "object") continue;
|
|
64
|
-
if (e.type === "agent_start") { current = undefined; toolResults.clear(); }
|
|
65
|
-
if (e.type === "message_start" && e.message?.role === "assistant")
|
|
66
|
-
current = undefined;
|
|
67
|
-
if (["message_start", "message_update", "message_end"].includes(e.type)) {
|
|
68
|
-
if (e.message?.role === "assistant") {
|
|
69
|
-
const text = contentText(e.message.content);
|
|
70
|
-
const b = assistant(timestamp);
|
|
71
|
-
if (text) b.text = text; // cumulative/final snapshots replace streamed deltas
|
|
72
|
-
if (e.message.errorMessage)
|
|
73
|
-
blocks.push({ kind: "error", text: String(e.message.errorMessage), timestamp });
|
|
74
|
-
if (e.type === "message_end") current = undefined;
|
|
75
|
-
} else if (
|
|
76
|
-
!e.message &&
|
|
77
|
-
e.assistantMessageEvent?.type === "text_delta" &&
|
|
78
|
-
typeof e.assistantMessageEvent.delta === "string"
|
|
79
|
-
) {
|
|
80
|
-
assistant(timestamp).text += e.assistantMessageEvent.delta;
|
|
81
|
-
} else if (e.type === "message_end" && e.message?.role === "toolResult") {
|
|
82
|
-
const id = e.message.toolCallId;
|
|
83
|
-
const text = contentText(e.message.content);
|
|
84
|
-
if (id && toolResults.has(id)) {
|
|
85
|
-
const b = toolResults.get(id)!;
|
|
86
|
-
b.text = text;
|
|
87
|
-
b.timestamp ??= timestamp;
|
|
88
|
-
b.kind = e.message.isError ? "error" : b.kind;
|
|
89
|
-
} else {
|
|
90
|
-
const b: Block = {
|
|
91
|
-
kind: e.message.isError ? "error" : "result",
|
|
92
|
-
text,
|
|
93
|
-
timestamp,
|
|
94
|
-
};
|
|
95
|
-
blocks.push(b);
|
|
96
|
-
if (id) toolResults.set(id, b);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
} else if (e.type === "tool_execution_start") {
|
|
100
|
-
blocks.push({
|
|
101
|
-
kind: "tool",
|
|
102
|
-
text: `${e.toolName ?? "tool"} ${JSON.stringify(e.args ?? {})}`,
|
|
103
|
-
timestamp,
|
|
104
|
-
});
|
|
105
|
-
} else if (e.type === "tool_execution_end") {
|
|
106
|
-
const b: Block = {
|
|
107
|
-
kind: e.isError ? "error" : "result",
|
|
108
|
-
timestamp,
|
|
109
|
-
text:
|
|
110
|
-
contentText(e.result?.content) ||
|
|
111
|
-
String(e.error?.message ?? e.error ?? ""),
|
|
112
|
-
};
|
|
113
|
-
if (b.text) {
|
|
114
|
-
blocks.push(b);
|
|
115
|
-
if (e.toolCallId) toolResults.set(e.toolCallId, b);
|
|
116
|
-
}
|
|
117
|
-
} else if (e.type === "error")
|
|
118
|
-
blocks.push({
|
|
119
|
-
kind: "error",
|
|
120
|
-
timestamp,
|
|
121
|
-
text: String(
|
|
122
|
-
e.message ?? e.error?.message ?? e.error ?? "Unknown error",
|
|
123
|
-
),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return blocks
|
|
127
|
-
.filter((b) => b.text)
|
|
128
|
-
.map((b) => ({ ...b, text: sanitize(b.text) }));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function dateLabel(timestamp: number): string {
|
|
132
|
-
const date = new Date(timestamp);
|
|
133
|
-
const pad = (n: number) => String(n).padStart(2, "0");
|
|
134
|
-
return `[${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}]`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function createLogViewer(options: LogViewerOptions) {
|
|
138
|
-
let header = "",
|
|
139
|
-
raw = "",
|
|
140
|
-
description = "",
|
|
141
|
-
isAgent = false;
|
|
142
|
-
let lineTimestamps: (number | undefined)[] | undefined;
|
|
143
|
-
let source: FileLogSource | undefined;
|
|
144
|
-
let pages: LogPage[] = [];
|
|
145
|
-
let sourceSize = 0;
|
|
146
|
-
let tailDirty = false;
|
|
147
|
-
let metadataVersion = "";
|
|
148
|
-
let adjacentPageStart: number | undefined;
|
|
149
|
-
const setPages = () => {
|
|
150
|
-
raw = pages.map((page) => page.text).join("");
|
|
151
|
-
lineTimestamps = pages.flatMap((page, i) =>
|
|
152
|
-
i < pages.length - 1 && /[\r\n]$/.test(page.text)
|
|
153
|
-
? page.lineTimestamps.slice(0, -1) : page.lineTimestamps);
|
|
154
|
-
body.invalidate();
|
|
155
|
-
};
|
|
156
|
-
let cache: string[] | undefined,
|
|
157
|
-
cacheWidth = -1,
|
|
158
|
-
cacheTheme: Theme | undefined;
|
|
159
|
-
const body: Component = {
|
|
160
|
-
invalidate() {
|
|
161
|
-
cache = undefined;
|
|
162
|
-
},
|
|
163
|
-
render(width) {
|
|
164
|
-
const theme = options.getTheme();
|
|
165
|
-
if (cache && cacheWidth === width && cacheTheme === theme) return cache;
|
|
166
|
-
const blocks = isAgent
|
|
167
|
-
? agentBlocks(raw, lineTimestamps)
|
|
168
|
-
: [{ kind: "result" as const, text: sanitize(raw, true) }];
|
|
169
|
-
const lines: string[] = description && (!source || pages[0]?.start === 0)
|
|
170
|
-
? [
|
|
171
|
-
theme.fg("muted", isAgent ? "[task]" : "[command]"),
|
|
172
|
-
...new Text(sanitize(description), 0, 0).render(width),
|
|
173
|
-
"",
|
|
174
|
-
]
|
|
175
|
-
: [];
|
|
176
|
-
for (const b of blocks) {
|
|
177
|
-
if (!isAgent && lineTimestamps?.some((time) => time !== undefined)) {
|
|
178
|
-
for (const [index, text] of raw.split(/\r\n|\r|\n/).entries()) {
|
|
179
|
-
const time = lineTimestamps[index];
|
|
180
|
-
const label = time === undefined ? "" : dateLabel(time);
|
|
181
|
-
const indent = label ? visibleWidth(label) + 1 : 0;
|
|
182
|
-
if (indent && width <= indent + 4) {
|
|
183
|
-
lines.push(...new Text(theme.fg("muted", label), 0, 0).render(width));
|
|
184
|
-
lines.push(...new Text(sanitize(text, true), 0, 0).render(width));
|
|
185
|
-
} else {
|
|
186
|
-
const wrapped = new Text(sanitize(text, true), 0, 0).render(width - indent);
|
|
187
|
-
lines.push(...wrapped.map((line, row) =>
|
|
188
|
-
(row === 0 && label ? theme.fg("muted", label) + " " : " ".repeat(indent)) + line));
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (isAgent && b.timestamp !== undefined) {
|
|
194
|
-
const date = theme.fg("muted", dateLabel(b.timestamp));
|
|
195
|
-
const kind = theme.fg(b.kind === "error" ? "error" : b.kind === "assistant" ? "accent" : "toolTitle", `[${b.kind}]`);
|
|
196
|
-
const indent = visibleWidth(date) + 1;
|
|
197
|
-
const kindIndent = visibleWidth(kind) + 1;
|
|
198
|
-
if (width <= indent + kindIndent + 4) {
|
|
199
|
-
lines.push(...new Text(date, 0, 0).render(width));
|
|
200
|
-
lines.push(...new Text(`${kind} ${b.text}`, 0, 0).render(width));
|
|
201
|
-
} else {
|
|
202
|
-
const contentWidth = width - indent - kindIndent;
|
|
203
|
-
const rendered = b.kind === "assistant"
|
|
204
|
-
? new Markdown(b.text, 0, 0, getMarkdownTheme()).render(contentWidth)
|
|
205
|
-
: new Text(b.text, 0, 0).render(contentWidth);
|
|
206
|
-
lines.push(...rendered.map((line, row) =>
|
|
207
|
-
`${date} ${row === 0 ? kind + " " : " ".repeat(kindIndent)}${line}`));
|
|
208
|
-
}
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (isAgent) {
|
|
212
|
-
lines.push(
|
|
213
|
-
theme.fg(
|
|
214
|
-
b.kind === "error"
|
|
215
|
-
? "error"
|
|
216
|
-
: b.kind === "assistant"
|
|
217
|
-
? "accent"
|
|
218
|
-
: "toolTitle",
|
|
219
|
-
`[${b.kind}]`,
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
lines.push(
|
|
224
|
-
...(b.kind === "assistant"
|
|
225
|
-
? new Markdown(b.text, 0, 0, getMarkdownTheme()).render(width)
|
|
226
|
-
: new Text(b.text, 0, 0).render(width)),
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
if (!blocks.some((block) => block.text.trim()))
|
|
230
|
-
lines.push(theme.fg("muted", "(no output available)"));
|
|
231
|
-
cache = lines;
|
|
232
|
-
cacheWidth = width;
|
|
233
|
-
cacheTheme = theme;
|
|
234
|
-
return cache;
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
const scrollView = new ScrollView(body, {
|
|
238
|
-
follow: "end",
|
|
239
|
-
overscroll: "contain",
|
|
240
|
-
scrollbar: "always",
|
|
241
|
-
scrollbarTrackStyle: (s) => options.getTheme().fg("scrollbarTrack", s),
|
|
242
|
-
scrollbarThumbStyle: (s) => options.getTheme().fg("scrollbarThumb", s),
|
|
243
|
-
});
|
|
244
|
-
const heading = {
|
|
245
|
-
invalidate() {},
|
|
246
|
-
render(width: number) {
|
|
247
|
-
const title = truncateToWidth(sanitize(header).replace(/\n/g, " "), width);
|
|
248
|
-
return [
|
|
249
|
-
options.getTheme().fg("accent", title) +
|
|
250
|
-
" ".repeat(
|
|
251
|
-
Math.max(0, width - visibleWidth(title)),
|
|
252
|
-
),
|
|
253
|
-
];
|
|
254
|
-
},
|
|
255
|
-
handleMouse(e: TuiMouseEvent) {
|
|
256
|
-
if (
|
|
257
|
-
e.button !== "left" ||
|
|
258
|
-
e.x < 0 ||
|
|
259
|
-
e.x >= e.width
|
|
260
|
-
)
|
|
261
|
-
return;
|
|
262
|
-
if (e.type === "press") {
|
|
263
|
-
options.onClose();
|
|
264
|
-
return { handled: true };
|
|
265
|
-
}
|
|
266
|
-
if (e.type === "click" || e.type === "release") return { handled: true };
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
const footer: Component = {
|
|
270
|
-
invalidate() {},
|
|
271
|
-
render: (width) => [
|
|
272
|
-
options.getTheme().fg("dim", truncateToWidth(source ? `↑↓ · bytes ${pages[0]?.start ?? 0}–${pages.at(-1)?.end ?? 0}/${source.size} (window)` : "↑↓", width)),
|
|
273
|
-
],
|
|
274
|
-
};
|
|
275
|
-
let geometry = {
|
|
276
|
-
width: 0,
|
|
277
|
-
height: 0,
|
|
278
|
-
thumbTop: 0,
|
|
279
|
-
thumbHeight: 0,
|
|
280
|
-
maxTop: 0,
|
|
281
|
-
};
|
|
282
|
-
let dragOffset: number | undefined;
|
|
283
|
-
// Stock widget Containers are opaque to native layout. Draw and route this
|
|
284
|
-
// viewport locally, retaining the public ScrollView's follow/scroll state.
|
|
285
|
-
return {
|
|
286
|
-
scrollView,
|
|
287
|
-
render(width: number) {
|
|
288
|
-
width = Math.max(0, Math.floor(width));
|
|
289
|
-
const height = Math.max(0, Math.floor(options.getHeight()) - 2);
|
|
290
|
-
const contentWidth = Math.max(0, width - 1);
|
|
291
|
-
const renderWidth = Math.max(1, contentWidth);
|
|
292
|
-
let lines = body.render(renderWidth);
|
|
293
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
294
|
-
const followAtEof = source && pages.at(-1)?.end === source.size && scrollView.isFollowingEnd;
|
|
295
|
-
if (source && tailDirty && scrollView.scrollTop + height >= lines.length) {
|
|
296
|
-
const anchor = scrollView.scrollTop;
|
|
297
|
-
pages[pages.length - 1] = source.after(pages.at(-1)!.start);
|
|
298
|
-
tailDirty = false; setPages(); lines = body.render(renderWidth);
|
|
299
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
300
|
-
scrollView.scrollTo(anchor, { disableFollow: true });
|
|
301
|
-
}
|
|
302
|
-
if (source && pages.length && height > 0) {
|
|
303
|
-
const top = scrollView.scrollTop;
|
|
304
|
-
const first = pages[0], last = pages[pages.length - 1];
|
|
305
|
-
if (top === 0 && first.start > 0) {
|
|
306
|
-
const page = source.before(first.start);
|
|
307
|
-
if (page.start < first.start) {
|
|
308
|
-
adjacentPageStart = page.start;
|
|
309
|
-
pages.unshift(page); setPages();
|
|
310
|
-
const anchor = top + body.render(renderWidth).length - lines.length;
|
|
311
|
-
lines = body.render(renderWidth);
|
|
312
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
313
|
-
if (followAtEof) scrollView.scrollToEnd();
|
|
314
|
-
else scrollView.scrollTo(anchor, { disableFollow: true });
|
|
315
|
-
}
|
|
316
|
-
} else if (top + height >= lines.length && last.end < source.size) {
|
|
317
|
-
const page = source.after(last.end);
|
|
318
|
-
if (page.end > last.end) {
|
|
319
|
-
adjacentPageStart = page.start;
|
|
320
|
-
pages.push(page); setPages();
|
|
321
|
-
const anchor = top;
|
|
322
|
-
lines = body.render(renderWidth);
|
|
323
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
324
|
-
scrollView.scrollTo(anchor, { disableFollow: true });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
// A resize may make formerly visible pages evictable. Keep only the
|
|
329
|
-
// small window plus pages that actually intersect the viewport.
|
|
330
|
-
while (source && pages.length > 3) {
|
|
331
|
-
const top = scrollView.scrollTop;
|
|
332
|
-
const first = pages.shift()!; setPages();
|
|
333
|
-
let next = body.render(renderWidth);
|
|
334
|
-
const delta = lines.length - next.length;
|
|
335
|
-
if (top >= delta && first.start !== adjacentPageStart) {
|
|
336
|
-
lines = next;
|
|
337
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
338
|
-
if (followAtEof) scrollView.scrollToEnd();
|
|
339
|
-
else scrollView.scrollTo(top - delta, { disableFollow: true });
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
pages.unshift(first);
|
|
343
|
-
const last = pages.pop()!; setPages(); next = body.render(renderWidth);
|
|
344
|
-
if (top + height <= next.length && last.start !== adjacentPageStart) {
|
|
345
|
-
lines = next;
|
|
346
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
347
|
-
if (followAtEof) scrollView.scrollToEnd();
|
|
348
|
-
else scrollView.scrollTo(top, { disableFollow: true });
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
pages.push(last); setPages(); lines = body.render(renderWidth);
|
|
352
|
-
break;
|
|
353
|
-
}
|
|
354
|
-
if (source && source.size === 0 && raw) {
|
|
355
|
-
pages = [source.before()]; tailDirty = false; setPages();
|
|
356
|
-
lines = body.render(renderWidth);
|
|
357
|
-
scrollView.updateLayout(lines.length, height, options.requestRender);
|
|
358
|
-
}
|
|
359
|
-
const visible = lines.slice(
|
|
360
|
-
scrollView.scrollTop,
|
|
361
|
-
scrollView.scrollTop + height,
|
|
362
|
-
);
|
|
363
|
-
const thumbHeight =
|
|
364
|
-
height === 0
|
|
365
|
-
? 0
|
|
366
|
-
: Math.max(
|
|
367
|
-
1,
|
|
368
|
-
Math.min(
|
|
369
|
-
height,
|
|
370
|
-
Math.round((height * height) / Math.max(1, lines.length)),
|
|
371
|
-
),
|
|
372
|
-
);
|
|
373
|
-
const maxTop = Math.max(0, lines.length - height);
|
|
374
|
-
const thumbTop =
|
|
375
|
-
maxTop === 0
|
|
376
|
-
? 0
|
|
377
|
-
: Math.round(
|
|
378
|
-
(scrollView.scrollTop / maxTop) * (height - thumbHeight),
|
|
379
|
-
);
|
|
380
|
-
geometry = { width, height, thumbTop, thumbHeight, maxTop };
|
|
381
|
-
return [
|
|
382
|
-
...heading.render(width),
|
|
383
|
-
...Array.from({ length: height }, (_, row) => {
|
|
384
|
-
if (width === 0) return "";
|
|
385
|
-
const text = truncateToWidth(visible[row] ?? "", contentWidth);
|
|
386
|
-
const thumb = row >= thumbTop && row < thumbTop + thumbHeight;
|
|
387
|
-
return (
|
|
388
|
-
text +
|
|
389
|
-
" ".repeat(Math.max(0, contentWidth - visibleWidth(text))) +
|
|
390
|
-
options
|
|
391
|
-
.getTheme()
|
|
392
|
-
.fg(
|
|
393
|
-
thumb ? "scrollbarThumb" : "scrollbarTrack",
|
|
394
|
-
thumb ? (dragOffset === undefined ? "┃" : "█") : "│",
|
|
395
|
-
)
|
|
396
|
-
);
|
|
397
|
-
}),
|
|
398
|
-
...footer.render(width),
|
|
399
|
-
];
|
|
400
|
-
},
|
|
401
|
-
handleMouse(e: TuiMouseEvent) {
|
|
402
|
-
if (dragOffset !== undefined) {
|
|
403
|
-
if (e.type === "release") {
|
|
404
|
-
dragOffset = undefined;
|
|
405
|
-
return { handled: true, render: true };
|
|
406
|
-
}
|
|
407
|
-
if (e.type === "drag" || e.type === "move") {
|
|
408
|
-
const travel = geometry.height - geometry.thumbHeight;
|
|
409
|
-
const offset = Math.max(0, Math.min(travel, e.y - 1 - dragOffset));
|
|
410
|
-
scrollView.scrollTo(
|
|
411
|
-
travel > 0
|
|
412
|
-
? Math.round((offset / travel) * geometry.maxTop)
|
|
413
|
-
: scrollView.scrollTop,
|
|
414
|
-
);
|
|
415
|
-
return { handled: true, render: true };
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
if (e.y === 0) return heading.handleMouse(e);
|
|
419
|
-
if (e.y < 1 || e.y > geometry.height || e.x < 0 || e.x >= geometry.width)
|
|
420
|
-
return;
|
|
421
|
-
if (e.type === "wheel") {
|
|
422
|
-
scrollView.scrollBy(e.wheelDelta ?? 0);
|
|
423
|
-
return { handled: true, render: true };
|
|
424
|
-
}
|
|
425
|
-
if (e.x === geometry.width - 1 && e.button === "left") {
|
|
426
|
-
if (e.type === "press") {
|
|
427
|
-
const row = e.y - 1;
|
|
428
|
-
if (
|
|
429
|
-
row >= geometry.thumbTop &&
|
|
430
|
-
row < geometry.thumbTop + geometry.thumbHeight
|
|
431
|
-
) {
|
|
432
|
-
dragOffset = row - geometry.thumbTop;
|
|
433
|
-
return { handled: true, capture: true, render: true };
|
|
434
|
-
}
|
|
435
|
-
scrollView.scrollBy(
|
|
436
|
-
row < geometry.thumbTop ? -geometry.height : geometry.height,
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
|
-
if (["press", "click", "release"].includes(e.type))
|
|
440
|
-
return { handled: true, render: true };
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
invalidate() {
|
|
444
|
-
body.invalidate();
|
|
445
|
-
},
|
|
446
|
-
dispose() { source?.dispose(); source = undefined; pages = []; raw = ""; body.invalidate(); },
|
|
447
|
-
updateSource(nextHeader: string, nextSource: FileLogSource, context = "") {
|
|
448
|
-
header = nextHeader;
|
|
449
|
-
const changed = source !== nextSource;
|
|
450
|
-
if (changed) { source?.dispose(); pages = []; }
|
|
451
|
-
source = nextSource;
|
|
452
|
-
const reset = source.refresh();
|
|
453
|
-
isAgent = source.isAgent;
|
|
454
|
-
if (description !== context) body.invalidate();
|
|
455
|
-
description = context;
|
|
456
|
-
if (changed || reset || !pages.length) {
|
|
457
|
-
adjacentPageStart = undefined;
|
|
458
|
-
tailDirty = false;
|
|
459
|
-
pages = [source.before()]; setPages(); scrollView.scrollToEnd();
|
|
460
|
-
} else if (source.size !== sourceSize) {
|
|
461
|
-
if (scrollView.isFollowingEnd && pages.at(-1)?.end === sourceSize) {
|
|
462
|
-
tailDirty = false;
|
|
463
|
-
pages = [source.before()]; setPages(); scrollView.scrollToEnd();
|
|
464
|
-
} else if (pages.at(-1)?.end === sourceSize) {
|
|
465
|
-
tailDirty = true;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (metadataVersion !== source.metadataVersion && !changed && !reset) {
|
|
469
|
-
pages = pages.map(page => source!.retime(page)); setPages();
|
|
470
|
-
}
|
|
471
|
-
metadataVersion = source.metadataVersion;
|
|
472
|
-
sourceSize = source.size;
|
|
473
|
-
options.requestRender();
|
|
474
|
-
},
|
|
475
|
-
update(nextHeader: string, logText: string, agent: boolean, context = "", timestamps?: (number | undefined)[]) {
|
|
476
|
-
if (source) { source.dispose(); source = undefined; pages = []; }
|
|
477
|
-
header = nextHeader;
|
|
478
|
-
if (raw !== logText || isAgent !== agent || description !== context ||
|
|
479
|
-
JSON.stringify(lineTimestamps) !== JSON.stringify(timestamps)) {
|
|
480
|
-
lineTimestamps = timestamps;
|
|
481
|
-
raw = logText;
|
|
482
|
-
isAgent = agent;
|
|
483
|
-
description = context;
|
|
484
|
-
body.invalidate();
|
|
485
|
-
}
|
|
486
|
-
options.requestRender();
|
|
487
|
-
},
|
|
488
|
-
handleKey(data: string): boolean {
|
|
489
|
-
if (matchesKey(data, Key.up)) scrollView.scrollBy(-1);
|
|
490
|
-
else if (matchesKey(data, Key.down)) scrollView.scrollBy(1);
|
|
491
|
-
else return false;
|
|
492
|
-
options.requestRender();
|
|
493
|
-
return true;
|
|
494
|
-
},
|
|
495
|
-
};
|
|
496
|
-
}
|