Skip to content

Commit 1a2212b

Browse files
authored
impl: support for Toolbox 2.7 (#135)
Things that were changed or added to TBX 2.7: **Support for Proxy Authentication** The new API introduced in TBX 2.7, and HTTP proxy authentication works flawlessly. However, SOCKS5 proxy authentication does not appear to be properly supported in the current TBX implementation. While users can configure a SOCKS5 proxy with basic authentication, Toolbox fails to authenticate successfully. Coder uses OkHttp as the HTTP client, which in turn delegates SOCKS5 authentication to the JVM (java.net.SocksSocketImpl). We can configure a java.net.Authenticator with the credentials exposed by the new TBX API. However, since the Authenticator is set globally, doing so would affect all plugins — including TBX itself — which may not be desirable. **Customizable messages while loading the workspaces** The new TBX 2.7 API allows us to change the message displayed while loading the workspaces from "Loading environments" to "Loading workspaces" **UI pages with customizable titles** **Support for custom aliases in the URI handling protocol**
1 parent dbf40e6 commit 1a2212b

16 files changed

+93
-90
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- support for basic authentication for HTTP/HTTPS proxy
8+
- support for Toolbox 2.7 release
9+
10+
### Changed
11+
12+
- improved message while loading the workspace
13+
14+
### Fixed
15+
16+
- URI protocol handler is now able to switch to the Coder provider even if the last opened provider was something else
17+
518
## 0.3.2 - 2025-06-25
619

720
### Changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ jetbrains://gateway/com.coder.toolbox
8282
&folder=<absolute-path-to-a-project-folder>
8383
```
8484

85+
Starting from Toolbox 2.7, you can use `coder` as a shortcut in place of the full plugin ID. The URI can be simplified as:
86+
```text
87+
jetbrains://gateway/coder?url=http(s)://<your-coder-deployment>
88+
```
89+
8590
| Query param | Description | Mandatory |
8691
|------------------|------------------------------------------------------------------------------|-----------|
8792
| url | Your Coder deployment URL (encoded) | Yes |
@@ -104,7 +109,7 @@ experience, it’s recommended to ensure the workspace is running prior to initi
104109

105110
## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy
106111

107-
This section explains how to set up a local proxy (without authentication which is not yet supported) and verify that
112+
This section explains how to set up a local proxy and verify that
108113
the plugin’s REST client works correctly when routed through it.
109114

110115
We’ll use [mitmproxy](https://mitmproxy.org/) for this — it can act as both an HTTP and SOCKS5 proxy with SSL
@@ -127,6 +132,12 @@ mitmproxy can do HTTP and SOCKS5 proxying. To configure one or the other:
127132
2. Navigate to `Options -> Edit Options`
128133
3. Update the `Mode` field to `regular` in order to activate HTTP/HTTPS or to `socks5`
129134
4. Proxy authentication can be enabled by updating the `proxyauth` to `username:password`
135+
5. Alternatively you can run the following commands:
136+
137+
```bash
138+
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode regular --proxyauth proxyUsername:proxyPassword
139+
mitmweb --ssl-insecure --set stream_large_bodies="10m" --mode socks5
140+
```
130141

131142
### Configure Proxy in Toolbox
132143

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

151+
> [!NOTE]
152+
> Coder Toolbox plugin handles only HTTP/HTTPS proxy authentication.
153+
> SOCKS5 proxy authentication is currently not supported due to limitations
154+
> described in: https://youtrack.jetbrains.com/issue/TBX-14532/Missing-proxy-authentication-settings#focus=Comments-27-12265861.0-0
155+
140156
## Debugging and Reporting issues
141157

142158
Enabling debug logging is essential for diagnosing issues with the Toolbox plugin, especially when SSH

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.3.2
1+
version=0.4.0
22
group=com.coder.toolbox
33
name=coder-toolbox

gradle/libs.versions.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
[versions]
2-
toolbox-plugin-api = "1.1.41749"
3-
kotlin = "2.1.10"
4-
coroutines = "1.10.1"
5-
serialization = "1.8.0"
2+
toolbox-plugin-api = "1.3.47293"
3+
kotlin = "2.1.20"
4+
coroutines = "1.10.2"
5+
serialization = "1.8.1"
66
okhttp = "4.12.0"
77
dependency-license-report = "2.9"
88
marketplace-client = "2.0.46"
99
gradle-wrapper = "0.14.0"
1010
exec = "1.12"
1111
moshi = "1.15.2"
12-
ksp = "2.1.10-1.0.31"
12+
ksp = "2.1.20-2.0.1"
1313
retrofit = "3.0.0"
1414
changelog = "2.2.1"
1515
gettext = "0.7.0"

src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,6 @@ class CoderRemoteEnvironment(
157157

158158
override fun beforeConnection() {
159159
context.logger.info("Connecting to $id...")
160-
context.cs.launch {
161-
state.update {
162-
wsRawStatus.toSshConnectingEnvState(context)
163-
}
164-
}
165160
isConnected.update { true }
166161
pollJob = pollNetworkMetrics()
167162
}

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.ex.APIResponseException
77
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
88
import com.coder.toolbox.util.CoderProtocolHandler
99
import com.coder.toolbox.util.DialogUi
10+
import com.coder.toolbox.util.waitForTrue
1011
import com.coder.toolbox.util.withPath
1112
import com.coder.toolbox.views.Action
1213
import com.coder.toolbox.views.CoderCliSetupWizardPage
@@ -63,9 +64,10 @@ class CoderRemoteProvider(
6364
// On the first load, automatically log in if we can.
6465
private var firstRun = true
6566
private val isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false)
66-
private var coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(context.deploymentUrl.toString()))
67+
private val coderHeaderPage = NewEnvironmentPage(context.i18n.pnotr(context.deploymentUrl.toString()))
6768
private val linkHandler = CoderProtocolHandler(context, dialogUi, isInitialized)
6869

70+
override val loadingEnvironmentsDescription: LocalizableString = context.i18n.ptrl("Loading workspaces...")
6971
override val environments: MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>> = MutableStateFlow(
7072
LoadableState.Loading
7173
)
@@ -167,7 +169,7 @@ class CoderRemoteProvider(
167169
close()
168170
// force auto-login
169171
firstRun = true
170-
goToEnvironmentsPage()
172+
context.envPageManager.showPluginEnvironmentsPage()
171173
break
172174
}
173175
}
@@ -315,27 +317,19 @@ class CoderRemoteProvider(
315317
) { restClient, cli ->
316318
// stop polling and de-initialize resources
317319
close()
320+
isInitialized.update {
321+
false
322+
}
318323
// start initialization with the new settings
319324
this@CoderRemoteProvider.client = restClient
320-
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(restClient.url.toString()))
325+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
321326

322327
environments.showLoadingMessage()
323328
pollJob = poll(restClient, cli)
329+
isInitialized.waitForTrue()
324330
}
325331
}
326332

327-
/**
328-
* Make Toolbox ask for the page again. Use any time we need to change the
329-
* root page (for example, sign-in or the environment list).
330-
*
331-
* When moving between related pages, instead use ui.showUiPage() and
332-
* ui.hideUiPage() which stacks and has built-in back navigation, rather
333-
* than using multiple root pages.
334-
*/
335-
private fun goToEnvironmentsPage() {
336-
context.envPageManager.showPluginEnvironmentsPage()
337-
}
338-
339333
/**
340334
* Return the sign-in page if we do not have a valid client.
341335
@@ -377,7 +371,7 @@ class CoderRemoteProvider(
377371

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

380-
private suspend fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
374+
private fun onConnect(client: CoderRestClient, cli: CoderCLIManager) {
381375
// Store the URL and token for use next time.
382376
context.secrets.lastDeploymentURL = client.url.toString()
383377
context.secrets.lastToken = client.token ?: ""
@@ -387,9 +381,9 @@ class CoderRemoteProvider(
387381
this.client = client
388382
pollJob?.cancel()
389383
environments.showLoadingMessage()
390-
coderHeaderPage = NewEnvironmentPage(context, context.i18n.pnotr(client.url.toString()))
384+
coderHeaderPage.setTitle(context.i18n.pnotr(client.url.toString()))
391385
pollJob = poll(client, cli)
392-
context.refreshMainPage()
386+
context.envPageManager.showPluginEnvironmentsPage()
393387
}
394388

395389
private fun MutableStateFlow<LoadableState<List<CoderRemoteEnvironment>>>.showLoadingMessage() {

src/main/kotlin/com/coder/toolbox/CoderToolboxContext.kt

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.coder.toolbox
33
import com.coder.toolbox.store.CoderSecretsStore
44
import com.coder.toolbox.store.CoderSettingsStore
55
import com.coder.toolbox.util.toURL
6-
import com.coder.toolbox.views.CoderPage
76
import com.jetbrains.toolbox.api.core.diagnostics.Logger
87
import com.jetbrains.toolbox.api.core.os.LocalDesktopManager
98
import com.jetbrains.toolbox.api.localization.LocalizableStringFactory
@@ -14,10 +13,8 @@ import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentStateColorPalette
1413
import com.jetbrains.toolbox.api.remoteDev.ui.EnvironmentUiPageManager
1514
import com.jetbrains.toolbox.api.ui.ToolboxUi
1615
import kotlinx.coroutines.CoroutineScope
17-
import kotlinx.coroutines.delay
1816
import java.net.URL
1917
import java.util.UUID
20-
import kotlin.time.Duration.Companion.milliseconds
2118

2219
@Suppress("UnstableApiUsage")
2320
data class CoderToolboxContext(
@@ -91,26 +88,4 @@ data class CoderToolboxContext(
9188
i18n.ptrl("OK")
9289
)
9390
}
94-
95-
/**
96-
* Forces the title bar on the main page to be refreshed
97-
*/
98-
suspend fun refreshMainPage() {
99-
// the url/title on the main page is only refreshed if
100-
// we're navigating to the main env page from another page.
101-
// If TBX is already on the main page the title is not refreshed
102-
// hence we force a navigation from a blank page.
103-
ui.showUiPage(CoderPage.emptyPage(this))
104-
105-
106-
// Toolbox uses an internal shared flow with a buffer of 4 items and a DROP_OLDEST strategy.
107-
// Both showUiPage and showPluginEnvironmentsPage send events to this flow.
108-
// If we emit two events back-to-back, the first one often gets dropped and only the second is shown.
109-
// To reduce this risk, we add a small delay to let the UI coroutine process the first event.
110-
// Simply yielding the coroutine isn't reliable, especially right after Toolbox starts via URI handling.
111-
// Based on my testing, a 5–10 ms delay is enough to ensure the blank page is processed,
112-
// while still short enough to be invisible to users.
113-
delay(10.milliseconds)
114-
envPageManager.showPluginEnvironmentsPage()
115-
}
11691
}

src/main/kotlin/com/coder/toolbox/models/WorkspaceAndAgentStatus.kt

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,10 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
6565
return CustomRemoteEnvironmentStateV2(
6666
context.i18n.pnotr(label),
6767
color = getStateColor(context),
68-
reachable = ready() || unhealthy(),
68+
isReachable = ready() || unhealthy(),
6969
// TODO@JB: How does this work? Would like a spinner for pending states.
70-
icon = getStateIcon()
70+
iconId = getStateIcon().id,
71+
isPriorityShow = true
7172
)
7273
}
7374

@@ -90,16 +91,6 @@ enum class WorkspaceAndAgentStatus(val label: String, val description: String) {
9091
else EnvironmentStateIcons.NoIcon
9192
}
9293

93-
fun toSshConnectingEnvState(context: CoderToolboxContext): CustomRemoteEnvironmentStateV2 {
94-
val existingState = toRemoteEnvironmentState(context)
95-
return CustomRemoteEnvironmentStateV2(
96-
context.i18n.pnotr("SSHing"),
97-
existingState.color,
98-
existingState.isReachable,
99-
EnvironmentStateIcons.Connecting
100-
)
101-
}
102-
10394
/**
10495
* Return true if the agent is in a connectable state.
10596
*/

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import com.coder.toolbox.util.coderTrustManagers
2424
import com.coder.toolbox.util.getArch
2525
import com.coder.toolbox.util.getHeaders
2626
import com.coder.toolbox.util.getOS
27+
import com.jetbrains.toolbox.api.remoteDev.connection.ProxyAuth
2728
import com.squareup.moshi.Moshi
29+
import okhttp3.Credentials
2830
import okhttp3.OkHttpClient
2931
import retrofit2.Response
3032
import retrofit2.Retrofit
@@ -78,18 +80,19 @@ open class CoderRestClient(
7880
builder.proxySelector(context.proxySettings.getProxySelector()!!)
7981
}
8082

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

9497
if (token != null) {
9598
builder = builder.addInterceptor {

src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ open class CoderProtocolHandler(
5454
context.logAndShowInfo("URI will not be handled", "No query parameters were provided")
5555
return
5656
}
57-
57+
// this switches to the main plugin screen, even
58+
// if last opened provider was not Coder
59+
context.envPageManager.showPluginEnvironmentsPage()
60+
markAsBusy()
5861
if (shouldWaitForAutoLogin) {
5962
isInitialized.waitForTrue()
6063
}
@@ -67,12 +70,11 @@ open class CoderProtocolHandler(
6770
val workspace = restClient.workspaces().matchName(workspaceName, deploymentURL) ?: return
6871

6972
val cli = configureCli(deploymentURL, restClient)
70-
reInitialize(restClient, cli)
7173

7274
var agent: WorkspaceAgent
7375
try {
74-
markAsBusy()
75-
context.refreshMainPage()
76+
reInitialize(restClient, cli)
77+
context.envPageManager.showPluginEnvironmentsPage()
7678
if (!prepareWorkspace(workspace, restClient, workspaceName, deploymentURL)) return
7779
// we resolve the agent after the workspace is started otherwise we can get misleading
7880
// errors like: no agent available while workspace is starting or stopping
@@ -86,6 +88,7 @@ open class CoderProtocolHandler(
8688
} finally {
8789
unmarkAsBusy()
8890
}
91+
delay(2.seconds)
8992
val environmentId = "${workspace.name}.${agent.name}"
9093
context.showEnvironmentPage(environmentId)
9194

src/main/kotlin/com/coder/toolbox/views/CoderCliSetupWizardPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class CoderCliSetupWizardPage(
2525
client: CoderRestClient,
2626
cli: CoderCLIManager,
2727
) -> Unit,
28-
) : CoderPage(context.i18n.ptrl("Setting up Coder"), false) {
28+
) : CoderPage(MutableStateFlow(context.i18n.ptrl("Setting up Coder")), false) {
2929
private val shouldAutoSetup = MutableStateFlow(initialAutoSetup)
3030
private val settingsAction = Action(context.i18n.ptrl("Settings"), actionBlock = {
3131
context.ui.showUiPage(settingsPage)

src/main/kotlin/com/coder/toolbox/views/CoderPage.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.jetbrains.toolbox.api.localization.LocalizableString
77
import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription
88
import com.jetbrains.toolbox.api.ui.components.UiPage
99
import kotlinx.coroutines.flow.MutableStateFlow
10+
import kotlinx.coroutines.flow.update
1011

1112
/**
1213
* Base page that handles the icon, displaying error notifications, and
@@ -19,9 +20,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
1920
* to use the mouse.
2021
*/
2122
abstract class CoderPage(
22-
title: LocalizableString,
23+
private val titleObservable: MutableStateFlow<LocalizableString>,
2324
showIcon: Boolean = true,
24-
) : UiPage(title) {
25+
) : UiPage(titleObservable) {
26+
27+
fun setTitle(title: LocalizableString) {
28+
titleObservable.update {
29+
title
30+
}
31+
}
2532

2633
/**
2734
* Return the icon, if showing one.

src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import kotlinx.coroutines.launch
2020
* I have not been able to test this page.
2121
*/
2222
class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
23-
CoderPage(context.i18n.ptrl("Coder Settings"), false) {
23+
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
2424
private val settings = context.settingsStore.readOnly()
2525

2626
// TODO: Copy over the descriptions, holding until I can test this page.

0 commit comments

Comments
 (0)