diff options
| author | Adam <[email protected]> | 2026-02-20 07:34:36 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-02-20 07:34:36 -0600 |
| commit | 4e9ef3ecc1506c5087511105ac905564d2b0c73f (patch) | |
| tree | 18fbcf3335def5699800082925a58325c1d9b8ba | |
| parent | 7e0e35af3fe468dd931dda955c62e03fd361fd9f (diff) | |
| download | opencode-4e9ef3ecc1506c5087511105ac905564d2b0c73f.tar.gz opencode-4e9ef3ecc1506c5087511105ac905564d2b0c73f.zip | |
fix(app): terminal issues (#14435)
| -rw-r--r-- | packages/app/e2e/terminal/terminal-init.spec.ts | 6 | ||||
| -rw-r--r-- | packages/app/src/components/terminal.tsx | 2 | ||||
| -rw-r--r-- | packages/app/src/pages/session/terminal-panel.tsx | 28 | ||||
| -rw-r--r-- | packages/opencode/src/pty/index.ts | 35 | ||||
| -rw-r--r-- | packages/opencode/test/pty/pty-output-isolation.test.ts | 44 |
5 files changed, 91 insertions, 24 deletions
diff --git a/packages/app/e2e/terminal/terminal-init.spec.ts b/packages/app/e2e/terminal/terminal-init.spec.ts index 87934b66e..18991bf76 100644 --- a/packages/app/e2e/terminal/terminal-init.spec.ts +++ b/packages/app/e2e/terminal/terminal-init.spec.ts @@ -6,6 +6,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes await gotoSession() const terminals = page.locator(terminalSelector) + const tabs = page.locator('#terminal-panel [data-slot="tabs-trigger"]') const opened = await terminals.first().isVisible() if (!opened) { @@ -21,6 +22,7 @@ test("smoke terminal mounts and can create a second tab", async ({ page, gotoSes await page.locator(promptSelector).click() await page.keyboard.press("Control+Alt+T") - await expect(terminals).toHaveCount(2) - await expect(terminals.nth(1).locator("textarea")).toHaveCount(1) + await expect(tabs).toHaveCount(2) + await expect(terminals).toHaveCount(1) + await expect(terminals.first().locator("textarea")).toHaveCount(1) }) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index bd7ab2447..ce811463f 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -540,7 +540,7 @@ export const Terminal = (props: TerminalProps) => { disposed = true if (fitFrame !== undefined) cancelAnimationFrame(fitFrame) if (sizeTimer !== undefined) clearTimeout(sizeTimer) - if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close() + if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000) const finalize = () => { persistTerminal({ term, addon: serializeAddon, cursor, pty: local.pty, onCleanup: props.onCleanup }) diff --git a/packages/app/src/pages/session/terminal-panel.tsx b/packages/app/src/pages/session/terminal-panel.tsx index 33421c386..27ea4e6f3 100644 --- a/packages/app/src/pages/session/terminal-panel.tsx +++ b/packages/app/src/pages/session/terminal-panel.tsx @@ -67,11 +67,11 @@ export function TerminalPanel() { on( () => terminal.active(), (activeId) => { - if (!activeId || !opened()) return + if (!activeId || !open()) return if (document.activeElement instanceof HTMLElement) { document.activeElement.blur() } - focusTerminalById(activeId) + setTimeout(() => focusTerminalById(activeId), 0) }, ), ) @@ -209,21 +209,17 @@ export function TerminalPanel() { </Tabs.List> </Tabs> <div class="flex-1 min-h-0 relative"> - <For each={all()}> - {(pty) => ( - <div - id={`terminal-wrapper-${pty.id}`} - class="absolute inset-0" - style={{ - display: terminal.active() === pty.id ? "block" : "none", - }} - > - <Show when={pty.id} keyed> - <Terminal pty={pty} onCleanup={terminal.update} onConnectError={() => terminal.clone(pty.id)} /> - </Show> - </div> + <Show when={terminal.active()} keyed> + {(id) => ( + <Show when={byId().get(id)}> + {(pty) => ( + <div id={`terminal-wrapper-${id}`} class="absolute inset-0"> + <Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} /> + </div> + )} + </Show> )} - </For> + </Show> </div> </div> <DragOverlay> diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts index 2dda403e1..33083485b 100644 --- a/packages/opencode/src/pty/index.ts +++ b/packages/opencode/src/pty/index.ts @@ -41,13 +41,38 @@ export namespace Pty { const token = (ws: Socket) => { const data = ws.data - if (!data || typeof data !== "object") return + if (data === undefined) return + if (data === null) return + if (typeof data !== "object") return data - const events = (data as { events?: unknown }).events - if (events && typeof events === "object") return events + const id = (data as { connId?: unknown }).connId + if (typeof id === "number" || typeof id === "string") return id + + const href = (data as { href?: unknown }).href + if (typeof href === "string") return href const url = (data as { url?: unknown }).url - if (url && typeof url === "object") return url + if (typeof url === "string") return url + if (url && typeof url === "object") { + const href = (url as { href?: unknown }).href + if (typeof href === "string") return href + return url + } + + const events = (data as { events?: unknown }).events + if (typeof events === "number" || typeof events === "string") return events + if (events && typeof events === "object") { + const id = (events as { connId?: unknown }).connId + if (typeof id === "number" || typeof id === "string") return id + + const id2 = (events as { connection?: unknown }).connection + if (typeof id2 === "number" || typeof id2 === "string") return id2 + + const id3 = (events as { id?: unknown }).id + if (typeof id3 === "number" || typeof id3 === "string") return id3 + + return events + } return data } @@ -210,7 +235,7 @@ export namespace Pty { continue } - if (sub.token !== undefined && token(ws) !== sub.token) { + if (token(ws) !== sub.token) { session.subscribers.delete(ws) continue } diff --git a/packages/opencode/test/pty/pty-output-isolation.test.ts b/packages/opencode/test/pty/pty-output-isolation.test.ts index 1b89a6374..07e86ea97 100644 --- a/packages/opencode/test/pty/pty-output-isolation.test.ts +++ b/packages/opencode/test/pty/pty-output-isolation.test.ts @@ -97,4 +97,48 @@ describe("pty", () => { }, }) }) + + test("does not leak output when socket data mutates in-place", async () => { + await using dir = await tmpdir({ git: true }) + + await Instance.provide({ + directory: dir.path, + fn: async () => { + const a = await Pty.create({ command: "cat", title: "a" }) + try { + const outA: string[] = [] + const outB: string[] = [] + + const ctx = { connId: 1 } + const ws = { + readyState: 1, + data: ctx, + send: (data: unknown) => { + outA.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + }, + close: () => { + // no-op + }, + } + + Pty.connect(a.id, ws as any) + outA.length = 0 + + // Simulate the runtime mutating per-connection data without + // swapping the reference (ws.data stays the same object). + ctx.connId = 2 + ws.send = (data: unknown) => { + outB.push(typeof data === "string" ? data : Buffer.from(data as Uint8Array).toString("utf8")) + } + + Pty.write(a.id, "AAA\n") + await Bun.sleep(100) + + expect(outB.join("")).not.toContain("AAA") + } finally { + await Pty.remove(a.id) + } + }, + }) + }) }) |
