diff --git a/CHANGELOG.md b/CHANGELOG.md index bb1e5b34..ad585f37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Log file picker when viewing logs without an active workspace connection. +### Fixed + +- Fixed false "setting changed" notifications appearing when connecting to a remote workspace. + ## [v1.11.5](https://github.com/coder/vscode-coder/releases/tag/v1.11.5) 2025-12-10 ### Added diff --git a/src/cliConfig.ts b/src/cliConfig.ts index 0ae0080f..1f23949d 100644 --- a/src/cliConfig.ts +++ b/src/cliConfig.ts @@ -3,17 +3,26 @@ import { type WorkspaceConfiguration } from "vscode"; import { getHeaderArgs } from "./headers"; import { escapeCommandArg } from "./util"; +/** + * Returns the raw global flags from user configuration. + */ +export function getGlobalFlagsRaw( + configs: Pick, +): string[] { + return configs.get("coder.globalFlags", []); +} + /** * Returns global configuration flags for Coder CLI commands. * Always includes the `--global-config` argument with the specified config directory. */ export function getGlobalFlags( - configs: WorkspaceConfiguration, + configs: Pick, configDir: string, ): string[] { // Last takes precedence/overrides previous ones return [ - ...(configs.get("coder.globalFlags") || []), + ...getGlobalFlagsRaw(configs), "--global-config", escapeCommandArg(configDir), ...getHeaderArgs(configs), @@ -23,7 +32,9 @@ export function getGlobalFlags( /** * Returns SSH flags for the `coder ssh` command from user configuration. */ -export function getSshFlags(configs: WorkspaceConfiguration): string[] { +export function getSshFlags( + configs: Pick, +): string[] { // Make sure to match this default with the one in the package.json return configs.get("coder.sshFlags", ["--disable-autostart"]); } diff --git a/src/headers.ts b/src/headers.ts index 6c69258c..435b2ad3 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -18,7 +18,7 @@ function isExecException(err: unknown): err is ExecException { } export function getHeaderCommand( - config: WorkspaceConfiguration, + config: Pick, ): string | undefined { const cmd = config.get("coder.headerCommand")?.trim() || @@ -27,7 +27,9 @@ export function getHeaderCommand( return cmd || undefined; } -export function getHeaderArgs(config: WorkspaceConfiguration): string[] { +export function getHeaderArgs( + config: Pick, +): string[] { // Escape a command line to be executed by the Coder binary, so ssh doesn't substitute variables. const escapeSubcommand: (str: string) => string = os.platform() === "win32" diff --git a/src/remote/remote.ts b/src/remote/remote.ts index 27a0477e..9aaea237 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -8,6 +8,7 @@ import * as jsonc from "jsonc-parser"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; +import { isDeepStrictEqual } from "node:util"; import * as semver from "semver"; import * as vscode from "vscode"; @@ -20,7 +21,7 @@ import { import { extractAgents } from "../api/api-helper"; import { CoderApi } from "../api/coderApi"; import { needToken } from "../api/utils"; -import { getGlobalFlags, getSshFlags } from "../cliConfig"; +import { getGlobalFlags, getGlobalFlagsRaw, getSshFlags } from "../cliConfig"; import { type Commands } from "../commands"; import { type CliManager } from "../core/cliManager"; import * as cliUtils from "../core/cliUtils"; @@ -28,6 +29,7 @@ import { type ServiceContainer } from "../core/container"; import { type ContextManager } from "../core/contextManager"; import { type PathResolver } from "../core/pathResolver"; import { featureSetForVersion, type FeatureSet } from "../featureSet"; +import { getHeaderCommand } from "../headers"; import { Inbox } from "../inbox"; import { type Logger } from "../logging/logger"; import { @@ -515,14 +517,34 @@ export class Remote { ...(await this.createAgentMetadataStatusBar(agent, workspaceClient)), ); - const settingsToWatch = [ - { setting: "coder.globalFlags", title: "Global flags" }, - { setting: "coder.sshFlags", title: "SSH flags" }, + const settingsToWatch: Array<{ + setting: string; + title: string; + getValue: () => unknown; + }> = [ + { + setting: "coder.globalFlags", + title: "Global Flags", + getValue: () => + getGlobalFlagsRaw(vscode.workspace.getConfiguration()), + }, + { + setting: "coder.headerCommand", + title: "Header Command", + getValue: () => + getHeaderCommand(vscode.workspace.getConfiguration()) ?? "", + }, + { + setting: "coder.sshFlags", + title: "SSH Flags", + getValue: () => getSshFlags(vscode.workspace.getConfiguration()), + }, ]; if (featureSet.proxyLogDirectory) { settingsToWatch.push({ setting: "coder.proxyLogDirectory", - title: "Proxy log directory", + title: "Proxy Log Directory", + getValue: () => this.getLogDir(featureSet), }); } disposables.push(this.watchSettings(settingsToWatch)); @@ -801,25 +823,46 @@ export class Remote { } private watchSettings( - settings: Array<{ setting: string; title: string }>, + settings: Array<{ + setting: string; + title: string; + getValue: () => unknown; + }>, ): vscode.Disposable { + // Capture applied values at setup time + const appliedValues = new Map( + settings.map((s) => [s.setting, s.getValue()]), + ); + return vscode.workspace.onDidChangeConfiguration((e) => { - for (const { setting, title } of settings) { + const changedTitles: string[] = []; + + for (const { setting, title, getValue } of settings) { if (!e.affectsConfiguration(setting)) { continue; } - vscode.window - .showInformationMessage( - `${title} setting changed. Reload window to apply.`, - "Reload", - ) - .then((action) => { - if (action === "Reload") { - vscode.commands.executeCommand("workbench.action.reloadWindow"); - } - }); - break; + + const newValue = getValue(); + + if (!isDeepStrictEqual(newValue, appliedValues.get(setting))) { + changedTitles.push(title); + } + } + + if (changedTitles.length === 0) { + return; } + + const message = + changedTitles.length === 1 + ? `${changedTitles[0]} setting changed. Reload window to apply.` + : `${changedTitles.join(", ")} settings changed. Reload window to apply.`; + + vscode.window.showInformationMessage(message, "Reload").then((action) => { + if (action === "Reload") { + vscode.commands.executeCommand("workbench.action.reloadWindow"); + } + }); }); } diff --git a/test/unit/cliConfig.test.ts b/test/unit/cliConfig.test.ts index d350dcbd..e35fa687 100644 --- a/test/unit/cliConfig.test.ts +++ b/test/unit/cliConfig.test.ts @@ -1,16 +1,14 @@ import { it, expect, describe } from "vitest"; -import { type WorkspaceConfiguration } from "vscode"; -import { getGlobalFlags, getSshFlags } from "@/cliConfig"; +import { getGlobalFlags, getGlobalFlagsRaw, getSshFlags } from "@/cliConfig"; +import { MockConfigurationProvider } from "../mocks/testHelpers"; import { isWindows } from "../utils/platform"; describe("cliConfig", () => { describe("getGlobalFlags", () => { it("should return global-config and header args when no global flags configured", () => { - const config = { - get: () => undefined, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ "--global-config", @@ -19,12 +17,11 @@ describe("cliConfig", () => { }); it("should return global flags from config with global-config appended", () => { - const config = { - get: (key: string) => - key === "coder.globalFlags" - ? ["--verbose", "--disable-direct-connections"] - : undefined, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); + config.set("coder.globalFlags", [ + "--verbose", + "--disable-direct-connections", + ]); expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ "--verbose", @@ -35,16 +32,12 @@ describe("cliConfig", () => { }); it("should not filter duplicate global-config flags, last takes precedence", () => { - const config = { - get: (key: string) => - key === "coder.globalFlags" - ? [ - "-v", - "--global-config /path/to/ignored", - "--disable-direct-connections", - ] - : undefined, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); + config.set("coder.globalFlags", [ + "-v", + "--global-config /path/to/ignored", + "--disable-direct-connections", + ]); expect(getGlobalFlags(config, "/config/dir")).toStrictEqual([ "-v", @@ -57,17 +50,13 @@ describe("cliConfig", () => { it("should not filter header-command flags, header args appended at end", () => { const headerCommand = "echo test"; - const config = { - get: (key: string) => { - if (key === "coder.headerCommand") { - return headerCommand; - } - if (key === "coder.globalFlags") { - return ["-v", "--header-command custom", "--no-feature-warning"]; - } - return undefined; - }, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); + config.set("coder.headerCommand", headerCommand); + config.set("coder.globalFlags", [ + "-v", + "--header-command custom", + "--no-feature-warning", + ]); const result = getGlobalFlags(config, "/config/dir"); expect(result).toStrictEqual([ @@ -82,22 +71,41 @@ describe("cliConfig", () => { }); }); + describe("getGlobalFlagsRaw", () => { + it("returns empty array when no global flags configured", () => { + const config = new MockConfigurationProvider(); + + expect(getGlobalFlagsRaw(config)).toStrictEqual([]); + }); + + it("returns global flags from config", () => { + const config = new MockConfigurationProvider(); + config.set("coder.globalFlags", [ + "--verbose", + "--disable-direct-connections", + ]); + + expect(getGlobalFlagsRaw(config)).toStrictEqual([ + "--verbose", + "--disable-direct-connections", + ]); + }); + }); + describe("getSshFlags", () => { it("returns default flags when no SSH flags configured", () => { - const config = { - get: (_key: string, defaultValue: unknown) => defaultValue, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); expect(getSshFlags(config)).toStrictEqual(["--disable-autostart"]); }); it("returns SSH flags from config", () => { - const config = { - get: (key: string) => - key === "coder.sshFlags" - ? ["--disable-autostart", "--wait=yes", "--ssh-host-prefix=custom"] - : undefined, - } as unknown as WorkspaceConfiguration; + const config = new MockConfigurationProvider(); + config.set("coder.sshFlags", [ + "--disable-autostart", + "--wait=yes", + "--ssh-host-prefix=custom", + ]); expect(getSshFlags(config)).toStrictEqual([ "--disable-autostart",