diff options
| author | Adam <[email protected]> | 2026-02-12 15:15:27 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-12 15:15:34 -0600 |
| commit | 548608b7ad1252af3181201ef764b16c05d0b786 (patch) | |
| tree | e33c2f8c5a7f30fbb2a7f068010ee98c13656491 /packages/app/src | |
| parent | 4e0f509e7b7d84395a541bdfa658f6c98f588221 (diff) | |
| download | opencode-548608b7ad1252af3181201ef764b16c05d0b786.tar.gz opencode-548608b7ad1252af3181201ef764b16c05d0b786.zip | |
fix(app): terminal pty isolation
Diffstat (limited to 'packages/app/src')
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 8 | ||||
| -rw-r--r-- | packages/app/src/utils/terminal-writer.test.ts | 33 | ||||
| -rw-r--r-- | packages/app/src/utils/terminal-writer.ts | 27 |
3 files changed, 66 insertions, 2 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index f6bb0b48a..ccf7012d2 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -10,6 +10,7 @@ import { resolveThemeVariant, useTheme, withAlpha, type HexColor } from "@openco import { useLanguage } from "@/context/language" import { showToast } from "@opencode-ai/ui/toast" import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters" +import { terminalWriter } from "@/utils/terminal-writer" const TOGGLE_TERMINAL_ID = "terminal.toggle" const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`" @@ -160,6 +161,7 @@ export const Terminal = (props: TerminalProps) => { const start = typeof local.pty.cursor === "number" && Number.isSafeInteger(local.pty.cursor) ? local.pty.cursor : undefined let cursor = start ?? 0 + let output: ReturnType<typeof terminalWriter> | undefined const cleanup = () => { if (!cleanups.length) return @@ -300,7 +302,7 @@ export const Terminal = (props: TerminalProps) => { fontSize: 14, fontFamily: monoFontFamily(settings.appearance.font()), allowTransparency: false, - convertEol: true, + convertEol: false, theme: terminalColors(), scrollback: 10_000, ghostty: g, @@ -312,6 +314,7 @@ export const Terminal = (props: TerminalProps) => { } ghostty = g term = t + output = terminalWriter((data) => t.write(data)) t.attachCustomKeyEventHandler((event) => { const key = event.key.toLowerCase() @@ -416,7 +419,7 @@ export const Terminal = (props: TerminalProps) => { const data = typeof event.data === "string" ? event.data : "" if (!data) return - t.write(data) + output?.push(data) cursor += data.length } socket.addEventListener("message", handleMessage) @@ -459,6 +462,7 @@ export const Terminal = (props: TerminalProps) => { onCleanup(() => { disposed = true + output?.flush() persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) cleanup() }) diff --git a/packages/app/src/utils/terminal-writer.test.ts b/packages/app/src/utils/terminal-writer.test.ts new file mode 100644 index 000000000..d48dd4f4e --- /dev/null +++ b/packages/app/src/utils/terminal-writer.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test" +import { terminalWriter } from "./terminal-writer" + +describe("terminalWriter", () => { + test("buffers and flushes once per schedule", () => { + const calls: string[] = [] + const scheduled: VoidFunction[] = [] + const writer = terminalWriter( + (data) => calls.push(data), + (flush) => scheduled.push(flush), + ) + + writer.push("a") + writer.push("b") + writer.push("c") + + expect(calls).toEqual([]) + expect(scheduled).toHaveLength(1) + + scheduled[0]?.() + expect(calls).toEqual(["abc"]) + }) + + test("flush is a no-op when empty", () => { + const calls: string[] = [] + const writer = terminalWriter( + (data) => calls.push(data), + (flush) => flush(), + ) + writer.flush() + expect(calls).toEqual([]) + }) +}) diff --git a/packages/app/src/utils/terminal-writer.ts b/packages/app/src/utils/terminal-writer.ts new file mode 100644 index 000000000..b6caff789 --- /dev/null +++ b/packages/app/src/utils/terminal-writer.ts @@ -0,0 +1,27 @@ +export function terminalWriter( + write: (data: string) => void, + schedule: (flush: VoidFunction) => void = queueMicrotask, +) { + let chunks: string[] | undefined + let scheduled = false + + const flush = () => { + scheduled = false + const items = chunks + if (!items?.length) return + chunks = undefined + write(items.join("")) + } + + const push = (data: string) => { + if (!data) return + if (chunks) chunks.push(data) + else chunks = [data] + + if (scheduled) return + scheduled = true + schedule(flush) + } + + return { push, flush } +} |
