summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-12 15:15:27 -0600
committerAdam <[email protected]>2026-02-12 15:15:34 -0600
commit548608b7ad1252af3181201ef764b16c05d0b786 (patch)
treee33c2f8c5a7f30fbb2a7f068010ee98c13656491 /packages/app/src
parent4e0f509e7b7d84395a541bdfa658f6c98f588221 (diff)
downloadopencode-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.tsx8
-rw-r--r--packages/app/src/utils/terminal-writer.test.ts33
-rw-r--r--packages/app/src/utils/terminal-writer.ts27
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 }
+}