summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-05 10:30:44 -0600
committerAdam <[email protected]>2025-12-05 10:30:48 -0600
commitcfbaf81ef8f360d1df621d800d10b0ac2b3019a8 (patch)
tree424961841d111017d38714010158e270bd688a18
parent87a791fdb9432d457202da85ec5e23e42f91db4d (diff)
downloadopencode-cfbaf81ef8f360d1df621d800d10b0ac2b3019a8.tar.gz
opencode-cfbaf81ef8f360d1df621d800d10b0ac2b3019a8.zip
fix(desktop): clone pty session on reconnect
-rw-r--r--packages/desktop/src/components/terminal.tsx9
-rw-r--r--packages/desktop/src/context/session.tsx5
-rw-r--r--packages/desktop/src/pages/session.tsx10
-rw-r--r--packages/opencode/src/server/server.ts42
4 files changed, 36 insertions, 30 deletions
diff --git a/packages/desktop/src/components/terminal.tsx b/packages/desktop/src/components/terminal.tsx
index 49a45a432..f7b44b0aa 100644
--- a/packages/desktop/src/components/terminal.tsx
+++ b/packages/desktop/src/components/terminal.tsx
@@ -1,6 +1,5 @@
import { init, Terminal as Term, FitAddon } from "ghostty-web"
import { ComponentProps, onCleanup, onMount, splitProps } from "solid-js"
-import { createReconnectingWS, ReconnectingWebSocket } from "@solid-primitives/websocket"
import { useSDK } from "@/context/sdk"
import { SerializeAddon } from "@/addons/serialize"
import { LocalPTY } from "@/context/session"
@@ -11,19 +10,20 @@ export interface TerminalProps extends ComponentProps<"div"> {
pty: LocalPTY
onSubmit?: () => void
onCleanup?: (pty: LocalPTY) => void
+ onConnectError?: (error: unknown) => void
}
export const Terminal = (props: TerminalProps) => {
const sdk = useSDK()
let container!: HTMLDivElement
- const [local, others] = splitProps(props, ["pty", "class", "classList"])
- let ws: ReconnectingWebSocket
+ const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
+ let ws: WebSocket
let term: Term
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
onMount(async () => {
- ws = createReconnectingWS(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
+ ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
term = new Term({
cursorBlink: true,
fontSize: 14,
@@ -115,6 +115,7 @@ export const Terminal = (props: TerminalProps) => {
})
ws.addEventListener("error", (error) => {
console.error("WebSocket error:", error)
+ props.onConnectError?.(error)
})
ws.addEventListener("close", () => {
console.log("WebSocket disconnected")
diff --git a/packages/desktop/src/context/session.tsx b/packages/desktop/src/context/session.tsx
index 690653992..b5972f3e3 100644
--- a/packages/desktop/src/context/session.tsx
+++ b/packages/desktop/src/context/session.tsx
@@ -26,7 +26,7 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
const params = useParams()
const sync = useSync()
const name = createMemo(
- () => `______${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}`,
+ () => `${base64Encode(sync.data.project.worktree)}/session${params.id ? "/" + params.id : ""}.v1`,
)
const [store, setStore] = makePersisted(
@@ -232,6 +232,9 @@ export const { use: useSession, provider: SessionProvider } = createSimpleContex
...pty,
...clone.data,
})
+ if (store.terminals.active === pty.id) {
+ setStore("terminals", "active", clone.data.id)
+ }
},
open(id: string) {
setStore("terminals", "active", id)
diff --git a/packages/desktop/src/pages/session.tsx b/packages/desktop/src/pages/session.tsx
index 773625334..8cd9e9d65 100644
--- a/packages/desktop/src/pages/session.tsx
+++ b/packages/desktop/src/pages/session.tsx
@@ -84,6 +84,10 @@ export default function Page() {
}
if (event.ctrlKey && event.key.toLowerCase() === "`") {
event.preventDefault()
+ if (event.shiftKey) {
+ session.terminal.new()
+ return
+ }
layout.terminal.toggle()
return
}
@@ -663,7 +667,11 @@ export default function Page() {
<For each={session.terminal.all()}>
{(terminal) => (
<Tabs.Content value={terminal.id}>
- <Terminal pty={terminal} onCleanup={session.terminal.update} />
+ <Terminal
+ pty={terminal}
+ onCleanup={session.terminal.update}
+ onConnectError={() => session.terminal.clone(terminal.id)}
+ />
</Tabs.Content>
)}
</For>
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index a74b7876f..7a105e746 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -208,53 +208,53 @@ export namespace Server {
return c.json(info)
},
)
- .put(
+ .get(
"/pty/:id",
describeRoute({
- description: "Update PTY session",
- operationId: "pty.update",
+ description: "Get PTY session info",
+ operationId: "pty.get",
responses: {
200: {
- description: "Updated session",
+ description: "Session info",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
- ...errors(400),
+ ...errors(404),
},
}),
validator("param", z.object({ id: z.string() })),
- validator("json", Pty.UpdateInput),
async (c) => {
- const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
+ const info = Pty.get(c.req.valid("param").id)
+ if (!info) {
+ throw new Storage.NotFoundError({ message: "Session not found" })
+ }
return c.json(info)
},
)
- .get(
+ .put(
"/pty/:id",
describeRoute({
- description: "Get PTY session info",
- operationId: "pty.get",
+ description: "Update PTY session",
+ operationId: "pty.update",
responses: {
200: {
- description: "Session info",
+ description: "Updated session",
content: {
"application/json": {
schema: resolver(Pty.Info),
},
},
},
- ...errors(404),
+ ...errors(400),
},
}),
validator("param", z.object({ id: z.string() })),
+ validator("json", Pty.UpdateInput),
async (c) => {
- const info = Pty.get(c.req.valid("param").id)
- if (!info) {
- throw new Storage.NotFoundError({ message: "Session not found" })
- }
+ const info = await Pty.update(c.req.valid("param").id, c.req.valid("json"))
return c.json(info)
},
)
@@ -295,20 +295,14 @@ export namespace Server {
},
},
},
- 404: {
- description: "Session not found",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
- },
- },
+ ...errors(404),
},
}),
validator("param", z.object({ id: z.string() })),
upgradeWebSocket((c) => {
const id = c.req.param("id")
let handler: ReturnType<typeof Pty.connect>
+ if (!Pty.get(id)) throw new Error("Session not found")
return {
onOpen(_event, ws) {
handler = Pty.connect(id, ws)