diff --git a/packages/protocol/src/terminal-snapshot.test.ts b/packages/protocol/src/terminal-snapshot.test.ts index 0262f0e45f..7b4b82fa88 100644 --- a/packages/protocol/src/terminal-snapshot.test.ts +++ b/packages/protocol/src/terminal-snapshot.test.ts @@ -7,6 +7,20 @@ function cells(text: string): TerminalState["grid"][number] { } describe("renderTerminalSnapshotToAnsi", () => { + it("preserves adjacent wide characters", () => { + const state: TerminalState = { + rows: 1, + cols: 4, + scrollback: [], + grid: [[{ char: "\u4e2d" }, { char: "" }, { char: "\u6587" }, { char: "" }]], + cursor: { row: 0, col: 4 }, + }; + + const ansi = renderTerminalSnapshotToAnsi(state); + + expect(ansi).toContain("\u4e2d\u6587"); + }); + it("renders soft-wrapped rows as one contiguous logical line when wrap flags are present", () => { // The server soft-wrapped one logical line "ABCDEFGHIJKLMNOP" at 10 cols into // two grid rows. gridWrapped[0] = true marks row 0 as continuing into row 1. diff --git a/packages/protocol/src/terminal-snapshot.ts b/packages/protocol/src/terminal-snapshot.ts index f978cd884b..c7a766836a 100644 --- a/packages/protocol/src/terminal-snapshot.ts +++ b/packages/protocol/src/terminal-snapshot.ts @@ -87,12 +87,15 @@ function renderTerminalRow(row: TerminalCell[], padToCols?: number): string { for (let index = 0; index < length; index += 1) { const cell = row[index] ?? { char: " " }; + if (cell.char === "") { + continue; + } const nextStyle = getTerminalStyle(cell); if (!terminalStylesEqual(previousStyle, nextStyle)) { output.push(styleToAnsi(nextStyle)); previousStyle = nextStyle; } - output.push(cell.char || " "); + output.push(cell.char); } if (!terminalStylesEqual(previousStyle, DEFAULT_STYLE)) { diff --git a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts index d96d85fa3e..0131c685ad 100644 --- a/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/terminal.e2e.test.ts @@ -1626,6 +1626,29 @@ describe("capture", () => { expect(capture.totalLines).toBeGreaterThan(0); }, 15000); + test("preserves adjacent CJK characters in captured output", async () => { + const cwd = tmpCwd(); + tempDirs.push(cwd); + const created = await createTerminalInWorkspace(ctx.client, { cwd }); + const terminalId = created.terminal!.id; + + await ctx.client.subscribeTerminal(terminalId); + ctx.client.sendTerminalInput(terminalId, { + type: "input", + data: `node -e "process.stdout.write('START\\u4e2d\\u6587END\\n')"\r`, + }); + await waitForTerminalOutput( + ctx.client, + terminalId, + (text) => text.includes("START\u4e2d\u6587END"), + 15000, + ); + + const capture = await ctx.client.captureTerminal(terminalId); + + expect(capture.lines).toContain("START\u4e2d\u6587END"); + }, 15000); + test("captures with start/end line range", async () => { const cwd = tmpCwd(); tempDirs.push(cwd); diff --git a/packages/server/src/terminal/terminal.ts b/packages/server/src/terminal/terminal.ts index 4d3a1aa7d0..ae352a06a7 100644 --- a/packages/server/src/terminal/terminal.ts +++ b/packages/server/src/terminal/terminal.ts @@ -494,7 +494,7 @@ function extractCell(terminal: TerminalType, row: number, col: number): Terminal const bg = bgMode !== 0 ? cell.getBgColor() : undefined; return { - char: cell.getChars() || " ", + char: cell.getWidth() === 0 ? "" : cell.getChars() || " ", fg, bg, fgMode: fgMode !== 0 ? fgMode : undefined, @@ -553,7 +553,7 @@ function extractScrollback( const fg = fgMode !== 0 ? cell.getFgColor() : undefined; const bg = bgMode !== 0 ? cell.getBgColor() : undefined; rowCells.push({ - char: cell.getChars() || " ", + char: cell.getWidth() === 0 ? "" : cell.getChars() || " ", fg, bg, fgMode: fgMode !== 0 ? fgMode : undefined,