summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/addons
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-04 23:16:11 -0600
committerAdam <[email protected]>2026-01-05 13:21:31 -0600
commitcdbb009ab04f702c78b11d2d8338cb5ee3b685da (patch)
tree9f731096175ca398e517bbdcf9eeb9a4a0dbcff8 /packages/app/src/addons
parent001b48635602470ab1bac21b6af9fe207ccd5a17 (diff)
downloadopencode-cdbb009ab04f702c78b11d2d8338cb5ee3b685da.tar.gz
opencode-cdbb009ab04f702c78b11d2d8338cb5ee3b685da.zip
fix(app): terminal flakiness
Diffstat (limited to 'packages/app/src/addons')
-rw-r--r--packages/app/src/addons/serialize.test.ts47
-rw-r--r--packages/app/src/addons/serialize.ts42
2 files changed, 66 insertions, 23 deletions
diff --git a/packages/app/src/addons/serialize.test.ts b/packages/app/src/addons/serialize.test.ts
index ad165f43f..7f6780557 100644
--- a/packages/app/src/addons/serialize.test.ts
+++ b/packages/app/src/addons/serialize.test.ts
@@ -242,6 +242,53 @@ describe("SerializeAddon", () => {
expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
})
+ test("serialized output should restore after Terminal.reset()", async () => {
+ const { term, addon } = createTerminal()
+
+ const content = [
+ "\x1b[1;32m❯\x1b[0m \x1b[34mcd\x1b[0m /some/path",
+ "\x1b[1;32m❯\x1b[0m \x1b[34mls\x1b[0m -la",
+ "total 42",
+ ].join("\r\n")
+
+ await writeAndWait(term, content)
+
+ const serialized = addon.serialize()
+
+ const { term: term2 } = createTerminal()
+ terminals.push(term2)
+ term2.reset()
+ await writeAndWait(term2, serialized)
+
+ expect(term2.buffer.active.getLine(0)?.translateToString(true)).toContain("cd /some/path")
+ expect(term2.buffer.active.getLine(1)?.translateToString(true)).toContain("ls -la")
+ expect(term2.buffer.active.getLine(2)?.translateToString(true)).toBe("total 42")
+ })
+
+ test("alternate buffer should round-trip without garbage", async () => {
+ const { term, addon } = createTerminal(20, 5)
+
+ await writeAndWait(term, "normal\r\n")
+ await writeAndWait(term, "\x1b[?1049h\x1b[HALT")
+
+ expect(term.buffer.active.type).toBe("alternate")
+
+ const serialized = addon.serialize()
+
+ const { term: term2 } = createTerminal(20, 5)
+ terminals.push(term2)
+ await writeAndWait(term2, serialized)
+
+ expect(term2.buffer.active.type).toBe("alternate")
+
+ const line = term2.buffer.active.getLine(0)
+ expect(line?.translateToString(true)).toBe("ALT")
+
+ // Ensure a cell beyond content isn't garbage
+ const cellCode = line?.getCell(10)?.getCode()
+ expect(cellCode === 0 || cellCode === 32).toBe(true)
+ })
+
test("serialized output written to new terminal should match original colors", async () => {
const { term, addon } = createTerminal(40, 5)
diff --git a/packages/app/src/addons/serialize.ts b/packages/app/src/addons/serialize.ts
index cb1ff8442..4309a725e 100644
--- a/packages/app/src/addons/serialize.ts
+++ b/packages/app/src/addons/serialize.ts
@@ -157,23 +157,6 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
abstract class BaseSerializeHandler {
constructor(protected readonly _buffer: IBuffer) {}
- private _isRealContent(codepoint: number): boolean {
- if (codepoint === 0) return false
- if (codepoint >= 0xf000) return false
- return true
- }
-
- private _findLastContentColumn(line: IBufferLine): number {
- let lastContent = -1
- for (let col = 0; col < line.length; col++) {
- const cell = line.getCell(col)
- if (cell && this._isRealContent(cell.getCode())) {
- lastContent = col
- }
- }
- return lastContent + 1
- }
-
public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string {
let oldCell = this._buffer.getNullCell()
@@ -182,14 +165,14 @@ abstract class BaseSerializeHandler {
const startColumn = range.start.x
const endColumn = range.end.x
- this._beforeSerialize(endRow - startRow, startRow, endRow)
+ this._beforeSerialize(endRow - startRow + 1, startRow, endRow)
for (let row = startRow; row <= endRow; row++) {
const line = this._buffer.getLine(row)
if (line) {
const startLineColumn = row === range.start.y ? startColumn : 0
- const maxColumn = row === range.end.y ? endColumn : this._findLastContentColumn(line)
- const endLineColumn = Math.min(maxColumn, line.length)
+ const endLineColumn = Math.min(endColumn, line.length)
+
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col)
if (!c) {
@@ -243,6 +226,13 @@ class StringSerializeHandler extends BaseSerializeHandler {
protected _beforeSerialize(rows: number, start: number, _end: number): void {
this._allRows = new Array<string>(rows)
+ this._allRowSeparators = new Array<string>(rows)
+ this._rowIndex = 0
+
+ this._currentRow = ""
+ this._nullCellCount = 0
+ this._cursorStyle = this._buffer.getNullCell()
+
this._lastContentCursorRow = start
this._lastCursorRow = start
this._firstRow = start
@@ -251,6 +241,11 @@ class StringSerializeHandler extends BaseSerializeHandler {
protected _rowEnd(row: number, isLastRow: boolean): void {
let rowSeparator = ""
+ if (this._nullCellCount > 0) {
+ this._currentRow += " ".repeat(this._nullCellCount)
+ this._nullCellCount = 0
+ }
+
if (!isLastRow) {
const nextLine = this._buffer.getLine(row + 1)
@@ -388,7 +383,8 @@ class StringSerializeHandler extends BaseSerializeHandler {
}
const codepoint = cell.getCode()
- const isGarbage = codepoint >= 0xf000
+ const isInvalidCodepoint = codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)
+ const isGarbage = isInvalidCodepoint || (codepoint >= 0xf000 && cell.getWidth() === 1)
const isEmptyCell = codepoint === 0 || cell.getChars() === "" || isGarbage
const sgrSeq = this._diffStyle(cell, this._cursorStyle)
@@ -397,7 +393,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (styleChanged) {
if (this._nullCellCount > 0) {
- this._currentRow += `\u001b[${this._nullCellCount}C`
+ this._currentRow += " ".repeat(this._nullCellCount)
this._nullCellCount = 0
}
@@ -417,7 +413,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
this._nullCellCount += cell.getWidth()
} else {
if (this._nullCellCount > 0) {
- this._currentRow += `\u001b[${this._nullCellCount}C`
+ this._currentRow += " ".repeat(this._nullCellCount)
this._nullCellCount = 0
}