mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-20 07:34:38 +00:00
fix(desktop): remote server switching (#17214)
Co-authored-by: Brendan Allan <git@brendonovich.dev>
This commit is contained in:
@@ -265,6 +265,15 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ServerKey(props: ParentProps) {
|
||||||
|
const server = useServer()
|
||||||
|
return (
|
||||||
|
<Show when={server.key} keyed>
|
||||||
|
{props.children}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function AppInterface(props: {
|
export function AppInterface(props: {
|
||||||
children?: JSX.Element
|
children?: JSX.Element
|
||||||
defaultServer: ServerConnection.Key
|
defaultServer: ServerConnection.Key
|
||||||
@@ -275,20 +284,22 @@ export function AppInterface(props: {
|
|||||||
return (
|
return (
|
||||||
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
<ServerProvider defaultServer={props.defaultServer} servers={props.servers}>
|
||||||
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
<ConnectionGate disableHealthCheck={props.disableHealthCheck}>
|
||||||
<GlobalSDKProvider>
|
<ServerKey>
|
||||||
<GlobalSyncProvider>
|
<GlobalSDKProvider>
|
||||||
<Dynamic
|
<GlobalSyncProvider>
|
||||||
component={props.router ?? Router}
|
<Dynamic
|
||||||
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
component={props.router ?? Router}
|
||||||
>
|
root={(routerProps) => <RouterRoot appChildren={props.children}>{routerProps.children}</RouterRoot>}
|
||||||
<Route path="/" component={HomeRoute} />
|
>
|
||||||
<Route path="/:dir" component={DirectoryLayout}>
|
<Route path="/" component={HomeRoute} />
|
||||||
<Route path="/" component={SessionIndexRoute} />
|
<Route path="/:dir" component={DirectoryLayout}>
|
||||||
<Route path="/session/:id?" component={SessionRoute} />
|
<Route path="/" component={SessionIndexRoute} />
|
||||||
</Route>
|
<Route path="/session/:id?" component={SessionRoute} />
|
||||||
</Dynamic>
|
</Route>
|
||||||
</GlobalSyncProvider>
|
</Dynamic>
|
||||||
</GlobalSDKProvider>
|
</GlobalSyncProvider>
|
||||||
|
</GlobalSDKProvider>
|
||||||
|
</ServerKey>
|
||||||
</ConnectionGate>
|
</ConnectionGate>
|
||||||
</ServerProvider>
|
</ServerProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -291,8 +291,8 @@ export function DialogSelectServer() {
|
|||||||
navigate("/")
|
navigate("/")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
server.setActive(ServerConnection.key(conn))
|
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddChange = (value: string) => {
|
const handleAddChange = (value: string) => {
|
||||||
|
|||||||
@@ -277,8 +277,8 @@ export function StatusPopover() {
|
|||||||
aria-disabled={isBlocked()}
|
aria-disabled={isBlocked()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isBlocked()) return
|
if (isBlocked()) return
|
||||||
server.setActive(key)
|
|
||||||
navigate("/")
|
navigate("/")
|
||||||
|
queueMicrotask(() => server.setActive(key))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ServerHealthIndicator health={health[key]} />
|
<ServerHealthIndicator health={health[key]} />
|
||||||
|
|||||||
@@ -165,6 +165,12 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
const server = useServer()
|
const server = useServer()
|
||||||
|
const directory = sdk.directory
|
||||||
|
const client = sdk.client
|
||||||
|
const url = sdk.url
|
||||||
|
const auth = server.current?.http
|
||||||
|
const username = auth?.username ?? "opencode"
|
||||||
|
const password = auth?.password ?? ""
|
||||||
let container!: HTMLDivElement
|
let container!: HTMLDivElement
|
||||||
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
const [local, others] = splitProps(props, ["pty", "class", "classList", "autoFocus", "onConnect", "onConnectError"])
|
||||||
const id = local.pty.id
|
const id = local.pty.id
|
||||||
@@ -215,7 +221,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pushSize = (cols: number, rows: number) => {
|
const pushSize = (cols: number, rows: number) => {
|
||||||
return sdk.client.pty
|
return client.pty
|
||||||
.update({
|
.update({
|
||||||
ptyID: id,
|
ptyID: id,
|
||||||
size: { cols, rows },
|
size: { cols, rows },
|
||||||
@@ -474,7 +480,7 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gone = () =>
|
const gone = () =>
|
||||||
sdk.client.pty
|
client.pty
|
||||||
.get({ ptyID: id })
|
.get({ ptyID: id })
|
||||||
.then(() => false)
|
.then(() => false)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -506,14 +512,14 @@ export const Terminal = (props: TerminalProps) => {
|
|||||||
if (disposed) return
|
if (disposed) return
|
||||||
drop?.()
|
drop?.()
|
||||||
|
|
||||||
const url = new URL(sdk.url + `/pty/${id}/connect`)
|
const next = new URL(url + `/pty/${id}/connect`)
|
||||||
url.searchParams.set("directory", sdk.directory)
|
next.searchParams.set("directory", directory)
|
||||||
url.searchParams.set("cursor", String(seek))
|
next.searchParams.set("cursor", String(seek))
|
||||||
url.protocol = url.protocol === "https:" ? "wss:" : "ws:"
|
next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
|
||||||
url.username = server.current?.http.username ?? "opencode"
|
next.username = username
|
||||||
url.password = server.current?.http.password ?? ""
|
next.password = password
|
||||||
|
|
||||||
const socket = new WebSocket(url)
|
const socket = new WebSocket(next)
|
||||||
socket.binaryType = "arraybuffer"
|
socket.binaryType = "arraybuffer"
|
||||||
ws = socket
|
ws = socket
|
||||||
|
|
||||||
|
|||||||
@@ -185,6 +185,60 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
onCleanup(unsub)
|
onCleanup(unsub)
|
||||||
|
|
||||||
|
const update = (client: ReturnType<typeof useSDK>["client"], pty: Partial<LocalPTY> & { id: string }) => {
|
||||||
|
const index = store.all.findIndex((x) => x.id === pty.id)
|
||||||
|
const previous = index >= 0 ? store.all[index] : undefined
|
||||||
|
if (index >= 0) {
|
||||||
|
setStore("all", index, (item) => ({ ...item, ...pty }))
|
||||||
|
}
|
||||||
|
client.pty
|
||||||
|
.update({
|
||||||
|
ptyID: pty.id,
|
||||||
|
title: pty.title,
|
||||||
|
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
if (previous) {
|
||||||
|
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
||||||
|
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
||||||
|
}
|
||||||
|
console.error("Failed to update terminal", error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clone = async (client: ReturnType<typeof useSDK>["client"], id: string) => {
|
||||||
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
|
const pty = store.all[index]
|
||||||
|
if (!pty) return
|
||||||
|
const next = await client.pty
|
||||||
|
.create({
|
||||||
|
title: pty.title,
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error("Failed to clone terminal", error)
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
if (!next?.data) return
|
||||||
|
|
||||||
|
const active = store.active === pty.id
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setStore("all", index, {
|
||||||
|
id: next.data.id,
|
||||||
|
title: next.data.title ?? pty.title,
|
||||||
|
titleNumber: pty.titleNumber,
|
||||||
|
buffer: undefined,
|
||||||
|
cursor: undefined,
|
||||||
|
scrollY: undefined,
|
||||||
|
rows: undefined,
|
||||||
|
cols: undefined,
|
||||||
|
})
|
||||||
|
if (active) {
|
||||||
|
setStore("active", next.data.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ready,
|
ready,
|
||||||
all: createMemo(() => store.all),
|
all: createMemo(() => store.all),
|
||||||
@@ -216,24 +270,7 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
update(pty: Partial<LocalPTY> & { id: string }) {
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
const index = store.all.findIndex((x) => x.id === pty.id)
|
update(sdk.client, pty)
|
||||||
const previous = index >= 0 ? store.all[index] : undefined
|
|
||||||
if (index >= 0) {
|
|
||||||
setStore("all", index, (item) => ({ ...item, ...pty }))
|
|
||||||
}
|
|
||||||
sdk.client.pty
|
|
||||||
.update({
|
|
||||||
ptyID: pty.id,
|
|
||||||
title: pty.title,
|
|
||||||
size: pty.cols && pty.rows ? { rows: pty.rows, cols: pty.cols } : undefined,
|
|
||||||
})
|
|
||||||
.catch((error: unknown) => {
|
|
||||||
if (previous) {
|
|
||||||
const currentIndex = store.all.findIndex((item) => item.id === pty.id)
|
|
||||||
if (currentIndex >= 0) setStore("all", currentIndex, previous)
|
|
||||||
}
|
|
||||||
console.error("Failed to update terminal", error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
trim(id: string) {
|
trim(id: string) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
@@ -248,37 +285,23 @@ function createWorkspaceTerminalSession(sdk: ReturnType<typeof useSDK>, dir: str
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async clone(id: string) {
|
async clone(id: string) {
|
||||||
const index = store.all.findIndex((x) => x.id === id)
|
await clone(sdk.client, id)
|
||||||
const pty = store.all[index]
|
},
|
||||||
if (!pty) return
|
bind() {
|
||||||
const clone = await sdk.client.pty
|
const client = sdk.client
|
||||||
.create({
|
return {
|
||||||
title: pty.title,
|
trim(id: string) {
|
||||||
})
|
const index = store.all.findIndex((x) => x.id === id)
|
||||||
.catch((error: unknown) => {
|
if (index === -1) return
|
||||||
console.error("Failed to clone terminal", error)
|
setStore("all", index, (pty) => trimTerminal(pty))
|
||||||
return undefined
|
},
|
||||||
})
|
update(pty: Partial<LocalPTY> & { id: string }) {
|
||||||
if (!clone?.data) return
|
update(client, pty)
|
||||||
|
},
|
||||||
const active = store.active === pty.id
|
async clone(id: string) {
|
||||||
|
await clone(client, id)
|
||||||
batch(() => {
|
},
|
||||||
setStore("all", index, {
|
}
|
||||||
id: clone.data.id,
|
|
||||||
title: clone.data.title ?? pty.title,
|
|
||||||
titleNumber: pty.titleNumber,
|
|
||||||
// New PTY process, so start clean.
|
|
||||||
buffer: undefined,
|
|
||||||
cursor: undefined,
|
|
||||||
scrollY: undefined,
|
|
||||||
rows: undefined,
|
|
||||||
cols: undefined,
|
|
||||||
})
|
|
||||||
if (active) {
|
|
||||||
setStore("active", clone.data.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
open(id: string) {
|
open(id: string) {
|
||||||
setStore("active", id)
|
setStore("active", id)
|
||||||
@@ -403,6 +426,7 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
|||||||
trim: (id: string) => workspace().trim(id),
|
trim: (id: string) => workspace().trim(id),
|
||||||
trimAll: () => workspace().trimAll(),
|
trimAll: () => workspace().trimAll(),
|
||||||
clone: (id: string) => workspace().clone(id),
|
clone: (id: string) => workspace().clone(id),
|
||||||
|
bind: () => workspace(),
|
||||||
open: (id: string) => workspace().open(id),
|
open: (id: string) => workspace().open(id),
|
||||||
close: (id: string) => workspace().close(id),
|
close: (id: string) => workspace().close(id),
|
||||||
move: (id: string, to: number) => workspace().move(id, to),
|
move: (id: string, to: number) => workspace().move(id, to),
|
||||||
|
|||||||
@@ -280,21 +280,24 @@ export function TerminalPanel() {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
<div class="flex-1 min-h-0 relative">
|
<div class="flex-1 min-h-0 relative">
|
||||||
<Show when={terminal.active()} keyed>
|
<Show when={terminal.active()} keyed>
|
||||||
{(id) => (
|
{(id) => {
|
||||||
<Show when={all().find((pty) => pty.id === id)}>
|
const ops = terminal.bind()
|
||||||
{(pty) => (
|
return (
|
||||||
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
<Show when={all().find((pty) => pty.id === id)}>
|
||||||
<Terminal
|
{(pty) => (
|
||||||
pty={pty()}
|
<div id={`terminal-wrapper-${id}`} class="absolute inset-0">
|
||||||
autoFocus={opened()}
|
<Terminal
|
||||||
onConnect={() => terminal.trim(id)}
|
pty={pty()}
|
||||||
onCleanup={terminal.update}
|
autoFocus={opened()}
|
||||||
onConnectError={() => terminal.clone(id)}
|
onConnect={() => ops.trim(id)}
|
||||||
/>
|
onCleanup={ops.update}
|
||||||
</div>
|
onConnectError={() => ops.clone(id)}
|
||||||
)}
|
/>
|
||||||
</Show>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user