diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 5a493cc28053..dfe696b958f9 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -1516,6 +1516,23 @@ it.layer( }), ); + it.effect("retains plain Unicode and control characters around a split query", () => + Effect.gen(function* () { + const { manager, ptyAdapter } = yield* createManager(); + yield* manager.open(openInput()); + const process = ptyAdapter.processes[0]!; + const before = "café\t界🙂\u0007\u009c\u0085"; + const after = "e\u0301\r\n"; + + process.emitData(before); + process.emitData("\u001b[5"); + process.emitData(`n${after}`); + yield* manager.close({ threadId: "thread-1" }); + + expect((yield* manager.open(openInput())).history).toBe(before + after); + }), + ); + it.effect("strips replay-unsafe terminal query and reply sequences from persisted history", () => Effect.gen(function* () { const { manager, ptyAdapter } = yield* createManager(); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 3c71a8849475..dd16b9d6e32c 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -1114,10 +1114,19 @@ function findEscapeSequenceEndIndex(input: string, start: number): number | null return isEscapeFinalByte(input.charCodeAt(cursor)) ? cursor + 1 : start + 1; } +// eslint-disable-next-line no-control-regex -- Terminal sequence introducers must reach the parser. +const HISTORY_SEQUENCE_START = /[\u001b\u0090\u009b\u009d\u009e\u009f]/; + function sanitizeTerminalHistoryChunk( pendingControlSequence: string, data: string, ): { visibleText: string; pendingControlSequence: string } { + // Plain output has nothing to filter. A pending sequence still needs the parser + // even when this chunk only contains its final bytes. + if (pendingControlSequence.length === 0 && !HISTORY_SEQUENCE_START.test(data)) { + return { visibleText: data, pendingControlSequence: "" }; + } + const input = `${pendingControlSequence}${data}`; let visibleText = ""; let index = 0;