Skip to content

impl: support for Toolbox 2.7 #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

### Added

- support for basic authentication for HTTP/HTTPS proxy
- support for Toolbox 2.7 release

### Changed

- improved message while loading the workspace

## 0.3.2 - 2025-06-25

### Changed
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ 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`
> [!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.

Expand All @@ -110,7 +111,7 @@ experience, it’s recommended to ensure the workspace is running prior to initi

## 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
This section explains how to set up a local proxy and verify that
the plugin’s REST client works correctly when routed through it.

We’ll use [mitmproxy](https://mitmproxy.org/) for this — it can act as both an HTTP and SOCKS5 proxy with SSL
Expand All @@ -134,6 +135,12 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
2. Navigate to `Options -> Edit Options`
3. Update the `Mode` field to `regular` in order to activate HTTP/HTTPS or to `socks5`
4. Proxy authentication can be enabled by updating the `proxyauth` to `username:password`
5. Alternatively you can run the following commands:

```bash
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode regular --proxyauth proxyUsername:proxyPassword
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5
```

### Configure Proxy in Toolbox

Expand All @@ -144,6 +151,11 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
5. Before authenticating to the Coder deployment we need to tell the plugin where can we find mitmproxy
certificates. In Coder's Settings page, set the `TLS CA path` to `~/.mitmproxy/mitmproxy-ca-cert.pem`

> [!NOTE]
> Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.
> SOCKS5 proxy authentication is currently not supported due to limitations
> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0

## Debugging and Reporting issues

Enabling debug logging is essential for diagnosing issues with the Toolbox plugin, especially when SSH
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.3.2
version=0.4.0
group=com.coder.toolbox
name=coder-toolbox
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
[versions]
toolbox-plugin-api = "1.1.41749"
kotlin = "2.1.10"
coroutines = "1.10.1"
serialization = "1.8.0"
toolbox-plugin-api = "1.3.47293"
kotlin = "2.1.20"
coroutines = "1.10.2"
serialization = "1.8.1"
okhttp = "4.12.0"
dependency-license-report = "2.9"
marketplace-client = "2.0.46"
gradle-wrapper = "0.14.0"
exec = "1.12"
moshi = "1.15.2"
ksp = "2.1.10-1.0.31"
ksp = "2.1.20-2.0.1"
retrofit = "3.0.0"
changelog = "2.2.1"
gettext = "0.7.0"
Expand Down
30 changes: 12 additions & 18 deletions src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
import com.coder.toolbox.util.CoderProtocolHandler
import com.coder.toolbox.util.DialogUi
import com.coder.toolbox.util.waitForTrue
import com.coder.toolbox.util.withPath
import com.coder.toolbox.views.Action
import com.coder.toolbox.views.CoderCliSetupWizardPage
Expand Down Expand Up @@ -63,9 +64,10 @@ class CoderRemoteProvider(
// On the first load, automatically log in if we can.
private var firstRun = true
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl.toString()))
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)

override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
LoadableState.Loading
)
Expand Down Expand Up @@ -167,7 +169,7 @@ class CoderRemoteProvider(
close()
// force auto-login
firstRun = true
goToEnvironmentsPage()
context.envPageManager.showPluginEnvironmentsPage()
break
}
}
Expand Down Expand Up @@ -315,27 +317,19 @@ class CoderRemoteProvider(
) { restClient, cli ->
// stop polling and de-initialize resources
close()
isInitialized.update {
false
}
// start initialization with the new settings
this@CoderRemoteProvider.client = restClient
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString()))
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))

environments.showLoadingMessage()
pollJob = poll(restClient, cli)
isInitialized.waitForTrue()
}
}

/**
* Make Toolbox ask for the page again. Use any time we need to change the
* root page (for example, sign-in or the environment list).
*
* When moving between related pages, instead use ui.showUiPage() and
* ui.hideUiPage() which stacks and has built-in back navigation, rather
* than using multiple root pages.
*/
private fun goToEnvironmentsPage() {
context.envPageManager.showPluginEnvironmentsPage()
}

/**
* Return the sign-in page if we do not have a valid client.

Expand Down Expand Up @@ -377,7 +371,7 @@ class CoderRemoteProvider(

private fun shouldDoAutoSetup(): Boolean = firstRun && context.secrets.rememberMe == true

private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
// Store the URL and token for use next time.
context.secrets.lastDeploymentURL = client.url.toString()
context.secrets.lastToken = client.token ?: ""
Expand All @@ -387,9 +381,9 @@ class CoderRemoteProvider(
this.client = client
pollJob?.cancel()
environments.showLoadingMessage()
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(client.url.toString()))
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
pollJob = poll(client, cli)
context.refreshMainPage()
context.envPageManager.showPluginEnvironmentsPage()
}

private fun MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>>.showLoadingMessage() {
Expand Down
25 changes: 0 additions & 25 deletions src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.coder.toolbox
import com.coder.toolbox.store.CoderSecretsStore
import com.coder.toolbox.store.CoderSettingsStore
import com.coder.toolbox.util.toURL
import com.coder.toolbox.views.CoderPage
import com.jetbrains.toolbox.api.core.diagnostics.Logger
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
Expand All @@ -14,10 +13,8 @@ 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 kotlinx.coroutines.delay
import java.net.URL
import java.util.UUID
import kotlin.time.Duration.Companion.milliseconds

@Suppress("UnstableApiUsage")
data class CoderToolboxContext(
Expand Down Expand Up @@ -91,26 +88,4 @@ data class CoderToolboxContext(
i18n.ptrl("OK")
)
}

/**
* Forces the title bar on the main page to be refreshed
*/
suspend fun refreshMainPage() {
// the url/title on the main page is only refreshed if
// we're navigating to the main env page from another page.
// If TBX is already on the main page the title is not refreshed
// hence we force a navigation from a blank page.
ui.showUiPage(CoderPage.emptyPage(this))


// Toolbox uses an internal shared flow with a buffer of 4 items and a DROP_OLDEST strategy.
// Both showUiPage and showPluginEnvironmentsPage send events to this flow.
// If we emit two events back-to-back, the first one often gets dropped and only the second is shown.
// To reduce this risk, we add a small delay to let the UI coroutine process the first event.
// Simply yielding the coroutine isn't reliable, especially right after Toolbox starts via URI handling.
// Based on my testing, a 5–10 ms delay is enough to ensure the blank page is processed,
// while still short enough to be invisible to users.
delay(10.milliseconds)
envPageManager.showPluginEnvironmentsPage()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
return CustomRemoteEnvironmentStateV2(
context.i18n.pnotr(label),
color = getStateColor(context),
reachable = ready() || unhealthy(),
isReachable = ready() || unhealthy(),
// TODO@JB: How does this work? Would like a spinner for pending states.
icon = getStateIcon()
iconId = getStateIcon().id,
isPriorityShow = true
)
}

Expand Down
27 changes: 15 additions & 12 deletions src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import com.coder.toolbox.util.coderTrustManagers
import com.coder.toolbox.util.getArch
import com.coder.toolbox.util.getHeaders
import com.coder.toolbox.util.getOS
import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
import com.squareup.moshi.Moshi
import okhttp3.Credentials
import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
Expand Down Expand Up @@ -78,18 +80,19 @@ open class CoderRestClient(
builder.proxySelector(context.proxySettings.getProxySelector()!!)
}

//TODO - add support for proxy auth. when Toolbox exposes them
// builder.proxyAuthenticator { _, response ->
// if (proxyValues.useAuth && proxyValues.username != null && proxyValues.password != null) {
// val credentials = Credentials.basic(proxyValues.username, proxyValues.password)
// response.request.newBuilder()
// .header("Proxy-Authorization", credentials)
// .build()
// } else {
// null
// }
// }
// }
// Note: This handles only HTTP/HTTPS proxy authentication.
// SOCKS5 proxy authentication is currently not supported due to limitations described in:
// https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
builder.proxyAuthenticator { _, response ->
val proxyAuth = context.proxySettings.getProxyAuth()
if (proxyAuth == null || proxyAuth !is ProxyAuth.Basic) {
return@proxyAuthenticator null
}
val credentials = Credentials.basic(proxyAuth.username, proxyAuth.password)
response.request.newBuilder()
.header("Proxy-Authorization", credentials)
.build()
}

if (token != null) {
builder = builder.addInterceptor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ open class CoderProtocolHandler(
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return

val cli = configureCli(deploymentURL, restClient)
reInitialize(restClient, cli)

var agent: WorkspaceAgent
try {
markAsBusy()
context.refreshMainPage()
reInitialize(restClient, cli)
context.envPageManager.showPluginEnvironmentsPage()
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
Expand All @@ -86,6 +86,7 @@ open class CoderProtocolHandler(
} finally {
unmarkAsBusy()
}
delay(2.seconds)
val environmentId = "${workspace.name}.${agent.name}"
context.showEnvironmentPage(environmentId)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class CoderCliSetupWizardPage(
client: CoderRestClient,
cli: CoderCLIManager,
) -> Unit,
) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) {
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
context.ui.showUiPage(settingsPage)
Expand Down
11 changes: 9 additions & 2 deletions src/main/kotlin/com/coder/toolbox/views/CoderPage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
import com.jetbrains.toolbox.api.ui.components.UiPage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

/**
* Base page that handles the icon, displaying error notifications, and
Expand All @@ -19,9 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
* to use the mouse.
*/
abstract class CoderPage(
title: LocalizableString,
private val titleObservable: MutableStateFlow<LocalizableString>,
showIcon: Boolean = true,
) : UiPage(title) {
) : UiPage(titleObservable) {

fun setTitle(title: LocalizableString) {
titleObservable.update {
title
}
}

/**
* Return the icon, if showing one.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
* I have not been able to test this page.
*/
class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
CoderPage(context.i18n.ptrl("Coder Settings"), false) {
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
private val settings = context.settingsStore.readOnly()

// TODO: Copy over the descriptions, holding until I can test this page.
Expand Down
5 changes: 2 additions & 3 deletions src/main/kotlin/com/coder/toolbox/views/NewEnvironmentPage.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.coder.toolbox.views

import com.coder.toolbox.CoderToolboxContext
import com.jetbrains.toolbox.api.localization.LocalizableString
import com.jetbrains.toolbox.api.ui.components.UiField
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -14,7 +13,7 @@ import kotlinx.coroutines.flow.StateFlow
* For now we just use this to display the deployment URL since we do not
* support creating environments from the plugin.
*/
class NewEnvironmentPage(context: CoderToolboxContext, deploymentURL: LocalizableString) :
CoderPage(deploymentURL) {
class NewEnvironmentPage(deploymentURL: LocalizableString) :
CoderPage(MutableStateFlow(deploymentURL)) {
override val fields: StateFlow<List<UiField>> = MutableStateFlow(emptyList())
}
3 changes: 3 additions & 0 deletions src/main/resources/localization/defaultMessages.po
Original file line number Diff line number Diff line change
Expand Up @@ -146,4 +146,7 @@ msgid "Error encountered while setting up Coder"
msgstr ""

msgid "Setting up Coder"
msgstr ""

msgid "Loading workspaces..."
msgstr ""
4 changes: 4 additions & 0 deletions src/test/kotlin/com/coder/toolbox/sdk/CoderRestClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.ProxyAuth
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
import com.jetbrains.toolbox.api.remoteDev.connection.ToolboxProxySettings
import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
Expand Down Expand Up @@ -114,6 +115,8 @@ class CoderRestClientTest {
object : ToolboxProxySettings {
override fun getProxy(): Proxy? = null
override fun getProxySelector(): ProxySelector? = null
override fun getProxyAuth(): ProxyAuth? = null

override fun addProxyChangeListener(listener: Runnable) {
}

Expand Down Expand Up @@ -579,6 +582,7 @@ class CoderRestClientTest {
}
}

override fun getProxyAuth(): ProxyAuth? = null
override fun addProxyChangeListener(listener: Runnable) {
}

Expand Down
Loading