agent-sh 0.15.8 → 0.15.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shell/input-handler.js +5 -3
- package/dist/shell/terminal.d.ts +2 -11
- package/dist/shell/terminal.js +37 -19
- package/dist/shell/tui-renderer.js +1 -1
- package/dist/utils/ansi.d.ts +7 -0
- package/dist/utils/ansi.js +20 -0
- package/dist/utils/floating-panel.js +5 -4
- package/dist/utils/line-editor.js +7 -4
- package/examples/extensions/ashi/package.json +1 -1
- package/package.json +1 -1
- package/src/shell/input-handler.ts +5 -3
- package/src/shell/terminal.ts +30 -19
- package/src/shell/tui-renderer.ts +1 -1
- package/src/utils/ansi.ts +21 -0
- package/src/utils/floating-panel.ts +5 -4
- package/src/utils/line-editor.ts +7 -4
|
@@ -57,7 +57,8 @@ export class InputHandler {
|
|
|
57
57
|
loadHistory() {
|
|
58
58
|
try {
|
|
59
59
|
const data = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
60
|
-
this.history = data.split("\n").filter(Boolean)
|
|
60
|
+
this.history = data.split("\n").filter(Boolean)
|
|
61
|
+
.map((l) => l.replace(/\\([\\n])/g, (_, c) => c === "n" ? "\n" : "\\"));
|
|
61
62
|
}
|
|
62
63
|
catch {
|
|
63
64
|
}
|
|
@@ -66,7 +67,8 @@ export class InputHandler {
|
|
|
66
67
|
try {
|
|
67
68
|
const { historySize } = getSettings();
|
|
68
69
|
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
69
|
-
const lines = this.history.slice(-historySize)
|
|
70
|
+
const lines = this.history.slice(-historySize)
|
|
71
|
+
.map((l) => l.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"));
|
|
70
72
|
fs.writeFileSync(HISTORY_FILE, lines.join("\n") + "\n");
|
|
71
73
|
}
|
|
72
74
|
catch {
|
|
@@ -373,7 +375,7 @@ export class InputHandler {
|
|
|
373
375
|
this.editor.clear();
|
|
374
376
|
this.view.resetCursor();
|
|
375
377
|
this.dismissAutocomplete();
|
|
376
|
-
if (query && query.startsWith("/")) {
|
|
378
|
+
if (query && query.startsWith("/") && !query.includes("\n")) {
|
|
377
379
|
const spaceIdx = query.indexOf(" ");
|
|
378
380
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
379
381
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
package/dist/shell/terminal.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal — the user-facing I/O endpoint that a Shell talks to.
|
|
3
|
-
*
|
|
4
|
-
* Shell wraps a *pseudo*-terminal (the PTY the child shell sees). This
|
|
5
|
-
* interface is the *real* terminal (or its substitute) on the other end:
|
|
6
|
-
* bytes in, bytes out, dimensions, resize notifications. The default
|
|
7
|
-
* factory wires it to process.stdin/stdout for the CLI; headless hosts
|
|
8
|
-
* (multi-session web hubs, tests) supply their own.
|
|
9
|
-
*/
|
|
10
1
|
import type { RenderSurface } from "../utils/compositor.js";
|
|
11
2
|
export interface Terminal {
|
|
12
3
|
write(data: string): void;
|
|
@@ -23,8 +14,8 @@ export interface Terminal {
|
|
|
23
14
|
resume(): void;
|
|
24
15
|
};
|
|
25
16
|
}
|
|
26
|
-
/** Default Terminal: wraps process.stdin/stdout. */
|
|
27
|
-
export declare function processTerminal(): Terminal;
|
|
17
|
+
/** Default Terminal: wraps process.stdin/stdout (injectable for tests). */
|
|
18
|
+
export declare function processTerminal(stdin?: NodeJS.ReadStream, stdout?: NodeJS.WriteStream): Terminal;
|
|
28
19
|
/**
|
|
29
20
|
* No-op terminal for non-rendering hosts (tests, agent-only embeds).
|
|
30
21
|
* Writes are discarded; input/resize never fire.
|
package/dist/shell/terminal.js
CHANGED
|
@@ -1,42 +1,60 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Terminal — the user-facing I/O endpoint that a Shell talks to.
|
|
3
|
+
*
|
|
4
|
+
* Shell wraps a *pseudo*-terminal (the PTY the child shell sees). This
|
|
5
|
+
* interface is the *real* terminal (or its substitute) on the other end:
|
|
6
|
+
* bytes in, bytes out, dimensions, resize notifications. The default
|
|
7
|
+
* factory wires it to process.stdin/stdout for the CLI; headless hosts
|
|
8
|
+
* (multi-session web hubs, tests) supply their own.
|
|
9
|
+
*/
|
|
10
|
+
import { StringDecoder } from "node:string_decoder";
|
|
11
|
+
/** Default Terminal: wraps process.stdin/stdout (injectable for tests). */
|
|
12
|
+
export function processTerminal(stdin = process.stdin, stdout = process.stdout) {
|
|
3
13
|
return {
|
|
4
14
|
write(data) {
|
|
5
|
-
if (
|
|
15
|
+
if (stdout.writable) {
|
|
6
16
|
try {
|
|
7
|
-
|
|
17
|
+
stdout.write(data);
|
|
8
18
|
}
|
|
9
19
|
catch { /* ignore */ }
|
|
10
20
|
}
|
|
11
21
|
},
|
|
12
22
|
onInput(cb) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
23
|
+
// Stateful decode: tty chunk boundaries can land mid-way through a
|
|
24
|
+
// multibyte UTF-8 sequence (large pastes), so per-chunk toString()
|
|
25
|
+
// would emit U+FFFD for the torn halves.
|
|
26
|
+
const decoder = new StringDecoder("utf-8");
|
|
27
|
+
const handler = (b) => {
|
|
28
|
+
const text = decoder.write(b);
|
|
29
|
+
if (text)
|
|
30
|
+
cb(text);
|
|
31
|
+
};
|
|
32
|
+
stdin.on("data", handler);
|
|
33
|
+
return () => { stdin.off("data", handler); };
|
|
16
34
|
},
|
|
17
35
|
onResize(cb) {
|
|
18
|
-
const handler = () => cb(
|
|
19
|
-
|
|
20
|
-
return () => {
|
|
36
|
+
const handler = () => cb(stdout.columns || 80, stdout.rows || 24);
|
|
37
|
+
stdout.on("resize", handler);
|
|
38
|
+
return () => { stdout.off("resize", handler); };
|
|
21
39
|
},
|
|
22
|
-
cols() { return
|
|
23
|
-
rows() { return
|
|
40
|
+
cols() { return stdout.columns || 80; },
|
|
41
|
+
rows() { return stdout.rows || 24; },
|
|
24
42
|
suspendInput() {
|
|
25
|
-
const wasRaw =
|
|
26
|
-
if (
|
|
43
|
+
const wasRaw = stdin.isTTY && stdin.isRaw;
|
|
44
|
+
if (stdin.isTTY) {
|
|
27
45
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
46
|
+
stdin.setRawMode(false);
|
|
47
|
+
stdin.pause();
|
|
30
48
|
}
|
|
31
49
|
catch { /* ignore */ }
|
|
32
50
|
}
|
|
33
51
|
return {
|
|
34
52
|
resume() {
|
|
35
|
-
if (
|
|
53
|
+
if (stdin.isTTY) {
|
|
36
54
|
try {
|
|
37
|
-
|
|
55
|
+
stdin.resume();
|
|
38
56
|
if (wasRaw)
|
|
39
|
-
|
|
57
|
+
stdin.setRawMode(true);
|
|
40
58
|
}
|
|
41
59
|
catch { /* ignore */ }
|
|
42
60
|
}
|
|
@@ -845,7 +845,7 @@ export default function activate(ctx) {
|
|
|
845
845
|
? getSettings().readOutputMaxLines
|
|
846
846
|
: getSettings().maxCommandOutputLines;
|
|
847
847
|
s.commandOutputBuffer += chunk;
|
|
848
|
-
const lines = s.commandOutputBuffer.split(
|
|
848
|
+
const lines = s.commandOutputBuffer.split(/\r?\n/);
|
|
849
849
|
s.commandOutputBuffer = lines.pop();
|
|
850
850
|
for (const line of lines) {
|
|
851
851
|
if (s.commandOutputLineCount < maxLines) {
|
package/dist/utils/ansi.d.ts
CHANGED
|
@@ -44,3 +44,10 @@ export declare function padEndToWidth(str: string, targetWidth: number): string;
|
|
|
44
44
|
* CSI, private-mode, 8-bit CSI, and newer variants). `\r` is not an escape
|
|
45
45
|
* but callers rely on it being stripped alongside. */
|
|
46
46
|
export declare function stripAnsi(str: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* Sanitize text for painting at a fixed screen position: SGR (color/style)
|
|
49
|
+
* passes through; anything else that would move the cursor or mutate
|
|
50
|
+
* terminal state mid-row is dropped. Tabs become a single space so painted
|
|
51
|
+
* width matches `stripAnsi`-based measurement.
|
|
52
|
+
*/
|
|
53
|
+
export declare function stripCursorControls(str: string): string;
|
package/dist/utils/ansi.js
CHANGED
|
@@ -134,3 +134,23 @@ export function padEndToWidth(str, targetWidth) {
|
|
|
134
134
|
export function stripAnsi(str) {
|
|
135
135
|
return stripAnsiPkg(str).replace(/\r/g, "");
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Sanitize text for painting at a fixed screen position: SGR (color/style)
|
|
139
|
+
* passes through; anything else that would move the cursor or mutate
|
|
140
|
+
* terminal state mid-row is dropped. Tabs become a single space so painted
|
|
141
|
+
* width matches `stripAnsi`-based measurement.
|
|
142
|
+
*/
|
|
143
|
+
export function stripCursorControls(str) {
|
|
144
|
+
// Park SGR behind NUL placeholders so the strips below can't eat their ESC bytes.
|
|
145
|
+
const sgr = [];
|
|
146
|
+
const cleaned = str
|
|
147
|
+
.replace(/\x00/g, "")
|
|
148
|
+
.replace(/\x1b\[[0-9;:]*m/g, (m) => { sgr.push(m); return "\x00"; })
|
|
149
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "")
|
|
150
|
+
.replace(/\x1b\[[0-9;:?]*[ -/]*[@-~]/g, "")
|
|
151
|
+
.replace(/\x1b./g, "")
|
|
152
|
+
.replace(/\t/g, " ")
|
|
153
|
+
.replace(/[\x01-\x08\x0a-\x1f\x7f]/g, "");
|
|
154
|
+
let i = 0;
|
|
155
|
+
return cleaned.replace(/\x00/g, () => sgr[i++] ?? "");
|
|
156
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* Usage from extensions:
|
|
31
31
|
* import { FloatingPanel } from "agent-sh/utils/floating-panel.js";
|
|
32
32
|
*/
|
|
33
|
-
import { stripAnsi } from "./ansi.js";
|
|
33
|
+
import { stripAnsi, stripCursorControls } from "./ansi.js";
|
|
34
34
|
import { wrapLine } from "./markdown.js";
|
|
35
35
|
import { LineEditor } from "./line-editor.js";
|
|
36
36
|
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
@@ -253,10 +253,11 @@ export class FloatingPanel {
|
|
|
253
253
|
this.handlers.define(`${p}:input`, (_data) => false);
|
|
254
254
|
// Default row builder: truncate and pad
|
|
255
255
|
this.handlers.define(`${p}:build-row`, (content, width) => {
|
|
256
|
-
const
|
|
256
|
+
const clean = stripCursorControls(content);
|
|
257
|
+
const plain = stripAnsi(clean);
|
|
257
258
|
const display = plain.length > width
|
|
258
|
-
?
|
|
259
|
-
:
|
|
259
|
+
? clean.slice(0, width - 1) + "\u2026"
|
|
260
|
+
: clean;
|
|
260
261
|
const pad = Math.max(0, width - stripAnsi(display).length);
|
|
261
262
|
return display + " ".repeat(pad);
|
|
262
263
|
});
|
|
@@ -121,7 +121,7 @@ export class LineEditor {
|
|
|
121
121
|
// paste, since typed input arrives one keystroke per chunk in raw mode.
|
|
122
122
|
if (!this.inPaste && data.length > 1 && /[\r\n]/.test(data)
|
|
123
123
|
&& data.indexOf("\x1b[200~") === -1) {
|
|
124
|
-
this.pasteAccum = data
|
|
124
|
+
this.pasteAccum = data;
|
|
125
125
|
actions.push(...this.commitPaste());
|
|
126
126
|
return actions;
|
|
127
127
|
}
|
|
@@ -247,7 +247,7 @@ export class LineEditor {
|
|
|
247
247
|
consumePasteChunk(data) {
|
|
248
248
|
const endIdx = data.indexOf(PASTE_END);
|
|
249
249
|
if (endIdx !== -1) {
|
|
250
|
-
this.pasteAccum += data.slice(0, endIdx)
|
|
250
|
+
this.pasteAccum += data.slice(0, endIdx);
|
|
251
251
|
return endIdx;
|
|
252
252
|
}
|
|
253
253
|
let suffixLen = 0;
|
|
@@ -258,14 +258,17 @@ export class LineEditor {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
const safeEnd = data.length - suffixLen;
|
|
261
|
-
this.pasteAccum += data.slice(0, safeEnd)
|
|
261
|
+
this.pasteAccum += data.slice(0, safeEnd);
|
|
262
262
|
if (suffixLen > 0)
|
|
263
263
|
this.pendingSeq = data.slice(safeEnd);
|
|
264
264
|
return -1;
|
|
265
265
|
}
|
|
266
266
|
commitPaste() {
|
|
267
267
|
this.inPaste = false;
|
|
268
|
-
|
|
268
|
+
// Pasted line separators arrive as \n, \r (xterm convention), or \r\n
|
|
269
|
+
// depending on terminal; normalize on the full accumulation so a \r\n
|
|
270
|
+
// pair split across chunks still collapses to one newline.
|
|
271
|
+
const accum = this.pasteAccum.replace(/\r\n?/g, "\n");
|
|
269
272
|
this.pasteAccum = "";
|
|
270
273
|
if (!accum)
|
|
271
274
|
return [];
|
package/package.json
CHANGED
|
@@ -88,7 +88,8 @@ export class InputHandler {
|
|
|
88
88
|
private loadHistory(): void {
|
|
89
89
|
try {
|
|
90
90
|
const data = fs.readFileSync(HISTORY_FILE, "utf-8");
|
|
91
|
-
this.history = data.split("\n").filter(Boolean)
|
|
91
|
+
this.history = data.split("\n").filter(Boolean)
|
|
92
|
+
.map((l) => l.replace(/\\([\\n])/g, (_, c: string) => c === "n" ? "\n" : "\\"));
|
|
92
93
|
} catch {
|
|
93
94
|
}
|
|
94
95
|
}
|
|
@@ -97,7 +98,8 @@ export class InputHandler {
|
|
|
97
98
|
try {
|
|
98
99
|
const { historySize } = getSettings();
|
|
99
100
|
fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
100
|
-
const lines = this.history.slice(-historySize)
|
|
101
|
+
const lines = this.history.slice(-historySize)
|
|
102
|
+
.map((l) => l.replace(/\\/g, "\\\\").replace(/\n/g, "\\n"));
|
|
101
103
|
fs.writeFileSync(HISTORY_FILE, lines.join("\n") + "\n");
|
|
102
104
|
} catch {
|
|
103
105
|
}
|
|
@@ -392,7 +394,7 @@ export class InputHandler {
|
|
|
392
394
|
this.editor.clear();
|
|
393
395
|
this.view.resetCursor();
|
|
394
396
|
this.dismissAutocomplete();
|
|
395
|
-
if (query && query.startsWith("/")) {
|
|
397
|
+
if (query && query.startsWith("/") && !query.includes("\n")) {
|
|
396
398
|
const spaceIdx = query.indexOf(" ");
|
|
397
399
|
const name = spaceIdx === -1 ? query : query.slice(0, spaceIdx);
|
|
398
400
|
const args = spaceIdx === -1 ? "" : query.slice(spaceIdx + 1).trim();
|
package/src/shell/terminal.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* factory wires it to process.stdin/stdout for the CLI; headless hosts
|
|
8
8
|
* (multi-session web hubs, tests) supply their own.
|
|
9
9
|
*/
|
|
10
|
+
import { StringDecoder } from "node:string_decoder";
|
|
10
11
|
import type { RenderSurface } from "../utils/compositor.js";
|
|
11
12
|
|
|
12
13
|
export interface Terminal {
|
|
@@ -23,40 +24,50 @@ export interface Terminal {
|
|
|
23
24
|
suspendInput?(): { resume(): void };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/** Default Terminal: wraps process.stdin/stdout. */
|
|
27
|
-
export function processTerminal(
|
|
27
|
+
/** Default Terminal: wraps process.stdin/stdout (injectable for tests). */
|
|
28
|
+
export function processTerminal(
|
|
29
|
+
stdin: NodeJS.ReadStream = process.stdin,
|
|
30
|
+
stdout: NodeJS.WriteStream = process.stdout,
|
|
31
|
+
): Terminal {
|
|
28
32
|
return {
|
|
29
33
|
write(data) {
|
|
30
|
-
if (
|
|
31
|
-
try {
|
|
34
|
+
if (stdout.writable) {
|
|
35
|
+
try { stdout.write(data); } catch { /* ignore */ }
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
38
|
onInput(cb) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// Stateful decode: tty chunk boundaries can land mid-way through a
|
|
40
|
+
// multibyte UTF-8 sequence (large pastes), so per-chunk toString()
|
|
41
|
+
// would emit U+FFFD for the torn halves.
|
|
42
|
+
const decoder = new StringDecoder("utf-8");
|
|
43
|
+
const handler = (b: Buffer) => {
|
|
44
|
+
const text = decoder.write(b);
|
|
45
|
+
if (text) cb(text);
|
|
46
|
+
};
|
|
47
|
+
stdin.on("data", handler);
|
|
48
|
+
return () => { stdin.off("data", handler); };
|
|
38
49
|
},
|
|
39
50
|
onResize(cb) {
|
|
40
|
-
const handler = () => cb(
|
|
41
|
-
|
|
42
|
-
return () => {
|
|
51
|
+
const handler = () => cb(stdout.columns || 80, stdout.rows || 24);
|
|
52
|
+
stdout.on("resize", handler);
|
|
53
|
+
return () => { stdout.off("resize", handler); };
|
|
43
54
|
},
|
|
44
|
-
cols() { return
|
|
45
|
-
rows() { return
|
|
55
|
+
cols() { return stdout.columns || 80; },
|
|
56
|
+
rows() { return stdout.rows || 24; },
|
|
46
57
|
suspendInput() {
|
|
47
|
-
const wasRaw =
|
|
48
|
-
if (
|
|
58
|
+
const wasRaw = stdin.isTTY && (stdin as { isRaw?: boolean }).isRaw;
|
|
59
|
+
if (stdin.isTTY) {
|
|
49
60
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
stdin.setRawMode(false);
|
|
62
|
+
stdin.pause();
|
|
52
63
|
} catch { /* ignore */ }
|
|
53
64
|
}
|
|
54
65
|
return {
|
|
55
66
|
resume() {
|
|
56
|
-
if (
|
|
67
|
+
if (stdin.isTTY) {
|
|
57
68
|
try {
|
|
58
|
-
|
|
59
|
-
if (wasRaw)
|
|
69
|
+
stdin.resume();
|
|
70
|
+
if (wasRaw) stdin.setRawMode(true);
|
|
60
71
|
} catch { /* ignore */ }
|
|
61
72
|
}
|
|
62
73
|
},
|
|
@@ -939,7 +939,7 @@ export default function activate(ctx: ExtensionContext): void {
|
|
|
939
939
|
? getSettings().readOutputMaxLines
|
|
940
940
|
: getSettings().maxCommandOutputLines;
|
|
941
941
|
s.commandOutputBuffer += chunk;
|
|
942
|
-
const lines = s.commandOutputBuffer.split(
|
|
942
|
+
const lines = s.commandOutputBuffer.split(/\r?\n/);
|
|
943
943
|
s.commandOutputBuffer = lines.pop()!;
|
|
944
944
|
for (const line of lines) {
|
|
945
945
|
if (s.commandOutputLineCount < maxLines) {
|
package/src/utils/ansi.ts
CHANGED
|
@@ -138,3 +138,24 @@ export function padEndToWidth(str: string, targetWidth: number): string {
|
|
|
138
138
|
export function stripAnsi(str: string): string {
|
|
139
139
|
return stripAnsiPkg(str).replace(/\r/g, "");
|
|
140
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Sanitize text for painting at a fixed screen position: SGR (color/style)
|
|
144
|
+
* passes through; anything else that would move the cursor or mutate
|
|
145
|
+
* terminal state mid-row is dropped. Tabs become a single space so painted
|
|
146
|
+
* width matches `stripAnsi`-based measurement.
|
|
147
|
+
*/
|
|
148
|
+
export function stripCursorControls(str: string): string {
|
|
149
|
+
// Park SGR behind NUL placeholders so the strips below can't eat their ESC bytes.
|
|
150
|
+
const sgr: string[] = [];
|
|
151
|
+
const cleaned = str
|
|
152
|
+
.replace(/\x00/g, "")
|
|
153
|
+
.replace(/\x1b\[[0-9;:]*m/g, (m) => { sgr.push(m); return "\x00"; })
|
|
154
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "")
|
|
155
|
+
.replace(/\x1b\[[0-9;:?]*[ -/]*[@-~]/g, "")
|
|
156
|
+
.replace(/\x1b./g, "")
|
|
157
|
+
.replace(/\t/g, " ")
|
|
158
|
+
.replace(/[\x01-\x08\x0a-\x1f\x7f]/g, "");
|
|
159
|
+
let i = 0;
|
|
160
|
+
return cleaned.replace(/\x00/g, () => sgr[i++] ?? "");
|
|
161
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* Usage from extensions:
|
|
31
31
|
* import { FloatingPanel } from "agent-sh/utils/floating-panel.js";
|
|
32
32
|
*/
|
|
33
|
-
import { stripAnsi } from "./ansi.js";
|
|
33
|
+
import { stripAnsi, stripCursorControls } from "./ansi.js";
|
|
34
34
|
import { wrapLine } from "./markdown.js";
|
|
35
35
|
import { LineEditor } from "./line-editor.js";
|
|
36
36
|
import { TerminalBuffer } from "./terminal-buffer.js";
|
|
@@ -408,10 +408,11 @@ export class FloatingPanel {
|
|
|
408
408
|
|
|
409
409
|
// Default row builder: truncate and pad
|
|
410
410
|
this.handlers.define(`${p}:build-row`, (content: string, width: number): string => {
|
|
411
|
-
const
|
|
411
|
+
const clean = stripCursorControls(content);
|
|
412
|
+
const plain = stripAnsi(clean);
|
|
412
413
|
const display = plain.length > width
|
|
413
|
-
?
|
|
414
|
-
:
|
|
414
|
+
? clean.slice(0, width - 1) + "\u2026"
|
|
415
|
+
: clean;
|
|
415
416
|
const pad = Math.max(0, width - stripAnsi(display).length);
|
|
416
417
|
return display + " ".repeat(pad);
|
|
417
418
|
});
|
package/src/utils/line-editor.ts
CHANGED
|
@@ -152,7 +152,7 @@ export class LineEditor {
|
|
|
152
152
|
// paste, since typed input arrives one keystroke per chunk in raw mode.
|
|
153
153
|
if (!this.inPaste && data.length > 1 && /[\r\n]/.test(data)
|
|
154
154
|
&& data.indexOf("\x1b[200~") === -1) {
|
|
155
|
-
this.pasteAccum = data
|
|
155
|
+
this.pasteAccum = data;
|
|
156
156
|
actions.push(...this.commitPaste());
|
|
157
157
|
return actions;
|
|
158
158
|
}
|
|
@@ -263,7 +263,7 @@ export class LineEditor {
|
|
|
263
263
|
private consumePasteChunk(data: string): number {
|
|
264
264
|
const endIdx = data.indexOf(PASTE_END);
|
|
265
265
|
if (endIdx !== -1) {
|
|
266
|
-
this.pasteAccum += data.slice(0, endIdx)
|
|
266
|
+
this.pasteAccum += data.slice(0, endIdx);
|
|
267
267
|
return endIdx;
|
|
268
268
|
}
|
|
269
269
|
let suffixLen = 0;
|
|
@@ -271,14 +271,17 @@ export class LineEditor {
|
|
|
271
271
|
if (data.endsWith(PASTE_END.slice(0, p))) { suffixLen = p; break; }
|
|
272
272
|
}
|
|
273
273
|
const safeEnd = data.length - suffixLen;
|
|
274
|
-
this.pasteAccum += data.slice(0, safeEnd)
|
|
274
|
+
this.pasteAccum += data.slice(0, safeEnd);
|
|
275
275
|
if (suffixLen > 0) this.pendingSeq = data.slice(safeEnd);
|
|
276
276
|
return -1;
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
private commitPaste(): LineEditAction[] {
|
|
280
280
|
this.inPaste = false;
|
|
281
|
-
|
|
281
|
+
// Pasted line separators arrive as \n, \r (xterm convention), or \r\n
|
|
282
|
+
// depending on terminal; normalize on the full accumulation so a \r\n
|
|
283
|
+
// pair split across chunks still collapses to one newline.
|
|
284
|
+
const accum = this.pasteAccum.replace(/\r\n?/g, "\n");
|
|
282
285
|
this.pasteAccum = "";
|
|
283
286
|
if (!accum) return [];
|
|
284
287
|
if (accum.indexOf("\n") === -1) {
|