@narumitw/pi-btw 0.58.1 → 0.60.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/README.md +8 -1
- package/dist/index.ts +490 -385
- package/dist/index.ts.map +4 -4
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +803 -856
- package/src/fullscreen-ui.ts +673 -674
- package/src/keybindings.ts +228 -270
- package/src/main-tree-picker.ts +309 -318
- package/src/menu.ts +645 -454
- package/src/settings.ts +208 -219
- package/src/side-thread.ts +173 -201
- package/src/text.ts +21 -23
- package/src/transcript-markdown.ts +65 -0
- package/src/transcript-pager.ts +611 -662
package/src/bring-to-main.ts
CHANGED
|
@@ -1,632 +1,579 @@
|
|
|
1
1
|
import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
type Component,
|
|
4
|
-
Key,
|
|
5
|
-
matchesKey,
|
|
6
|
-
type TUI,
|
|
7
|
-
truncateToWidth,
|
|
8
|
-
visibleWidth,
|
|
9
|
-
} from "@earendil-works/pi-tui";
|
|
2
|
+
import { type Component, Key, matchesKey, type TUI, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
3
|
import type { SideThreadTurn } from "./side-thread.js";
|
|
11
4
|
|
|
12
5
|
const RESERVED_APP_ROWS = 3;
|
|
13
6
|
const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
14
7
|
|
|
15
8
|
export interface BtwBringToMainSegment {
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
role: "user" | "assistant";
|
|
10
|
+
text: string;
|
|
18
11
|
}
|
|
19
12
|
|
|
20
13
|
export interface BtwBringToMainSummary {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
lines: number;
|
|
15
|
+
messages: number;
|
|
16
|
+
tokens: number;
|
|
24
17
|
}
|
|
25
18
|
|
|
26
19
|
export interface BtwSelectionLine {
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
role: BtwBringToMainSegment["role"];
|
|
21
|
+
text: string;
|
|
29
22
|
}
|
|
30
23
|
|
|
31
24
|
export interface BtwTextPosition {
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
line: number;
|
|
26
|
+
column: number;
|
|
34
27
|
}
|
|
35
28
|
|
|
36
29
|
export interface BtwTextRangeSelectorState {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
30
|
+
cursor: BtwTextPosition;
|
|
31
|
+
anchor?: BtwTextPosition;
|
|
32
|
+
lineAnchor?: number;
|
|
33
|
+
preferredColumn: number;
|
|
34
|
+
scrollOffset: number;
|
|
35
|
+
horizontalOffset: number;
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export type BtwQuickBringToMainScope =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
| { kind: "latest" }
|
|
40
|
+
| { kind: "from"; answeredTurnIndex: number }
|
|
41
|
+
| { kind: "entire" };
|
|
49
42
|
|
|
50
43
|
export type BtwTextRangeSelectorAction =
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export function getAnsweredTurns(
|
|
56
|
-
|
|
57
|
-
): Array<Extract<SideThreadTurn, { kind: "answered" }>> {
|
|
58
|
-
return turns.filter(
|
|
59
|
-
(turn): turn is Extract<SideThreadTurn, { kind: "answered" }> => turn.kind === "answered",
|
|
60
|
-
);
|
|
44
|
+
| { kind: "confirm"; segments: BtwBringToMainSegment[] }
|
|
45
|
+
| { kind: "back" }
|
|
46
|
+
| { kind: "close" };
|
|
47
|
+
|
|
48
|
+
export function getAnsweredTurns(turns: readonly SideThreadTurn[]): Extract<SideThreadTurn, { kind: "answered" }>[] {
|
|
49
|
+
return turns.filter((turn): turn is Extract<SideThreadTurn, { kind: "answered" }> => turn.kind === "answered");
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
export function buildQuickBringToMainSegments(
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
turns: readonly SideThreadTurn[],
|
|
54
|
+
scope: BtwQuickBringToMainScope,
|
|
66
55
|
): BtwBringToMainSegment[] {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
56
|
+
const answered = getAnsweredTurns(turns);
|
|
57
|
+
const selected =
|
|
58
|
+
scope.kind === "latest"
|
|
59
|
+
? answered.slice(-1)
|
|
60
|
+
: scope.kind === "from"
|
|
61
|
+
? answered.slice(Math.max(0, scope.answeredTurnIndex))
|
|
62
|
+
: answered;
|
|
63
|
+
return selected.flatMap((turn) => [
|
|
64
|
+
{ role: "user" as const, text: turn.question },
|
|
65
|
+
{ role: "assistant" as const, text: turn.answer },
|
|
66
|
+
]);
|
|
78
67
|
}
|
|
79
68
|
|
|
80
69
|
export function buildBtwSelectionLines(turns: readonly SideThreadTurn[]): BtwSelectionLine[] {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
return buildQuickBringToMainSegments(turns, { kind: "entire" }).flatMap((segment) =>
|
|
71
|
+
segment.text.split("\n").map((text) => ({ role: segment.role, text })),
|
|
72
|
+
);
|
|
84
73
|
}
|
|
85
74
|
|
|
86
75
|
export function segmentsFromLineRange(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
lines: readonly BtwSelectionLine[],
|
|
77
|
+
anchor: number,
|
|
78
|
+
cursor: number,
|
|
90
79
|
): BtwBringToMainSegment[] {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
80
|
+
if (lines.length === 0) return [];
|
|
81
|
+
const start = Math.max(0, Math.min(anchor, cursor, lines.length - 1));
|
|
82
|
+
const end = Math.max(0, Math.min(Math.max(anchor, cursor), lines.length - 1));
|
|
83
|
+
const segments: BtwBringToMainSegment[] = [];
|
|
84
|
+
for (const line of lines.slice(start, end + 1)) {
|
|
85
|
+
const previous = segments.at(-1);
|
|
86
|
+
if (previous?.role === line.role) {
|
|
87
|
+
previous.text += `\n${line.text}`;
|
|
88
|
+
} else {
|
|
89
|
+
segments.push({ role: line.role, text: line.text });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return segments;
|
|
104
93
|
}
|
|
105
94
|
|
|
106
95
|
export function segmentsFromTextRange(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
lines: readonly BtwSelectionLine[],
|
|
97
|
+
anchor: BtwTextPosition,
|
|
98
|
+
cursor: BtwTextPosition,
|
|
110
99
|
): BtwBringToMainSegment[] {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
100
|
+
if (lines.length === 0) return [];
|
|
101
|
+
const first = clampTextPosition(lines, anchor);
|
|
102
|
+
const second = clampTextPosition(lines, cursor);
|
|
103
|
+
const [start, end] = compareTextPositions(first, second) <= 0 ? [first, second] : [second, first];
|
|
104
|
+
if (compareTextPositions(start, end) === 0) return [];
|
|
105
|
+
|
|
106
|
+
const segments: BtwBringToMainSegment[] = [];
|
|
107
|
+
for (let lineIndex = start.line; lineIndex <= end.line; lineIndex += 1) {
|
|
108
|
+
const line = lines[lineIndex];
|
|
109
|
+
if (!line) continue;
|
|
110
|
+
const characters = splitGraphemes(line.text);
|
|
111
|
+
const from = lineIndex === start.line ? start.column : 0;
|
|
112
|
+
const to = lineIndex === end.line ? end.column : characters.length;
|
|
113
|
+
const text = characters.slice(from, to).join("");
|
|
114
|
+
if (text) {
|
|
115
|
+
const previous = segments.at(-1);
|
|
116
|
+
if (previous?.role === line.role) previous.text += text;
|
|
117
|
+
else segments.push({ role: line.role, text });
|
|
118
|
+
}
|
|
119
|
+
const crossesSameRoleLine = lineIndex < end.line && lines[lineIndex + 1]?.role === line.role;
|
|
120
|
+
if (crossesSameRoleLine) {
|
|
121
|
+
const current = segments.at(-1);
|
|
122
|
+
if (current?.role === line.role) current.text += "\n";
|
|
123
|
+
else segments.push({ role: line.role, text: "\n" });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return segments;
|
|
138
127
|
}
|
|
139
128
|
|
|
140
129
|
export function estimateBringToMainTokens(segments: readonly BtwBringToMainSegment[]): number {
|
|
141
|
-
|
|
142
|
-
Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4,
|
|
143
|
-
);
|
|
130
|
+
return Math.ceil(Buffer.byteLength(segments.map((segment) => segment.text).join("\n"), "utf8") / 4);
|
|
144
131
|
}
|
|
145
132
|
|
|
146
|
-
export function summarizeBringToMain(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
tokens: estimateBringToMainTokens(segments),
|
|
153
|
-
};
|
|
133
|
+
export function summarizeBringToMain(segments: readonly BtwBringToMainSegment[]): BtwBringToMainSummary {
|
|
134
|
+
return {
|
|
135
|
+
lines: segments.reduce((count, segment) => count + segment.text.split("\n").length, 0),
|
|
136
|
+
messages: segments.length,
|
|
137
|
+
tokens: estimateBringToMainTokens(segments),
|
|
138
|
+
};
|
|
154
139
|
}
|
|
155
140
|
|
|
156
141
|
export function formatBtwBringToMain(segments: readonly BtwBringToMainSegment[]): string {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
body,
|
|
169
|
-
"</btw_context>",
|
|
170
|
-
].join("\n");
|
|
142
|
+
const body = segments
|
|
143
|
+
.map((segment) => `${segment.role === "user" ? "User" : "Assistant"}:\n${escapeBringToMainText(segment.text)}`)
|
|
144
|
+
.join("\n\n");
|
|
145
|
+
return [
|
|
146
|
+
"The following context was brought back from a /btw side discussion.",
|
|
147
|
+
"Treat it as discussion context, not as work already completed.",
|
|
148
|
+
"",
|
|
149
|
+
"<btw_context>",
|
|
150
|
+
body,
|
|
151
|
+
"</btw_context>",
|
|
152
|
+
].join("\n");
|
|
171
153
|
}
|
|
172
154
|
|
|
173
155
|
export class BtwTextRangeSelector implements Component {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
private keepCursorHorizontallyVisible(width: number): void {
|
|
489
|
-
const characters = splitGraphemes(this.lines[this.cursor.line]?.text ?? "");
|
|
490
|
-
const displayWidths = characters.map((character) =>
|
|
491
|
-
visibleWidth(escapeTerminalControls(character)),
|
|
492
|
-
);
|
|
493
|
-
const currentWidth = displayWidths[this.cursor.column] ?? 0;
|
|
494
|
-
let usedWidth = 1 + Math.min(currentWidth, Math.max(0, width - 1));
|
|
495
|
-
let offset = this.cursor.column;
|
|
496
|
-
for (let index = this.cursor.column - 1; index >= 0; index -= 1) {
|
|
497
|
-
const nextWidth = usedWidth + (displayWidths[index] ?? 0) + (index > 0 ? 1 : 0);
|
|
498
|
-
if (nextWidth > width) break;
|
|
499
|
-
usedWidth += displayWidths[index] ?? 0;
|
|
500
|
-
offset = index;
|
|
501
|
-
}
|
|
502
|
-
this.horizontalOffset = offset;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
private finish(action: BtwTextRangeSelectorAction): void {
|
|
506
|
-
if (this.finished) return;
|
|
507
|
-
this.finished = true;
|
|
508
|
-
this.onAction(action);
|
|
509
|
-
}
|
|
156
|
+
private readonly lines: BtwSelectionLine[];
|
|
157
|
+
private cursor: BtwTextPosition = { line: 0, column: 0 };
|
|
158
|
+
private anchor: BtwTextPosition | undefined;
|
|
159
|
+
private lineAnchor: number | undefined;
|
|
160
|
+
private preferredColumn = 0;
|
|
161
|
+
private scrollOffset = 0;
|
|
162
|
+
private horizontalOffset = 0;
|
|
163
|
+
private warning: string | undefined;
|
|
164
|
+
private finished = false;
|
|
165
|
+
|
|
166
|
+
constructor(
|
|
167
|
+
private readonly tui: TUI,
|
|
168
|
+
private readonly theme: Theme,
|
|
169
|
+
private readonly keybindings: KeybindingsManager,
|
|
170
|
+
turns: readonly SideThreadTurn[],
|
|
171
|
+
private readonly onAction: (action: BtwTextRangeSelectorAction) => void,
|
|
172
|
+
initialState?: BtwTextRangeSelectorState,
|
|
173
|
+
) {
|
|
174
|
+
this.lines = buildBtwSelectionLines(turns);
|
|
175
|
+
if (initialState) {
|
|
176
|
+
this.cursor = clampTextPosition(this.lines, initialState.cursor);
|
|
177
|
+
this.anchor = initialState.anchor ? clampTextPosition(this.lines, initialState.anchor) : undefined;
|
|
178
|
+
this.lineAnchor =
|
|
179
|
+
initialState.lineAnchor === undefined
|
|
180
|
+
? undefined
|
|
181
|
+
: Math.max(0, Math.min(this.lines.length - 1, initialState.lineAnchor));
|
|
182
|
+
this.preferredColumn = Math.max(0, initialState.preferredColumn);
|
|
183
|
+
this.scrollOffset = Math.max(0, initialState.scrollOffset);
|
|
184
|
+
this.horizontalOffset = Math.max(0, initialState.horizontalOffset);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
getState(): BtwTextRangeSelectorState {
|
|
189
|
+
return {
|
|
190
|
+
cursor: { ...this.cursor },
|
|
191
|
+
anchor: this.anchor ? { ...this.anchor } : undefined,
|
|
192
|
+
lineAnchor: this.lineAnchor,
|
|
193
|
+
preferredColumn: this.preferredColumn,
|
|
194
|
+
scrollOffset: this.scrollOffset,
|
|
195
|
+
horizontalOffset: this.horizontalOffset,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
render(width: number): string[] {
|
|
200
|
+
const safeWidth = Math.max(1, width);
|
|
201
|
+
const availableRows = Math.max(1, this.tui.terminal.rows - RESERVED_APP_ROWS);
|
|
202
|
+
const showStatus = availableRows >= 4;
|
|
203
|
+
const showFooter = availableRows >= 3;
|
|
204
|
+
const viewportHeight = Math.max(1, availableRows - 1 - (showStatus ? 1 : 0) - (showFooter ? 1 : 0));
|
|
205
|
+
this.keepCursorVisible(viewportHeight);
|
|
206
|
+
const textWidth = Math.max(1, safeWidth - visibleWidth("●> Assistant │ "));
|
|
207
|
+
this.keepCursorHorizontallyVisible(textWidth);
|
|
208
|
+
const range = this.getSelectionRange();
|
|
209
|
+
const lineRange = this.getLineSelectionRange();
|
|
210
|
+
const visible = this.lines.slice(this.scrollOffset, this.scrollOffset + viewportHeight);
|
|
211
|
+
const rows = visible.map((line, visibleIndex) => {
|
|
212
|
+
const lineIndex = this.scrollOffset + visibleIndex;
|
|
213
|
+
const role = line.role === "user" ? "User" : "Assistant";
|
|
214
|
+
const lineSelected = lineRange ? lineIndex >= lineRange.start && lineIndex <= lineRange.end : false;
|
|
215
|
+
const prefix = `${lineSelected ? "●" : " "}${lineIndex === this.cursor.line ? ">" : " "} ${role.padEnd(9)} │ `;
|
|
216
|
+
const text = this.renderTextLine(line, lineIndex, range, lineSelected);
|
|
217
|
+
return truncateToWidth(
|
|
218
|
+
lineIndex === this.cursor.line ? this.theme.fg("accent", prefix) + text : prefix + text,
|
|
219
|
+
safeWidth,
|
|
220
|
+
"",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
const selected = this.getSelectedSegments();
|
|
224
|
+
const summary = summarizeBringToMain(selected);
|
|
225
|
+
const status =
|
|
226
|
+
selected.length === 0
|
|
227
|
+
? "Selected: none"
|
|
228
|
+
: `Selected: ${summary.lines} ${summary.lines === 1 ? "line" : "lines"} · ${summary.messages} ${summary.messages === 1 ? "message" : "messages"} · ~${summary.tokens} ${summary.tokens === 1 ? "token" : "tokens"}`;
|
|
229
|
+
const confirm = confirmKeyLabel(this.keybindings);
|
|
230
|
+
const back = keybindingLabel(this.keybindings, "tui.select.cancel", ["ctrl+c"]);
|
|
231
|
+
const vertical = `${keybindingLabel(this.keybindings, "tui.select.up")}/${keybindingLabel(this.keybindings, "tui.select.down")}`;
|
|
232
|
+
const confirmUsesSpace = this.keybindings.matches(" ", "tui.select.confirm");
|
|
233
|
+
const detailedFooter = this.warning
|
|
234
|
+
? `${this.warning} • ${confirmUsesSpace ? "Shift+Arrows select" : "Space lines • Shift+Arrows text"} • ${back} back • Ctrl+C close`
|
|
235
|
+
: this.lineAnchor !== undefined
|
|
236
|
+
? `${confirmUsesSpace ? `${vertical} extend lines` : `Space clear • ${vertical} extend lines`} • Shift+Arrows text • ${confirm} bring • ${back} back • Ctrl+C close`
|
|
237
|
+
: `Shift+Arrows select • Arrows move${confirmUsesSpace ? "" : " • Space lines"} • ${confirm} bring • ${back} back • Ctrl+C close`;
|
|
238
|
+
const criticalFooter = `${confirm} bring • ${back} back • Ctrl+C close`;
|
|
239
|
+
const footer = visibleWidth(detailedFooter) <= safeWidth ? detailedFooter : criticalFooter;
|
|
240
|
+
return fitRows(
|
|
241
|
+
[
|
|
242
|
+
truncateToWidth(this.theme.fg("accent", this.theme.bold("Select text to bring to main")), safeWidth, ""),
|
|
243
|
+
...(showStatus ? [truncateToWidth(this.theme.fg("muted", status), safeWidth, "")] : []),
|
|
244
|
+
...rows,
|
|
245
|
+
...(showFooter
|
|
246
|
+
? [truncateToWidth(this.theme.fg(this.warning ? "warning" : "muted", footer), safeWidth, "")]
|
|
247
|
+
: []),
|
|
248
|
+
],
|
|
249
|
+
availableRows,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
handleInput(data: string): void {
|
|
254
|
+
if (this.finished) return;
|
|
255
|
+
if (matchesKey(data, Key.ctrl("c"))) {
|
|
256
|
+
this.finish({ kind: "close" });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
260
|
+
this.finish({ kind: "back" });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (matchesConfirm(data, this.keybindings)) {
|
|
264
|
+
if (this.lines.length > 0) {
|
|
265
|
+
const segments = this.getSelectedSegments();
|
|
266
|
+
if (segments.length === 0) {
|
|
267
|
+
this.warning = "Select text first";
|
|
268
|
+
this.tui.requestRender();
|
|
269
|
+
} else {
|
|
270
|
+
this.finish({ kind: "confirm", segments });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (matchesKey(data, Key.space)) {
|
|
276
|
+
this.anchor = undefined;
|
|
277
|
+
this.lineAnchor = this.lineAnchor === undefined ? this.cursor.line : undefined;
|
|
278
|
+
this.afterMove();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (matchesKey(data, Key.shift("left"))) {
|
|
282
|
+
this.moveHorizontal(-1, true);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (matchesKey(data, Key.shift("right"))) {
|
|
286
|
+
this.moveHorizontal(1, true);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (matchesKey(data, Key.shift("up"))) {
|
|
290
|
+
this.moveVertical(-1, true);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (matchesKey(data, Key.shift("down"))) {
|
|
294
|
+
this.moveVertical(1, true);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (matchesKey(data, Key.left)) {
|
|
298
|
+
this.moveHorizontal(-1, false);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (matchesKey(data, Key.right)) {
|
|
302
|
+
this.moveHorizontal(1, false);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
306
|
+
this.moveVertical(-1, false);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (this.keybindings.matches(data, "tui.select.down")) {
|
|
310
|
+
this.moveVertical(1, false);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
314
|
+
this.moveVertical(-10, false);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
318
|
+
this.moveVertical(10, false);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
invalidate(): void {}
|
|
324
|
+
|
|
325
|
+
private renderTextLine(
|
|
326
|
+
line: BtwSelectionLine,
|
|
327
|
+
lineIndex: number,
|
|
328
|
+
range: { start: BtwTextPosition; end: BtwTextPosition } | undefined,
|
|
329
|
+
lineSelected: boolean,
|
|
330
|
+
): string {
|
|
331
|
+
const characters = splitGraphemes(line.text);
|
|
332
|
+
let rendered = this.horizontalOffset > 0 ? this.theme.fg("muted", "…") : "";
|
|
333
|
+
let buffer = "";
|
|
334
|
+
let bufferSelected = false;
|
|
335
|
+
const flush = () => {
|
|
336
|
+
if (!buffer) return;
|
|
337
|
+
rendered += bufferSelected ? this.theme.bg("selectedBg", this.theme.fg("text", buffer)) : buffer;
|
|
338
|
+
buffer = "";
|
|
339
|
+
};
|
|
340
|
+
for (let column = this.horizontalOffset; column <= characters.length; column += 1) {
|
|
341
|
+
if (range && lineIndex === range.start.line && column === range.start.column) {
|
|
342
|
+
flush();
|
|
343
|
+
rendered += this.theme.fg("accent", "[");
|
|
344
|
+
}
|
|
345
|
+
if (lineIndex === this.cursor.line && column === this.cursor.column) {
|
|
346
|
+
flush();
|
|
347
|
+
rendered += this.theme.fg("accent", "│");
|
|
348
|
+
}
|
|
349
|
+
if (range && lineIndex === range.end.line && column === range.end.column) {
|
|
350
|
+
flush();
|
|
351
|
+
rendered += this.theme.fg("accent", "]");
|
|
352
|
+
}
|
|
353
|
+
const character = characters[column];
|
|
354
|
+
if (character === undefined) continue;
|
|
355
|
+
const selected = lineSelected || (range ? positionFallsInside(lineIndex, column, range) : false);
|
|
356
|
+
if (buffer && selected !== bufferSelected) flush();
|
|
357
|
+
bufferSelected = selected;
|
|
358
|
+
buffer += escapeTerminalControls(character);
|
|
359
|
+
}
|
|
360
|
+
flush();
|
|
361
|
+
return rendered;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getSelectionRange(): { start: BtwTextPosition; end: BtwTextPosition } | undefined {
|
|
365
|
+
if (!this.anchor || compareTextPositions(this.anchor, this.cursor) === 0) return undefined;
|
|
366
|
+
return compareTextPositions(this.anchor, this.cursor) < 0
|
|
367
|
+
? { start: this.anchor, end: this.cursor }
|
|
368
|
+
: { start: this.cursor, end: this.anchor };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private getLineSelectionRange(): { start: number; end: number } | undefined {
|
|
372
|
+
return this.lineAnchor === undefined
|
|
373
|
+
? undefined
|
|
374
|
+
: {
|
|
375
|
+
start: Math.min(this.lineAnchor, this.cursor.line),
|
|
376
|
+
end: Math.max(this.lineAnchor, this.cursor.line),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private getSelectedSegments(): BtwBringToMainSegment[] {
|
|
381
|
+
if (this.lineAnchor !== undefined) {
|
|
382
|
+
return segmentsFromLineRange(this.lines, this.lineAnchor, this.cursor.line);
|
|
383
|
+
}
|
|
384
|
+
return this.anchor ? segmentsFromTextRange(this.lines, this.anchor, this.cursor) : [];
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private moveHorizontal(delta: -1 | 1, extend: boolean): void {
|
|
388
|
+
if (this.lines.length === 0) return;
|
|
389
|
+
if (!extend) this.lineAnchor = undefined;
|
|
390
|
+
if (!extend && this.anchor) {
|
|
391
|
+
const range = this.getSelectionRange();
|
|
392
|
+
if (range) this.cursor = delta < 0 ? range.start : range.end;
|
|
393
|
+
this.anchor = undefined;
|
|
394
|
+
this.preferredColumn = this.cursor.column;
|
|
395
|
+
this.afterMove();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
this.beginOrClearSelection(extend);
|
|
399
|
+
const line = this.lines[this.cursor.line];
|
|
400
|
+
const length = line ? splitGraphemes(line.text).length : 0;
|
|
401
|
+
if (delta < 0) {
|
|
402
|
+
if (this.cursor.column > 0) this.cursor = { ...this.cursor, column: this.cursor.column - 1 };
|
|
403
|
+
else if (this.cursor.line > 0) {
|
|
404
|
+
const previousLine = this.lines[this.cursor.line - 1];
|
|
405
|
+
this.cursor = {
|
|
406
|
+
line: this.cursor.line - 1,
|
|
407
|
+
column: previousLine ? splitGraphemes(previousLine.text).length : 0,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
} else if (this.cursor.column < length) {
|
|
411
|
+
this.cursor = { ...this.cursor, column: this.cursor.column + 1 };
|
|
412
|
+
} else if (this.cursor.line < this.lines.length - 1) {
|
|
413
|
+
this.cursor = { line: this.cursor.line + 1, column: 0 };
|
|
414
|
+
}
|
|
415
|
+
this.preferredColumn = this.cursor.column;
|
|
416
|
+
this.afterMove();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private moveVertical(delta: number, extend: boolean): void {
|
|
420
|
+
if (this.lines.length === 0) return;
|
|
421
|
+
if (extend || this.lineAnchor === undefined) this.beginOrClearSelection(extend);
|
|
422
|
+
const line = Math.max(0, Math.min(this.lines.length - 1, this.cursor.line + delta));
|
|
423
|
+
const target = this.lines[line];
|
|
424
|
+
this.cursor = {
|
|
425
|
+
line,
|
|
426
|
+
column: Math.min(this.preferredColumn, target ? splitGraphemes(target.text).length : 0),
|
|
427
|
+
};
|
|
428
|
+
this.afterMove();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
private beginOrClearSelection(extend: boolean): void {
|
|
432
|
+
if (extend) this.lineAnchor = undefined;
|
|
433
|
+
if (extend && !this.anchor) this.anchor = { ...this.cursor };
|
|
434
|
+
if (!extend) this.anchor = undefined;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private afterMove(): void {
|
|
438
|
+
this.warning = undefined;
|
|
439
|
+
this.tui.requestRender();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private keepCursorVisible(height: number): void {
|
|
443
|
+
if (height <= 0) return;
|
|
444
|
+
if (this.cursor.line < this.scrollOffset) this.scrollOffset = this.cursor.line;
|
|
445
|
+
if (this.cursor.line >= this.scrollOffset + height) {
|
|
446
|
+
this.scrollOffset = this.cursor.line - height + 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private keepCursorHorizontallyVisible(width: number): void {
|
|
451
|
+
const characters = splitGraphemes(this.lines[this.cursor.line]?.text ?? "");
|
|
452
|
+
const displayWidths = characters.map((character) => visibleWidth(escapeTerminalControls(character)));
|
|
453
|
+
const currentWidth = displayWidths[this.cursor.column] ?? 0;
|
|
454
|
+
let usedWidth = 1 + Math.min(currentWidth, Math.max(0, width - 1));
|
|
455
|
+
let offset = this.cursor.column;
|
|
456
|
+
for (let index = this.cursor.column - 1; index >= 0; index -= 1) {
|
|
457
|
+
const nextWidth = usedWidth + (displayWidths[index] ?? 0) + (index > 0 ? 1 : 0);
|
|
458
|
+
if (nextWidth > width) break;
|
|
459
|
+
usedWidth += displayWidths[index] ?? 0;
|
|
460
|
+
offset = index;
|
|
461
|
+
}
|
|
462
|
+
this.horizontalOffset = offset;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private finish(action: BtwTextRangeSelectorAction): void {
|
|
466
|
+
if (this.finished) return;
|
|
467
|
+
this.finished = true;
|
|
468
|
+
this.onAction(action);
|
|
469
|
+
}
|
|
510
470
|
}
|
|
511
471
|
|
|
512
|
-
function clampTextPosition(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
line,
|
|
520
|
-
column: Math.max(0, Math.min(splitGraphemes(text).length, position.column)),
|
|
521
|
-
};
|
|
472
|
+
function clampTextPosition(lines: readonly BtwSelectionLine[], position: BtwTextPosition): BtwTextPosition {
|
|
473
|
+
const line = Math.max(0, Math.min(lines.length - 1, position.line));
|
|
474
|
+
const text = lines[line]?.text ?? "";
|
|
475
|
+
return {
|
|
476
|
+
line,
|
|
477
|
+
column: Math.max(0, Math.min(splitGraphemes(text).length, position.column)),
|
|
478
|
+
};
|
|
522
479
|
}
|
|
523
480
|
|
|
524
481
|
function confirmKeyLabel(keybindings: KeybindingsManager): string {
|
|
525
|
-
|
|
482
|
+
return keybindingLabel(keybindings, "tui.select.confirm", ["ctrl+c"], "enter");
|
|
526
483
|
}
|
|
527
484
|
|
|
528
485
|
function matchesConfirm(data: string, keybindings: KeybindingsManager): boolean {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
keybindings.matches(data, "tui.select.confirm") ||
|
|
536
|
-
(!hasUsableBinding && matchesKey(data, Key.enter))
|
|
537
|
-
);
|
|
486
|
+
if (matchesKey(data, Key.ctrl("c"))) return false;
|
|
487
|
+
const hasUsableBinding = keybindings
|
|
488
|
+
.getKeys("tui.select.confirm")
|
|
489
|
+
.map(String)
|
|
490
|
+
.some((key) => key.toLowerCase() !== "ctrl+c");
|
|
491
|
+
return keybindings.matches(data, "tui.select.confirm") || (!hasUsableBinding && matchesKey(data, Key.enter));
|
|
538
492
|
}
|
|
539
493
|
|
|
540
494
|
function keybindingLabel(
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
495
|
+
keybindings: KeybindingsManager,
|
|
496
|
+
keybinding:
|
|
497
|
+
| "tui.select.confirm"
|
|
498
|
+
| "tui.select.cancel"
|
|
499
|
+
| "tui.select.up"
|
|
500
|
+
| "tui.select.down"
|
|
501
|
+
| "tui.select.pageUp"
|
|
502
|
+
| "tui.select.pageDown",
|
|
503
|
+
excluded: readonly string[] = [],
|
|
504
|
+
fallback?: string,
|
|
551
505
|
): string {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
506
|
+
const key = keybindings
|
|
507
|
+
.getKeys(keybinding)
|
|
508
|
+
.map(String)
|
|
509
|
+
.find((candidate) => !excluded.includes(candidate.toLowerCase()));
|
|
510
|
+
return formatKeyLabel(key ?? fallback ?? keybinding);
|
|
557
511
|
}
|
|
558
512
|
|
|
559
513
|
function formatKeyLabel(key: string): string {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
})
|
|
575
|
-
.join("+");
|
|
514
|
+
return key
|
|
515
|
+
.split("+")
|
|
516
|
+
.map((part) => {
|
|
517
|
+
const lower = part.toLowerCase();
|
|
518
|
+
if (lower === "ctrl") return "Ctrl";
|
|
519
|
+
if (lower === "alt") return "Alt";
|
|
520
|
+
if (lower === "shift") return "Shift";
|
|
521
|
+
if (lower === "escape" || lower === "esc") return "Esc";
|
|
522
|
+
if (lower === "enter" || lower === "return") return "Enter";
|
|
523
|
+
if (lower === "pageup") return "PgUp";
|
|
524
|
+
if (lower === "pagedown") return "PgDn";
|
|
525
|
+
return part.length === 1 ? part.toUpperCase() : `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`;
|
|
526
|
+
})
|
|
527
|
+
.join("+");
|
|
576
528
|
}
|
|
577
529
|
|
|
578
530
|
function splitGraphemes(text: string): string[] {
|
|
579
|
-
|
|
531
|
+
return [...GRAPHEME_SEGMENTER.segment(text)].map(({ segment }) => segment);
|
|
580
532
|
}
|
|
581
533
|
|
|
582
534
|
function compareTextPositions(first: BtwTextPosition, second: BtwTextPosition): number {
|
|
583
|
-
|
|
535
|
+
return first.line === second.line ? first.column - second.column : first.line - second.line;
|
|
584
536
|
}
|
|
585
537
|
|
|
586
538
|
function positionFallsInside(
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
539
|
+
line: number,
|
|
540
|
+
column: number,
|
|
541
|
+
range: { start: BtwTextPosition; end: BtwTextPosition },
|
|
590
542
|
): boolean {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
compareTextPositions(position, range.start) >= 0 &&
|
|
594
|
-
compareTextPositions(position, range.end) < 0
|
|
595
|
-
);
|
|
543
|
+
const position = { line, column };
|
|
544
|
+
return compareTextPositions(position, range.start) >= 0 && compareTextPositions(position, range.end) < 0;
|
|
596
545
|
}
|
|
597
546
|
|
|
598
547
|
function fitRows(rows: string[], availableRows: number): string[] {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
548
|
+
if (rows.length <= availableRows) return rows;
|
|
549
|
+
if (availableRows <= 1) return rows.slice(0, 1);
|
|
550
|
+
return [rows[0] ?? "", ...rows.slice(rows.length - availableRows + 1)];
|
|
602
551
|
}
|
|
603
552
|
|
|
604
553
|
function escapeBringToMainText(text: string): string {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
terminator.replaceAll("<", "<").replaceAll(">", ">"),
|
|
619
|
-
);
|
|
554
|
+
return [...text]
|
|
555
|
+
.map((character) => {
|
|
556
|
+
if (character === "\n") return character;
|
|
557
|
+
if (character === "\t") return " ";
|
|
558
|
+
const code = character.charCodeAt(0);
|
|
559
|
+
if (code <= 31 || (code >= 127 && code <= 159)) {
|
|
560
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
561
|
+
}
|
|
562
|
+
return character;
|
|
563
|
+
})
|
|
564
|
+
.join("")
|
|
565
|
+
.replace(/<btw_context(?=[ \t\r\n>])/g, "<btw_context")
|
|
566
|
+
.replace(/<\/btw_context[ \t\r\n]*>/g, (terminator) => terminator.replaceAll("<", "<").replaceAll(">", ">"));
|
|
620
567
|
}
|
|
621
568
|
|
|
622
569
|
function escapeTerminalControls(text: string): string {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
570
|
+
return [...text]
|
|
571
|
+
.map((character) => {
|
|
572
|
+
const code = character.charCodeAt(0);
|
|
573
|
+
if (code <= 31 || (code >= 127 && code <= 159)) {
|
|
574
|
+
return `\\x${code.toString(16).padStart(2, "0")}`;
|
|
575
|
+
}
|
|
576
|
+
return character;
|
|
577
|
+
})
|
|
578
|
+
.join("");
|
|
632
579
|
}
|