From 628b3d9331c41aaf90455c8925f6ad17d00ae8e9 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 3 Aug 2026 15:37:06 +0300 Subject: [PATCH] fix: read SSH settings from the active extension's section Windsurf/Devin and Antigravity forked open-remote-ssh and renamed the whole `remote.SSH` configuration section, so reading `remote.SSH.configFile` silently missed a custom config file on those editors and the workspace host was written to `~/.ssh/config`, which they never read. Map each known Remote-SSH extension to the sections it consults and read the first non-empty value from those. Windsurf reads both `remote.devinSSH` and `remote.windsurfSSH`, preferring the new name. --- CHANGELOG.md | 6 +++ src/remote/remote.ts | 5 +-- src/remote/sshExtension.ts | 28 +++++++++++++ test/unit/remote/sshExtension.test.ts | 59 +++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 test/unit/remote/sshExtension.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b8fb9a6578..b3506cd456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,12 @@ ### Fixed +- Write the SSH config to the file the active Remote-SSH extension actually + reads. Windsurf/Devin and Antigravity renamed the whole `remote.SSH` settings + section, so a custom config file set as `remote.devinSSH.configFile`, + `remote.windsurfSSH.configFile`, or `remote.antigravitySSH.configFile` was + ignored and the workspace host was written to `~/.ssh/config` instead, where + those editors never looked for it. - Apply a 60-second default timeout to REST requests, so requests hung on a half-open TCP connection don't stall pollers forever. - Change `coder.binarySource`, `coder.binaryDestination`, `coder.headerCommand`, diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 36d8687dcb..51d1a37a1b 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -55,6 +55,7 @@ import { parseCoderSshOptions, parseSshConfig, } from "./sshConfig"; +import { getRemoteSshSetting } from "./sshExtension"; import { applySettingOverrides, buildSshOverrides } from "./sshOverrides"; import { SshProcessMonitor } from "./sshProcess"; import { computeSshProperties, sshSupportsSetEnv } from "./sshSupport"; @@ -918,9 +919,7 @@ export class Remote { } private getSshConfigPath(): string { - const configured = vscode.workspace - .getConfiguration() - .get("remote.SSH.configFile"); + const configured = getRemoteSshSetting("configFile"); return expandPath(configured || path.join("~", ".ssh", "config")); } diff --git a/src/remote/sshExtension.ts b/src/remote/sshExtension.ts index eae3d5ca4b..a56e980607 100644 --- a/src/remote/sshExtension.ts +++ b/src/remote/sshExtension.ts @@ -10,6 +10,34 @@ export const REMOTE_SSH_EXTENSION_IDS = [ export type RemoteSshExtensionId = (typeof REMOTE_SSH_EXTENSION_IDS)[number]; +/** + * Sections each extension reads, in order. The rebranded forks renamed the + * whole `remote.SSH` section, so reading it directly misses them. + */ +const SETTING_SECTIONS: Readonly< + Record +> = { + "jeanp413.open-remote-ssh": ["remote.SSH"], + // Windsurf became Devin and reads both, preferring the new name. + "codeium.windsurf-remote-openssh": ["remote.devinSSH", "remote.windsurfSSH"], + "anysphere.remote-ssh": ["remote.SSH"], + "ms-vscode-remote.remote-ssh": ["remote.SSH"], + "google.antigravity-remote-openssh": ["remote.antigravitySSH"], +}; + +/** First non-empty value for a string setting, e.g. `configFile`. */ +export function getRemoteSshSetting(key: string): string | undefined { + const id = getRemoteSshExtension()?.id; + const sections = id ? SETTING_SECTIONS[id] : ["remote.SSH"]; + for (const section of sections) { + const value = vscode.workspace.getConfiguration(section).get(key); + if (value) { + return value; + } + } + return undefined; +} + /** * VS Code Remote-SSH log layout, shared by the live SSH monitor and the * support-bundle collector so a future layout change updates one place. diff --git a/test/unit/remote/sshExtension.test.ts b/test/unit/remote/sshExtension.test.ts new file mode 100644 index 0000000000..e3283fb16c --- /dev/null +++ b/test/unit/remote/sshExtension.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; +import * as vscode from "vscode"; + +import { getRemoteSshSetting } from "@/remote/sshExtension"; + +import { config, type Settings } from "../../mocks/testHelpers"; + +/** Activate `extensionId`, or no Remote-SSH extension when empty. */ +function setup(extensionId: string, settings: Settings = {}): void { + config(settings); + vi.mocked(vscode.extensions.getExtension).mockImplementation((id) => + id === extensionId ? ({ id } as vscode.Extension) : undefined, + ); +} + +describe("getRemoteSshSetting", () => { + it.each([ + ["ms-vscode-remote.remote-ssh", "remote.SSH.configFile"], + ["anysphere.remote-ssh", "remote.SSH.configFile"], + ["jeanp413.open-remote-ssh", "remote.SSH.configFile"], + ["google.antigravity-remote-openssh", "remote.antigravitySSH.configFile"], + ["codeium.windsurf-remote-openssh", "remote.devinSSH.configFile"], + ])("reads the section %s uses", (extensionId, settingKey) => { + setup(extensionId, { [settingKey]: "/custom/config" }); + + expect(getRemoteSshSetting("configFile")).toBe("/custom/config"); + }); + + it("falls back to the legacy Windsurf section", () => { + setup("codeium.windsurf-remote-openssh", { + "remote.windsurfSSH.configFile": "/legacy/config", + }); + + expect(getRemoteSshSetting("configFile")).toBe("/legacy/config"); + }); + + it("prefers the Devin section over the legacy Windsurf one", () => { + setup("codeium.windsurf-remote-openssh", { + "remote.devinSSH.configFile": "/devin/config", + "remote.windsurfSSH.configFile": "/legacy/config", + }); + + expect(getRemoteSshSetting("configFile")).toBe("/devin/config"); + }); + + it("ignores another extension's section", () => { + setup("google.antigravity-remote-openssh", { + "remote.SSH.configFile": "/custom/config", + }); + + expect(getRemoteSshSetting("configFile")).toBeUndefined(); + }); + + it("defaults to remote.SSH when no extension is installed", () => { + setup("", { "remote.SSH.configFile": "/custom/config" }); + + expect(getRemoteSshSetting("configFile")).toBe("/custom/config"); + }); +});