diff --git a/CHANGELOG.md b/CHANGELOG.md
index 893daa4..fa101fd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Unreleased
 
+### Added
+
+- support for Toolbox 2.6.3 with improved URI handling
+
 ## 0.2.3 - 2025-05-26
 
 ### Changed
diff --git a/README.md b/README.md
index 2b749e3..2034504 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,11 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open
 page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable
 experience, it’s recommended to ensure the workspace is running prior to initiating the connection.
 
+> ⚠️ Note: `folder` should point to a remote IDEA project that has already been opened and appears in the `Projects` tab.
+> If the path refers to a project that doesn't exist, the remote IDE won’t start or load it.
+
+> Until [TBX-14952](https://youtrack.jetbrains.com/issue/TBX-14952/) is fixed, it's best to either use a path to a previously opened project or leave it empty.
+
 ## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy
 
 This section explains how to set up a local proxy (without authentication which is not yet supported) and verify that
diff --git a/gradle.properties b/gradle.properties
index 8433f61..a6129a9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,3 +1,3 @@
-version=0.2.3
+version=0.3.0
 group=com.coder.toolbox
 name=coder-toolbox
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8546bd8..3cf5531 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
 [versions]
-toolbox-plugin-api = "1.0.38881"
-kotlin = "2.1.0"
+toolbox-plugin-api = "1.1.41749"
+kotlin = "2.1.10"
 coroutines = "1.10.1"
 serialization = "1.8.0"
 okhttp = "4.12.0"
@@ -9,7 +9,7 @@ marketplace-client = "2.0.46"
 gradle-wrapper = "0.14.0"
 exec = "1.12"
 moshi = "1.15.2"
-ksp = "2.1.0-1.0.29"
+ksp = "2.1.10-1.0.31"
 retrofit = "2.11.0"
 changelog = "2.2.1"
 gettext = "0.7.0"
diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt
index e6118c3..0608817 100644
--- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt
+++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt
@@ -27,6 +27,7 @@ import com.squareup.moshi.Moshi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
@@ -203,7 +204,7 @@ class CoderRemoteEnvironment(
 
     private fun File.doesNotExists(): Boolean = !this.exists()
 
-    override fun afterDisconnect() {
+    override fun afterDisconnect(isManual: Boolean) {
         context.logger.info("Stopping the network metrics poll job for $id")
         pollJob?.cancel()
         this.connectionRequest.update { false }
@@ -269,7 +270,7 @@ class CoderRemoteEnvironment(
         }
     }
 
-    override fun onDelete() {
+    override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow {
         context.cs.launch {
             try {
                 client.removeWorkspace(workspace)
diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
index 06e1496..4291321 100644
--- a/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
+++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
@@ -7,18 +7,22 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
 import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
 import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
 import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
 import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
 import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
 import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
 import com.jetbrains.toolbox.api.ui.ToolboxUi
 import kotlinx.coroutines.CoroutineScope
 import java.net.URL
+import java.util.UUID
 
+@Suppress("UnstableApiUsage")
 data class CoderToolboxContext(
     val ui: ToolboxUi,
     val envPageManager: EnvironmentUiPageManager,
     val envStateColorPalette: EnvironmentStateColorPalette,
-    val ideOrchestrator: ClientHelper,
+    val remoteIdeOrchestrator: RemoteToolsHelper,
+    val jbClientOrchestrator: ClientHelper,
     val desktop: LocalDesktopManager,
     val cs: CoroutineScope,
     val logger: Logger,
@@ -44,4 +48,44 @@ data class CoderToolboxContext(
             }
             return this.settingsStore.defaultURL.toURL()
         }
+
+    suspend fun logAndShowError(title: String, error: String) {
+        logger.error(error)
+        ui.showSnackbar(
+            UUID.randomUUID().toString(),
+            i18n.pnotr(title),
+            i18n.pnotr(error),
+            i18n.ptrl("OK")
+        )
+    }
+
+    suspend fun logAndShowError(title: String, error: String, exception: Exception) {
+        logger.error(exception, error)
+        ui.showSnackbar(
+            UUID.randomUUID().toString(),
+            i18n.pnotr(title),
+            i18n.pnotr(error),
+            i18n.ptrl("OK")
+        )
+    }
+
+    suspend fun logAndShowWarning(title: String, warning: String) {
+        logger.warn(warning)
+        ui.showSnackbar(
+            UUID.randomUUID().toString(),
+            i18n.pnotr(title),
+            i18n.pnotr(warning),
+            i18n.ptrl("OK")
+        )
+    }
+
+    suspend fun logAndShowInfo(title: String, info: String) {
+        logger.info(info)
+        ui.showSnackbar(
+            UUID.randomUUID().toString(),
+            i18n.pnotr(title),
+            i18n.pnotr(info),
+            i18n.ptrl("OK")
+        )
+    }
 }
diff --git a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt
index 05424ae..5cfcd11 100644
--- a/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt
+++ b/src/main/kotlin/com/coder/toolbox/CoderToolboxExtension.kt
@@ -13,6 +13,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
 import com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
 import com.jetbrains.toolbox.api.remoteDev.RemoteProvider
 import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
 import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
 import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
 import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -31,6 +32,7 @@ class CoderToolboxExtension : RemoteDevExtension {
                 serviceLocator.getService<ToolboxUi>(),
                 serviceLocator.getService<EnvironmentUiPageManager>(),
                 serviceLocator.getService<EnvironmentStateColorPalette>(),
+                serviceLocator.getService<RemoteToolsHelper>(),
                 serviceLocator.getService<ClientHelper>(),
                 serviceLocator.getService<LocalDesktopManager>(),
                 serviceLocator.getService<CoroutineScope>(),
diff --git a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt
index 3c9ad5f..cc04dfe 100644
--- a/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt
+++ b/src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt
@@ -7,7 +7,7 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgentLifecycleState
 import com.coder.toolbox.sdk.v2.models.WorkspaceAgentStatus
 import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
 import com.jetbrains.toolbox.api.core.ui.color.StateColor
-import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentState
+import com.jetbrains.toolbox.api.remoteDev.states.CustomRemoteEnvironmentStateV2
 import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateIcons
 import com.jetbrains.toolbox.api.remoteDev.states.StandardRemoteEnvironmentState
 
@@ -61,9 +61,9 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
      * Note that a reachable environment will always display "connected" or
      * "disconnected" regardless of the label we give that status.
      */
-    fun toRemoteEnvironmentState(context: CoderToolboxContext): CustomRemoteEnvironmentState {
-        return CustomRemoteEnvironmentState(
-            label,
+    fun toRemoteEnvironmentState(context: CoderToolboxContext): CustomRemoteEnvironmentStateV2 {
+        return CustomRemoteEnvironmentStateV2(
+            context.i18n.pnotr(label),
             color = getStateColor(context),
             reachable = ready() || unhealthy(),
             // TODO@JB: How does this work?  Would like a spinner for pending states.
@@ -90,10 +90,10 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
         else EnvironmentStateIcons.NoIcon
     }
 
-    fun toSshConnectingEnvState(context: CoderToolboxContext): CustomRemoteEnvironmentState {
+    fun toSshConnectingEnvState(context: CoderToolboxContext): CustomRemoteEnvironmentStateV2 {
         val existingState = toRemoteEnvironmentState(context)
-        return CustomRemoteEnvironmentState(
-            "SSHing",
+        return CustomRemoteEnvironmentStateV2(
+            context.i18n.pnotr("SSHing"),
             existingState.color,
             existingState.isReachable,
             EnvironmentStateIcons.Connecting
diff --git a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
index 6785675..9f619bc 100644
--- a/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
+++ b/src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
@@ -192,12 +192,12 @@ open class CoderRestClient(
     }
 
     /**
-     * Maps the list of workspaces to the associated agents.
+     * Maps the available workspaces to the associated agents.
      */
-    suspend fun groupByAgents(workspaces: List<Workspace>): Set<Pair<Workspace, WorkspaceAgent>> {
+    suspend fun workspacesByAgents(): Set<Pair<Workspace, WorkspaceAgent>> {
         // It is possible for there to be resources with duplicate names so we
         // need to use a set.
-        return workspaces.flatMap { ws ->
+        return workspaces().flatMap { ws ->
             when (ws.latestBuild.status) {
                 WorkspaceStatus.RUNNING -> ws.latestBuild.resources
                 else -> resources(ws)
diff --git a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
index ad42d18..af07548 100644
--- a/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
+++ b/src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt
@@ -9,19 +9,23 @@ import com.coder.toolbox.sdk.CoderRestClient
 import com.coder.toolbox.sdk.v2.models.Workspace
 import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
 import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
-import com.jetbrains.toolbox.api.localization.LocalizableString
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.time.withTimeout
-import java.net.HttpURLConnection
 import java.net.URI
-import java.net.URL
+import java.util.UUID
+import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import kotlin.time.Duration.Companion.seconds
 import kotlin.time.toJavaDuration
 
+private const val CAN_T_HANDLE_URI_TITLE = "Can't handle URI"
+
+@Suppress("UnstableApiUsage")
 open class CoderProtocolHandler(
     private val context: CoderToolboxContext,
     private val dialogUi: DialogUi,
@@ -41,113 +45,245 @@ open class CoderProtocolHandler(
         shouldWaitForAutoLogin: Boolean,
         reInitialize: suspend (CoderRestClient, CoderCLIManager) -> Unit
     ) {
-        context.popupPluginMainPage()
         val params = uri.toQueryParameters()
         if (params.isEmpty()) {
             // probably a plugin installation scenario
+            context.logAndShowInfo("URI will not be handled", "No query parameters were provided")
             return
         }
 
+        if (shouldWaitForAutoLogin) {
+            isInitialized.waitForTrue()
+        }
+
+        context.logger.info("Handling $uri...")
+        val deploymentURL = resolveDeploymentUrl(params) ?: return
+        val token = resolveToken(params) ?: return
+        val workspaceName = resolveWorkspaceName(params) ?: return
+        val restClient = buildRestClient(deploymentURL, token) ?: return
+        val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
+        if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return
+
+        // we resolve the agent after the workspace is started otherwise we can get misleading
+        // errors like: no agent available while workspace is starting or stopping
+        val agent = resolveAgent(params, workspace) ?: return
+        if (!ensureAgentIsReady(workspace, agent)) return
+
+        val cli = configureCli(deploymentURL, restClient)
+        reInitialize(restClient, cli)
+
+        val environmentId = "${workspace.name}.${agent.name}"
+        context.showEnvironmentPage(environmentId)
+
+        val productCode = params.ideProductCode()
+        val buildNumber = params.ideBuildNumber()
+        val projectFolder = params.projectFolder()
+
+        if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
+            launchIde(environmentId, productCode, buildNumber, projectFolder)
+        }
+    }
+
+    private suspend fun resolveDeploymentUrl(params: Map<String, String>): String? {
         val deploymentURL = params.url() ?: askUrl()
         if (deploymentURL.isNullOrBlank()) {
-            context.logger.error("Query parameter \"$URL\" is missing from URI $uri")
-            context.showErrorPopup(MissingArgumentException("Can't handle URI because query parameter \"$URL\" is missing"))
-            return
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$URL\" is missing from URI")
+            return null
         }
+        return deploymentURL
+    }
 
-        val queryToken = params.token()
-        val restClient = try {
-            authenticate(deploymentURL, queryToken)
-        } catch (ex: Exception) {
-            context.logger.error(ex, "Query parameter \"$TOKEN\" is missing from URI $uri")
-            context.showErrorPopup(IllegalStateException(humanizeConnectionError(deploymentURL.toURL(), true, ex)))
-            return
+    private suspend fun resolveToken(params: Map<String, String>): String? {
+        val token = params.token()
+        if (token.isNullOrBlank()) {
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$TOKEN\" is missing from URI")
+            return null
+        }
+        return token
+    }
+
+    private suspend fun resolveWorkspaceName(params: Map<String, String>): String? {
+        val workspace = params.workspace()
+        if (workspace.isNullOrBlank()) {
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$WORKSPACE\" is missing from URI")
+            return null
         }
+        return workspace
+    }
 
-        // TODO: Show a dropdown and ask for the workspace if missing. Right now it's not possible because dialogs are quite limited
-        val workspaceName = params.workspace()
-        if (workspaceName.isNullOrBlank()) {
-            context.logger.error("Query parameter \"$WORKSPACE\" is missing from URI $uri")
-            context.showErrorPopup(MissingArgumentException("Can't handle URI because query parameter \"$WORKSPACE\" is missing"))
-            return
+    private suspend fun buildRestClient(deploymentURL: String, token: String): CoderRestClient? {
+        try {
+            return authenticate(deploymentURL, token)
+        } catch (ex: Exception) {
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, humanizeConnectionError(deploymentURL.toURL(), true, ex))
+            return null
         }
+    }
+
+    /**
+     * Returns an authenticated Coder CLI.
+     */
+    private suspend fun authenticate(deploymentURL: String, token: String): CoderRestClient {
+        val client = CoderRestClient(
+            context,
+            deploymentURL.toURL(),
+            if (settings.requireTokenAuth) token else null,
+            PluginManager.pluginInfo.version
+        )
+        client.authenticate()
+        return client
+    }
 
-        val workspaces = restClient.workspaces()
-        val workspace = workspaces.firstOrNull { it.name == workspaceName }
+    private suspend fun List<Workspace>.matchName(workspaceName: String, deploymentURL: String): Workspace? {
+        val workspace = this.firstOrNull { it.name == workspaceName }
         if (workspace == null) {
-            context.logger.error("There is no workspace with name $workspaceName on $deploymentURL")
-            context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace with name $workspaceName does not exist"))
-            return
+            context.logAndShowError(
+                CAN_T_HANDLE_URI_TITLE,
+                "There is no workspace with name $workspaceName on $deploymentURL"
+            )
+            return null
         }
+        return workspace
+    }
 
+    private suspend fun prepareWorkspace(
+        workspace: Workspace,
+        restClient: CoderRestClient,
+        workspaceName: String,
+        deploymentURL: String
+    ): Boolean {
         when (workspace.latestBuild.status) {
             WorkspaceStatus.PENDING, WorkspaceStatus.STARTING ->
-                if (restClient.waitForReady(workspace) != true) {
-                    context.logger.error("$workspaceName from $deploymentURL could not be ready on time")
-                    context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace $workspaceName could not be ready on time"))
-                    return
+                if (!restClient.waitForReady(workspace)) {
+                    context.logAndShowError(
+                        CAN_T_HANDLE_URI_TITLE,
+                        "$workspaceName from $deploymentURL could not be ready on time"
+                    )
+                    return false
                 }
 
             WorkspaceStatus.STOPPING, WorkspaceStatus.STOPPED,
             WorkspaceStatus.CANCELING, WorkspaceStatus.CANCELED -> {
                 if (settings.disableAutostart) {
-                    context.logger.warn("$workspaceName from $deploymentURL is not started and autostart is disabled.")
-                    context.showInfoPopup(
-                        context.i18n.pnotr("$workspaceName is not running"),
-                        context.i18n.ptrl("Can't handle URI because workspace is not running and autostart is disabled. Please start the workspace manually and execute the URI again."),
-                        context.i18n.ptrl("OK")
+                    context.logAndShowWarning(
+                        CAN_T_HANDLE_URI_TITLE,
+                        "$workspaceName from $deploymentURL is not running and autostart is disabled"
                     )
-                    return
+                    return false
                 }
 
                 try {
                     restClient.startWorkspace(workspace)
                 } catch (e: Exception) {
-                    context.logger.error(
-                        e,
-                        "$workspaceName from $deploymentURL could not be started while handling URI"
+                    context.logAndShowError(
+                        CAN_T_HANDLE_URI_TITLE,
+                        "$workspaceName from $deploymentURL could not be started",
+                        e
                     )
-                    context.showErrorPopup(MissingArgumentException("Can't handle URI because an error was encountered while trying to start workspace $workspaceName"))
-                    return
+                    return false
                 }
-                if (restClient.waitForReady(workspace) != true) {
-                    context.logger.error("$workspaceName from $deploymentURL could not be started on time")
-                    context.showErrorPopup(MissingArgumentException("Can't handle URI because workspace $workspaceName could not be started on time"))
-                    return
+
+                if (!restClient.waitForReady(workspace)) {
+                    context.logAndShowError(
+                        CAN_T_HANDLE_URI_TITLE,
+                        "$workspaceName from $deploymentURL could not be started on time",
+                    )
+                    return false
                 }
             }
 
             WorkspaceStatus.FAILED, WorkspaceStatus.DELETING, WorkspaceStatus.DELETED -> {
-                context.logger.error("Unable to connect to $workspaceName from $deploymentURL")
-                context.showErrorPopup(MissingArgumentException("Can't handle URI because because we're unable to connect to workspace $workspaceName"))
-                return
+                context.logAndShowError(
+                    CAN_T_HANDLE_URI_TITLE,
+                    "Unable to connect to $workspaceName from $deploymentURL"
+                )
+                return false
             }
 
-            WorkspaceStatus.RUNNING -> Unit // All is well
+            WorkspaceStatus.RUNNING -> return true // All is well
         }
+        return true
+    }
 
-        // TODO: Show a dropdown and ask for an agent if missing.
-        val agent: WorkspaceAgent
+    private suspend fun resolveAgent(
+        params: Map<String, String>,
+        workspace: Workspace
+    ): WorkspaceAgent? {
         try {
-            agent = getMatchingAgent(params, workspace)
+            return getMatchingAgent(params, workspace)
         } catch (e: IllegalArgumentException) {
-            context.logger.error(e, "Can't resolve an agent for workspace $workspaceName from $deploymentURL")
-            context.showErrorPopup(
-                MissingArgumentException(
-                    "Can't handle URI because we can't resolve an agent for workspace $workspaceName from $deploymentURL",
-                    e
-                )
+            context.logAndShowError(
+                CAN_T_HANDLE_URI_TITLE,
+                "Can't resolve an agent for workspace ${workspace.name}",
+                e
             )
-            return
+            return null
+        }
+    }
+
+    /**
+     * Return the agent matching the provided agent ID or name in the parameters.
+     *
+     * @throws [IllegalArgumentException]
+     */
+    internal suspend fun getMatchingAgent(
+        parameters: Map<String, String?>,
+        workspace: Workspace,
+    ): WorkspaceAgent? {
+        val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
+        if (agents.isEmpty()) {
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "The workspace \"${workspace.name}\" has no agents")
+            return null
+        }
+
+        // If the agent is missing and the workspace has only one, use that.
+        val agent =
+            if (!parameters.agentID().isNullOrBlank()) {
+                agents.firstOrNull { it.id.toString() == parameters.agentID() }
+            } else if (agents.size == 1) {
+                agents.first()
+            } else {
+                null
+            }
+
+        if (agent == null) {
+            if (!parameters.agentID().isNullOrBlank()) {
+                context.logAndShowError(
+                    CAN_T_HANDLE_URI_TITLE,
+                    "The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\""
+                )
+                return null
+            } else {
+                context.logAndShowError(
+                    CAN_T_HANDLE_URI_TITLE,
+                    "Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent"
+                )
+                return null
+            }
         }
+        return agent
+    }
+
+    private suspend fun ensureAgentIsReady(
+        workspace: Workspace,
+        agent: WorkspaceAgent
+    ): Boolean {
         val status = WorkspaceAndAgentStatus.from(workspace, agent)
 
         if (!status.ready()) {
-            context.logger.error("Agent ${agent.name} for workspace $workspaceName from $deploymentURL is not ready")
-            context.showErrorPopup(MissingArgumentException("Can't handle URI because agent ${agent.name} for workspace $workspaceName from $deploymentURL is not ready"))
-            return
+            context.logAndShowError(
+                CAN_T_HANDLE_URI_TITLE,
+                "Agent ${agent.name} for workspace ${workspace.name} is not ready"
+            )
+            return false
         }
+        return true
+    }
 
+    private suspend fun configureCli(
+        deploymentURL: String,
+        restClient: CoderRestClient
+    ): CoderCLIManager {
         val cli = ensureCLI(
             context,
             deploymentURL.toURL(),
@@ -161,31 +297,92 @@ open class CoderProtocolHandler(
         }
 
         context.logger.info("Configuring Coder CLI...")
-        cli.configSsh(restClient.groupByAgents(workspaces))
+        cli.configSsh(restClient.workspacesByAgents())
+        return cli
+    }
 
-        if (shouldWaitForAutoLogin) {
-            isInitialized.waitForTrue()
+    private fun launchIde(
+        environmentId: String,
+        productCode: String,
+        buildNumber: String,
+        projectFolder: String?
+    ) {
+        context.cs.launch {
+            val selectedIde = selectAndInstallRemoteIde(productCode, buildNumber, environmentId) ?: return@launch
+            context.logger.info("$productCode-$buildNumber is already on $environmentId. Going to launch JBClient")
+            installJBClient(selectedIde, environmentId).join()
+            launchJBClient(selectedIde, environmentId, projectFolder)
         }
-        reInitialize(restClient, cli)
+    }
 
-        val environmentId = "${workspace.name}.${agent.name}"
-        context.popupPluginMainPage()
-        context.envPageManager.showEnvironmentPage(environmentId, false)
-        val productCode = params.ideProductCode()
-        val buildNumber = params.ideBuildNumber()
-        val projectFolder = params.projectFolder()
-        if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
-            context.cs.launch {
-                val ideVersion = "$productCode-$buildNumber"
-                context.logger.info("installing $ideVersion on $environmentId")
-                val job = context.cs.launch {
-                    context.ideOrchestrator.prepareClient(environmentId, ideVersion)
-                }
-                job.join()
-                context.logger.info("launching $ideVersion on $environmentId")
-                context.ideOrchestrator.connectToIde(environmentId, ideVersion, projectFolder)
+    private suspend fun selectAndInstallRemoteIde(
+        productCode: String,
+        buildNumber: String,
+        environmentId: String
+    ): String? {
+        val installedIdes = context.remoteIdeOrchestrator.getInstalledRemoteTools(environmentId, productCode)
+
+        var selectedIde = "$productCode-$buildNumber"
+        if (installedIdes.firstOrNull { it.contains(buildNumber) } != null) {
+            context.logger.info("$selectedIde is already installed on $environmentId")
+            return selectedIde
+        }
+
+        selectedIde = resolveAvailableIde(environmentId, productCode, buildNumber) ?: return null
+
+        // needed otherwise TBX will install it again
+        if (!installedIdes.contains(selectedIde)) {
+            context.logger.info("Installing $selectedIde on $environmentId...")
+            context.remoteIdeOrchestrator.installRemoteTool(environmentId, selectedIde)
+
+            if (context.remoteIdeOrchestrator.waitForIdeToBeInstalled(environmentId, selectedIde)) {
+                context.logger.info("Successfully installed $selectedIde on $environmentId...")
+                return selectedIde
+            } else {
+                context.ui.showSnackbar(
+                    UUID.randomUUID().toString(),
+                    context.i18n.pnotr("$selectedIde could not be installed"),
+                    context.i18n.pnotr("$selectedIde could not be installed on time. Check the logs for more details"),
+                    context.i18n.ptrl("OK")
+                )
+                return null
             }
+        } else {
+            context.logger.info("$selectedIde is already present on $environmentId...")
+            return selectedIde
+        }
+    }
+
+    private suspend fun resolveAvailableIde(environmentId: String, productCode: String, buildNumber: String): String? {
+        val availableVersions = context
+            .remoteIdeOrchestrator
+            .getAvailableRemoteTools(environmentId, productCode)
+
+        if (availableVersions.isEmpty()) {
+            context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "$productCode is not available on $environmentId")
+            return null
         }
+
+        val matchingBuildNumber = availableVersions.firstOrNull { it.contains(buildNumber) } != null
+        if (!matchingBuildNumber) {
+            val selectedIde = availableVersions.maxOf { it }
+            context.logAndShowInfo(
+                "$productCode-$buildNumber not available",
+                "$productCode-$buildNumber is not available, we've selected the latest $selectedIde"
+            )
+            return selectedIde
+        }
+        return null
+    }
+
+    private fun installJBClient(selectedIde: String, environmentId: String): Job = context.cs.launch {
+        context.logger.info("Downloading and installing JBClient counterpart to $selectedIde locally")
+        context.jbClientOrchestrator.prepareClient(environmentId, selectedIde)
+    }
+
+    private fun launchJBClient(selectedIde: String, environmentId: String, projectFolder: String?) {
+        context.logger.info("Launching $selectedIde on $environmentId")
+        context.jbClientOrchestrator.connectToIde(environmentId, selectedIde, projectFolder)
     }
 
     private suspend fun CoderRestClient.waitForReady(workspace: Workspace): Boolean {
@@ -203,6 +400,25 @@ open class CoderProtocolHandler(
         }
     }
 
+    private suspend fun RemoteToolsHelper.waitForIdeToBeInstalled(
+        environmentId: String,
+        ideHint: String,
+        waitTime: Duration = 2.minutes
+    ): Boolean {
+        var isInstalled = false
+        try {
+            withTimeout(waitTime.toJavaDuration()) {
+                while (!isInstalled) {
+                    delay(5.seconds)
+                    isInstalled = getInstalledRemoteTools(environmentId, ideHint).isNotEmpty()
+                }
+            }
+            return true
+        } catch (_: TimeoutCancellationException) {
+            return false
+        }
+    }
+
     private suspend fun askUrl(): String? {
         context.popupPluginMainPage()
         return dialogUi.ask(
@@ -210,119 +426,6 @@ open class CoderProtocolHandler(
             context.i18n.ptrl("Enter the full URL of your Coder deployment")
         )
     }
-
-    /**
-     * Return an authenticated Coder CLI, asking for the token.
-     * Throw MissingArgumentException if the user aborts. Any network or invalid
-     * token error may also be thrown.
-     */
-    private suspend fun authenticate(
-        deploymentURL: String,
-        tryToken: String?
-    ): CoderRestClient {
-        val token =
-            if (settings.requireTokenAuth) {
-                // Try the provided token immediately on the first attempt.
-                if (!tryToken.isNullOrBlank()) {
-                    tryToken
-                } else {
-                    context.popupPluginMainPage()
-                    // Otherwise ask for a new token, showing the previous token.
-                    dialogUi.askToken(deploymentURL.toURL())
-                }
-            } else {
-                null
-            }
-
-        if (settings.requireTokenAuth && token == null) { // User aborted.
-            throw MissingArgumentException("Token is required")
-        }
-        val client = CoderRestClient(
-            context,
-            deploymentURL.toURL(),
-            token,
-            PluginManager.pluginInfo.version
-        )
-        client.authenticate()
-        return client
-    }
-
-}
-
-/**
- * Follow a URL's redirects to its final destination.
- */
-internal fun resolveRedirects(url: URL): URL {
-    var location = url
-    val maxRedirects = 10
-    for (i in 1..maxRedirects) {
-        val conn = location.openConnection() as HttpURLConnection
-        conn.instanceFollowRedirects = false
-        conn.connect()
-        val code = conn.responseCode
-        val nextLocation = conn.getHeaderField("Location")
-        conn.disconnect()
-        // Redirects are triggered by any code starting with 3 plus a
-        // location header.
-        if (code < 300 || code >= 400 || nextLocation.isNullOrBlank()) {
-            return location
-        }
-        // Location headers might be relative.
-        location = URL(location, nextLocation)
-    }
-    throw Exception("Too many redirects")
-}
-
-/**
- * Return the agent matching the provided agent ID or name in the parameters.
- *
- * @throws [IllegalArgumentException]
- */
-internal fun getMatchingAgent(
-    parameters: Map<String, String?>,
-    workspace: Workspace,
-): WorkspaceAgent {
-    val agents = workspace.latestBuild.resources.filter { it.agents != null }.flatMap { it.agents!! }
-    if (agents.isEmpty()) {
-        throw IllegalArgumentException("The workspace \"${workspace.name}\" has no agents")
-    }
-
-    // If the agent is missing and the workspace has only one, use that.
-    // Prefer the ID over the name if both are set.
-    val agent =
-        if (!parameters.agentID().isNullOrBlank()) {
-            agents.firstOrNull { it.id.toString() == parameters.agentID() }
-        } else if (agents.size == 1) {
-            agents.first()
-        } else {
-            null
-        }
-
-    if (agent == null) {
-        if (!parameters.agentID().isNullOrBlank()) {
-            throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"")
-        } else {
-            throw MissingArgumentException(
-                "Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent",
-            )
-        }
-    }
-
-    return agent
-}
-
-private suspend fun CoderToolboxContext.showErrorPopup(error: Throwable) {
-    popupPluginMainPage()
-    this.ui.showErrorInfoPopup(error)
-}
-
-private suspend fun CoderToolboxContext.showInfoPopup(
-    title: LocalizableString,
-    message: LocalizableString,
-    okLabel: LocalizableString
-) {
-    popupPluginMainPage()
-    this.ui.showInfoPopup(title, message, okLabel)
 }
 
 private fun CoderToolboxContext.popupPluginMainPage() {
@@ -330,4 +433,9 @@ private fun CoderToolboxContext.popupPluginMainPage() {
     this.envPageManager.showPluginEnvironmentsPage(true)
 }
 
+private suspend fun CoderToolboxContext.showEnvironmentPage(envId: String) {
+    this.ui.showWindow()
+    this.envPageManager.showEnvironmentPage(envId, false)
+}
+
 class MissingArgumentException(message: String, ex: Throwable? = null) : IllegalArgumentException(message, ex)
diff --git a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt
index d3adabc..3678813 100644
--- a/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt
+++ b/src/main/kotlin/com/coder/toolbox/util/Dialogs.kt
@@ -1,10 +1,8 @@
 package com.coder.toolbox.util
 
 import com.coder.toolbox.CoderToolboxContext
-import com.coder.toolbox.browser.browse
 import com.jetbrains.toolbox.api.localization.LocalizableString
 import com.jetbrains.toolbox.api.ui.components.TextType
-import java.net.URL
 
 /**
  * Dialog implementation for standalone Gateway.
@@ -26,34 +24,4 @@ class DialogUi(private val context: CoderToolboxContext) {
             title, description, placeholder, TextType.General, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel")
         )
     }
-
-    suspend fun askPassword(
-        title: LocalizableString,
-        description: LocalizableString,
-        placeholder: LocalizableString? = null,
-    ): String? {
-        return context.ui.showTextInputPopup(
-            title, description, placeholder, TextType.Password, context.i18n.ptrl("OK"), context.i18n.ptrl("Cancel")
-        )
-    }
-
-    private suspend fun openUrl(url: URL) {
-        context.desktop.browse(url.toString()) {
-            context.ui.showErrorInfoPopup(it)
-        }
-    }
-
-    /**
-     * Open a dialog for providing the token.
-     */
-    suspend fun askToken(
-        url: URL,
-    ): String? {
-        openUrl(url.withPath("/login?redirect=%2Fcli-auth"))
-        return askPassword(
-            title = context.i18n.ptrl("Session Token"),
-            description = context.i18n.pnotr("Please paste the session token from the web-page"),
-            placeholder = context.i18n.pnotr("")
-        )
-    }
 }
diff --git a/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt b/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt
index 1135227..0a15db8 100644
--- a/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt
+++ b/src/main/kotlin/com/coder/toolbox/util/LinkMap.kt
@@ -3,7 +3,6 @@ package com.coder.toolbox.util
 const val URL = "url"
 const val TOKEN = "token"
 const val WORKSPACE = "workspace"
-const val AGENT_NAME = "agent"
 const val AGENT_ID = "agent_id"
 private const val IDE_PRODUCT_CODE = "ide_product_code"
 private const val IDE_BUILD_NUMBER = "ide_build_number"
diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po
index ceba2e9..73da796 100644
--- a/src/main/resources/localization/defaultMessages.po
+++ b/src/main/resources/localization/defaultMessages.po
@@ -137,4 +137,7 @@ msgid "Network Status"
 msgstr ""
 
 msgid "Create workspace"
+msgstr ""
+
+msgid "Error encountered while handling Coder URI"
 msgstr ""
\ No newline at end of file
diff --git a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt
index a7c6f72..4603fda 100644
--- a/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt
+++ b/src/test/kotlin/com/coder/toolbox/cli/CoderCLIManagerTest.kt
@@ -34,6 +34,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
 import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
 import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
 import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
 import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
 import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
 import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -66,6 +67,7 @@ internal class CoderCLIManagerTest {
         mockk<ToolboxUi>(),
         mockk<EnvironmentUiPageManager>(),
         mockk<EnvironmentStateColorPalette>(),
+        mockk<RemoteToolsHelper>(),
         mockk<ClientHelper>(),
         mockk<LocalDesktopManager>(),
         mockk<CoroutineScope>(),
diff --git a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
index c32e7b1..2727228 100644
--- a/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
+++ b/src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
@@ -24,6 +24,7 @@ import com.jetbrains.toolbox.api.core.diagnostics.Logger
 import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
 import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
 import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
 import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
 import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
 import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
@@ -102,6 +103,7 @@ class CoderRestClientTest {
         mockk<ToolboxUi>(),
         mockk<EnvironmentUiPageManager>(),
         mockk<EnvironmentStateColorPalette>(),
+        mockk<RemoteToolsHelper>(),
         mockk<ClientHelper>(),
         mockk<LocalDesktopManager>(),
         mockk<CoroutineScope>(),
diff --git a/src/test/kotlin/com/coder/toolbox/util/LinkHandlerTest.kt b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt
similarity index 59%
rename from src/test/kotlin/com/coder/toolbox/util/LinkHandlerTest.kt
rename to src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt
index bb87151..2914eae 100644
--- a/src/test/kotlin/com/coder/toolbox/util/LinkHandlerTest.kt
+++ b/src/test/kotlin/com/coder/toolbox/util/CoderProtocolHandlerTest.kt
@@ -1,41 +1,49 @@
 package com.coder.toolbox.util
 
+import com.coder.toolbox.CoderToolboxContext
 import com.coder.toolbox.sdk.DataGen
-import com.sun.net.httpserver.HttpHandler
-import com.sun.net.httpserver.HttpServer
-import java.net.HttpURLConnection
-import java.net.InetSocketAddress
+import com.coder.toolbox.settings.Environment
+import com.coder.toolbox.store.CoderSecretsStore
+import com.coder.toolbox.store.CoderSettingsStore
+import com.jetbrains.toolbox.api.core.diagnostics.Logger
+import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
+import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
+import com.jetbrains.toolbox.api.remoteDev.connection.ClientHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
+import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
+import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
+import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
+import com.jetbrains.toolbox.api.ui.ToolboxUi
+import io.mockk.mockk
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.runBlocking
 import java.util.UUID
 import kotlin.test.Test
-import kotlin.test.assertContains
 import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
-
-internal class LinkHandlerTest {
-    /**
-     * Create, start, and return a server that uses the provided handler.
-     */
-    private fun mockServer(handler: HttpHandler): Pair<HttpServer, String> {
-        val srv = HttpServer.create(InetSocketAddress(0), 0)
-        srv.createContext("/", handler)
-        srv.start()
-        return Pair(srv, "http://localhost:" + srv.address.port)
-    }
-
-    /**
-     * Create, start, and return a server that mocks redirects.
-     */
-    private fun mockRedirectServer(
-        location: String,
-        temp: Boolean,
-    ): Pair<HttpServer, String> = mockServer { exchange ->
-        exchange.responseHeaders.set("Location", location)
-        exchange.sendResponseHeaders(
-            if (temp) HttpURLConnection.HTTP_MOVED_TEMP else HttpURLConnection.HTTP_MOVED_PERM,
-            -1,
-        )
-        exchange.close()
-    }
+import kotlin.test.assertNull
+
+internal class CoderProtocolHandlerTest {
+    private val context = CoderToolboxContext(
+        mockk<ToolboxUi>(relaxed = true),
+        mockk<EnvironmentUiPageManager>(),
+        mockk<EnvironmentStateColorPalette>(),
+        mockk<RemoteToolsHelper>(),
+        mockk<ClientHelper>(),
+        mockk<LocalDesktopManager>(),
+        mockk<CoroutineScope>(),
+        mockk<Logger>(relaxed = true),
+        mockk<LocalizableStringFactory>(relaxed = true),
+        CoderSettingsStore(pluginTestSettingsStore(), Environment(), mockk<Logger>(relaxed = true)),
+        mockk<CoderSecretsStore>(),
+        mockk<ToolboxProxySettings>()
+    )
+
+    private val protocolHandler = CoderProtocolHandler(
+        context,
+        DialogUi(context),
+        MutableStateFlow(false)
+    )
 
     private val agents =
         mapOf(
@@ -49,7 +57,7 @@ internal class LinkHandlerTest {
         )
 
     @Test
-    fun getMatchingAgent() {
+    fun tstgetMatchingAgent() {
         val ws = DataGen.workspace("ws", agents = agents)
 
         val tests =
@@ -74,9 +82,10 @@ internal class LinkHandlerTest {
                     "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
                 ),
             )
-
-        tests.forEach {
-            assertEquals(UUID.fromString(it.second), getMatchingAgent(it.first, ws).id)
+        runBlocking {
+            tests.forEach {
+                assertEquals(UUID.fromString(it.second), protocolHandler.getMatchingAgent(it.first, ws)?.id)
+            }
         }
     }
 
@@ -104,14 +113,10 @@ internal class LinkHandlerTest {
                     "agent with ID",
                 ),
             )
-
-        tests.forEach {
-            val ex =
-                assertFailsWith(
-                    exceptionClass = it.second,
-                    block = { getMatchingAgent(it.first, ws).id },
-                )
-            assertContains(ex.message.toString(), it.third)
+        runBlocking {
+            tests.forEach {
+                assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
+            }
         }
     }
 
@@ -126,15 +131,16 @@ internal class LinkHandlerTest {
                 mapOf("agent" to null),
                 mapOf("agent_id" to null),
             )
-
-        tests.forEach {
-            assertEquals(
-                UUID.fromString("b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
-                getMatchingAgent(
-                    it,
-                    ws,
-                ).id,
-            )
+        runBlocking {
+            tests.forEach {
+                assertEquals(
+                    UUID.fromString("b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
+                    protocolHandler.getMatchingAgent(
+                        it,
+                        ws,
+                    )?.id,
+                )
+            }
         }
     }
 
@@ -149,14 +155,10 @@ internal class LinkHandlerTest {
                     "agent with ID"
                 ),
             )
-
-        tests.forEach {
-            val ex =
-                assertFailsWith(
-                    exceptionClass = it.second,
-                    block = { getMatchingAgent(it.first, ws).id },
-                )
-            assertContains(ex.message.toString(), it.third)
+        runBlocking {
+            tests.forEach {
+                assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
+            }
         }
     }
 
@@ -177,43 +179,10 @@ internal class LinkHandlerTest {
                     "has no agents"
                 ),
             )
-
-        tests.forEach {
-            val ex =
-                assertFailsWith(
-                    exceptionClass = it.second,
-                    block = { getMatchingAgent(it.first, ws).id },
-                )
-            assertContains(ex.message.toString(), it.third)
-        }
-    }
-
-    @Test
-    fun followsRedirects() {
-        val (srv1, url1) =
-            mockServer { exchange ->
-                exchange.sendResponseHeaders(HttpURLConnection.HTTP_OK, -1)
-                exchange.close()
+        runBlocking {
+            tests.forEach {
+                assertNull(protocolHandler.getMatchingAgent(it.first, ws)?.id)
             }
-        val (srv2, url2) = mockRedirectServer(url1, false)
-        val (srv3, url3) = mockRedirectServer(url2, true)
-
-        assertEquals(url1.toURL(), resolveRedirects(java.net.URL(url3)))
-
-        srv1.stop(0)
-        srv2.stop(0)
-        srv3.stop(0)
-    }
-
-    @Test
-    fun followsMaximumRedirects() {
-        val (srv, url) = mockRedirectServer(".", true)
-
-        assertFailsWith(
-            exceptionClass = Exception::class,
-            block = { resolveRedirects(java.net.URL(url)) },
-        )
-
-        srv.stop(0)
+        }
     }
 }