| 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 | 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 | 1536 | it.effect("strips replay-unsafe terminal query and reply sequences from persisted history", () => |
| 1520 | 1537 | Effect.gen(function* () { |
| 1521 | 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 | 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 | 1121 | function sanitizeTerminalHistoryChunk( |
| 1119 | 1122 | pendingControlSequence: string, |
| 1120 | 1123 | data: string, |
| 1121 | 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 | 1131 | const input = `${pendingControlSequence}${data}`; |
| 1123 | 1132 | let visibleText = ""; |
| 1124 | 1133 | let index = 0; |