| author | |
| committer | |
| log | 7f0b789380f95032a55c777817cde8b49e2dae5a |
| tree | 7bfaf98e711474e9073e1b5cd7c31eb4526dd156 |
| parent | cf8479e605eb3cc8662dee38cbf1925020a1c1a7 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 26 insertions(+), 0 deletions(-)
apps/server/src/terminal/Manager.test.ts+17| ... | @@ -1516,6 +1516,23 @@ it.layer( | ... | @@ -1516,6 +1516,23 @@ it.layer( |
| 1516 | }), | 1516 | }), |
| 1517 | ); | 1517 | ); |
| 1518 | 1518 | ||
| 1519 | it.effect("retains plain Unicode and control characters around a split query", () => | ||
| 1520 | Effect.gen(function* () { | ||
| 1521 | const { manager, ptyAdapter } = yield* createManager(); | ||
| 1522 | yield* manager.open(openInput()); | ||
| 1523 | const process = ptyAdapter.processes[0]!; | ||
| 1524 | const before = "café\t界🙂\u0007\u009c\u0085"; | ||
| 1525 | const after = "e\u0301\r\n"; | ||
| 1526 | |||
| 1527 | process.emitData(before); | ||
| 1528 | process.emitData("\u001b[5"); | ||
| 1529 | process.emitData(`n${after}`); | ||
| 1530 | yield* manager.close({ threadId: "thread-1" }); | ||
| 1531 | |||
| 1532 | expect((yield* manager.open(openInput())).history).toBe(before + after); | ||
| 1533 | }), | ||
| 1534 | ); | ||
| 1535 | |||
| 1519 | it.effect("strips replay-unsafe terminal query and reply sequences from persisted history", () => | 1536 | it.effect("strips replay-unsafe terminal query and reply sequences from persisted history", () => |
| 1520 | Effect.gen(function* () { | 1537 | Effect.gen(function* () { |
| 1521 | const { manager, ptyAdapter } = yield* createManager(); | 1538 | const { manager, ptyAdapter } = yield* createManager(); |
apps/server/src/terminal/Manager.ts+9| ... | @@ -1115,10 +1115,19 @@ function findEscapeSequenceEndIndex(input: string, start: number): number | null | ... | @@ -1115,10 +1115,19 @@ function findEscapeSequenceEndIndex(input: string, start: number): number | null |
| 1115 | return isEscapeFinalByte(input.charCodeAt(cursor)) ? cursor + 1 : start + 1; | 1115 | return isEscapeFinalByte(input.charCodeAt(cursor)) ? cursor + 1 : start + 1; |
| 1116 | } | 1116 | } |
| 1117 | 1117 | ||
| 1118 | // eslint-disable-next-line no-control-regex -- Terminal sequence introducers must reach the parser. | ||
| 1119 | const HISTORY_SEQUENCE_START = /[\u001b\u0090\u009b\u009d\u009e\u009f]/; | ||
| 1120 | |||
| 1118 | function sanitizeTerminalHistoryChunk( | 1121 | function sanitizeTerminalHistoryChunk( |
| 1119 | pendingControlSequence: string, | 1122 | pendingControlSequence: string, |
| 1120 | data: string, | 1123 | data: string, |
| 1121 | ): { visibleText: string; pendingControlSequence: string } { | 1124 | ): { visibleText: string; pendingControlSequence: string } { |
| 1125 | // Plain output has nothing to filter. A pending sequence still needs the parser | ||
| 1126 | // even when this chunk only contains its final bytes. | ||
| 1127 | if (pendingControlSequence.length === 0 && !HISTORY_SEQUENCE_START.test(data)) { | ||
| 1128 | return { visibleText: data, pendingControlSequence: "" }; | ||
| 1129 | } | ||
| 1130 | |||
| 1122 | const input = `${pendingControlSequence}${data}`; | 1131 | const input = `${pendingControlSequence}${data}`; |
| 1123 | let visibleText = ""; | 1132 | let visibleText = ""; |
| 1124 | let index = 0; | 1133 | let index = 0; |