Skip to content

Commit bd4527b

Browse files
fix(desktop): remote server switching (#17214)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
1 parent f4a9fe2 commit bd4527b

File tree

6 files changed

+133
-89
lines changed

6 files changed

+133
-89
lines changed

packages/app/src/app.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
265265
)
266266
}
267267

268+
function ServerKey(props: ParentProps) {
269+
const server = useServer()
270+
return (
271+
<Show when={server.key} keyed>
272+
{props.children}
273+
</Show>
274+
)
275+
}
276+
268277
export function AppInterface(props: {
269278
children?: JSX.Element
270279
defaultServer: ServerConnection.Key
@@ -275,20 +284,22 @@ export function AppInterface(props: {
275284
return (
276285
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
277286
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
278-
<GlobalSDKProvider>
279-
<GlobalSyncProvider>
280-
<Dynamic
281-
component={props.router ?? Router}
282-
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
283-
>
284-
<Route path="/" component={HomeRoute} />
285-
<Route path="/:dir" component={DirectoryLayout}>
286-
<Route path="/" component={SessionIndexRoute} />
287-
<Route path="/session/:id?" component={SessionRoute} />
288-
</Route>
289-
</Dynamic>
290-
</GlobalSyncProvider>
291-
</GlobalSDKProvider>
287+
<ServerKey>
288+
<GlobalSDKProvider>
289+
<GlobalSyncProvider>
290+
<Dynamic
291+
component={props.router ?? Router}
292+
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
293+
>
294+
<Route path="/" component={HomeRoute} />
295+
<Route path="/:dir" component={DirectoryLayout}>
296+
<Route path="/" component={SessionIndexRoute} />
297+
<Route path="/session/:id?" component={SessionRoute} />
298+
</Route>
299+
</Dynamic>
300+
</GlobalSyncProvider>
301+
</GlobalSDKProvider>
302+
</ServerKey>
292303
</ConnectionGate>
293304
</ServerProvider>
294305
)

packages/app/src/components/dialog-select-server.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,8 @@ export function DialogSelectServer() {
291291
navigate("/")
292292
return
293293
}
294-
server.setActive(ServerConnection.key(conn))
295294
navigate("/")
295+
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
296296
}
297297

298298
const handleAddChange = (value: string) => {

packages/app/src/components/status-popover.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,8 @@ export function StatusPopover() {
277277
aria-disabled={isBlocked()}
278278
onClick={() => {
279279
if (isBlocked()) return
280-
server.setActive(key)
281280
navigate("/")
281+
queueMicrotask(() => server.setActive(key))
282282
}}
283283
>
284284
<ServerHealthIndicator health={health[key]} />

packages/app/src/components/terminal.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ export const Terminal = (props: TerminalProps) => {
165165
const theme = useTheme()
166166
const language = useLanguage()
167167
const server = useServer()
168+
const directory = sdk.directory
169+
const client = sdk.client
170+
const url = sdk.url
171+
const auth = server.current?.http
172+
const username = auth?.username ?? "opencode"
173+
const password = auth?.password ?? ""
168174
let container!: HTMLDivElement
169175
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
170176
const id = local.pty.id
@@ -215,7 +221,7 @@ export const Terminal = (props: TerminalProps) => {
215221
}
216222

217223
const pushSize = (cols: number, rows: number) => {
218-
return sdk.client.pty
224+
return client.pty
219225
.update({
220226
ptyID: id,
221227
size: { cols, rows },
@@ -474,7 +480,7 @@ export const Terminal = (props: TerminalProps) => {
474480
}
475481

476482
const gone = () =>
477-
sdk.client.pty
483+
client.pty
478484
.get({ ptyID: id })
479485
.then(() => false)
480486
.catch((err) => {
@@ -506,14 +512,14 @@ export const Terminal = (props: TerminalProps) => {
506512
if (disposed) return
507513
drop?.()
508514

509-
const url = new URL(sdk.url + `/pty/${id}/connect`)
510-
url.searchParams.set("directory", sdk.directory)
511-
url.searchParams.set("cursor", String(seek))
512-
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
513-
url.username = server.current?.http.username ?? "opencode"
514-
url.password = server.current?.http.password ?? ""
515+
const next = new URL(url + `/pty/${id}/connect`)
516+
next.searchParams.set("directory", directory)
517+
next.searchParams.set("cursor", String(seek))
518+
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
519+
next.username = username
520+
next.password = password
515521

516-
const socket = new WebSocket(url)
522+
const socket = new WebSocket(next)
517523
socket.binaryType = "arraybuffer"
518524
ws = socket
519525

packages/app/src/context/terminal.tsx

Lines changed: 73 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,60 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
185185
})
186186
onCleanup(unsub)
187187

188+
const update = (client: ReturnType<typeof useSDK>["client"], pty: Partial<LocalPTY> & { id: string }) => {
189+
const index = store.all.findIndex((x) => x.id === pty.id)
190+
const previous = index >= 0 ? store.all[index] : undefined
191+
if (index >= 0) {
192+
setStore("all", index, (item) => ({ ...item, ...pty }))
193+
}
194+
client.pty
195+
.update({
196+
ptyID: pty.id,
197+
title: pty.title,
198+
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
199+
})
200+
.catch((error: unknown) => {
201+
if (previous) {
202+
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
203+
if (currentIndex >= 0) setStore("all", currentIndex, previous)
204+
}
205+
console.error("Failed to update terminal", error)
206+
})
207+
}
208+
209+
const clone = async (client: ReturnType<typeof useSDK>["client"], id: string) => {
210+
const index = store.all.findIndex((x) => x.id === id)
211+
const pty = store.all[index]
212+
if (!pty) return
213+
const next = await client.pty
214+
.create({
215+
title: pty.title,
216+
})
217+
.catch((error: unknown) => {
218+
console.error("Failed to clone terminal", error)
219+
return undefined
220+
})
221+
if (!next?.data) return
222+
223+
const active = store.active === pty.id
224+
225+
batch(() => {
226+
setStore("all", index, {
227+
id: next.data.id,
228+
title: next.data.title ?? pty.title,
229+
titleNumber: pty.titleNumber,
230+
buffer: undefined,
231+
cursor: undefined,
232+
scrollY: undefined,
233+
rows: undefined,
234+
cols: undefined,
235+
})
236+
if (active) {
237+
setStore("active", next.data.id)
238+
}
239+
})
240+
}
241+
188242
return {
189243
ready,
190244
all: createMemo(() => store.all),
@@ -216,24 +270,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
216270
})
217271
},
218272
update(pty: Partial<LocalPTY> & { id: string }) {
219-
const index = store.all.findIndex((x) => x.id === pty.id)
220-
const previous = index >= 0 ? store.all[index] : undefined
221-
if (index >= 0) {
222-
setStore("all", index, (item) => ({ ...item, ...pty }))
223-
}
224-
sdk.client.pty
225-
.update({
226-
ptyID: pty.id,
227-
title: pty.title,
228-
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
229-
})
230-
.catch((error: unknown) => {
231-
if (previous) {
232-
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
233-
if (currentIndex >= 0) setStore("all", currentIndex, previous)
234-
}
235-
console.error("Failed to update terminal", error)
236-
})
273+
update(sdk.client, pty)
237274
},
238275
trim(id: string) {
239276
const index = store.all.findIndex((x) => x.id === id)
@@ -248,37 +285,23 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
248285
})
249286
},
250287
async clone(id: string) {
251-
const index = store.all.findIndex((x) => x.id === id)
252-
const pty = store.all[index]
253-
if (!pty) return
254-
const clone = await sdk.client.pty
255-
.create({
256-
title: pty.title,
257-
})
258-
.catch((error: unknown) => {
259-
console.error("Failed to clone terminal", error)
260-
return undefined
261-
})
262-
if (!clone?.data) return
263-
264-
const active = store.active === pty.id
265-
266-
batch(() => {
267-
setStore("all", index, {
268-
id: clone.data.id,
269-
title: clone.data.title ?? pty.title,
270-
titleNumber: pty.titleNumber,
271-
// New PTY process, so start clean.
272-
buffer: undefined,
273-
cursor: undefined,
274-
scrollY: undefined,
275-
rows: undefined,
276-
cols: undefined,
277-
})
278-
if (active) {
279-
setStore("active", clone.data.id)
280-
}
281-
})
288+
await clone(sdk.client, id)
289+
},
290+
bind() {
291+
const client = sdk.client
292+
return {
293+
trim(id: string) {
294+
const index = store.all.findIndex((x) => x.id === id)
295+
if (index === -1) return
296+
setStore("all", index, (pty) => trimTerminal(pty))
297+
},
298+
update(pty: Partial<LocalPTY> & { id: string }) {
299+
update(client, pty)
300+
},
301+
async clone(id: string) {
302+
await clone(client, id)
303+
},
304+
}
282305
},
283306
open(id: string) {
284307
setStore("active", id)
@@ -403,6 +426,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
403426
trim: (id: string) => workspace().trim(id),
404427
trimAll: () => workspace().trimAll(),
405428
clone: (id: string) => workspace().clone(id),
429+
bind: () => workspace(),
406430
open: (id: string) => workspace().open(id),
407431
close: (id: string) => workspace().close(id),
408432
move: (id: string, to: number) => workspace().move(id, to),

packages/app/src/pages/session/terminal-panel.tsx

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,21 +280,24 @@ export function TerminalPanel() {
280280
</Tabs>
281281
<div class="flex-1 min-h-0 relative">
282282
<Show when={terminal.active()} keyed>
283-
{(id) => (
284-
<Show when={all().find((pty) => pty.id === id)}>
285-
{(pty) => (
286-
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
287-
<Terminal
288-
pty={pty()}
289-
autoFocus={opened()}
290-
onConnect={() => terminal.trim(id)}
291-
onCleanup={terminal.update}
292-
onConnectError={() => terminal.clone(id)}
293-
/>
294-
</div>
295-
)}
296-
</Show>
297-
)}
283+
{(id) => {
284+
const ops = terminal.bind()
285+
return (
286+
<Show when={all().find((pty) => pty.id === id)}>
287+
{(pty) => (
288+
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
289+
<Terminal
290+
pty={pty()}
291+
autoFocus={opened()}
292+
onConnect={() => ops.trim(id)}
293+
onCleanup={ops.update}
294+
onConnectError={() => ops.clone(id)}
295+
/>
296+
</div>
297+
)}
298+
</Show>
299+
)
300+
}}
298301
</Show>
299302
</div>
300303
</div>

0 commit comments

Comments
 (0)