fix(app): terminal issues (#14435)

This commit is contained in:
Adam
2026-02-20 07:34:36 -06:00
committed by GitHub
parent 7e0e35af3f
commit 4e9ef3ecc1
5 changed files with 91 additions and 24 deletions

View File

@@ -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)
})

View File

@@ -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 })

View File

@@ -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()}>
<Show when={terminal.active()} keyed>
{(id) => (
<Show when={byId().get(id)}>
{(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 id={`terminal-wrapper-${id}`} class="absolute inset-0">
<Terminal pty={pty()} onCleanup={terminal.update} onConnectError={() => terminal.clone(id)} />
</div>
)}
</For>
</Show>
)}
</Show>
</div>
</div>
<DragOverlay>

View File

@@ -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
}

View File

@@ -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)
}
},
})
})
})