Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
version=0.9.3
version=0.9.4
group=com.coder.toolbox
name=coder-toolbox
28 changes: 9 additions & 19 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
}

/**
Expand All @@ -71,8 +68,7 @@ class CoderRemoteEnvironment(
private val workspaceRefreshTrigger: Channel<Boolean>,
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)
Expand All @@ -86,13 +82,6 @@ class CoderRemoteEnvironment(
override val additionalEnvironmentInformation: MutableMap<LocalizableString, String> = mutableMapOf()
override val actionsList: MutableStateFlow<List<ActionDescription>> = 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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
}
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/kotlin/com/coder/toolbox/views/EnvironmentView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions src/test/kotlin/com/coder/toolbox/views/EnvironmentViewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CoderToolboxContext>(relaxed = true)
val cli = mockk<CoderCLIManager>()
val workspace = mockk<Workspace>()
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<WorkspaceAgent> {
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<CoderToolboxContext>(relaxed = true)
Expand Down
Loading