From 24f644b82a256dce7f9e7c4f36f1c23f981e01b7 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 16 Dec 2025 12:37:08 +0300 Subject: [PATCH 1/4] Fix command palette visibility with proper "when" clause placement Move "when" clauses from command definitions to menus.commandPalette section. This fixes commands like Logout and Create Workspace appearing in the palette before the user is authenticated. --- package.json | 71 ++++++++++++++++++++++++++++++---------- src/api/utils.ts | 2 +- src/remote/sshProcess.ts | 7 ++-- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index b048a607..a8fedaaf 100644 --- a/package.json +++ b/package.json @@ -192,67 +192,67 @@ "commands": [ { "command": "coder.login", - "title": "Coder: Login" + "title": "Login", + "category": "Coder", + "icon": "$(sign-in)" }, { "command": "coder.logout", - "title": "Coder: Logout", - "when": "coder.authenticated", + "title": "Logout", + "category": "Coder", "icon": "$(sign-out)" }, { "command": "coder.open", "title": "Open Workspace", - "icon": "$(play)", - "category": "Coder" + "category": "Coder", + "icon": "$(play)" }, { "command": "coder.openFromSidebar", - "title": "Coder: Open Workspace", + "title": "Open Workspace", + "category": "Coder", "icon": "$(play)" }, { "command": "coder.createWorkspace", "title": "Create Workspace", "category": "Coder", - "when": "coder.authenticated", "icon": "$(add)" }, { "command": "coder.navigateToWorkspace", "title": "Navigate to Workspace Page", - "when": "coder.authenticated", + "category": "Coder", "icon": "$(link-external)" }, { "command": "coder.navigateToWorkspaceSettings", "title": "Edit Workspace Settings", - "when": "coder.authenticated", + "category": "Coder", "icon": "$(settings-gear)" }, { "command": "coder.workspace.update", - "title": "Coder: Update Workspace", - "when": "coder.workspace.updatable" + "title": "Update Workspace", + "category": "Coder" }, { "command": "coder.refreshWorkspaces", "title": "Refresh Workspace", "category": "Coder", - "icon": "$(refresh)", - "when": "coder.authenticated" + "icon": "$(refresh)" }, { "command": "coder.viewLogs", "title": "Coder: View Logs", - "icon": "$(list-unordered)", - "when": "coder.authenticated" + "icon": "$(list-unordered)" }, { "command": "coder.openAppStatus", - "title": "Coder: Open App Status", - "icon": "$(robot)", - "when": "coder.authenticated" + "title": "Open App Status", + "category": "Coder", + "icon": "$(robot)" }, { "command": "coder.searchMyWorkspaces", @@ -274,6 +274,41 @@ ], "menus": { "commandPalette": [ + { + "command": "coder.login", + "when": "!coder.authenticated" + }, + { + "command": "coder.logout", + "when": "coder.authenticated" + }, + { + "command": "coder.createWorkspace", + "when": "coder.authenticated" + }, + { + "command": "coder.navigateToWorkspace", + "when": "coder.authenticated" + }, + { + "command": "coder.navigateToWorkspaceSettings", + "when": "coder.authenticated" + }, + { + "command": "coder.workspace.update", + "when": "coder.workspace.updatable" + }, + { + "command": "coder.refreshWorkspaces", + "when": "coder.authenticated" + }, + { + "command": "coder.viewLogs" + }, + { + "command": "coder.openAppStatus", + "when": "coder.authenticated" + }, { "command": "coder.debug.listDeployments", "when": "coder.devMode" diff --git a/src/api/utils.ts b/src/api/utils.ts index 0f13288e..86604e3e 100644 --- a/src/api/utils.ts +++ b/src/api/utils.ts @@ -1,4 +1,4 @@ -import fs from "fs/promises"; +import fs from "node:fs/promises"; import { ProxyAgent } from "proxy-agent"; import { type WorkspaceConfiguration } from "vscode"; diff --git a/src/remote/sshProcess.ts b/src/remote/sshProcess.ts index 248e071f..b610d6e4 100644 --- a/src/remote/sshProcess.ts +++ b/src/remote/sshProcess.ts @@ -256,8 +256,9 @@ export class SshProcessMonitor implements vscode.Disposable { const targetPid = this.currentPid; while (!this.disposed && this.currentPid === targetPid) { try { - const logFiles = await fs.readdir(logDir); - logFiles.sort().reverse(); + const logFiles = (await fs.readdir(logDir)) + .sort((a, b) => a.localeCompare(b)) + .reverse(); const logFileName = logFiles.find( (file) => file === `${targetPid}.log` || file.endsWith(`-${targetPid}.log`), @@ -420,7 +421,7 @@ async function findRemoteSshLogPath( const dirs = await fs.readdir(logsParentDir); const outputDirs = dirs .filter((d) => d.startsWith("output_logging_")) - .sort() + .sort((a, b) => a.localeCompare(b)) .reverse(); if (outputDirs.length > 0) { From 117f90f75e8d1e29d1bdbbbcb8d5f658ceb7cb0b Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 16 Dec 2025 15:05:49 +0300 Subject: [PATCH 2/4] Hide more commands --- package.json | 13 +++++++++---- src/commands.ts | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a8fedaaf..7dc96343 100644 --- a/package.json +++ b/package.json @@ -303,15 +303,16 @@ "when": "coder.authenticated" }, { - "command": "coder.viewLogs" + "command": "coder.viewLogs", + "when": "true" }, { "command": "coder.openAppStatus", - "when": "coder.authenticated" + "when": "false" }, { - "command": "coder.debug.listDeployments", - "when": "coder.devMode" + "command": "coder.open", + "when": "coder.authenticated" }, { "command": "coder.openFromSidebar", @@ -324,6 +325,10 @@ { "command": "coder.searchAllWorkspaces", "when": "false" + }, + { + "command": "coder.debug.listDeployments", + "when": "coder.devMode" } ], "view/title": [ diff --git a/src/commands.ts b/src/commands.ts index ec06700e..ef97bdda 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -242,7 +242,7 @@ export class Commands { * * Otherwise, the currently connected workspace is used (if any). */ - public async navigateToWorkspace(item: OpenableTreeItem) { + public async navigateToWorkspace(item?: OpenableTreeItem) { if (item) { const baseUrl = this.requireExtensionBaseUrl(); const workspaceId = createWorkspaceIdentifier(item.workspace); @@ -266,7 +266,7 @@ export class Commands { * * Otherwise, the currently connected workspace is used (if any). */ - public async navigateToWorkspaceSettings(item: OpenableTreeItem) { + public async navigateToWorkspaceSettings(item?: OpenableTreeItem) { if (item) { const baseUrl = this.requireExtensionBaseUrl(); const workspaceId = createWorkspaceIdentifier(item.workspace); From f9f7e69da1d599de81ec4ab45669f7908dba4e75 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Tue, 16 Dec 2025 15:35:49 +0300 Subject: [PATCH 3/4] Hide navigate to workspace commands when not connected to a deployment --- package.json | 4 ++-- src/core/contextManager.ts | 1 + src/remote/remote.ts | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7dc96343..7a47413b 100644 --- a/package.json +++ b/package.json @@ -288,11 +288,11 @@ }, { "command": "coder.navigateToWorkspace", - "when": "coder.authenticated" + "when": "coder.workspace.connected" }, { "command": "coder.navigateToWorkspaceSettings", - "when": "coder.authenticated" + "when": "coder.workspace.connected" }, { "command": "coder.workspace.update", diff --git a/src/core/contextManager.ts b/src/core/contextManager.ts index 405850a2..60d3cfa6 100644 --- a/src/core/contextManager.ts +++ b/src/core/contextManager.ts @@ -4,6 +4,7 @@ const CONTEXT_DEFAULTS = { "coder.authenticated": false, "coder.isOwner": false, "coder.loaded": false, + "coder.workspace.connected": false, "coder.workspace.updatable": false, } as const; diff --git a/src/remote/remote.ts b/src/remote/remote.ts index ed5235bf..974d956d 100644 --- a/src/remote/remote.ts +++ b/src/remote/remote.ts @@ -556,6 +556,7 @@ export class Remote { throw ex; } + this.contextManager.set("coder.workspace.connected", true); this.logger.info("Remote setup complete"); // Returning the URL and token allows the plugin to authenticate its own From 4bde31306cf1a14c20867fafedb5bdcc83ab4916 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 17 Dec 2025 14:38:40 +0300 Subject: [PATCH 4/4] Add localeCompare tests --- test/unit/remote/sshProcess.test.ts | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/test/unit/remote/sshProcess.test.ts b/test/unit/remote/sshProcess.test.ts index 5e30f533..befd068b 100644 --- a/test/unit/remote/sshProcess.test.ts +++ b/test/unit/remote/sshProcess.test.ts @@ -127,6 +127,27 @@ describe("SshProcessMonitor", () => { expect(find).toHaveBeenCalledWith("port", 33333); }); + it("sorts output_logging_ directories using localeCompare for consistent ordering", async () => { + // localeCompare differs from default sort() for mixed case + vol.fromJSON({ + "/logs/output_logging_a/1-Remote - SSH.log": "-> socksPort 11111 ->", + "/logs/output_logging_Z/1-Remote - SSH.log": "-> socksPort 22222 ->", + }); + + mockReaddirOrder("/logs", [ + "output_logging_a", + "output_logging_Z", + "window1", + ]); + + const monitor = createMonitor({ codeLogDir: "/logs/window1" }); + await waitForEvent(monitor.onPidChange); + + // With localeCompare: ["a", "Z"] -> reversed -> "Z" first (port 22222) + // With plain sort(): ["Z", "a"] -> reversed -> "a" first (port 11111) + expect(find).toHaveBeenCalledWith("port", 22222); + }); + it("falls back to output_logging_ when extension folder has no SSH log", async () => { // Extension folder exists but doesn't have Remote SSH log vol.fromJSON({ @@ -301,6 +322,28 @@ describe("SshProcessMonitor", () => { expect(logPath).toBe("/proxy-logs/2024-01-03-999.log"); }); + + it("sorts log files using localeCompare for consistent cross-platform ordering", async () => { + // localeCompare differs from default sort() for mixed case + vol.fromJSON({ + "/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log": + "-> socksPort 12345 ->", + "/proxy-logs/a-999.log": "", + "/proxy-logs/Z-999.log": "", + }); + + mockReaddirOrder("/proxy-logs", ["a-999.log", "Z-999.log"]); + + const monitor = createMonitor({ + codeLogDir: "/logs/window1", + proxyLogDir: "/proxy-logs", + }); + const logPath = await waitForEvent(monitor.onLogFilePathChange); + + // With localeCompare: ["a", "Z"] -> reversed -> "Z" first + // With plain sort(): ["Z", "a"] -> reversed -> "a" first (WRONG) + expect(logPath).toBe("/proxy-logs/Z-999.log"); + }); }); describe("network status", () => { @@ -483,7 +526,7 @@ function mockReaddirOrder(dirPath: string, files: string[]): void { if (path === dirPath) { return Promise.resolve(files); } - return originalReaddir(path) as Promise; + return originalReaddir(path); }; vi.spyOn(fsPromises, "readdir").mockImplementation( mockImpl as typeof fsPromises.readdir,