@oh-my-pi/pi-coding-agent 12.4.0 → 12.5.1
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/CHANGELOG.md +56 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +4 -1
- package/src/config/settings-schema.ts +67 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/main.ts +7 -1
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +14 -0
- package/src/prompts/tools/hashline.md +63 -72
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +87 -9
- package/src/session/agent-session.ts +137 -29
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +10 -2
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
import { copyToClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
|
+
import { type Component, matchesKey, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { theme } from "../modes/theme/theme";
|
|
4
|
+
import { replaceTabs } from "../tools/render-utils";
|
|
5
|
+
import {
|
|
6
|
+
formatDebugLogExpandedLines,
|
|
7
|
+
formatDebugLogLine,
|
|
8
|
+
parseDebugLogPid,
|
|
9
|
+
parseDebugLogTimestampMs,
|
|
10
|
+
} from "./log-formatting";
|
|
11
|
+
import type { DebugLogSource } from "./report-bundle";
|
|
12
|
+
|
|
13
|
+
export const SESSION_BOUNDARY_WARNING = "### WARNING - Logs above are older than current session!";
|
|
14
|
+
export const LOAD_OLDER_LABEL = "### MOVE UP TO LOAD MORE...";
|
|
15
|
+
|
|
16
|
+
const INITIAL_LOG_CHUNK = 50;
|
|
17
|
+
const LOAD_OLDER_CHUNK = 50;
|
|
18
|
+
|
|
19
|
+
type LogEntry = {
|
|
20
|
+
rawLine: string;
|
|
21
|
+
timestampMs: number | undefined;
|
|
22
|
+
pid: number | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CursorToken = { kind: "log"; logIndex: number } | { kind: "load-older" };
|
|
26
|
+
|
|
27
|
+
type DebugLogViewerModelOptions = {
|
|
28
|
+
processStartMs?: number;
|
|
29
|
+
processPid?: number;
|
|
30
|
+
hasOlderLogs?: () => boolean;
|
|
31
|
+
loadOlderLogs?: (limitDays?: number) => Promise<string>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ViewerRow =
|
|
35
|
+
| {
|
|
36
|
+
kind: "warning";
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
kind: "load-older";
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
kind: "log";
|
|
43
|
+
logIndex: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function getProcessStartMs(): number {
|
|
47
|
+
return Date.now() - process.uptime() * 1000;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function splitLogText(logText: string): string[] {
|
|
51
|
+
return logText.split("\n").filter(line => line.length > 0);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function buildLogCopyPayload(lines: string[]): string {
|
|
55
|
+
return lines
|
|
56
|
+
.map(line => sanitizeText(line))
|
|
57
|
+
.filter(line => line.length > 0)
|
|
58
|
+
.join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class DebugLogViewerModel {
|
|
62
|
+
#entries: LogEntry[];
|
|
63
|
+
#rows: ViewerRow[];
|
|
64
|
+
#visibleLogIndices: number[];
|
|
65
|
+
#selectableRowIndices: number[];
|
|
66
|
+
#cursorSelectableIndex = 0;
|
|
67
|
+
#selectionAnchorSelectableIndex: number | undefined;
|
|
68
|
+
#expandedLogIndices = new Set<number>();
|
|
69
|
+
#filterQuery = "";
|
|
70
|
+
#processStartMs: number;
|
|
71
|
+
#loadedStartIndex: number;
|
|
72
|
+
#processFilterEnabled = false;
|
|
73
|
+
#processPid: number;
|
|
74
|
+
#hasOlderLogs?: () => boolean;
|
|
75
|
+
#loadOlderLogs?: (limitDays?: number) => Promise<string>;
|
|
76
|
+
|
|
77
|
+
constructor(logText: string, options: DebugLogViewerModelOptions = {}) {
|
|
78
|
+
const { processStartMs = getProcessStartMs(), processPid = process.pid, hasOlderLogs, loadOlderLogs } = options;
|
|
79
|
+
this.#entries = splitLogText(logText).map(rawLine => ({
|
|
80
|
+
rawLine,
|
|
81
|
+
timestampMs: parseDebugLogTimestampMs(rawLine),
|
|
82
|
+
pid: parseDebugLogPid(rawLine),
|
|
83
|
+
}));
|
|
84
|
+
this.#processStartMs = processStartMs;
|
|
85
|
+
this.#processPid = processPid;
|
|
86
|
+
this.#hasOlderLogs = hasOlderLogs;
|
|
87
|
+
this.#loadOlderLogs = loadOlderLogs;
|
|
88
|
+
this.#loadedStartIndex = Math.max(0, this.#entries.length - INITIAL_LOG_CHUNK);
|
|
89
|
+
this.#rows = [];
|
|
90
|
+
this.#visibleLogIndices = [];
|
|
91
|
+
this.#selectableRowIndices = [];
|
|
92
|
+
this.#rebuildRows();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get logCount(): number {
|
|
96
|
+
return this.#entries.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get visibleLogCount(): number {
|
|
100
|
+
return this.#visibleLogIndices.length;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get rows(): readonly ViewerRow[] {
|
|
104
|
+
return this.#rows;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get cursorRowIndex(): number | undefined {
|
|
108
|
+
return this.#selectableRowIndices[this.#cursorSelectableIndex];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get cursorLogIndex(): number | undefined {
|
|
112
|
+
const row = this.#getCursorRow();
|
|
113
|
+
return row?.kind === "log" ? row.logIndex : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get filterQuery(): string {
|
|
117
|
+
return this.#filterQuery;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get cursorRowKind(): ViewerRow["kind"] | undefined {
|
|
121
|
+
return this.#getCursorRow()?.kind;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get expandedCount(): number {
|
|
125
|
+
return this.#expandedLogIndices.size;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isProcessFilterEnabled(): boolean {
|
|
129
|
+
return this.#processFilterEnabled;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
isCursorAtFirstSelectableRow(): boolean {
|
|
133
|
+
return this.#cursorSelectableIndex === 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getRawLine(logIndex: number): string {
|
|
137
|
+
return this.#entries[logIndex]?.rawLine ?? "";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setFilterQuery(query: string): void {
|
|
141
|
+
if (query === this.#filterQuery) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.#filterQuery = query;
|
|
145
|
+
this.#rebuildRows();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
toggleProcessFilter(): void {
|
|
149
|
+
this.#processFilterEnabled = !this.#processFilterEnabled;
|
|
150
|
+
this.#rebuildRows();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
moveCursor(delta: number, extendSelection: boolean): void {
|
|
154
|
+
if (this.#selectableRowIndices.length === 0) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (extendSelection && this.#selectionAnchorSelectableIndex === undefined) {
|
|
159
|
+
const row = this.#getCursorRow();
|
|
160
|
+
if (row?.kind === "log") {
|
|
161
|
+
this.#selectionAnchorSelectableIndex = this.#cursorSelectableIndex;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
this.#cursorSelectableIndex = Math.max(
|
|
166
|
+
0,
|
|
167
|
+
Math.min(this.#selectableRowIndices.length - 1, this.#cursorSelectableIndex + delta),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!extendSelection) {
|
|
171
|
+
this.#selectionAnchorSelectableIndex = undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.#getCursorRow()?.kind !== "log" && !extendSelection) {
|
|
175
|
+
this.#selectionAnchorSelectableIndex = undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
getSelectedLogIndices(): number[] {
|
|
180
|
+
if (this.#selectableRowIndices.length === 0) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const cursorRow = this.#getCursorRow();
|
|
185
|
+
if (this.#selectionAnchorSelectableIndex === undefined) {
|
|
186
|
+
if (cursorRow?.kind !== "log") {
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
return [cursorRow.logIndex];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const min = Math.min(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
|
|
193
|
+
const max = Math.max(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
|
|
194
|
+
const selected: number[] = [];
|
|
195
|
+
for (let i = min; i <= max; i++) {
|
|
196
|
+
const rowIndex = this.#selectableRowIndices[i];
|
|
197
|
+
const row = rowIndex === undefined ? undefined : this.#rows[rowIndex];
|
|
198
|
+
if (row?.kind === "log") {
|
|
199
|
+
selected.push(row.logIndex);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return selected;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getSelectedCount(): number {
|
|
206
|
+
return this.getSelectedLogIndices().length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
isSelected(logIndex: number): boolean {
|
|
210
|
+
const selected = this.getSelectedLogIndices();
|
|
211
|
+
return selected.includes(logIndex);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
isExpanded(logIndex: number): boolean {
|
|
215
|
+
return this.#expandedLogIndices.has(logIndex);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
expandSelected(): void {
|
|
219
|
+
for (const index of this.getSelectedLogIndices()) {
|
|
220
|
+
this.#expandedLogIndices.add(index);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
collapseSelected(): void {
|
|
225
|
+
for (const index of this.getSelectedLogIndices()) {
|
|
226
|
+
this.#expandedLogIndices.delete(index);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getSelectedRawLines(): string[] {
|
|
231
|
+
const selectedIndices = this.getSelectedLogIndices();
|
|
232
|
+
return selectedIndices.map(index => this.getRawLine(index));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
selectAllVisible(): void {
|
|
236
|
+
if (this.#selectableRowIndices.length === 0) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let firstLogIndex: number | undefined;
|
|
241
|
+
let lastLogIndex: number | undefined;
|
|
242
|
+
for (let i = 0; i < this.#selectableRowIndices.length; i++) {
|
|
243
|
+
const rowIndex = this.#selectableRowIndices[i];
|
|
244
|
+
const row = rowIndex === undefined ? undefined : this.#rows[rowIndex];
|
|
245
|
+
if (row?.kind === "log") {
|
|
246
|
+
if (firstLogIndex === undefined) {
|
|
247
|
+
firstLogIndex = i;
|
|
248
|
+
}
|
|
249
|
+
lastLogIndex = i;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (firstLogIndex === undefined || lastLogIndex === undefined) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
this.#selectionAnchorSelectableIndex = firstLogIndex;
|
|
258
|
+
this.#cursorSelectableIndex = lastLogIndex;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
canLoadOlder(): boolean {
|
|
262
|
+
return this.#loadedStartIndex > 0 || this.#hasExternalOlderLogs();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async loadOlder(additionalCount: number = LOAD_OLDER_CHUNK): Promise<boolean> {
|
|
266
|
+
if (this.#loadedStartIndex > 0) {
|
|
267
|
+
return this.#loadOlderInMemory(additionalCount);
|
|
268
|
+
}
|
|
269
|
+
if (!this.#loadOlderLogs || !this.#hasExternalOlderLogs()) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const olderText = await this.#loadOlderLogs();
|
|
273
|
+
if (olderText.length === 0) {
|
|
274
|
+
if (!this.#hasExternalOlderLogs()) {
|
|
275
|
+
this.#rebuildRows();
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
const added = this.prependLogs(olderText);
|
|
280
|
+
if (added === 0) {
|
|
281
|
+
if (!this.#hasExternalOlderLogs()) {
|
|
282
|
+
this.#rebuildRows();
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
return this.#loadOlderInMemory(additionalCount);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
prependLogs(logText: string): number {
|
|
290
|
+
const previousCursor = this.#getCursorToken();
|
|
291
|
+
const previousAnchorLogIndex = this.#getAnchorLogIndex();
|
|
292
|
+
const newEntries = splitLogText(logText).map(rawLine => ({
|
|
293
|
+
rawLine,
|
|
294
|
+
timestampMs: parseDebugLogTimestampMs(rawLine),
|
|
295
|
+
pid: parseDebugLogPid(rawLine),
|
|
296
|
+
}));
|
|
297
|
+
if (newEntries.length === 0) {
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
const offset = newEntries.length;
|
|
301
|
+
this.#entries = [...newEntries, ...this.#entries];
|
|
302
|
+
this.#loadedStartIndex += offset;
|
|
303
|
+
this.#expandedLogIndices = new Set([...this.#expandedLogIndices].map(logIndex => logIndex + offset));
|
|
304
|
+
const adjustedCursor: CursorToken | undefined =
|
|
305
|
+
previousCursor?.kind === "log" ? { kind: "log", logIndex: previousCursor.logIndex + offset } : previousCursor;
|
|
306
|
+
const adjustedAnchor = previousAnchorLogIndex === undefined ? undefined : previousAnchorLogIndex + offset;
|
|
307
|
+
this.#rebuildRows(adjustedCursor, adjustedAnchor);
|
|
308
|
+
return offset;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#loadOlderInMemory(additionalCount: number = LOAD_OLDER_CHUNK): boolean {
|
|
312
|
+
if (this.#loadedStartIndex === 0) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
const requested = Math.max(1, additionalCount);
|
|
316
|
+
const nextStart = Math.max(0, this.#loadedStartIndex - requested);
|
|
317
|
+
if (nextStart === this.#loadedStartIndex) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
this.#loadedStartIndex = nextStart;
|
|
321
|
+
this.#rebuildRows();
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#rebuildRows(
|
|
326
|
+
previousCursor: CursorToken | undefined = this.#getCursorToken(),
|
|
327
|
+
previousAnchorLogIndex = this.#getAnchorLogIndex(),
|
|
328
|
+
): void {
|
|
329
|
+
const query = this.#filterQuery.toLowerCase();
|
|
330
|
+
const visible: number[] = [];
|
|
331
|
+
for (let i = this.#loadedStartIndex; i < this.#entries.length; i++) {
|
|
332
|
+
const entry = this.#entries[i];
|
|
333
|
+
if (!entry) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (this.#matchesFilters(entry, query)) {
|
|
337
|
+
visible.push(i);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
this.#visibleLogIndices = visible;
|
|
341
|
+
|
|
342
|
+
const rows: ViewerRow[] = [];
|
|
343
|
+
if (this.#hasOlderEntries(query)) {
|
|
344
|
+
rows.push({ kind: "load-older" });
|
|
345
|
+
}
|
|
346
|
+
let olderSeen = false;
|
|
347
|
+
let warningInserted = false;
|
|
348
|
+
for (const logIndex of visible) {
|
|
349
|
+
const timestampMs = this.#entries[logIndex]?.timestampMs;
|
|
350
|
+
if (timestampMs !== undefined) {
|
|
351
|
+
if (timestampMs < this.#processStartMs) {
|
|
352
|
+
olderSeen = true;
|
|
353
|
+
} else if (olderSeen && !warningInserted) {
|
|
354
|
+
rows.push({ kind: "warning" });
|
|
355
|
+
warningInserted = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
rows.push({ kind: "log", logIndex });
|
|
359
|
+
}
|
|
360
|
+
this.#rows = rows;
|
|
361
|
+
this.#selectableRowIndices = rows
|
|
362
|
+
.map((row, index) => (row.kind === "warning" ? undefined : index))
|
|
363
|
+
.filter((index): index is number => index !== undefined);
|
|
364
|
+
|
|
365
|
+
if (this.#selectableRowIndices.length === 0) {
|
|
366
|
+
this.#cursorSelectableIndex = 0;
|
|
367
|
+
this.#selectionAnchorSelectableIndex = undefined;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (previousCursor?.kind === "log") {
|
|
372
|
+
const rowIndex = this.#rows.findIndex(row => row.kind === "log" && row.logIndex === previousCursor.logIndex);
|
|
373
|
+
const selectableIndex = this.#selectableRowIndices.indexOf(rowIndex);
|
|
374
|
+
if (selectableIndex >= 0) {
|
|
375
|
+
this.#cursorSelectableIndex = selectableIndex;
|
|
376
|
+
} else {
|
|
377
|
+
this.#cursorSelectableIndex = this.#selectableRowIndices.length - 1;
|
|
378
|
+
}
|
|
379
|
+
} else if (previousCursor?.kind === "load-older") {
|
|
380
|
+
const rowIndex = this.#rows.findIndex(row => row.kind === "load-older");
|
|
381
|
+
const selectableIndex = this.#selectableRowIndices.indexOf(rowIndex);
|
|
382
|
+
this.#cursorSelectableIndex = selectableIndex >= 0 ? selectableIndex : this.#selectableRowIndices.length - 1;
|
|
383
|
+
} else {
|
|
384
|
+
this.#cursorSelectableIndex = this.#selectableRowIndices.length - 1;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (previousAnchorLogIndex !== undefined) {
|
|
388
|
+
const rowIndex = this.#rows.findIndex(row => row.kind === "log" && row.logIndex === previousAnchorLogIndex);
|
|
389
|
+
const selectableIndex = this.#selectableRowIndices.indexOf(rowIndex);
|
|
390
|
+
this.#selectionAnchorSelectableIndex = selectableIndex >= 0 ? selectableIndex : undefined;
|
|
391
|
+
} else {
|
|
392
|
+
this.#selectionAnchorSelectableIndex = undefined;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#matchesFilters(entry: LogEntry, query: string): boolean {
|
|
397
|
+
if (query.length > 0 && !entry.rawLine.toLowerCase().includes(query)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
if (!this.#processFilterEnabled) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
return entry.pid === this.#processPid;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#hasOlderEntries(query: string): boolean {
|
|
407
|
+
if (this.#hasExternalOlderLogs()) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
if (this.#loadedStartIndex === 0) {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
for (let i = 0; i < this.#loadedStartIndex; i++) {
|
|
414
|
+
const entry = this.#entries[i];
|
|
415
|
+
if (entry && this.#matchesFilters(entry, query)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#hasExternalOlderLogs(): boolean {
|
|
423
|
+
return this.#hasOlderLogs?.() ?? false;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
#getCursorRow(): ViewerRow | undefined {
|
|
427
|
+
const rowIndex = this.cursorRowIndex;
|
|
428
|
+
return rowIndex === undefined ? undefined : this.#rows[rowIndex];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
#getCursorToken(): CursorToken | undefined {
|
|
432
|
+
const row = this.#getCursorRow();
|
|
433
|
+
if (!row) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
if (row.kind === "log") {
|
|
437
|
+
return { kind: "log", logIndex: row.logIndex };
|
|
438
|
+
}
|
|
439
|
+
if (row.kind === "load-older") {
|
|
440
|
+
return { kind: "load-older" };
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#getAnchorLogIndex(): number | undefined {
|
|
446
|
+
if (this.#selectionAnchorSelectableIndex === undefined) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
const rowIndex = this.#selectableRowIndices[this.#selectionAnchorSelectableIndex];
|
|
450
|
+
const row = rowIndex === undefined ? undefined : this.#rows[rowIndex];
|
|
451
|
+
return row?.kind === "log" ? row.logIndex : undefined;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
interface DebugLogViewerComponentOptions {
|
|
456
|
+
logs: string;
|
|
457
|
+
terminalRows: number;
|
|
458
|
+
onExit: () => void;
|
|
459
|
+
onStatus?: (message: string) => void;
|
|
460
|
+
onError?: (message: string) => void;
|
|
461
|
+
processStartMs?: number;
|
|
462
|
+
processPid?: number;
|
|
463
|
+
logSource?: DebugLogSource;
|
|
464
|
+
onUpdate?: () => void;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export class DebugLogViewerComponent implements Component {
|
|
468
|
+
#model: DebugLogViewerModel;
|
|
469
|
+
#terminalRows: number;
|
|
470
|
+
#onExit: () => void;
|
|
471
|
+
#onStatus?: (message: string) => void;
|
|
472
|
+
#onError?: (message: string) => void;
|
|
473
|
+
#onUpdate?: () => void;
|
|
474
|
+
#logSource?: DebugLogSource;
|
|
475
|
+
#lastRenderWidth = 80;
|
|
476
|
+
#scrollRowOffset = 0;
|
|
477
|
+
#statusMessage: string | undefined;
|
|
478
|
+
#loadingOlder = false;
|
|
479
|
+
|
|
480
|
+
constructor(options: DebugLogViewerComponentOptions) {
|
|
481
|
+
this.#logSource = options.logSource;
|
|
482
|
+
this.#model = new DebugLogViewerModel(options.logs, {
|
|
483
|
+
processStartMs: options.processStartMs,
|
|
484
|
+
processPid: options.processPid,
|
|
485
|
+
hasOlderLogs: this.#logSource?.hasOlderLogs.bind(this.#logSource),
|
|
486
|
+
loadOlderLogs: this.#logSource?.loadOlderLogs.bind(this.#logSource),
|
|
487
|
+
});
|
|
488
|
+
this.#terminalRows = options.terminalRows;
|
|
489
|
+
this.#onExit = options.onExit;
|
|
490
|
+
this.#onStatus = options.onStatus;
|
|
491
|
+
this.#onError = options.onError;
|
|
492
|
+
this.#onUpdate = options.onUpdate;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
handleInput(keyData: string): void {
|
|
496
|
+
if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
|
|
497
|
+
this.#onExit();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (matchesKey(keyData, "ctrl+c")) {
|
|
502
|
+
void this.#copySelected();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (matchesKey(keyData, "ctrl+p")) {
|
|
507
|
+
this.#statusMessage = undefined;
|
|
508
|
+
this.#model.toggleProcessFilter();
|
|
509
|
+
this.#ensureCursorVisible();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (matchesKey(keyData, "ctrl+a")) {
|
|
514
|
+
this.#statusMessage = undefined;
|
|
515
|
+
this.#model.selectAllVisible();
|
|
516
|
+
this.#ensureCursorVisible();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (matchesKey(keyData, "ctrl+o")) {
|
|
521
|
+
this.#statusMessage = undefined;
|
|
522
|
+
void this.#handleLoadOlder(this.#bodyHeight() + 1);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return")) {
|
|
527
|
+
if (this.#model.cursorRowKind === "load-older") {
|
|
528
|
+
this.#statusMessage = undefined;
|
|
529
|
+
void this.#handleLoadOlder();
|
|
530
|
+
}
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (matchesKey(keyData, "shift+up")) {
|
|
535
|
+
this.#statusMessage = undefined;
|
|
536
|
+
void this.#handleMoveUp(true);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (matchesKey(keyData, "shift+down")) {
|
|
541
|
+
this.#statusMessage = undefined;
|
|
542
|
+
this.#model.moveCursor(1, true);
|
|
543
|
+
this.#ensureCursorVisible();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (matchesKey(keyData, "up")) {
|
|
548
|
+
this.#statusMessage = undefined;
|
|
549
|
+
void this.#handleMoveUp(false);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (matchesKey(keyData, "down")) {
|
|
554
|
+
this.#statusMessage = undefined;
|
|
555
|
+
this.#model.moveCursor(1, false);
|
|
556
|
+
this.#ensureCursorVisible();
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (matchesKey(keyData, "right")) {
|
|
561
|
+
this.#statusMessage = undefined;
|
|
562
|
+
if (this.#model.cursorRowKind === "load-older") {
|
|
563
|
+
void this.#handleLoadOlder();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
this.#model.expandSelected();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (matchesKey(keyData, "left")) {
|
|
571
|
+
this.#statusMessage = undefined;
|
|
572
|
+
this.#model.collapseSelected();
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (matchesKey(keyData, "backspace")) {
|
|
577
|
+
if (this.#model.filterQuery.length > 0) {
|
|
578
|
+
this.#statusMessage = undefined;
|
|
579
|
+
this.#model.setFilterQuery(this.#model.filterQuery.slice(0, -1));
|
|
580
|
+
this.#ensureCursorVisible();
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const hasControlChars = [...keyData].some(ch => {
|
|
586
|
+
const code = ch.charCodeAt(0);
|
|
587
|
+
return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
|
|
588
|
+
});
|
|
589
|
+
if (!hasControlChars && keyData.length > 0) {
|
|
590
|
+
this.#statusMessage = undefined;
|
|
591
|
+
this.#model.setFilterQuery(this.#model.filterQuery + keyData);
|
|
592
|
+
this.#ensureCursorVisible();
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
invalidate(): void {
|
|
597
|
+
// no cached child state
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
render(width: number): string[] {
|
|
601
|
+
this.#lastRenderWidth = Math.max(20, width);
|
|
602
|
+
this.#ensureCursorVisible();
|
|
603
|
+
|
|
604
|
+
const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
|
|
605
|
+
const bodyHeight = this.#bodyHeight();
|
|
606
|
+
|
|
607
|
+
const rows = this.#renderRows(innerWidth);
|
|
608
|
+
const visibleBodyLines = this.#renderVisibleBodyLines(rows, innerWidth, bodyHeight);
|
|
609
|
+
|
|
610
|
+
return [
|
|
611
|
+
this.#frameTop(innerWidth),
|
|
612
|
+
this.#frameLine(this.#summaryText(), innerWidth),
|
|
613
|
+
this.#frameSeparator(innerWidth),
|
|
614
|
+
this.#frameLine(this.#filterText(), innerWidth),
|
|
615
|
+
this.#frameSeparator(innerWidth),
|
|
616
|
+
...visibleBodyLines,
|
|
617
|
+
this.#frameLine(this.#statusText(), innerWidth),
|
|
618
|
+
this.#frameBottom(innerWidth),
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
#summaryText(): string {
|
|
623
|
+
return ` # ${this.#model.visibleLogCount}/${this.#model.logCount} logs | ${this.#controlsText()}`;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
#controlsText(): string {
|
|
627
|
+
return "Esc: back Ctrl+C: copy Up/Down: move Shift+Up/Down: select range Left/Right: collapse/expand Ctrl+A: select all Ctrl+O: load older Ctrl+P: pid filter";
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
#filterText(): string {
|
|
631
|
+
const sanitized = replaceTabs(sanitizeText(this.#model.filterQuery));
|
|
632
|
+
const query = sanitized.length === 0 ? "" : theme.fg("accent", sanitized);
|
|
633
|
+
const pidStatus = this.#model.isProcessFilterEnabled()
|
|
634
|
+
? theme.fg("success", "pid:on")
|
|
635
|
+
: theme.fg("muted", "pid:off");
|
|
636
|
+
return ` filter: ${query} ${pidStatus}`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
#statusText(): string {
|
|
640
|
+
const base = ` Selected: ${this.#model.getSelectedCount()} Expanded: ${this.#model.expandedCount}`;
|
|
641
|
+
if (this.#statusMessage) {
|
|
642
|
+
return `${base} ${this.#statusMessage}`;
|
|
643
|
+
}
|
|
644
|
+
return base;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#bodyHeight(): number {
|
|
648
|
+
return Math.max(3, this.#terminalRows - 8);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async #handleLoadOlder(additionalCount: number = LOAD_OLDER_CHUNK): Promise<void> {
|
|
652
|
+
const loaded = await this.#loadOlder(additionalCount);
|
|
653
|
+
if (loaded) {
|
|
654
|
+
this.#ensureCursorVisible();
|
|
655
|
+
this.#onUpdate?.();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async #handleMoveUp(extendSelection: boolean): Promise<void> {
|
|
660
|
+
if (this.#model.cursorRowKind === "load-older") {
|
|
661
|
+
const loaded = await this.#loadOlder(LOAD_OLDER_CHUNK);
|
|
662
|
+
if (loaded) {
|
|
663
|
+
this.#ensureCursorVisible();
|
|
664
|
+
this.#onUpdate?.();
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (this.#model.canLoadOlder() && this.#model.isCursorAtFirstSelectableRow()) {
|
|
670
|
+
const loaded = await this.#loadOlder(LOAD_OLDER_CHUNK);
|
|
671
|
+
if (loaded) {
|
|
672
|
+
this.#model.moveCursor(-1, extendSelection);
|
|
673
|
+
this.#ensureCursorVisible();
|
|
674
|
+
this.#onUpdate?.();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
this.#model.moveCursor(-1, extendSelection);
|
|
680
|
+
this.#ensureCursorVisible();
|
|
681
|
+
this.#onUpdate?.();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
async #loadOlder(additionalCount: number): Promise<boolean> {
|
|
685
|
+
if (this.#loadingOlder || !this.#model.canLoadOlder()) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
this.#loadingOlder = true;
|
|
689
|
+
const previousCursorRowIndex = this.#model.cursorRowIndex;
|
|
690
|
+
const previousScrollOffset = this.#scrollRowOffset;
|
|
691
|
+
try {
|
|
692
|
+
const didLoad = await this.#model.loadOlder(additionalCount);
|
|
693
|
+
if (didLoad) {
|
|
694
|
+
this.#preserveScrollPosition(previousCursorRowIndex, previousScrollOffset);
|
|
695
|
+
}
|
|
696
|
+
return didLoad;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
699
|
+
this.#statusMessage = `Load older failed: ${message}`;
|
|
700
|
+
this.#onError?.(`Failed to load older logs: ${message}`);
|
|
701
|
+
this.#onUpdate?.();
|
|
702
|
+
return false;
|
|
703
|
+
} finally {
|
|
704
|
+
this.#loadingOlder = false;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
#preserveScrollPosition(previousCursorRowIndex: number | undefined, previousScrollOffset: number): void {
|
|
709
|
+
const cursorRowIndex = this.#model.cursorRowIndex;
|
|
710
|
+
if (previousCursorRowIndex === undefined || cursorRowIndex === undefined) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const delta = cursorRowIndex - previousCursorRowIndex;
|
|
714
|
+
const nextOffset = previousScrollOffset + delta;
|
|
715
|
+
const maxOffset = Math.max(0, this.#model.rows.length - this.#bodyHeight());
|
|
716
|
+
this.#scrollRowOffset = Math.max(0, Math.min(maxOffset, nextOffset));
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
#renderRows(innerWidth: number): Array<{ lines: string[]; rowIndex: number }> {
|
|
720
|
+
const rendered: Array<{ lines: string[]; rowIndex: number }> = [];
|
|
721
|
+
|
|
722
|
+
for (let rowIndex = 0; rowIndex < this.#model.rows.length; rowIndex++) {
|
|
723
|
+
const row = this.#model.rows[rowIndex];
|
|
724
|
+
if (!row) {
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (row.kind === "warning") {
|
|
729
|
+
rendered.push({
|
|
730
|
+
rowIndex,
|
|
731
|
+
lines: [theme.fg("muted", truncateToWidth(SESSION_BOUNDARY_WARNING, innerWidth))],
|
|
732
|
+
});
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (row.kind === "load-older") {
|
|
737
|
+
const active = this.#model.cursorRowIndex === rowIndex;
|
|
738
|
+
const marker = active ? theme.fg("accent", "❯") : " ";
|
|
739
|
+
const prefix = `${marker} `;
|
|
740
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
|
|
741
|
+
const label = truncateToWidth(LOAD_OLDER_LABEL, contentWidth);
|
|
742
|
+
rendered.push({
|
|
743
|
+
rowIndex,
|
|
744
|
+
lines: [truncateToWidth(`${prefix}${theme.fg("muted", label)}`, innerWidth)],
|
|
745
|
+
});
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const logIndex = row.logIndex;
|
|
750
|
+
const selected = this.#model.isSelected(logIndex);
|
|
751
|
+
const cursorLogIndex = this.#model.cursorLogIndex;
|
|
752
|
+
const active = cursorLogIndex !== undefined && cursorLogIndex === logIndex;
|
|
753
|
+
const expanded = this.#model.isExpanded(logIndex);
|
|
754
|
+
const marker = active ? theme.fg("accent", "❯") : selected ? theme.fg("accent", "•") : " ";
|
|
755
|
+
const fold = expanded ? theme.fg("accent", "▾") : theme.fg("muted", "▸");
|
|
756
|
+
const prefix = `${marker}${fold} `;
|
|
757
|
+
const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
|
|
758
|
+
|
|
759
|
+
if (expanded) {
|
|
760
|
+
const wrapped = formatDebugLogExpandedLines(this.#model.getRawLine(logIndex), contentWidth);
|
|
761
|
+
const indent = padding(visibleWidth(prefix));
|
|
762
|
+
const lines = wrapped.map((segment, index) => {
|
|
763
|
+
const content = selected ? theme.bold(segment) : segment;
|
|
764
|
+
return truncateToWidth(`${index === 0 ? prefix : indent}${content}`, innerWidth);
|
|
765
|
+
});
|
|
766
|
+
rendered.push({ rowIndex, lines });
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
const preview = formatDebugLogLine(this.#model.getRawLine(logIndex), contentWidth);
|
|
771
|
+
const content = selected ? theme.bold(preview) : preview;
|
|
772
|
+
rendered.push({ rowIndex, lines: [truncateToWidth(`${prefix}${content}`, innerWidth)] });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return rendered;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
#renderVisibleBodyLines(
|
|
779
|
+
rows: Array<{ lines: string[]; rowIndex: number }>,
|
|
780
|
+
innerWidth: number,
|
|
781
|
+
bodyHeight: number,
|
|
782
|
+
): string[] {
|
|
783
|
+
const lines: string[] = [];
|
|
784
|
+
if (rows.length === 0) {
|
|
785
|
+
lines.push(this.#frameLine(theme.fg("muted", "no matches"), innerWidth));
|
|
786
|
+
}
|
|
787
|
+
for (let i = this.#scrollRowOffset; i < rows.length; i++) {
|
|
788
|
+
const row = rows[i];
|
|
789
|
+
if (!row) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
for (const line of row.lines) {
|
|
794
|
+
if (lines.length >= bodyHeight) {
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
lines.push(this.#frameLine(line, innerWidth));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (lines.length >= bodyHeight) {
|
|
801
|
+
break;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
while (lines.length < bodyHeight) {
|
|
806
|
+
lines.push(this.#frameLine("", innerWidth));
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return lines;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Returns the number of rendered screen lines for a given row index. */
|
|
813
|
+
#getRowRenderedLineCount(rowIndex: number, innerWidth: number): number {
|
|
814
|
+
const row = this.#model.rows[rowIndex];
|
|
815
|
+
if (!row || row.kind === "warning" || row.kind === "load-older") {
|
|
816
|
+
return 1;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (!this.#model.isExpanded(row.logIndex)) {
|
|
820
|
+
return 1;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Expanded row: compute prefix visible width and get wrapped line count
|
|
824
|
+
// Prefix is marker(1) + fold(1) + space(1) = 3 visible chars
|
|
825
|
+
const prefixVisibleWidth = 3;
|
|
826
|
+
const contentWidth = Math.max(1, innerWidth - prefixVisibleWidth);
|
|
827
|
+
const wrapped = formatDebugLogExpandedLines(this.#model.getRawLine(row.logIndex), contentWidth);
|
|
828
|
+
return Math.max(1, wrapped.length);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
#ensureCursorVisible(): void {
|
|
832
|
+
const cursorRowIndex = this.#model.cursorRowIndex;
|
|
833
|
+
if (cursorRowIndex === undefined) {
|
|
834
|
+
this.#scrollRowOffset = 0;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const bodyHeight = Math.max(1, this.#bodyHeight());
|
|
838
|
+
const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
|
|
839
|
+
|
|
840
|
+
// Scroll up: cursor is above viewport
|
|
841
|
+
if (cursorRowIndex < this.#scrollRowOffset) {
|
|
842
|
+
this.#scrollRowOffset = cursorRowIndex;
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// Scroll down: sum rendered line heights from scrollRowOffset to cursorRowIndex (inclusive)
|
|
846
|
+
// to check if the cursor row actually fits in the viewport
|
|
847
|
+
let usedLines = 0;
|
|
848
|
+
for (let i = this.#scrollRowOffset; i <= cursorRowIndex; i++) {
|
|
849
|
+
usedLines += this.#getRowRenderedLineCount(i, innerWidth);
|
|
850
|
+
}
|
|
851
|
+
if (usedLines > bodyHeight) {
|
|
852
|
+
// Advance scrollRowOffset until the cursor row fits
|
|
853
|
+
while (this.#scrollRowOffset < cursorRowIndex) {
|
|
854
|
+
usedLines -= this.#getRowRenderedLineCount(this.#scrollRowOffset, innerWidth);
|
|
855
|
+
this.#scrollRowOffset++;
|
|
856
|
+
if (usedLines <= bodyHeight) {
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
#frameTop(innerWidth: number): string {
|
|
864
|
+
return `${theme.boxSharp.topLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.topRight}`;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#frameSeparator(innerWidth: number): string {
|
|
868
|
+
return `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.teeLeft}`;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
#frameBottom(innerWidth: number): string {
|
|
872
|
+
return `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.bottomRight}`;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
#frameLine(content: string, innerWidth: number): string {
|
|
876
|
+
const truncated = truncateToWidth(content, innerWidth);
|
|
877
|
+
const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
|
|
878
|
+
return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
async #copySelected(): Promise<void> {
|
|
882
|
+
const selectedPayload = buildLogCopyPayload(this.#model.getSelectedRawLines());
|
|
883
|
+
const selected = selectedPayload.length === 0 ? [] : selectedPayload.split("\n");
|
|
884
|
+
|
|
885
|
+
if (selected.length === 0) {
|
|
886
|
+
const message = "No log entry selected";
|
|
887
|
+
this.#statusMessage = message;
|
|
888
|
+
this.#onStatus?.(message);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
try {
|
|
893
|
+
await copyToClipboard(selectedPayload);
|
|
894
|
+
const message = `Copied ${selected.length} log ${selected.length === 1 ? "entry" : "entries"}`;
|
|
895
|
+
this.#statusMessage = message;
|
|
896
|
+
this.#onStatus?.(message);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
899
|
+
this.#statusMessage = `Copy failed: ${message}`;
|
|
900
|
+
this.#onError?.(`Failed to copy logs: ${message}`);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|