From 95b5b4eb89b3d67a98c6e03bc5b40e2aefa23193 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 12:34:30 +1000
Subject: [PATCH 01/14] feat: coder connect integration

---
 package.json              |   1 +
 src/commands.ts           | 110 +++++++++++++++++++++++++++++++++-----
 src/workspacesProvider.ts |   2 +-
 yarn.lock                 |  20 ++++++-
 4 files changed, 117 insertions(+), 16 deletions(-)

diff --git a/package.json b/package.json
index a85fd235..7004e518 100644
--- a/package.json
+++ b/package.json
@@ -316,6 +316,7 @@
     "node-forge": "^1.3.1",
     "pretty-bytes": "^6.1.1",
     "proxy-agent": "^6.4.0",
+    "range_check": "^3.2.0",
     "semver": "^7.6.2",
     "ua-parser-js": "^1.0.38",
     "ws": "^8.18.1",
diff --git a/src/commands.ts b/src/commands.ts
index d24df729..7ef3a608 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -1,6 +1,10 @@
+import { isAxiosError } from "axios"
 import { Api } from "coder/site/src/api/api"
 import { getErrorMessage } from "coder/site/src/api/errors"
 import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
+import { lookup } from "dns"
+import { inRange } from "range_check"
+import { promisify } from "util"
 import * as vscode from "vscode"
 import { makeCoderSdk, needToken } from "./api"
 import { extractAgents } from "./api-helper"
@@ -392,14 +396,33 @@ export class Commands {
       if (!baseUrl) {
         throw new Error("You are not logged in")
       }
-      await openWorkspace(
-        baseUrl,
-        treeItem.workspaceOwner,
-        treeItem.workspaceName,
-        treeItem.workspaceAgent,
-        treeItem.workspaceFolderPath,
-        true,
-      )
+
+      let agent = treeItem.workspaceAgent
+      if (!agent) {
+        // `openFromSidebar` is only callable on agents or single-agent workspaces,
+        // where this will always be set.
+        return
+      }
+
+      try {
+        await openWorkspace(
+          this.restClient,
+          baseUrl,
+          treeItem.workspaceOwner,
+          treeItem.workspaceName,
+          agent,
+          treeItem.workspaceFolderPath,
+          true,
+        )
+      } catch (err) {
+        const message = getErrorMessage(err, "no response from the server")
+        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
+        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
+          detail: message,
+          modal: true,
+          useCustom: true,
+        })
+      }
     } else {
       // If there is no tree item, then the user manually ran this command.
       // Default to the regular open instead.
@@ -491,12 +514,30 @@ export class Commands {
     } else {
       workspaceOwner = args[0] as string
       workspaceName = args[1] as string
-      // workspaceAgent is reserved for args[2], but multiple agents aren't supported yet.
+      workspaceAgent = args[2] as string
       folderPath = args[3] as string | undefined
       openRecent = args[4] as boolean | undefined
     }
 
-    await openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
+    try {
+      await openWorkspace(
+        this.restClient,
+        baseUrl,
+        workspaceOwner,
+        workspaceName,
+        workspaceAgent,
+        folderPath,
+        openRecent,
+      )
+    } catch (err) {
+      const message = getErrorMessage(err, "no response from the server")
+      this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
+      this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
+        detail: message,
+        modal: true,
+        useCustom: true,
+      })
+    }
   }
 
   /**
@@ -547,16 +588,42 @@ export class Commands {
  * both to the Remote SSH plugin in the form of a remote authority URI.
  */
 async function openWorkspace(
+  restClient: Api,
   baseUrl: string,
   workspaceOwner: string,
   workspaceName: string,
-  workspaceAgent: string | undefined,
+  workspaceAgent: string,
   folderPath: string | undefined,
   openRecent: boolean | undefined,
 ) {
-  // A workspace can have multiple agents, but that's handled
-  // when opening a workspace unless explicitly specified.
-  const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
+  let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
+
+  let hostnameSuffix = "coder"
+  try {
+    const sshConfig = await restClient.getDeploymentSSHConfig()
+    // If the field is undefined, it's an older server, and always 'coder'
+    hostnameSuffix = sshConfig.hostname_suffix ?? hostnameSuffix
+  } catch (error) {
+    if (!isAxiosError(error)) {
+      throw error
+    }
+    switch (error.response?.status) {
+      case 404: {
+        // Likely a very old deployment, just use the default.
+        break
+      }
+      case 401: {
+        throw error
+      }
+      default:
+        throw error
+    }
+  }
+
+  const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix)
+  if (coderConnectAddr) {
+    remoteAuthority = `ssh-remote+${coderConnectAddr}`
+  }
 
   let newWindow = true
   // Open in the existing window if no workspaces are open.
@@ -616,6 +683,21 @@ async function openWorkspace(
   })
 }
 
+async function maybeCoderConnectAddr(
+  agent: string,
+  workspace: string,
+  owner: string,
+  hostnameSuffix: string,
+): Promise<string | undefined> {
+  const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
+  try {
+    const res = await promisify(lookup)(coderConnectHostname)
+    return res.family == 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
+  } catch {
+    return undefined
+  }
+}
+
 async function openDevContainer(
   baseUrl: string,
   workspaceOwner: string,
diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts
index 0709487e..ea9d4084 100644
--- a/src/workspacesProvider.ts
+++ b/src/workspacesProvider.ts
@@ -353,7 +353,7 @@ export class WorkspaceTreeItem extends OpenableTreeItem {
       showOwner ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
       workspace.owner_name,
       workspace.name,
-      undefined,
+      agents.length > 0 ? agents[0].name : undefined,
       agents[0]?.expanded_directory,
       agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
     )
diff --git a/yarn.lock b/yarn.lock
index efc2df73..c0f76803 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1749,7 +1749,7 @@ co@3.1.0:
 
 "coder@https://github.com/coder/coder#main":
   version "0.0.0"
-  resolved "https://github.com/coder/coder#3ac844ad3d341d2910542b83d4f33df7bd0be85e"
+  resolved "https://github.com/coder/coder#f8971bb3cc01d81b3085b2b3c9253d8d340d125c"
 
 collapse-white-space@^1.0.2:
   version "1.0.6"
@@ -3441,6 +3441,16 @@ ip-address@^9.0.5:
     jsbn "1.1.0"
     sprintf-js "^1.1.3"
 
+ip6@^0.2.10:
+  version "0.2.11"
+  resolved "https://registry.yarnpkg.com/ip6/-/ip6-0.2.11.tgz#b7cf71864ef16c7418c29f7b1f2f5db892a189ec"
+  integrity sha512-OmTP7FyIp+ZoNvZ7Xr97bWrCgypa3BeuYuRFNTOPT8Y11cxMW1pW1VC70kHZP1onSHHMotADcjdg5QyECiIMUw==
+
+ipaddr.js@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
+  integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
+
 irregular-plurals@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-2.0.0.tgz#39d40f05b00f656d0b7fa471230dd3b714af2872"
@@ -4834,6 +4844,14 @@ randombytes@^2.1.0:
   dependencies:
     safe-buffer "^5.1.0"
 
+range_check@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/range_check/-/range_check-3.2.0.tgz#6ef17940bb382a7fb905ecda8204f2f28ce7f61d"
+  integrity sha512-JxiMqvzQJJLt5vaKSUm7f++UkDM1TuMbkQsqRZJYaSvvCTTVtoUMkE/rm+ZNgLXNFAQPhO74WgMPHJaxz/JOEA==
+  dependencies:
+    ip6 "^0.2.10"
+    ipaddr.js "^2.2.0"
+
 rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"

From a65e550753b99702c4cc222627c11b67d0fe18fd Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 12:40:57 +1000
Subject: [PATCH 02/14] lint

---
 src/commands.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index 7ef3a608..6e43cd25 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -397,7 +397,7 @@ export class Commands {
         throw new Error("You are not logged in")
       }
 
-      let agent = treeItem.workspaceAgent
+      const agent = treeItem.workspaceAgent
       if (!agent) {
         // `openFromSidebar` is only callable on agents or single-agent workspaces,
         // where this will always be set.
@@ -692,7 +692,7 @@ async function maybeCoderConnectAddr(
   const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
   try {
     const res = await promisify(lookup)(coderConnectHostname)
-    return res.family == 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
+    return res.family === 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
   } catch {
     return undefined
   }

From 2ecf1dff733b05c7aeec99d3f830e6e8b8bc5292 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 12:55:38 +1000
Subject: [PATCH 03/14] support stopped workspaces

---
 src/commands.ts | 59 +++++++++++++++++++++++--------------------------
 1 file changed, 28 insertions(+), 31 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index 6e43cd25..327d355f 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -397,20 +397,13 @@ export class Commands {
         throw new Error("You are not logged in")
       }
 
-      const agent = treeItem.workspaceAgent
-      if (!agent) {
-        // `openFromSidebar` is only callable on agents or single-agent workspaces,
-        // where this will always be set.
-        return
-      }
-
       try {
         await openWorkspace(
           this.restClient,
           baseUrl,
           treeItem.workspaceOwner,
           treeItem.workspaceName,
-          agent,
+          treeItem.workspaceAgent,
           treeItem.workspaceFolderPath,
           true,
         )
@@ -592,37 +585,41 @@ async function openWorkspace(
   baseUrl: string,
   workspaceOwner: string,
   workspaceName: string,
-  workspaceAgent: string,
+  workspaceAgent: string | undefined,
   folderPath: string | undefined,
   openRecent: boolean | undefined,
 ) {
   let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
 
-  let hostnameSuffix = "coder"
-  try {
-    const sshConfig = await restClient.getDeploymentSSHConfig()
-    // If the field is undefined, it's an older server, and always 'coder'
-    hostnameSuffix = sshConfig.hostname_suffix ?? hostnameSuffix
-  } catch (error) {
-    if (!isAxiosError(error)) {
-      throw error
-    }
-    switch (error.response?.status) {
-      case 404: {
-        // Likely a very old deployment, just use the default.
-        break
-      }
-      case 401: {
+  // When called from `openFromSidebar`, the workspaceAgent will only not be set
+  // if the workspace is stopped, in which case we can't use Coder Connect
+  // When called from `open`, the workspaceAgent will always be set.
+  if (workspaceAgent) {
+    let hostnameSuffix = "coder"
+    try {
+      const sshConfig = await restClient.getDeploymentSSHConfig()
+      // If the field is undefined, it's an older server, and always 'coder'
+      hostnameSuffix = sshConfig.hostname_suffix ?? hostnameSuffix
+    } catch (error) {
+      if (!isAxiosError(error)) {
         throw error
       }
-      default:
-        throw error
+      switch (error.response?.status) {
+        case 404: {
+          // Likely a very old deployment, just use the default.
+          break
+        }
+        case 401: {
+          throw error
+        }
+        default:
+          throw error
+      }
+    }
+    const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix)
+    if (coderConnectAddr) {
+      remoteAuthority = `ssh-remote+${coderConnectAddr}`
     }
-  }
-
-  const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix)
-  if (coderConnectAddr) {
-    remoteAuthority = `ssh-remote+${coderConnectAddr}`
   }
 
   let newWindow = true

From fb9a263c4a498e06291e2097ba5777da1aace274 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 13:01:17 +1000
Subject: [PATCH 04/14] fixup

---
 src/workspacesProvider.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/workspacesProvider.ts b/src/workspacesProvider.ts
index ea9d4084..12718546 100644
--- a/src/workspacesProvider.ts
+++ b/src/workspacesProvider.ts
@@ -353,7 +353,7 @@ export class WorkspaceTreeItem extends OpenableTreeItem {
       showOwner ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.Expanded,
       workspace.owner_name,
       workspace.name,
-      agents.length > 0 ? agents[0].name : undefined,
+      agents[0]?.name,
       agents[0]?.expanded_directory,
       agents.length > 1 ? "coderWorkspaceMultipleAgents" : "coderWorkspaceSingleAgent",
     )

From 3a77138674ac2bdc852bc1a8034af6e14f294aba Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 13:49:01 +1000
Subject: [PATCH 05/14] review

---
 package.json    |   2 +-
 src/commands.ts | 232 ++++++++++++++++++++++++------------------------
 yarn.lock       |  26 +++---
 3 files changed, 125 insertions(+), 135 deletions(-)

diff --git a/package.json b/package.json
index 7004e518..2e60495b 100644
--- a/package.json
+++ b/package.json
@@ -311,12 +311,12 @@
     "date-fns": "^3.6.0",
     "eventsource": "^3.0.6",
     "find-process": "https://github.com/coder/find-process#fix/sequoia-compat",
+    "ip-range-check": "^0.2.0",
     "jsonc-parser": "^3.3.1",
     "memfs": "^4.9.3",
     "node-forge": "^1.3.1",
     "pretty-bytes": "^6.1.1",
     "proxy-agent": "^6.4.0",
-    "range_check": "^3.2.0",
     "semver": "^7.6.2",
     "ua-parser-js": "^1.0.38",
     "ws": "^8.18.1",
diff --git a/src/commands.ts b/src/commands.ts
index 327d355f..6ae18d37 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -3,7 +3,7 @@ import { Api } from "coder/site/src/api/api"
 import { getErrorMessage } from "coder/site/src/api/errors"
 import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
 import { lookup } from "dns"
-import { inRange } from "range_check"
+import ipRangeCheck from "ip-range-check"
 import { promisify } from "util"
 import * as vscode from "vscode"
 import { makeCoderSdk, needToken } from "./api"
@@ -396,26 +396,14 @@ export class Commands {
       if (!baseUrl) {
         throw new Error("You are not logged in")
       }
-
-      try {
-        await openWorkspace(
-          this.restClient,
-          baseUrl,
-          treeItem.workspaceOwner,
-          treeItem.workspaceName,
-          treeItem.workspaceAgent,
-          treeItem.workspaceFolderPath,
-          true,
-        )
-      } catch (err) {
-        const message = getErrorMessage(err, "no response from the server")
-        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
-        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
-          detail: message,
-          modal: true,
-          useCustom: true,
-        })
-      }
+      await this.openWorkspace(
+        baseUrl,
+        treeItem.workspaceOwner,
+        treeItem.workspaceName,
+        treeItem.workspaceAgent,
+        treeItem.workspaceFolderPath,
+        true,
+      )
     } else {
       // If there is no tree item, then the user manually ran this command.
       // Default to the regular open instead.
@@ -513,15 +501,7 @@ export class Commands {
     }
 
     try {
-      await openWorkspace(
-        this.restClient,
-        baseUrl,
-        workspaceOwner,
-        workspaceName,
-        workspaceAgent,
-        folderPath,
-        openRecent,
-      )
+      await this.openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
     } catch (err) {
       const message = getErrorMessage(err, "no response from the server")
       this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
@@ -574,32 +554,112 @@ export class Commands {
       await this.workspaceRestClient.updateWorkspaceVersion(this.workspace)
     }
   }
-}
 
-/**
- * Given a workspace, build the host name, find a directory to open, and pass
- * both to the Remote SSH plugin in the form of a remote authority URI.
- */
-async function openWorkspace(
-  restClient: Api,
-  baseUrl: string,
-  workspaceOwner: string,
-  workspaceName: string,
-  workspaceAgent: string | undefined,
-  folderPath: string | undefined,
-  openRecent: boolean | undefined,
-) {
-  let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
+  /**
+   * Given a workspace, build the host name, find a directory to open, and pass
+   * both to the Remote SSH plugin in the form of a remote authority URI.
+   */
+  private async openWorkspace(
+    baseUrl: string,
+    workspaceOwner: string,
+    workspaceName: string,
+    workspaceAgent: string | undefined,
+    folderPath: string | undefined,
+    openRecent: boolean | undefined,
+  ) {
+    let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
+
+    // When called from `openFromSidebar`, the workspaceAgent will only not be set
+    // if the workspace is stopped, in which case we can't use Coder Connect
+    // When called from `open`, the workspaceAgent will always be set.
+    if (workspaceAgent) {
+      let hostnameSuffix = "coder"
+      try {
+        // If the field was undefined, it's an older server, and always 'coder'
+        hostnameSuffix = (await this.fetchHostnameSuffix()) ?? hostnameSuffix
+      } catch (error) {
+        const message = getErrorMessage(error, "no response from the server")
+        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
+        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
+          detail: message,
+          modal: true,
+          useCustom: true,
+        })
+      }
 
-  // When called from `openFromSidebar`, the workspaceAgent will only not be set
-  // if the workspace is stopped, in which case we can't use Coder Connect
-  // When called from `open`, the workspaceAgent will always be set.
-  if (workspaceAgent) {
-    let hostnameSuffix = "coder"
+      const coderConnectAddr = await maybeCoderConnectAddr(
+        workspaceAgent,
+        workspaceName,
+        workspaceOwner,
+        hostnameSuffix,
+      )
+      if (coderConnectAddr) {
+        remoteAuthority = `ssh-remote+${coderConnectAddr}`
+      }
+    }
+
+    let newWindow = true
+    // Open in the existing window if no workspaces are open.
+    if (!vscode.workspace.workspaceFolders?.length) {
+      newWindow = false
+    }
+
+    // If a folder isn't specified or we have been asked to open the most recent,
+    // we can try to open a recently opened folder/workspace.
+    if (!folderPath || openRecent) {
+      const output: {
+        workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[]
+      } = await vscode.commands.executeCommand("_workbench.getRecentlyOpened")
+      const opened = output.workspaces.filter(
+        // Remove recents that do not belong to this connection.  The remote
+        // authority maps to a workspace or workspace/agent combination (using the
+        // SSH host name).  This means, at the moment, you can have a different
+        // set of recents for a workspace versus workspace/agent combination, even
+        // if that agent is the default for the workspace.
+        (opened) => opened.folderUri?.authority === remoteAuthority,
+      )
+
+      // openRecent will always use the most recent.  Otherwise, if there are
+      // multiple we ask the user which to use.
+      if (opened.length === 1 || (opened.length > 1 && openRecent)) {
+        folderPath = opened[0].folderUri.path
+      } else if (opened.length > 1) {
+        const items = opened.map((f) => f.folderUri.path)
+        folderPath = await vscode.window.showQuickPick(items, {
+          title: "Select a recently opened folder",
+        })
+        if (!folderPath) {
+          // User aborted.
+          return
+        }
+      }
+    }
+
+    if (folderPath) {
+      await vscode.commands.executeCommand(
+        "vscode.openFolder",
+        vscode.Uri.from({
+          scheme: "vscode-remote",
+          authority: remoteAuthority,
+          path: folderPath,
+        }),
+        // Open this in a new window!
+        newWindow,
+      )
+      return
+    }
+
+    // This opens the workspace without an active folder opened.
+    await vscode.commands.executeCommand("vscode.newWindow", {
+      remoteAuthority: remoteAuthority,
+      reuseWindow: !newWindow,
+    })
+  }
+
+  private async fetchHostnameSuffix(): Promise<string | undefined> {
     try {
-      const sshConfig = await restClient.getDeploymentSSHConfig()
-      // If the field is undefined, it's an older server, and always 'coder'
-      hostnameSuffix = sshConfig.hostname_suffix ?? hostnameSuffix
+      const sshConfig = await this.restClient.getDeploymentSSHConfig()
+      return sshConfig.hostname_suffix
     } catch (error) {
       if (!isAxiosError(error)) {
         throw error
@@ -609,75 +669,11 @@ async function openWorkspace(
           // Likely a very old deployment, just use the default.
           break
         }
-        case 401: {
-          throw error
-        }
         default:
           throw error
       }
     }
-    const coderConnectAddr = await maybeCoderConnectAddr(workspaceAgent, workspaceName, workspaceOwner, hostnameSuffix)
-    if (coderConnectAddr) {
-      remoteAuthority = `ssh-remote+${coderConnectAddr}`
-    }
-  }
-
-  let newWindow = true
-  // Open in the existing window if no workspaces are open.
-  if (!vscode.workspace.workspaceFolders?.length) {
-    newWindow = false
   }
-
-  // If a folder isn't specified or we have been asked to open the most recent,
-  // we can try to open a recently opened folder/workspace.
-  if (!folderPath || openRecent) {
-    const output: {
-      workspaces: { folderUri: vscode.Uri; remoteAuthority: string }[]
-    } = await vscode.commands.executeCommand("_workbench.getRecentlyOpened")
-    const opened = output.workspaces.filter(
-      // Remove recents that do not belong to this connection.  The remote
-      // authority maps to a workspace or workspace/agent combination (using the
-      // SSH host name).  This means, at the moment, you can have a different
-      // set of recents for a workspace versus workspace/agent combination, even
-      // if that agent is the default for the workspace.
-      (opened) => opened.folderUri?.authority === remoteAuthority,
-    )
-
-    // openRecent will always use the most recent.  Otherwise, if there are
-    // multiple we ask the user which to use.
-    if (opened.length === 1 || (opened.length > 1 && openRecent)) {
-      folderPath = opened[0].folderUri.path
-    } else if (opened.length > 1) {
-      const items = opened.map((f) => f.folderUri.path)
-      folderPath = await vscode.window.showQuickPick(items, {
-        title: "Select a recently opened folder",
-      })
-      if (!folderPath) {
-        // User aborted.
-        return
-      }
-    }
-  }
-
-  if (folderPath) {
-    await vscode.commands.executeCommand(
-      "vscode.openFolder",
-      vscode.Uri.from({
-        scheme: "vscode-remote",
-        authority: remoteAuthority,
-        path: folderPath,
-      }),
-      // Open this in a new window!
-      newWindow,
-    )
-    return
-  }
-
-  // This opens the workspace without an active folder opened.
-  await vscode.commands.executeCommand("vscode.newWindow", {
-    remoteAuthority: remoteAuthority,
-    reuseWindow: !newWindow,
-  })
 }
 
 async function maybeCoderConnectAddr(
@@ -689,7 +685,7 @@ async function maybeCoderConnectAddr(
   const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
   try {
     const res = await promisify(lookup)(coderConnectHostname)
-    return res.family === 6 && inRange(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
+    return res.family === 6 && ipRangeCheck(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
   } catch {
     return undefined
   }
diff --git a/yarn.lock b/yarn.lock
index c0f76803..72bc49da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3441,15 +3441,17 @@ ip-address@^9.0.5:
     jsbn "1.1.0"
     sprintf-js "^1.1.3"
 
-ip6@^0.2.10:
-  version "0.2.11"
-  resolved "https://registry.yarnpkg.com/ip6/-/ip6-0.2.11.tgz#b7cf71864ef16c7418c29f7b1f2f5db892a189ec"
-  integrity sha512-OmTP7FyIp+ZoNvZ7Xr97bWrCgypa3BeuYuRFNTOPT8Y11cxMW1pW1VC70kHZP1onSHHMotADcjdg5QyECiIMUw==
+ip-range-check@^0.2.0:
+  version "0.2.0"
+  resolved "https://registry.yarnpkg.com/ip-range-check/-/ip-range-check-0.2.0.tgz#e67f126c8fb36c8f11d4c07d7924b7e364365157"
+  integrity sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw==
+  dependencies:
+    ipaddr.js "^1.0.1"
 
-ipaddr.js@^2.2.0:
-  version "2.2.0"
-  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8"
-  integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
+ipaddr.js@^1.0.1:
+  version "1.9.1"
+  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
+  integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
 
 irregular-plurals@^2.0.0:
   version "2.0.0"
@@ -4844,14 +4846,6 @@ randombytes@^2.1.0:
   dependencies:
     safe-buffer "^5.1.0"
 
-range_check@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/range_check/-/range_check-3.2.0.tgz#6ef17940bb382a7fb905ecda8204f2f28ce7f61d"
-  integrity sha512-JxiMqvzQJJLt5vaKSUm7f++UkDM1TuMbkQsqRZJYaSvvCTTVtoUMkE/rm+ZNgLXNFAQPhO74WgMPHJaxz/JOEA==
-  dependencies:
-    ip6 "^0.2.10"
-    ipaddr.js "^2.2.0"
-
 rc@^1.2.7:
   version "1.2.8"
   resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"

From 2a3500e84d5f09ab394c8df675b498020bc98851 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 13:51:25 +1000
Subject: [PATCH 06/14] fixup

---
 src/commands.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/commands.ts b/src/commands.ts
index 6ae18d37..44260f84 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -585,6 +585,7 @@ export class Commands {
           modal: true,
           useCustom: true,
         })
+        return
       }
 
       const coderConnectAddr = await maybeCoderConnectAddr(

From 9252fff15c48b36ec24846b16956564f0d2cf5ea Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 13:59:23 +1000
Subject: [PATCH 07/14] fixup

---
 src/commands.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/commands.ts b/src/commands.ts
index 44260f84..e85572cf 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -686,6 +686,8 @@ async function maybeCoderConnectAddr(
   const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
   try {
     const res = await promisify(lookup)(coderConnectHostname)
+    // Captive DNS portals may return an unrelated address, so we check it's
+    // within the Coder Service Prefix.
     return res.family === 6 && ipRangeCheck(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
   } catch {
     return undefined

From 195151a2b8d80d7cb1510471be3cd5516e8a5c85 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 14:56:04 +1000
Subject: [PATCH 08/14] switch to coder connect dynamically

---
 src/api.ts      | 35 ++++++++++++++++++++++++--
 src/commands.ts | 52 +++++----------------------------------
 src/remote.ts   | 65 ++++++++++++++++++++++++++++---------------------
 src/util.ts     | 20 +++++++++++++++
 4 files changed, 96 insertions(+), 76 deletions(-)

diff --git a/src/api.ts b/src/api.ts
index fdb83b81..b239df68 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -1,7 +1,7 @@
-import { AxiosInstance } from "axios"
+import { AxiosInstance, isAxiosError } from "axios"
 import { spawn } from "child_process"
 import { Api } from "coder/site/src/api/api"
-import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
+import { ProvisionerJobLog, SSHConfigResponse, Workspace } from "coder/site/src/api/typesGenerated"
 import { FetchLikeInit } from "eventsource"
 import fs from "fs/promises"
 import { ProxyAgent } from "proxy-agent"
@@ -280,3 +280,34 @@ export async function waitForBuild(
   writeEmitter.fire(`Workspace is now ${updatedWorkspace.latest_build.status}\r\n`)
   return updatedWorkspace
 }
+
+export async function fetchSSHConfig(restClient: Api, vsc: typeof vscode): Promise<SSHConfigResponse> {
+  try {
+    const sshConfig = await restClient.getDeploymentSSHConfig()
+    return {
+      hostname_prefix: sshConfig.hostname_prefix,
+      hostname_suffix: sshConfig.hostname_suffix ?? "coder",
+      ssh_config_options: sshConfig.ssh_config_options,
+    }
+  } catch (error) {
+    if (!isAxiosError(error)) {
+      throw error
+    }
+    switch (error.response?.status) {
+      case 404: {
+        // Very old deployment that doesn't support SSH config
+        return {
+          hostname_prefix: "coder",
+          hostname_suffix: "coder",
+          ssh_config_options: {},
+        }
+      }
+      case 401: {
+        vsc.window.showErrorMessage("Your session expired...")
+        throw error
+      }
+      default:
+        throw error
+    }
+  }
+}
diff --git a/src/commands.ts b/src/commands.ts
index e85572cf..dfa5f16a 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -1,16 +1,12 @@
-import { isAxiosError } from "axios"
 import { Api } from "coder/site/src/api/api"
 import { getErrorMessage } from "coder/site/src/api/errors"
 import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
-import { lookup } from "dns"
-import ipRangeCheck from "ip-range-check"
-import { promisify } from "util"
 import * as vscode from "vscode"
-import { makeCoderSdk, needToken } from "./api"
+import { fetchSSHConfig, makeCoderSdk, needToken } from "./api"
 import { extractAgents } from "./api-helper"
 import { CertificateError } from "./error"
 import { Storage } from "./storage"
-import { toRemoteAuthority, toSafeHost } from "./util"
+import { maybeCoderConnectAddr, toRemoteAuthority, toSafeHost } from "./util"
 import { OpenableTreeItem } from "./workspacesProvider"
 
 export class Commands {
@@ -573,10 +569,10 @@ export class Commands {
     // if the workspace is stopped, in which case we can't use Coder Connect
     // When called from `open`, the workspaceAgent will always be set.
     if (workspaceAgent) {
-      let hostnameSuffix = "coder"
+      let sshConfig
       try {
-        // If the field was undefined, it's an older server, and always 'coder'
-        hostnameSuffix = (await this.fetchHostnameSuffix()) ?? hostnameSuffix
+        // Fetch (or get defaults) for the SSH config.
+        sshConfig = await fetchSSHConfig(this.restClient, this.vscodeProposed)
       } catch (error) {
         const message = getErrorMessage(error, "no response from the server")
         this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
@@ -592,7 +588,7 @@ export class Commands {
         workspaceAgent,
         workspaceName,
         workspaceOwner,
-        hostnameSuffix,
+        sshConfig.hostname_suffix,
       )
       if (coderConnectAddr) {
         remoteAuthority = `ssh-remote+${coderConnectAddr}`
@@ -656,42 +652,6 @@ export class Commands {
       reuseWindow: !newWindow,
     })
   }
-
-  private async fetchHostnameSuffix(): Promise<string | undefined> {
-    try {
-      const sshConfig = await this.restClient.getDeploymentSSHConfig()
-      return sshConfig.hostname_suffix
-    } catch (error) {
-      if (!isAxiosError(error)) {
-        throw error
-      }
-      switch (error.response?.status) {
-        case 404: {
-          // Likely a very old deployment, just use the default.
-          break
-        }
-        default:
-          throw error
-      }
-    }
-  }
-}
-
-async function maybeCoderConnectAddr(
-  agent: string,
-  workspace: string,
-  owner: string,
-  hostnameSuffix: string,
-): Promise<string | undefined> {
-  const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
-  try {
-    const res = await promisify(lookup)(coderConnectHostname)
-    // Captive DNS portals may return an unrelated address, so we check it's
-    // within the Coder Service Prefix.
-    return res.family === 6 && ipRangeCheck(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
-  } catch {
-    return undefined
-  }
 }
 
 async function openDevContainer(
diff --git a/src/remote.ts b/src/remote.ts
index 5b8a9694..b1234c4a 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -1,6 +1,6 @@
 import { isAxiosError } from "axios"
 import { Api } from "coder/site/src/api/api"
-import { Workspace } from "coder/site/src/api/typesGenerated"
+import { SSHConfigResponse, Workspace } from "coder/site/src/api/typesGenerated"
 import find from "find-process"
 import * as fs from "fs/promises"
 import * as jsonc from "jsonc-parser"
@@ -9,7 +9,14 @@ import * as path from "path"
 import prettyBytes from "pretty-bytes"
 import * as semver from "semver"
 import * as vscode from "vscode"
-import { createHttpAgent, makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
+import {
+  createHttpAgent,
+  fetchSSHConfig,
+  makeCoderSdk,
+  needToken,
+  startWorkspaceIfStoppedOrFailed,
+  waitForBuild,
+} from "./api"
 import { extractAgents } from "./api-helper"
 import * as cli from "./cliManager"
 import { Commands } from "./commands"
@@ -19,7 +26,7 @@ import { Inbox } from "./inbox"
 import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"
 import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
 import { Storage } from "./storage"
-import { AuthorityPrefix, expandPath, parseRemoteAuthority } from "./util"
+import { AuthorityPrefix, expandPath, maybeCoderConnectAddr, parseRemoteAuthority } from "./util"
 import { WorkspaceMonitor } from "./workspaceMonitor"
 
 export interface RemoteDetails extends vscode.Disposable {
@@ -469,9 +476,19 @@ export class Remote {
     //
     // If we didn't write to the SSH config file, connecting would fail with
     // "Host not found".
+    let sshConfigResponse: SSHConfigResponse
     try {
       this.storage.writeToCoderOutputChannel("Updating SSH config...")
-      await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir, featureSet)
+      sshConfigResponse = await fetchSSHConfig(workspaceRestClient, this.vscodeProposed)
+      await this.updateSSHConfig(
+        workspaceRestClient,
+        parts.label,
+        parts.host,
+        binaryPath,
+        logDir,
+        featureSet,
+        sshConfigResponse,
+      )
     } catch (error) {
       this.storage.writeToCoderOutputChannel(`Failed to configure SSH: ${error}`)
       throw error
@@ -503,6 +520,20 @@ export class Remote {
 
     this.storage.writeToCoderOutputChannel("Remote setup complete")
 
+    // If Coder Connect is available for this workspace, switch to that
+    const coderConnectAddr = await maybeCoderConnectAddr(
+      agent.name,
+      parts.workspace,
+      parts.username,
+      sshConfigResponse.hostname_suffix,
+    )
+    if (coderConnectAddr) {
+      await vscode.commands.executeCommand("vscode.newWindow", {
+        remoteAuthority: `ssh-remote+${coderConnectAddr}`,
+        reuseWindow: true,
+      })
+    }
+
     // Returning the URL and token allows the plugin to authenticate its own
     // client, for example to display the list of workspaces belonging to this
     // deployment in the sidebar.  We use our own client in here for reasons
@@ -550,30 +581,8 @@ export class Remote {
     binaryPath: string,
     logDir: string,
     featureSet: FeatureSet,
+    sshConfigResponse: SSHConfigResponse,
   ) {
-    let deploymentSSHConfig = {}
-    try {
-      const deploymentConfig = await restClient.getDeploymentSSHConfig()
-      deploymentSSHConfig = deploymentConfig.ssh_config_options
-    } catch (error) {
-      if (!isAxiosError(error)) {
-        throw error
-      }
-      switch (error.response?.status) {
-        case 404: {
-          // Deployment does not support overriding ssh config yet. Likely an
-          // older version, just use the default.
-          break
-        }
-        case 401: {
-          await this.vscodeProposed.window.showErrorMessage("Your session expired...")
-          throw error
-        }
-        default:
-          throw error
-      }
-    }
-
     // deploymentConfig is now set from the remote coderd deployment.
     // Now override with the user's config.
     const userConfigSSH = vscode.workspace.getConfiguration("coder").get<string[]>("sshConfig") || []
@@ -596,7 +605,7 @@ export class Remote {
       },
       {} as Record<string, string>,
     )
-    const sshConfigOverrides = mergeSSHConfigValues(deploymentSSHConfig, userConfig)
+    const sshConfigOverrides = mergeSSHConfigValues(sshConfigResponse.ssh_config_options, userConfig)
 
     let sshConfigFile = vscode.workspace.getConfiguration().get<string>("remote.SSH.configFile")
     if (!sshConfigFile) {
diff --git a/src/util.ts b/src/util.ts
index 8253f152..92b0cd35 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,5 +1,8 @@
+import { lookup } from "dns"
+import ipRangeCheck from "ip-range-check"
 import * as os from "os"
 import url from "url"
+import { promisify } from "util"
 
 export interface AuthorityParts {
   agent: string | undefined
@@ -61,6 +64,23 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
   }
 }
 
+export async function maybeCoderConnectAddr(
+  agent: string,
+  workspace: string,
+  owner: string,
+  hostnameSuffix: string,
+): Promise<string | undefined> {
+  const coderConnectHostname = `${agent}.${workspace}.${owner}.${hostnameSuffix}`
+  try {
+    const res = await promisify(lookup)(coderConnectHostname)
+    // Captive DNS portals may return an unrelated address, so we check it's
+    // within the Coder Service Prefix.
+    return res.family === 6 && ipRangeCheck(res.address, "fd60:627a:a42b::/48") ? coderConnectHostname : undefined
+  } catch {
+    return undefined
+  }
+}
+
 export function toRemoteAuthority(
   baseUrl: string,
   workspaceOwner: string,

From e7cad8219b361742109dec0bb070ccf551e22955 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 22:45:27 +1000
Subject: [PATCH 09/14] handle dev containers

---
 src/commands.ts  | 88 +++++++++++++++++++++++++++++++-----------------
 src/remote.ts    | 30 ++++++++++++++---
 src/util.test.ts | 14 ++++++++
 src/util.ts      | 23 ++++++++++---
 4 files changed, 117 insertions(+), 38 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index dfa5f16a..701f3bc2 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -491,7 +491,7 @@ export class Commands {
     } else {
       workspaceOwner = args[0] as string
       workspaceName = args[1] as string
-      workspaceAgent = args[2] as string
+      workspaceAgent = args[2] as string | undefined
       folderPath = args[3] as string | undefined
       openRecent = args[4] as boolean | undefined
     }
@@ -522,11 +522,11 @@ export class Commands {
 
     const workspaceOwner = args[0] as string
     const workspaceName = args[1] as string
-    const workspaceAgent = undefined // args[2] is reserved, but we do not support multiple agents yet.
+    const workspaceAgent = args[2] as string | undefined
     const devContainerName = args[3] as string
     const devContainerFolder = args[4] as string
 
-    await openDevContainer(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
+    await this.openDevContainerInner(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
   }
 
   /**
@@ -652,33 +652,61 @@ export class Commands {
       reuseWindow: !newWindow,
     })
   }
-}
 
-async function openDevContainer(
-  baseUrl: string,
-  workspaceOwner: string,
-  workspaceName: string,
-  workspaceAgent: string | undefined,
-  devContainerName: string,
-  devContainerFolder: string,
-) {
-  const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
-
-  const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
-  const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
-
-  let newWindow = true
-  if (!vscode.workspace.workspaceFolders?.length) {
-    newWindow = false
-  }
+  private async openDevContainerInner(
+    baseUrl: string,
+    workspaceOwner: string,
+    workspaceName: string,
+    workspaceAgent: string | undefined,
+    devContainerName: string,
+    devContainerFolder: string,
+  ) {
+    let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
+
+    if (workspaceAgent) {
+      let sshConfig
+      try {
+        // Fetch (or get defaults) for the SSH config.
+        sshConfig = await fetchSSHConfig(this.restClient, this.vscodeProposed)
+      } catch (error) {
+        const message = getErrorMessage(error, "no response from the server")
+        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
+        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
+          detail: message,
+          modal: true,
+          useCustom: true,
+        })
+        return
+      }
+
+      const coderConnectAddr = await maybeCoderConnectAddr(
+        workspaceAgent,
+        workspaceName,
+        workspaceOwner,
+        sshConfig.hostname_suffix,
+      )
+      if (coderConnectAddr) {
+        remoteAuthority = `ssh-remote+${coderConnectAddr}`
+      }
+    }
+
+    const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
+    const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
 
-  await vscode.commands.executeCommand(
-    "vscode.openFolder",
-    vscode.Uri.from({
-      scheme: "vscode-remote",
-      authority: devContainerAuthority,
-      path: devContainerFolder,
-    }),
-    newWindow,
-  )
+    let newWindow = true
+    if (!vscode.workspace.workspaceFolders?.length) {
+      newWindow = false
+    }
+
+    await vscode.commands.executeCommand(
+      "vscode.openFolder",
+      vscode.Uri.from({
+        scheme: "vscode-remote",
+        authority: devContainerAuthority,
+        path: devContainerFolder,
+      }),
+      newWindow,
+    )
+  }
 }
+
diff --git a/src/remote.ts b/src/remote.ts
index b1234c4a..ef7e679c 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -528,10 +528,32 @@ export class Remote {
       sshConfigResponse.hostname_suffix,
     )
     if (coderConnectAddr) {
-      await vscode.commands.executeCommand("vscode.newWindow", {
-        remoteAuthority: `ssh-remote+${coderConnectAddr}`,
-        reuseWindow: true,
-      })
+      // Find the path of the current workspace, which will have the same authority
+      const folderPath = this.vscodeProposed.workspace.workspaceFolders
+      ?.find(folder => folder.uri.authority === remoteAuthority)
+      ?.uri.path;
+      let newRemoteAuthority = `ssh-remote+${coderConnectAddr}`
+      if (parts.containerNameHex) {
+        newRemoteAuthority = `attached-container+${parts.containerNameHex}@${newRemoteAuthority}`
+      }
+
+      if (folderPath) {
+        await vscode.commands.executeCommand(
+          "vscode.openFolder",
+          vscode.Uri.from({
+            scheme: "vscode-remote",
+            authority: newRemoteAuthority,
+            path: folderPath,
+          }),
+          //`ForceNewWindow`
+          false,
+        )
+      } else {
+        await vscode.commands.executeCommand("vscode.newWindow", {
+          remoteAuthority: newRemoteAuthority,
+          reuseWindow: true,
+        })
+      }
     }
 
     // Returning the URL and token allows the plugin to authenticate its own
diff --git a/src/util.test.ts b/src/util.test.ts
index 4fffcc75..ac46a08e 100644
--- a/src/util.test.ts
+++ b/src/util.test.ts
@@ -9,6 +9,7 @@ it("ignore unrelated authorities", async () => {
     "vscode://ssh-remote+coder-vscode-test--foo--bar",
     "vscode://ssh-remote+coder-vscode-foo--bar",
     "vscode://ssh-remote+coder--foo--bar",
+    "vscode://attached-container+namehash@ssh-remote+dev.foo.admin.coder"
   ]
   for (const test of tests) {
     expect(parseRemoteAuthority(test)).toBe(null)
@@ -29,6 +30,7 @@ it("should error on invalid authorities", async () => {
 
 it("should parse authority", async () => {
   expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode--foo--bar")).toStrictEqual({
+    containerNameHex: undefined,
     agent: "",
     host: "coder-vscode--foo--bar",
     label: "",
@@ -36,6 +38,7 @@ it("should parse authority", async () => {
     workspace: "bar",
   })
   expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode--foo--bar--baz")).toStrictEqual({
+    containerNameHex: undefined,
     agent: "baz",
     host: "coder-vscode--foo--bar--baz",
     label: "",
@@ -43,6 +46,7 @@ it("should parse authority", async () => {
     workspace: "bar",
   })
   expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar")).toStrictEqual({
+    containerNameHex: undefined,
     agent: "",
     host: "coder-vscode.dev.coder.com--foo--bar",
     label: "dev.coder.com",
@@ -50,6 +54,7 @@ it("should parse authority", async () => {
     workspace: "bar",
   })
   expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar--baz")).toStrictEqual({
+    containerNameHex: undefined,
     agent: "baz",
     host: "coder-vscode.dev.coder.com--foo--bar--baz",
     label: "dev.coder.com",
@@ -57,6 +62,15 @@ it("should parse authority", async () => {
     workspace: "bar",
   })
   expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
+    containerNameHex: undefined,
+    agent: "baz",
+    host: "coder-vscode.dev.coder.com--foo--bar.baz",
+    label: "dev.coder.com",
+    username: "foo",
+    workspace: "bar",
+  })
+  expect(parseRemoteAuthority("vscode://attached-container+namehash@ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
+    containerNameHex: "namehash",
     agent: "baz",
     host: "coder-vscode.dev.coder.com--foo--bar.baz",
     label: "dev.coder.com",
diff --git a/src/util.ts b/src/util.ts
index 92b0cd35..f45ad8e3 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,10 +1,12 @@
 import { lookup } from "dns"
 import ipRangeCheck from "ip-range-check"
+import { ssh } from "node-forge"
 import * as os from "os"
 import url from "url"
 import { promisify } from "util"
 
 export interface AuthorityParts {
+  containerNameHex: string | undefined
   agent: string | undefined
   host: string
   label: string
@@ -24,14 +26,26 @@ export const AuthorityPrefix = "coder-vscode"
  * Throw an error if the host is invalid.
  */
 export function parseRemoteAuthority(authority: string): AuthorityParts | null {
-  // The authority looks like: vscode://ssh-remote+<ssh host name>
-  const authorityParts = authority.split("+")
+  // The Dev Container authority looks like: vscode://attached-container+containerNameHex@ssh-remote+<ssh host name>
+  // The SSH authority looks like: vscode://ssh-remote+<ssh host name>
+  const authorityParts = authority.split("@")
+  let containerNameHex = undefined
+  let sshAuthority
+  if (authorityParts.length == 1) {
+    sshAuthority = authorityParts[0]
+  }  else if (authorityParts.length == 2 && authorityParts[0].includes("attached-container+")) {
+    sshAuthority = authorityParts[1]
+    containerNameHex = authorityParts[0].split("+")[1]
+  } else {
+    return null
+  }
+  const sshAuthorityParts = sshAuthority.split("+")
 
   // We create SSH host names in a format matching:
   // coder-vscode(--|.)<username>--<workspace>(--|.)<agent?>
   // The agent can be omitted; the user will be prompted for it instead.
   // Anything else is unrelated to Coder and can be ignored.
-  const parts = authorityParts[1].split("--")
+  const parts = sshAuthorityParts[1].split("--")
   if (parts.length <= 1 || (parts[0] !== AuthorityPrefix && !parts[0].startsWith(`${AuthorityPrefix}.`))) {
     return null
   }
@@ -56,8 +70,9 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
   }
 
   return {
+    containerNameHex: containerNameHex,
     agent: agent,
-    host: authorityParts[1],
+    host: sshAuthorityParts[1],
     label: parts[0].replace(/^coder-vscode\.?/, ""),
     username: parts[1],
     workspace: workspace,

From ea4b17958d78f23572d18d1c718cad568334d098 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 22:47:35 +1000
Subject: [PATCH 10/14] lint

---
 src/commands.ts  | 10 ++++++++--
 src/remote.ts    |  6 +++---
 src/util.test.ts |  6 ++++--
 src/util.ts      |  2 +-
 4 files changed, 16 insertions(+), 8 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index 701f3bc2..22caa69f 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -526,7 +526,14 @@ export class Commands {
     const devContainerName = args[3] as string
     const devContainerFolder = args[4] as string
 
-    await this.openDevContainerInner(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
+    await this.openDevContainerInner(
+      baseUrl,
+      workspaceOwner,
+      workspaceName,
+      workspaceAgent,
+      devContainerName,
+      devContainerFolder,
+    )
   }
 
   /**
@@ -709,4 +716,3 @@ export class Commands {
     )
   }
 }
-
diff --git a/src/remote.ts b/src/remote.ts
index ef7e679c..3f4897b8 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -529,9 +529,9 @@ export class Remote {
     )
     if (coderConnectAddr) {
       // Find the path of the current workspace, which will have the same authority
-      const folderPath = this.vscodeProposed.workspace.workspaceFolders
-      ?.find(folder => folder.uri.authority === remoteAuthority)
-      ?.uri.path;
+      const folderPath = this.vscodeProposed.workspace.workspaceFolders?.find(
+        (folder) => folder.uri.authority === remoteAuthority,
+      )?.uri.path
       let newRemoteAuthority = `ssh-remote+${coderConnectAddr}`
       if (parts.containerNameHex) {
         newRemoteAuthority = `attached-container+${parts.containerNameHex}@${newRemoteAuthority}`
diff --git a/src/util.test.ts b/src/util.test.ts
index ac46a08e..b3583da1 100644
--- a/src/util.test.ts
+++ b/src/util.test.ts
@@ -9,7 +9,7 @@ it("ignore unrelated authorities", async () => {
     "vscode://ssh-remote+coder-vscode-test--foo--bar",
     "vscode://ssh-remote+coder-vscode-foo--bar",
     "vscode://ssh-remote+coder--foo--bar",
-    "vscode://attached-container+namehash@ssh-remote+dev.foo.admin.coder"
+    "vscode://attached-container+namehash@ssh-remote+dev.foo.admin.coder",
   ]
   for (const test of tests) {
     expect(parseRemoteAuthority(test)).toBe(null)
@@ -69,7 +69,9 @@ it("should parse authority", async () => {
     username: "foo",
     workspace: "bar",
   })
-  expect(parseRemoteAuthority("vscode://attached-container+namehash@ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
+  expect(
+    parseRemoteAuthority("vscode://attached-container+namehash@ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz"),
+  ).toStrictEqual({
     containerNameHex: "namehash",
     agent: "baz",
     host: "coder-vscode.dev.coder.com--foo--bar.baz",
diff --git a/src/util.ts b/src/util.ts
index f45ad8e3..42d6c403 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -33,7 +33,7 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
   let sshAuthority
   if (authorityParts.length == 1) {
     sshAuthority = authorityParts[0]
-  }  else if (authorityParts.length == 2 && authorityParts[0].includes("attached-container+")) {
+  } else if (authorityParts.length == 2 && authorityParts[0].includes("attached-container+")) {
     sshAuthority = authorityParts[1]
     containerNameHex = authorityParts[0].split("+")[1]
   } else {

From 5e4e79507e2f5f0e191abd10ff260b3e713e70ed Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 22:55:57 +1000
Subject: [PATCH 11/14] comment

---
 src/commands.ts | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index 22caa69f..e5dce2cc 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -572,9 +572,14 @@ export class Commands {
   ) {
     let remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
 
-    // When called from `openFromSidebar`, the workspaceAgent will only not be set
-    // if the workspace is stopped, in which case we can't use Coder Connect
-    // When called from `open`, the workspaceAgent will always be set.
+    // We can't connect using Coder Connect straightaway if `workspaceAgent`
+    // is undefined. This happens when:
+    // 1. The workspace is stopped
+    // 2. A `vscode://coder.coder-remote` URI does not include the agent as a
+    // query parameter.
+    //
+    // For 1. `Remote.setup` will migrate us to Coder Connect once the workspace is built.
+    // For 2. `Remote.setup` will call `maybeAskAgent` and then migrate us to Coder Connect
     if (workspaceAgent) {
       let sshConfig
       try {

From c3287eb6212f3e6de12a556f8b3d131c54f037b3 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 22:57:21 +1000
Subject: [PATCH 12/14] lint

---
 src/util.ts | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/util.ts b/src/util.ts
index 42d6c403..29bbe9fc 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,5 @@
 import { lookup } from "dns"
 import ipRangeCheck from "ip-range-check"
-import { ssh } from "node-forge"
 import * as os from "os"
 import url from "url"
 import { promisify } from "util"
@@ -31,9 +30,9 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
   const authorityParts = authority.split("@")
   let containerNameHex = undefined
   let sshAuthority
-  if (authorityParts.length == 1) {
+  if (authorityParts.length === 1) {
     sshAuthority = authorityParts[0]
-  } else if (authorityParts.length == 2 && authorityParts[0].includes("attached-container+")) {
+  } else if (authorityParts.length === 2 && authorityParts[0].includes("attached-container+")) {
     sshAuthority = authorityParts[1]
     containerNameHex = authorityParts[0].split("+")[1]
   } else {

From a2df5ccad169663e3d0436768ff5b8095b691972 Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Thu, 17 Apr 2025 23:11:15 +1000
Subject: [PATCH 13/14] fixup

---
 src/commands.ts | 12 +-----------
 1 file changed, 1 insertion(+), 11 deletions(-)

diff --git a/src/commands.ts b/src/commands.ts
index e5dce2cc..5583edea 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -496,17 +496,7 @@ export class Commands {
       openRecent = args[4] as boolean | undefined
     }
 
-    try {
-      await this.openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
-    } catch (err) {
-      const message = getErrorMessage(err, "no response from the server")
-      this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
-      this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
-        detail: message,
-        modal: true,
-        useCustom: true,
-      })
-    }
+    await this.openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
   }
 
   /**

From feb1021be7d38cbb98f07b66841f4cefdc6fb1ba Mon Sep 17 00:00:00 2001
From: Ethan Dickson <ethan@coder.com>
Date: Fri, 18 Apr 2025 15:42:12 +1000
Subject: [PATCH 14/14] review

---
 src/api.ts      |  6 +----
 src/commands.ts | 72 ++++++++++++++++++++++++++++++++++++++-----------
 src/remote.ts   |  2 +-
 src/util.ts     | 16 ++++-------
 4 files changed, 63 insertions(+), 33 deletions(-)

diff --git a/src/api.ts b/src/api.ts
index b239df68..0c78d7cd 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -281,7 +281,7 @@ export async function waitForBuild(
   return updatedWorkspace
 }
 
-export async function fetchSSHConfig(restClient: Api, vsc: typeof vscode): Promise<SSHConfigResponse> {
+export async function fetchSSHConfig(restClient: Api): Promise<SSHConfigResponse> {
   try {
     const sshConfig = await restClient.getDeploymentSSHConfig()
     return {
@@ -302,10 +302,6 @@ export async function fetchSSHConfig(restClient: Api, vsc: typeof vscode): Promi
           ssh_config_options: {},
         }
       }
-      case 401: {
-        vsc.window.showErrorMessage("Your session expired...")
-        throw error
-      }
       default:
         throw error
     }
diff --git a/src/commands.ts b/src/commands.ts
index 5583edea..f9019856 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -1,3 +1,4 @@
+import { isAxiosError } from "axios"
 import { Api } from "coder/site/src/api/api"
 import { getErrorMessage } from "coder/site/src/api/errors"
 import { User, Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated"
@@ -574,16 +575,11 @@ export class Commands {
       let sshConfig
       try {
         // Fetch (or get defaults) for the SSH config.
-        sshConfig = await fetchSSHConfig(this.restClient, this.vscodeProposed)
+        sshConfig = await fetchSSHConfig(this.restClient)
       } catch (error) {
-        const message = getErrorMessage(error, "no response from the server")
-        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
-        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
-          detail: message,
-          modal: true,
-          useCustom: true,
+        return this.handleInitialRequestError(error, workspaceName, baseUrl, async () => {
+          await this.openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
         })
-        return
       }
 
       const coderConnectAddr = await maybeCoderConnectAddr(
@@ -669,16 +665,18 @@ export class Commands {
       let sshConfig
       try {
         // Fetch (or get defaults) for the SSH config.
-        sshConfig = await fetchSSHConfig(this.restClient, this.vscodeProposed)
+        sshConfig = await fetchSSHConfig(this.restClient)
       } catch (error) {
-        const message = getErrorMessage(error, "no response from the server")
-        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
-        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
-          detail: message,
-          modal: true,
-          useCustom: true,
+        return this.handleInitialRequestError(error, workspaceName, baseUrl, async () => {
+          await this.openDevContainerInner(
+            baseUrl,
+            workspaceOwner,
+            workspaceName,
+            workspaceAgent,
+            devContainerName,
+            devContainerFolder,
+          )
         })
-        return
       }
 
       const coderConnectAddr = await maybeCoderConnectAddr(
@@ -710,4 +708,46 @@ export class Commands {
       newWindow,
     )
   }
+
+  private async handleInitialRequestError(
+    error: unknown,
+    workspaceName: string,
+    baseUrl: string,
+    retryCallback: () => Promise<void>,
+  ) {
+    if (!isAxiosError(error)) {
+      throw error
+    }
+    switch (error.response?.status) {
+      case 401: {
+        const result = await this.vscodeProposed.window.showInformationMessage(
+          "Your session expired...",
+          {
+            useCustom: true,
+            modal: true,
+            detail: `You must log in to access ${workspaceName}.`,
+          },
+          "Log In",
+        )
+        if (!result) {
+          // User declined to log in.
+          return
+        }
+        // Log in then try again
+        await vscode.commands.executeCommand("coder.login", baseUrl, undefined, undefined)
+        await retryCallback()
+        return
+      }
+      default: {
+        const message = getErrorMessage(error, "no response from the server")
+        this.storage.writeToCoderOutputChannel(`Failed to open workspace: ${message}`)
+        this.vscodeProposed.window.showErrorMessage("Failed to open workspace", {
+          detail: message,
+          modal: true,
+          useCustom: true,
+        })
+        return
+      }
+    }
+  }
 }
diff --git a/src/remote.ts b/src/remote.ts
index 3f4897b8..5ef942a9 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -479,7 +479,7 @@ export class Remote {
     let sshConfigResponse: SSHConfigResponse
     try {
       this.storage.writeToCoderOutputChannel("Updating SSH config...")
-      sshConfigResponse = await fetchSSHConfig(workspaceRestClient, this.vscodeProposed)
+      sshConfigResponse = await fetchSSHConfig(workspaceRestClient)
       await this.updateSSHConfig(
         workspaceRestClient,
         parts.label,
diff --git a/src/util.ts b/src/util.ts
index 29bbe9fc..c42dcd72 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -27,17 +27,11 @@ export const AuthorityPrefix = "coder-vscode"
 export function parseRemoteAuthority(authority: string): AuthorityParts | null {
   // The Dev Container authority looks like: vscode://attached-container+containerNameHex@ssh-remote+<ssh host name>
   // The SSH authority looks like: vscode://ssh-remote+<ssh host name>
-  const authorityParts = authority.split("@")
-  let containerNameHex = undefined
-  let sshAuthority
-  if (authorityParts.length === 1) {
-    sshAuthority = authorityParts[0]
-  } else if (authorityParts.length === 2 && authorityParts[0].includes("attached-container+")) {
-    sshAuthority = authorityParts[1]
-    containerNameHex = authorityParts[0].split("+")[1]
-  } else {
-    return null
-  }
+  const authorityURI = authority.startsWith("vscode://") ? authority : `vscode://${authority}`
+  const authorityParts = new URL(authorityURI)
+  const containerParts = authorityParts.username.split("+")
+  const containerNameHex = containerParts[1]
+  const sshAuthority = authorityParts.host
   const sshAuthorityParts = sshAuthority.split("+")
 
   // We create SSH host names in a format matching: