Răsfoiți Sursa

fix(app): terminal issues (#14435)

Adam 1 lună în urmă
părinte
comite
4e9ef3ecc1

+ 4 - 2
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)
 })

+ 1 - 1
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 })

+ 12 - 16
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>

+ 30 - 5
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
         }

+ 44 - 0
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)
+        }
+      },
+    })
+  })
 })