Skip to content

Commit f4710e2

Browse files
authored
doc: initial guide to the plugin (#78)
The following topics are covered: - documentation on how to install the plugin - URI handling documentation - release procedure - resolves #64 This PR also includes: - improved Error Reporting for Unresolved Agents, now it shows a clear, human-readable error message instead - a rename of `project_path` query param to `folder` - and a fix to no longer ask for coder deployment details when user wants to install the plugin via URI. P.S: Some of the topics were copied without shame from the old Coder Gateway plugin.
1 parent e313f45 commit f4710e2

File tree

7 files changed

+163
-57
lines changed

7 files changed

+163
-57
lines changed

Diff for: CHANGELOG.md

+2-4
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
### Fixed
66

77
- SSH connection to a Workspace is no longer established only once
8+
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder deployment
89

910
### Changed
1011

1112
- action buttons on the token input step were swapped to achieve better keyboard navigation
12-
13-
### Fixed
14-
15-
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder deployment
13+
- URI `project_path` query parameter was renamed to `folder`
1614

1715
## 0.1.3 - 2025-04-09
1816

Diff for: README.md

+105-8
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,109 @@
1-
# Toolbox Gateway plugin sample
1+
# Coder Toolbox plugin
22

3-
To load plugin into the provided Toolbox App, run `./gradlew build copyPlugin`
3+
[!["Join us onDiscord"](https://img.shields.io/badge/join-us%20on%20Discord-gray.svg?longCache=true&logo=discord&colorB=purple)](https://discord.gg/coder)
4+
[![Twitter Follow](https://img.shields.io/twitter/follow/CoderHQ?label=%40CoderHQ&style=social)](https://twitter.com/coderhq)
5+
[![Coder Toolbox Plugin Build](https://github.com/coder/coder-jetbrains-toolbox/actions/workflows/build.yml/badge.svg)](https://github.com/coder/coder-jetbrains-toolbox/actions/workflows/build.yml)
46

5-
or put files in the following directory:
7+
Connects your JetBrains IDE to Coder workspaces
68

7-
* Windows: `%LocalAppData%/JetBrains/Toolbox/cache/plugins/plugin-id`
8-
* macOS: `~/Library/Caches/JetBrains/Toolbox/plugins/plugin-id`
9-
* Linux: `~/.local/share/JetBrains/Toolbox/plugins/plugin-id`
9+
## Getting Started
1010

11-
Put all required .jar files (do not include any dependencies already included with the Toolbox App to avoid possible resolution conflicts),
12-
`extensions.json` and `icon.svg` in this directory.
11+
To install this plugin using JetBrains Toolbox, follow the steps below.
12+
13+
1. Install [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/). Make sure it's the `2.6.0.40284` release or
14+
above.
15+
2. Launch the Toolbox app and sign in with your JetBrains account (if needed).
16+
17+
### Install Coder plugin via URI
18+
19+
You can quickly install the plugin using this JetBrains hyperlink.
20+
21+
👉 [Install plugin](jetbrains://gateway/com.coder.toolbox)
22+
23+
This will open JetBrains Toolbox and prompt you to install the Coder Toolbox plugin automatically.
24+
25+
Alternatively, you can paste `jetbrains://gateway/com.coder.toolbox` into a browser.
26+
27+
### Manual install
28+
29+
There are two ways Coder Toolbox plugin can be installed. The first option is to manually download the plugin
30+
artifact from [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/26968-coder/versions)
31+
or from [Coder's Github Release page](https://github.com/coder/coder-jetbrains-toolbox/releases).
32+
33+
The next step is to copy the zip content to one of the following locations, depending on your OS:
34+
35+
* Windows: `%LocalAppData%/JetBrains/Toolbox/plugins/com.coder.toolbox`
36+
* macOS: `~/Library/Caches/JetBrains/Toolbox/plugins/com.coder.toolbox`
37+
* Linux: `~/.local/share/JetBrains/Toolbox/plugins/com.coder.toolbox`
38+
39+
Alternatively, you can install it using the _Gradle_ tasks included in the project:
40+
41+
```shell
42+
43+
./gradlew cleanAll build copyPlugin
44+
```
45+
46+
Make sure Toolbox is closed before running the command.
47+
48+
## Connect to a Coder Workspace via JetBrains Toolbox URI
49+
50+
You can use specially crafted JetBrains Gateway URIs to automatically:
51+
52+
1. Open Toolbox
53+
54+
2. Install the Coder Toolbox plugin (if not already installed)
55+
56+
3. Connect to a specific Coder deployment using a URL and a token.
57+
58+
4. Select a running workspace
59+
60+
5. Install a specified JetBrains IDE on that Workspace
61+
62+
6. Open a project folder directly in the remote IDE
63+
64+
### Example URIs
65+
66+
```text
67+
jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fdev.coder.com&token=zeoX4SbSpP-j2qGpajkdwxR9jBdcekXS2&workspace=bobiverse-bob&agent=dev&ide_product_code=GO&ide_build_number=241.23774.119&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs
68+
69+
jetbrains://gateway/com.coder.toolbox?url=https%3A%2F%2Fj5gj2r1so5nbi.pit-1.try.coder.app%2F&token=gqEirOoI1U-FfCQ6uj8iOLtybBIk99rr8&workspace=bobiverse-riker&agent=dev&ide_product_code=RR&ide_build_number=243.26053.17&folder=%2Fhome%2Fcoder%2Fworkspace%2Fhello-world-rs
70+
```
71+
72+
### URI Breakdown
73+
74+
```text
75+
jetbrains://gateway/com.coder.toolbox
76+
?url=http(s)://<your-coder-deployment>
77+
&token=<auth-token>
78+
&workspace=<workspace-name>
79+
&agent_id=<agent--id>
80+
&ide_product_code=<IDE-code>
81+
&ide_build_number=<IDE-build>
82+
&folder=<absolute-path-to-a-project-folder>
83+
```
84+
85+
| Query param | Description | Mandatory |
86+
|------------------|------------------------------------------------------------------------------|-----------|
87+
| url | Your Coder deployment URL (encoded) | Yes |
88+
| token | Coder authentication token | Yes |
89+
| workspace | Name of the Coder workspace to connect to. | Yes |
90+
| agent_id | ID of the agent associated with the workspace | No |
91+
| ide_product_code | JetBrains IDE product code (e.g., GO for GoLand, RR for Rider) | No |
92+
| ide_build_number | Specific build number of the JetBrains IDE to install on the workspace | No |
93+
| folder | Absolute path to the project folder to open in the remote IDE (URL-encoded) | No |
94+
95+
If only a single agent is available, specifying an agent ID is optional. However, if multiple agents exist,
96+
you must provide either the ID to target a specific one. Note that this version of the Coder Toolbox plugin
97+
does not automatically start agents if they are offline, so please ensure the selected agent is running before
98+
proceeding.
99+
100+
If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open and highlight the workspace environment
101+
page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable
102+
experience, it’s recommended to ensure the workspace is running prior to initiating the connection.
103+
104+
## Releasing
105+
106+
1. Check that the changelog lists all the important changes.
107+
2. Update the gradle.properties version.
108+
3. Publish the resulting draft release after validating it.
109+
4. Merge the resulting changelog PR.

Diff for: build.gradle.kts

+4-2
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,10 @@ private fun getPluginInstallDir(): Path {
191191
} / "JetBrains" / "Toolbox"
192192

193193
val pluginsDir = when {
194-
SystemInfoRt.isWindows -> toolboxCachesDir / "cache"
195-
SystemInfoRt.isLinux || SystemInfoRt.isMac -> toolboxCachesDir
194+
SystemInfoRt.isWindows ||
195+
SystemInfoRt.isLinux ||
196+
SystemInfoRt.isMac -> toolboxCachesDir
197+
196198
else -> error("Unknown os")
197199
} / "plugins"
198200

Diff for: gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.1.3
1+
version=0.1.4
22
group=com.coder.toolbox
33
name=coder-toolbox

Diff for: src/main/kotlin/com/coder/toolbox/util/CoderProtocolHandler.kt

+22-13
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ open class CoderProtocolHandler(
4242
shouldWaitForAutoLogin: Boolean,
4343
reInitialize: suspend (CoderRestClient, CoderCLIManager) -> Unit
4444
) {
45+
context.popupPluginMainPage()
4546
val params = uri.toQueryParameters()
47+
if (params.isEmpty()) {
48+
// probably a plugin installation scenario
49+
return
50+
}
4651

4752
val deploymentURL = params.url() ?: askUrl()
4853
if (deploymentURL.isNullOrBlank()) {
@@ -123,7 +128,19 @@ open class CoderProtocolHandler(
123128
}
124129

125130
// TODO: Show a dropdown and ask for an agent if missing.
126-
val agent = getMatchingAgent(params, workspace)
131+
val agent: WorkspaceAgent
132+
try {
133+
agent = getMatchingAgent(params, workspace)
134+
} catch (e: IllegalArgumentException) {
135+
context.logger.error(e, "Can't resolve an agent for workspace $workspaceName from $deploymentURL")
136+
context.showErrorPopup(
137+
MissingArgumentException(
138+
"Can't handle URI because we can't resolve an agent for workspace $workspaceName from $deploymentURL",
139+
e
140+
)
141+
)
142+
return
143+
}
127144
val status = WorkspaceAndAgentStatus.from(workspace, agent)
128145

129146
if (!status.ready()) {
@@ -157,7 +174,7 @@ open class CoderProtocolHandler(
157174
context.envPageManager.showEnvironmentPage(environmentId, false)
158175
val productCode = params.ideProductCode()
159176
val buildNumber = params.ideBuildNumber()
160-
val projectPath = params.projectPath()
177+
val projectFolder = params.projectFolder()
161178
if (!productCode.isNullOrBlank() && !buildNumber.isNullOrBlank()) {
162179
context.cs.launch {
163180
val ideVersion = "$productCode-$buildNumber"
@@ -167,7 +184,7 @@ open class CoderProtocolHandler(
167184
}
168185
job.join()
169186
context.logger.info("launching $ideVersion on $environmentId")
170-
context.ideOrchestrator.connectToIde(environmentId, ideVersion, projectPath)
187+
context.ideOrchestrator.connectToIde(environmentId, ideVersion, projectFolder)
171188
}
172189
}
173190
}
@@ -262,10 +279,8 @@ internal fun resolveRedirects(url: URL): URL {
262279

263280
/**
264281
* Return the agent matching the provided agent ID or name in the parameters.
265-
* The name is ignored if the ID is set. If neither was supplied and the
266-
* workspace has only one agent, return that. Otherwise throw an error.
267282
*
268-
* @throws [MissingArgumentException, IllegalArgumentException]
283+
* @throws [IllegalArgumentException]
269284
*/
270285
internal fun getMatchingAgent(
271286
parameters: Map<String, String?>,
@@ -281,8 +296,6 @@ internal fun getMatchingAgent(
281296
val agent =
282297
if (!parameters.agentID().isNullOrBlank()) {
283298
agents.firstOrNull { it.id.toString() == parameters.agentID() }
284-
} else if (!parameters.agentName().isNullOrBlank()) {
285-
agents.firstOrNull { it.name == parameters.agentName() }
286299
} else if (agents.size == 1) {
287300
agents.first()
288301
} else {
@@ -292,13 +305,9 @@ internal fun getMatchingAgent(
292305
if (agent == null) {
293306
if (!parameters.agentID().isNullOrBlank()) {
294307
throw IllegalArgumentException("The workspace \"${workspace.name}\" does not have an agent with ID \"${parameters.agentID()}\"")
295-
} else if (!parameters.agentName().isNullOrBlank()) {
296-
throw IllegalArgumentException(
297-
"The workspace \"${workspace.name}\"does not have an agent named \"${parameters.agentName()}\"",
298-
)
299308
} else {
300309
throw MissingArgumentException(
301-
"Unable to determine which agent to connect to; one of \"$AGENT_NAME\" or \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent",
310+
"Unable to determine which agent to connect to; \"$AGENT_ID\" must be set because the workspace \"${workspace.name}\" has more than one agent",
302311
)
303312
}
304313
}

Diff for: src/main/kotlin/com/coder/toolbox/util/LinkMap.kt

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,24 @@
11
package com.coder.toolbox.util
22

3-
// These are keys that we support in our Gateway links and must not be changed.
4-
private const val TYPE = "type"
53
const val URL = "url"
64
const val TOKEN = "token"
75
const val WORKSPACE = "workspace"
86
const val AGENT_NAME = "agent"
97
const val AGENT_ID = "agent_id"
108
private const val IDE_PRODUCT_CODE = "ide_product_code"
119
private const val IDE_BUILD_NUMBER = "ide_build_number"
12-
private const val PROJECT_PATH = "project_path"
13-
14-
// Helper functions for reading from the map. Prefer these to directly
15-
// interacting with the map.
16-
17-
fun Map<String, String>.isCoder(): Boolean = this[TYPE] == "coder"
10+
private const val FOLDER = "folder"
1811

1912
fun Map<String, String>.url() = this[URL]
2013

2114
fun Map<String, String>.token() = this[TOKEN]
2215

2316
fun Map<String, String>.workspace() = this[WORKSPACE]
2417

25-
fun Map<String, String?>.agentName() = this[AGENT_NAME]
26-
2718
fun Map<String, String?>.agentID() = this[AGENT_ID]
2819

2920
fun Map<String, String>.ideProductCode() = this[IDE_PRODUCT_CODE]
3021

3122
fun Map<String, String>.ideBuildNumber() = this[IDE_BUILD_NUMBER]
3223

33-
fun Map<String, String>.projectPath() = this[PROJECT_PATH]
24+
fun Map<String, String>.projectFolder() = this[FOLDER]

Diff for: src/test/kotlin/com/coder/toolbox/util/LinkHandlerTest.kt

+27-18
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,21 @@ internal class LinkHandlerTest {
5454

5555
val tests =
5656
listOf(
57-
Pair(mapOf("agent" to "agent_name"), "9a920eee-47fb-4571-9501-e4b3120c12f2"),
58-
Pair(mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"), "9a920eee-47fb-4571-9501-e4b3120c12f2"),
59-
Pair(mapOf("agent" to "agent_name_2"), "fb3daea4-da6b-424d-84c7-36b90574cfef"),
60-
Pair(mapOf("agent_id" to "fb3daea4-da6b-424d-84c7-36b90574cfef"), "fb3daea4-da6b-424d-84c7-36b90574cfef"),
61-
Pair(mapOf("agent" to "agent_name_3"), "b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
62-
Pair(mapOf("agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24"), "b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
57+
Pair(
58+
mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"),
59+
"9a920eee-47fb-4571-9501-e4b3120c12f2"
60+
),
61+
Pair(
62+
mapOf("agent_id" to "fb3daea4-da6b-424d-84c7-36b90574cfef"),
63+
"fb3daea4-da6b-424d-84c7-36b90574cfef"
64+
),
65+
Pair(
66+
mapOf("agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24"),
67+
"b0e4c54d-9ba9-4413-8512-11ca1e826a24"
68+
),
6369
// Prefer agent_id.
6470
Pair(
6571
mapOf(
66-
"agent" to "agent_name",
6772
"agent_id" to "b0e4c54d-9ba9-4413-8512-11ca1e826a24",
6873
),
6974
"b0e4c54d-9ba9-4413-8512-11ca1e826a24",
@@ -81,15 +86,14 @@ internal class LinkHandlerTest {
8186
val tests =
8287
listOf(
8388
Triple(emptyMap(), MissingArgumentException::class, "Unable to determine"),
84-
Triple(mapOf("agent" to ""), MissingArgumentException::class, "Unable to determine"),
8589
Triple(mapOf("agent_id" to ""), MissingArgumentException::class, "Unable to determine"),
86-
Triple(mapOf("agent" to null), MissingArgumentException::class, "Unable to determine"),
8790
Triple(mapOf("agent_id" to null), MissingArgumentException::class, "Unable to determine"),
88-
Triple(mapOf("agent" to "ws"), IllegalArgumentException::class, "agent named"),
89-
Triple(mapOf("agent" to "ws.agent_name"), IllegalArgumentException::class, "agent named"),
90-
Triple(mapOf("agent" to "agent_name_4"), IllegalArgumentException::class, "agent named"),
9191
Triple(mapOf("agent_id" to "not-a-uuid"), IllegalArgumentException::class, "agent with ID"),
92-
Triple(mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"), IllegalArgumentException::class, "agent with ID"),
92+
Triple(
93+
mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"),
94+
IllegalArgumentException::class,
95+
"agent with ID"
96+
),
9397
// Will ignore agent if agent_id is set even if agent matches.
9498
Triple(
9599
mapOf(
@@ -139,10 +143,11 @@ internal class LinkHandlerTest {
139143
val ws = DataGen.workspace("ws", agents = oneAgent)
140144
val tests =
141145
listOf(
142-
Triple(mapOf("agent" to "ws"), IllegalArgumentException::class, "agent named"),
143-
Triple(mapOf("agent" to "ws.agent_name_3"), IllegalArgumentException::class, "agent named"),
144-
Triple(mapOf("agent" to "agent_name_4"), IllegalArgumentException::class, "agent named"),
145-
Triple(mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"), IllegalArgumentException::class, "agent with ID"),
146+
Triple(
147+
mapOf("agent_id" to "ceaa7bcf-1612-45d7-b484-2e0da9349168"),
148+
IllegalArgumentException::class,
149+
"agent with ID"
150+
),
146151
)
147152

148153
tests.forEach {
@@ -166,7 +171,11 @@ internal class LinkHandlerTest {
166171
Triple(mapOf("agent" to null), IllegalArgumentException::class, "has no agents"),
167172
Triple(mapOf("agent_id" to null), IllegalArgumentException::class, "has no agents"),
168173
Triple(mapOf("agent" to "agent_name"), IllegalArgumentException::class, "has no agents"),
169-
Triple(mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"), IllegalArgumentException::class, "has no agents"),
174+
Triple(
175+
mapOf("agent_id" to "9a920eee-47fb-4571-9501-e4b3120c12f2"),
176+
IllegalArgumentException::class,
177+
"has no agents"
178+
),
170179
)
171180

172181
tests.forEach {

0 commit comments

Comments
 (0)