diff --git a/CHANGELOG.md b/CHANGELOG.md index 104c217..9cb69ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ writing to `~/.ssh/config` - the header command falls back to the `CODER_HEADER_COMMAND` environment variable when the setting is blank, matching the Coder CLI and the VS Code extension +### Fixed + +- pass the workspace agent's operating system to Toolbox before it deploys the remote agent + ## 0.9.3 - 2026-08-11 ### Changed @@ -21,8 +25,8 @@ - keep workspace polling alive when SSH config updates fail - improved validation and handling of workspace connection data -- expand `~` and `$HOME` in the SSH log directory and network info directory settings, consistent with how the data - and binary directories are already resolved +- expand `~` and `$HOME` in the SSH log directory and network info directory settings, consistent with how the data and + binary directories are already resolved ### Changed @@ -58,8 +62,8 @@ - skip the Coder TLS alternate hostname when fetching IDE metadata from JetBrains - notifications are now persistent popups instead of snackbars, so they survive a hidden window and no longer get dropped -- workspace lists now default to `My workspaces`, so users initially see only workspaces they own. Users can - switch to `All workspaces`, and that selection is persisted per Coder deployment hostname. +- workspace lists now default to `My workspaces`, so users initially see only workspaces they own. Users can switch to + `All workspaces`, and that selection is persisted per Coder deployment hostname. ## 0.9.0 - 2026-05-14 @@ -282,8 +286,8 @@ ### Changed -- the plugin will now remember the SSH connection state for each workspace, and it will try to automatically - establish it after an expired token was refreshed. +- the plugin will now remember the SSH connection state for each workspace, and it will try to automatically establish + it after an expired token was refreshed. ### Fixed diff --git a/gradle.properties b/gradle.properties index 32d4626..98f923f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.9.3 +version=0.9.4 group=com.coder.toolbox name=coder-toolbox \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 2810535..e43ff0b 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -22,10 +22,7 @@ import com.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook import com.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment -import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentSettings -import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView -import com.jetbrains.toolbox.api.remoteDev.environments.WithDeploymentSettings import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription @@ -52,11 +49,11 @@ private val POLL_INTERVAL = 5.seconds private fun environmentId(workspace: Workspace, agent: WorkspaceAgent?): String = agent?.let { "${workspace.name}.${it.name}" } ?: workspace.name -private fun OS?.toDeploymentTarget(): DeploymentTarget = when (this) { - OS.LINUX -> DeploymentTarget.LINUX - OS.MAC -> DeploymentTarget.MACOS - OS.WINDOWS -> DeploymentTarget.WINDOWS - else -> DeploymentTarget.AUTO +private fun OS?.displayName(): String = when (this) { + OS.LINUX -> "Linux" + OS.WINDOWS -> "Windows" + OS.MAC -> "macOS" + null -> "unknown-OS" } /** @@ -71,8 +68,7 @@ class CoderRemoteEnvironment( private val workspaceRefreshTrigger: Channel, private var workspace: Workspace, private var agent: WorkspaceAgent?, -) : RemoteProviderEnvironment(environmentId(workspace, agent)), BeforeConnectionHook, AfterDisconnectHook, - WithDeploymentSettings { +) : RemoteProviderEnvironment(environmentId(workspace, agent)), BeforeConnectionHook, AfterDisconnectHook { private var environmentStatus = WorkspaceAndAgentStatus.from(workspace, agent) override var name: String = environmentId(workspace, agent) @@ -86,13 +82,6 @@ class CoderRemoteEnvironment( override val additionalEnvironmentInformation: MutableMap = mutableMapOf() override val actionsList: MutableStateFlow> = MutableStateFlow(emptyList()) - /** - * Derives the deployment target from the agent's reported OS, falling back to - * AUTO (automatic remote OS detection) when the agent or its OS is unknown. - */ - override val deploymentSettings: DeploymentSettings - get() = DeploymentSettings.Default.copy(deploymentTarget = agent?.operatingSystem.toDeploymentTarget()) - private val networkMetricsMarshaller = Moshi.Builder().build().adapter(NetworkMetrics::class.java) private val proxyCommandHandle = SshCommandProcessHandle(context) private var pollJob: Job? = null @@ -236,7 +225,9 @@ class CoderRemoteEnvironment( if (agent == null) { return } - context.logger.info("Connecting to $id...") + context.logger.info( + "Launching SSH connection to $id on a ${agent?.operatingSystem.displayName()} machine" + ) isConnected.update { true } context.settingsStore.updateAutoConnect(this.id, true) pollJob = pollNetworkMetrics() @@ -378,7 +369,6 @@ class CoderRemoteEnvironment( connectionRequest.update { true } - context.logger.info("Workspace status is ready and there is no existing connection, resuming SSH connection to $id") } } diff --git a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt index 4978db0..0e0d3e5 100644 --- a/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt +++ b/src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt @@ -5,11 +5,21 @@ import com.coder.toolbox.cli.CoderCLIManager import com.coder.toolbox.cli.WorkspaceAddress import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent +import com.coder.toolbox.util.OS +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentSettings +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import com.jetbrains.toolbox.api.remoteDev.environments.SshEnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.ssh.SshConnectionInfo import java.net.URL import kotlin.time.Duration.Companion.seconds +private fun OS?.toDeploymentTarget(): DeploymentTarget = when (this) { + OS.LINUX -> DeploymentTarget.LINUX + OS.MAC -> DeploymentTarget.MACOS + OS.WINDOWS -> DeploymentTarget.WINDOWS + else -> DeploymentTarget.AUTO +} + /** * A view for a single environment. It displays the projects and IDEs. * @@ -25,6 +35,14 @@ class EnvironmentView( private val workspace: Workspace, private val agent: WorkspaceAgent, ) : SshEnvironmentContentsView { + + /** + * Derives the deployment target from the agent's reported OS, falling back to + * AUTO (automatic remote OS detection) when its OS is unknown. + */ + override val deploymentSettings: DeploymentSettings + get() = DeploymentSettings.Default.copy(deploymentTarget = agent.operatingSystem.toDeploymentTarget()) + override suspend fun getConnectionInfo(): SshConnectionInfo = WorkspaceSshConnectionInfo(context, url, cli, workspace, agent) } diff --git a/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt b/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt index 397a9c7..9ead00b 100644 --- a/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt +++ b/src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt @@ -6,6 +6,8 @@ import com.coder.toolbox.cli.WorkspaceAddress import com.coder.toolbox.sdk.v2.models.Workspace import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.store.CoderSettingsStore +import com.coder.toolbox.util.OS +import com.jetbrains.toolbox.api.remoteDev.deploy.DeploymentTarget import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -14,6 +16,30 @@ import kotlin.test.Test import kotlin.test.assertEquals class EnvironmentViewTest { + @Test + fun `deployment target follows the workspace agent operating system`() { + val context = mockk(relaxed = true) + val cli = mockk() + val workspace = mockk() + val url = URL("https://coder.example.com") + val cases = listOf( + OS.LINUX to DeploymentTarget.LINUX, + OS.MAC to DeploymentTarget.MACOS, + OS.WINDOWS to DeploymentTarget.WINDOWS, + null to DeploymentTarget.AUTO, + ) + + cases.forEach { (operatingSystem, expectedTarget) -> + val agent = mockk { + every { this@mockk.operatingSystem } returns operatingSystem + } + + val deploymentSettings = EnvironmentView(context, url, cli, workspace, agent).deploymentSettings + + assertEquals(expectedTarget, deploymentSettings.deploymentTarget) + } + } + @Test fun `connection info passes the configured SSH config path to Toolbox`() = runBlocking { val context = mockk(relaxed = true)