fix(process): preserve terminal cell widths during normalization (#4142)

This commit is contained in:
Chip Li authored and GitHub committed 2026-10-23 02:30:54 +08:00
1 parent 02a0e72f70
commit 1ba178befb
3 files changed
+58 -5

No files matched your search

@@ -100,6 +100,27 @@ it("trims only after normalization sees the full-width row", async () => {
expect(frames[0]?.lines[0]).toBe("pid <pid>");
});
it.each([
{ data: "界", cells: 2 },
// The pinned xterm Unicode table treats this emoji as one cell.
{ data: "😀", cells: 1 },
{ data: "e\u0301", cells: 1 },
{ data: "ab", cells: 2 },
])(
"normalizes the terminal cells of $data without extra padding",
async ({ data, cells }) => {
const { frames } = await renderFrames(data, (value) =>
value.replaceAll(" ", "_"),
);
expect(frames[0]?.lines).toEqual([
data + "_".repeat(20 - cells),
"_".repeat(20),
"_".repeat(20),
"_".repeat(20),
]);
},
);
it("omits rendered frames with unknown expanded size and keeps later bounded states", async () => {
const renderer = new TerminalRenderer({
columns: 10,
+5 -5
View File
@@ -131,14 +131,14 @@ export class TerminalRenderer {
let bytes = 0;
for (let row = 0; row < this.#terminal.rows; row += 1) {
const line = buffer.getLine(buffer.viewportY + row);
const paddedLine = (
line?.translateToString(false, 0, this.#terminal.cols) ?? ""
).padEnd(this.#terminal.cols, " ");
const renderedLine =
line?.translateToString(false, 0, this.#terminal.cols) ??
" ".repeat(this.#terminal.cols);
const trailingSpaceBytes = Buffer.byteLength(
paddedLine.match(TRAILING_SPACES)?.[0] ?? "",
renderedLine.match(TRAILING_SPACES)?.[0] ?? "",
);
const normalized = this.options.normalize(
paddedLine,
renderedLine,
Math.min(Number.MAX_SAFE_INTEGER, remainingBytes + trailingSpaceBytes),
);
if (normalized.state === "over_budget") {
@@ -7,6 +7,38 @@ import { itWithCaptureCapability } from "./processCaptureCapability.js";
import { captureProcessScenario } from "../../../src/process/capture/ProcessHarness.js";
import { parseProcessScenario } from "../../../src/domain/process/processScenario.js";
itWithCaptureCapability(
"normalizes wide terminal cells without inventing blank cells",
async () => {
const result = await captureProcessScenario(
parseProcessScenario({
executable: process.execPath,
arguments: ["-e", 'process.stdout.write("界")'],
terminal: { columns: 4, rows: 1, scrollback: 0 },
normalization: {
patterns: [{ pattern: " ", replacement: "_" }],
},
}),
);
if (!result.ok) throw result.error;
expect(
result.value.frames
.map(({ raw_data, data }) => raw_data ?? data)
.join(""),
).toBe("界");
expect(result.value.rendered_frames.at(-1)).toMatchObject({
columns: 4,
cursor_x: 2,
lines: ["界__"],
});
expect(result.value.cleanup).toMatchObject({
owned_process_group: "verified",
temporary_root: "removed",
});
},
20_000,
);
itWithCaptureCapability(
"captures one command's terminal output, selected files, and owned cleanup",
async () => {