diff --git a/package.json b/package.json index b048a607..7a47413b 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", @@ -275,8 +275,44 @@ "menus": { "commandPalette": [ { - "command": "coder.debug.listDeployments", - "when": "coder.devMode" + "command": "coder.login", + "when": "!coder.authenticated" + }, + { + "command": "coder.logout", + "when": "coder.authenticated" + }, + { + "command": "coder.createWorkspace", + "when": "coder.authenticated" + }, + { + "command": "coder.navigateToWorkspace", + "when": "coder.workspace.connected" + }, + { + "command": "coder.navigateToWorkspaceSettings", + "when": "coder.workspace.connected" + }, + { + "command": "coder.workspace.update", + "when": "coder.workspace.updatable" + }, + { + "command": "coder.refreshWorkspaces", + "when": "coder.authenticated" + }, + { + "command": "coder.viewLogs", + "when": "true" + }, + { + "command": "coder.openAppStatus", + "when": "false" + }, + { + "command": "coder.open", + "when": "coder.authenticated" }, { "command": "coder.openFromSidebar", @@ -289,6 +325,10 @@ { "command": "coder.searchAllWorkspaces", "when": "false" + }, + { + "command": "coder.debug.listDeployments", + "when": "coder.devMode" } ], "view/title": [ 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/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); 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 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) { 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,