Skip to content

Commit 709dafc

Browse files
authored
fix(server): support desktop PTY websockets with HttpApi (anomalyco#25598)
1 parent ea66789 commit 709dafc

17 files changed

Lines changed: 564 additions & 436 deletions

packages/app/src/components/terminal.tsx

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { terminalFontFamily, useSettings } from "@/context/settings"
1515
import type { LocalPTY } from "@/context/terminal"
1616
import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
1717
import { terminalWriter } from "@/utils/terminal-writer"
18+
import { terminalWebSocketURL } from "@/utils/terminal-websocket-url"
1819

1920
const TOGGLE_TERMINAL_ID = "terminal.toggle"
2021
const DEFAULT_TOGGLE_TERMINAL_KEYBIND = "ctrl+`"
@@ -67,13 +68,6 @@ const debugTerminal = (...values: unknown[]) => {
6768
console.debug("[terminal]", ...values)
6869
}
6970

70-
const errorName = (err: unknown) => {
71-
if (!err || typeof err !== "object") return
72-
if (!("name" in err)) return
73-
const errorName = err.name
74-
return typeof errorName === "string" ? errorName : undefined
75-
}
76-
7771
const useTerminalUiBindings = (input: {
7872
container: HTMLDivElement
7973
term: Term
@@ -478,10 +472,9 @@ export const Terminal = (props: TerminalProps) => {
478472

479473
const gone = () =>
480474
client.pty
481-
.get({ ptyID: id })
482-
.then(() => false)
475+
.get({ ptyID: id }, { throwOnError: false })
476+
.then((result) => result.response.status === 404)
483477
.catch((err) => {
484-
if (errorName(err) === "NotFoundError") return true
485478
debugTerminal("failed to inspect terminal session", err)
486479
return false
487480
})
@@ -509,18 +502,9 @@ export const Terminal = (props: TerminalProps) => {
509502
if (disposed) return
510503
drop?.()
511504

512-
const next = new URL(url + `/pty/${id}/connect`)
513-
next.searchParams.set("directory", directory)
514-
next.searchParams.set("cursor", String(seek))
515-
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
516-
if (!sameOrigin && password) {
517-
next.searchParams.set("auth_token", btoa(`${username}:${password}`))
518-
// For same-origin requests, let the browser reuse the page's existing auth.
519-
next.username = username
520-
next.password = password
521-
}
522-
523-
const socket = new WebSocket(next)
505+
const socket = new WebSocket(
506+
terminalWebSocketURL({ url, id, directory, cursor: seek, sameOrigin, username, password }),
507+
)
524508
socket.binaryType = "arraybuffer"
525509
ws = socket
526510

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { terminalWebSocketURL } from "./terminal-websocket-url"
3+
4+
describe("terminalWebSocketURL", () => {
5+
test("uses query auth without embedding credentials in websocket URL", () => {
6+
const url = terminalWebSocketURL({
7+
url: "http://127.0.0.1:49365",
8+
id: "pty_test",
9+
directory: "/tmp/project",
10+
cursor: 0,
11+
sameOrigin: false,
12+
username: "opencode",
13+
password: "secret",
14+
})
15+
16+
expect(url.protocol).toBe("ws:")
17+
expect(url.username).toBe("")
18+
expect(url.password).toBe("")
19+
expect(url.searchParams.get("auth_token")).toBe(btoa("opencode:secret"))
20+
})
21+
22+
test("omits query auth for same-origin websocket URL", () => {
23+
const url = terminalWebSocketURL({
24+
url: "https://app.example.test",
25+
id: "pty_test",
26+
directory: "/tmp/project",
27+
cursor: 10,
28+
sameOrigin: true,
29+
username: "opencode",
30+
password: "secret",
31+
})
32+
33+
expect(url.protocol).toBe("wss:")
34+
expect(url.searchParams.has("auth_token")).toBe(false)
35+
})
36+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export function terminalWebSocketURL(input: {
2+
url: string
3+
id: string
4+
directory: string
5+
cursor: number
6+
sameOrigin: boolean
7+
username: string
8+
password?: string
9+
}) {
10+
const next = new URL(`${input.url}/pty/${input.id}/connect`)
11+
next.searchParams.set("directory", input.directory)
12+
next.searchParams.set("cursor", String(input.cursor))
13+
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
14+
if (!input.sameOrigin && input.password) next.searchParams.set("auth_token", btoa(`${input.username}:${input.password}`))
15+
return next
16+
}

packages/opencode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737
"bun": "./src/server/adapter.bun.ts",
3838
"node": "./src/server/adapter.node.ts",
3939
"default": "./src/server/adapter.bun.ts"
40+
},
41+
"#httpapi-server": {
42+
"bun": "./src/server/httpapi-server.node.ts",
43+
"node": "./src/server/httpapi-server.node.ts",
44+
"default": "./src/server/httpapi-server.node.ts"
4045
}
4146
},
4247
"devDependencies": {

packages/opencode/src/server/httpapi-listener.ts

Lines changed: 0 additions & 244 deletions
This file was deleted.

0 commit comments

Comments
 (0)