From 4261fe749db380ceccf24144588ed014ff17d5b5 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 13:24:38 +0800 Subject: [PATCH] fix(desktop): repair SSH server updates (#48442) Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com> --- .../app/component-tests/ssh-reconnect.spec.ts | 17 +++++++++++++++-- packages/app/src/servers/ssh/dialog.stories.tsx | 1 + packages/app/src/servers/ssh/dialog.tsx | 16 ++++++++++++++-- packages/desktop/src/main/remote/cli.test.ts | 4 ++-- packages/desktop/src/main/remote/cli.ts | 6 +++--- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/packages/app/component-tests/ssh-reconnect.spec.ts b/packages/app/component-tests/ssh-reconnect.spec.ts index 870c3c12812..5351e5e9655 100644 --- a/packages/app/component-tests/ssh-reconnect.spec.ts +++ b/packages/app/component-tests/ssh-reconnect.spec.ts @@ -19,11 +19,13 @@ story("cancelling a version mismatch permits reconnecting again", async ({ mount const component = await mount("app-dialog-ssh--incompatible-session") await component.getByRole("button", { name: "Reconnect", exact: true }).click() const dialog = page.getByRole("dialog") - await expect(dialog.getByRole("alert")).toBeVisible() + await expect(dialog.getByRole("status")).toContainText("Server update required") + await expect(dialog.getByRole("textbox")).toHaveCount(0) await dialog.getByRole("button", { name: "Cancel", exact: true }).click() await expect(dialog).toHaveCount(0) await component.getByRole("button", { name: "Reconnect", exact: true }).click() - await expect(dialog.getByRole("alert")).toBeVisible() + await expect(dialog.getByRole("status")).toContainText("Server update required") + await expect(dialog.getByRole("textbox")).toHaveCount(0) await dialog.getByRole("button", { name: "Cancel", exact: true }).click() await expect(dialog).toHaveCount(0) }) @@ -43,6 +45,17 @@ story("adding a server keeps all SSH challenges in the original connection dialo await expect(dialog).toHaveCount(0) }) +story("adding an incompatible server advances to a dedicated update step", async ({ mount, page }) => { + await mount("app-dialog-ssh--incompatible-host") + const dialog = page.getByRole("dialog") + await dialog.getByRole("textbox", { name: "Host or SSH command" }).fill("ssh devbox") + await dialog.getByRole("button", { name: "Add server", exact: true }).click() + await expect(dialog.getByRole("status")).toContainText("Server update required") + await expect(dialog.getByRole("textbox")).toHaveCount(0) + await expect(dialog.getByRole("alert")).toHaveCount(0) + await expect(dialog.getByRole("button", { name: "Update and reconnect", exact: true })).toBeVisible() +}) + story("updating an incompatible connection continues authentication in the same dialog", async ({ mount, page }) => { const component = await mount("app-dialog-ssh--incompatible-session") await component.getByRole("button", { name: "Reconnect", exact: true }).click() diff --git a/packages/app/src/servers/ssh/dialog.stories.tsx b/packages/app/src/servers/ssh/dialog.stories.tsx index 96a45d655b7..b6604778050 100644 --- a/packages/app/src/servers/ssh/dialog.stories.tsx +++ b/packages/app/src/servers/ssh/dialog.stories.tsx @@ -278,6 +278,7 @@ function Open(props: { initial?: string }) { export default { title: "App/Dialogs/SSH", id: "app-dialog-ssh" } export const AuthenticationRequired = { render: () => } export const SettingsReconnect = { render: () => } +export const IncompatibleHost = { render: () => } export const IncompatibleSession = { render: () => } export const InactiveSession = { render: () => } export const KeyReconnect = { render: () => } diff --git a/packages/app/src/servers/ssh/dialog.tsx b/packages/app/src/servers/ssh/dialog.tsx index 0c6530cbc96..5e09b8dc3fe 100644 --- a/packages/app/src/servers/ssh/dialog.tsx +++ b/packages/app/src/servers/ssh/dialog.tsx @@ -120,7 +120,13 @@ export function DialogSsh(props: {
- +
+ +
+ {language.t("ssh.stage.incompatible")} + {language.t("ssh.error.version")} +
+
{(prompt) => (
@@ -195,7 +207,7 @@ export function DialogSsh(props: {
)}
- + {(error) => ( {error()} diff --git a/packages/desktop/src/main/remote/cli.test.ts b/packages/desktop/src/main/remote/cli.test.ts index 25236626e86..96b2c74b74e 100644 --- a/packages/desktop/src/main/remote/cli.test.ts +++ b/packages/desktop/src/main/remote/cli.test.ts @@ -22,7 +22,7 @@ it.live( Effect.provideService( HttpClient.HttpClient, HttpClient.make((request) => { - expect(request.url).toBe("https://registry.npmjs.org/@opencode-ai%2fcli/beta") + expect(request.url).toBe("https://registry.npmjs.org/@opencode%2fcli/beta") return Effect.succeed(HttpClientResponse.fromWeb(request, response)) }), ), @@ -65,7 +65,7 @@ posix( test("pins platform-specific artifacts and rejects unsafe inputs", () => { expect(RemoteCli.archiveUrl("linux-x64-baseline-musl", "2.0.0-beta.1")).toBe( - "https://registry.npmjs.org/@opencode-ai/cli-linux-x64-baseline-musl/-/cli-linux-x64-baseline-musl-2.0.0-beta.1.tgz", + "https://registry.npmjs.org/@opencode/cli-linux-x64-baseline-musl/-/cli-linux-x64-baseline-musl-2.0.0-beta.1.tgz", ) expect(() => RemoteCli.installScript({ version: '2.0.0"; whoami', source: { type: "installer" } })).toThrow() expect(() => RemoteCli.archiveUrl("linux-x64;whoami", "2.0.0")).toThrow() diff --git a/packages/desktop/src/main/remote/cli.ts b/packages/desktop/src/main/remote/cli.ts index f71a68dbd03..96cdef26460 100644 --- a/packages/desktop/src/main/remote/cli.ts +++ b/packages/desktop/src/main/remote/cli.ts @@ -68,7 +68,7 @@ printf 'OPENCODE_REMOTE_TARGET=%s\\n' "$target" export function archiveUrl(target: string, version: string) { if (!/^(linux|darwin)-(x64-baseline|arm64)(-musl)?$/.test(target)) throw new Failure({ code: "platform", detail: target }) - return `https://registry.npmjs.org/@opencode-ai/cli-${target}/-/cli-${target}-${requireVersion(version)}.tgz` + return `https://registry.npmjs.org/@opencode/cli-${target}/-/cli-${target}-${requireVersion(version)}.tgz` } type Source = { type: "download"; url: string } | { type: "archive" } | { type: "installer"; binary?: string } @@ -114,12 +114,12 @@ const Beta = Schema.Struct({ version: Schema.String.check(Schema.isPattern(/^0\. export const latestBeta = Effect.fn("RemoteCli.latestBeta")(function* () { const http = yield* HttpClient.HttpClient - const metadata = yield* http.get("https://registry.npmjs.org/@opencode-ai%2fcli/beta").pipe( + const metadata = yield* http.get("https://registry.npmjs.org/@opencode%2fcli/beta").pipe( Effect.flatMap(HttpClientResponse.filterStatusOk), Effect.flatMap(HttpClientResponse.schemaBodyJson(Beta)), Effect.timeout("30 seconds"), Effect.mapError( - () => new Failure({ code: "install", detail: "https://registry.npmjs.org/@opencode-ai%2fcli/beta" }), + () => new Failure({ code: "install", detail: "https://registry.npmjs.org/@opencode%2fcli/beta" }), ), ) return metadata.version