Skip to content

Commit b839ad5

Browse files
committed
Merge branch 'main' into brett-429/add-debuggin-to-the-plugin
2 parents 5fbc7a7 + 1400383 commit b839ad5

10 files changed

+395
-252
lines changed

.prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
],
1010
"options": {
1111
"printWidth": 80,
12-
"proseWrap": "always"
12+
"proseWrap": "preserve"
1313
}
1414
}
1515
]

CHANGELOG.md

+20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
## Unreleased
44

5+
## [v1.7.0](https://github.com/coder/vscode-coder/releases/tag/v1.7.0) (2025-04-03)
6+
7+
### Added
8+
9+
- Add new `/openDevContainer` path, similar to the `/open` path, except this
10+
allows connecting to a dev container inside a workspace. For now, the dev
11+
container must already be running for this to work.
12+
13+
### Fixed
14+
15+
- When not using token authentication, avoid setting `undefined` for the token
16+
header, as Node will throw an error when headers are undefined. Now, we will
17+
not set any header at all.
18+
19+
## [v1.6.0](https://github.com/coder/vscode-coder/releases/tag/v1.6.0) (2025-04-01)
20+
21+
### Added
22+
23+
- Add support for Coder inbox.
24+
525
## [v1.5.0](https://github.com/coder/vscode-coder/releases/tag/v1.5.0) (2025-03-20)
626

727
### Fixed

README.md

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Coder Remote
22

33
[![Visual Studio Marketplace](https://vsmarketplacebadges.dev/version/coder.coder-remote.svg)](https://marketplace.visualstudio.com/items?itemName=coder.coder-remote)
4+
[![Open VSX Version](https://img.shields.io/open-vsx/v/coder/coder-remote)](https://open-vsx.org/extension/coder/coder-remote)
45
[!["Join us on
56
Discord"](https://badgen.net/discord/online-members/coder)](https://coder.com/chat?utm_source=github.com/coder/vscode-coder&utm_medium=github&utm_campaign=readme.md)
67

@@ -13,11 +14,11 @@ workspaces with a single click.
1314
- Works in air-gapped or restricted networks. Just connect to your Coder
1415
deployment!
1516
- Supports multiple editors: VS Code, Cursor, and Windsurf.
16-
> [!NOTE]
17-
> The extension builds on VSCode provided implementations of SSH. Make sure
18-
> you have the correct ssh extension installed for your editor
19-
> (ms-vscode-remote.remote-ssh or codeium.windsurf-remote-openssh for
20-
> windsurf)
17+
18+
> [!NOTE]
19+
> The extension builds on VS Code-provided implementations of SSH. Make
20+
> sure you have the correct SSH extension installed for your editor
21+
> (`ms-vscode-remote.remote-ssh` or `codeium.windsurf-remote-openssh` for Windsurf).
2122
2223
![Demo](https://github.com/coder/vscode-coder/raw/main/demo.gif?raw=true)
2324

@@ -26,19 +27,18 @@ workspaces with a single click.
2627
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press
2728
enter.
2829

29-
```text
30+
```shell
3031
ext install coder.coder-remote
3132
```
3233

3334
Alternatively, manually install the VSIX from the
3435
[latest release](https://github.com/coder/vscode-coder/releases/latest).
3536

36-
#### Variables Reference
37+
### Variables Reference
3738

38-
Coder uses
39-
${userHome} from VS Code's
39+
Coder uses `${userHome}` from VS Code's
4040
[variables reference](https://code.visualstudio.com/docs/editor/variables-reference).
41-
Use this when formatting paths in the Coder extension settings rather than ~ or
42-
$HOME.
41+
Use this when formatting paths in the Coder extension settings rather than `~`
42+
or `$HOME`.
4343

4444
Example: ${userHome}/foo/bar.baz

package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"displayName": "Coder",
55
"description": "Open any workspace with a single click.",
66
"repository": "https://github.com/coder/vscode-coder",
7-
"version": "1.5.0",
7+
"version": "1.7.0",
88
"engines": {
99
"vscode": "^1.73.0"
1010
},
@@ -303,13 +303,13 @@
303303
"utf-8-validate": "^6.0.5",
304304
"vitest": "^0.34.6",
305305
"vscode-test": "^1.5.0",
306-
"webpack": "^5.94.0",
306+
"webpack": "^5.98.0",
307307
"webpack-cli": "^5.1.4"
308308
},
309309
"dependencies": {
310-
"axios": "1.7.7",
310+
"axios": "1.8.4",
311311
"date-fns": "^3.6.0",
312-
"eventsource": "^3.0.5",
312+
"eventsource": "^3.0.6",
313313
"find-process": "^1.4.7",
314314
"jsonc-parser": "^3.3.1",
315315
"memfs": "^4.9.3",

src/api.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { getProxyForUrl } from "./proxy"
1313
import { Storage } from "./storage"
1414
import { expandPath } from "./util"
1515

16+
export const coderSessionTokenHeader = "Coder-Session-Token"
17+
1618
/**
1719
* Return whether the API will need a token for authorization.
1820
* If mTLS is in use (as specified by the cert or key files being set) then
@@ -242,14 +244,15 @@ export async function waitForBuild(
242244
const baseUrl = new URL(baseUrlRaw)
243245
const proto = baseUrl.protocol === "https:" ? "wss:" : "ws:"
244246
const socketUrlRaw = `${proto}//${baseUrl.host}${path}`
247+
const token = restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as string | undefined
245248
const socket = new ws.WebSocket(new URL(socketUrlRaw), {
246-
headers: {
247-
"Coder-Session-Token": restClient.getAxiosInstance().defaults.headers.common["Coder-Session-Token"] as
248-
| string
249-
| undefined,
250-
},
251-
followRedirects: true,
252249
agent: agent,
250+
followRedirects: true,
251+
headers: token
252+
? {
253+
[coderSessionTokenHeader]: token,
254+
}
255+
: undefined,
253256
})
254257
socket.binaryType = "nodebuffer"
255258
socket.on("message", (data) => {

src/commands.ts

+51-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { makeCoderSdk, needToken } from "./api"
66
import { extractAgents } from "./api-helper"
77
import { CertificateError } from "./error"
88
import { Storage } from "./storage"
9-
import { AuthorityPrefix, toSafeHost } from "./util"
9+
import { toRemoteAuthority, toSafeHost } from "./util"
1010
import { OpenableTreeItem } from "./workspacesProvider"
1111

1212
export class Commands {
@@ -499,6 +499,26 @@ export class Commands {
499499
await openWorkspace(baseUrl, workspaceOwner, workspaceName, workspaceAgent, folderPath, openRecent)
500500
}
501501

502+
/**
503+
* Open a devcontainer from a workspace belonging to the currently logged-in deployment.
504+
*
505+
* Throw if not logged into a deployment.
506+
*/
507+
public async openDevContainer(...args: string[]): Promise<void> {
508+
const baseUrl = this.restClient.getAxiosInstance().defaults.baseURL
509+
if (!baseUrl) {
510+
throw new Error("You are not logged in")
511+
}
512+
513+
const workspaceOwner = args[0] as string
514+
const workspaceName = args[1] as string
515+
const workspaceAgent = undefined // args[2] is reserved, but we do not support multiple agents yet.
516+
const devContainerName = args[3] as string
517+
const devContainerFolder = args[4] as string
518+
519+
await openDevContainer(baseUrl, workspaceOwner, workspaceName, workspaceAgent, devContainerName, devContainerFolder)
520+
}
521+
502522
/**
503523
* Update the current workspace. If there is no active workspace connection,
504524
* this is a no-op.
@@ -536,10 +556,7 @@ async function openWorkspace(
536556
) {
537557
// A workspace can have multiple agents, but that's handled
538558
// when opening a workspace unless explicitly specified.
539-
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
540-
if (workspaceAgent) {
541-
remoteAuthority += `.${workspaceAgent}`
542-
}
559+
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
543560

544561
let newWindow = true
545562
// Open in the existing window if no workspaces are open.
@@ -598,3 +615,32 @@ async function openWorkspace(
598615
reuseWindow: !newWindow,
599616
})
600617
}
618+
619+
async function openDevContainer(
620+
baseUrl: string,
621+
workspaceOwner: string,
622+
workspaceName: string,
623+
workspaceAgent: string | undefined,
624+
devContainerName: string,
625+
devContainerFolder: string,
626+
) {
627+
const remoteAuthority = toRemoteAuthority(baseUrl, workspaceOwner, workspaceName, workspaceAgent)
628+
629+
const devContainer = Buffer.from(JSON.stringify({ containerName: devContainerName }), "utf-8").toString("hex")
630+
const devContainerAuthority = `attached-container+${devContainer}@${remoteAuthority}`
631+
632+
let newWindow = true
633+
if (!vscode.workspace.workspaceFolders?.length) {
634+
newWindow = false
635+
}
636+
637+
await vscode.commands.executeCommand(
638+
"vscode.openFolder",
639+
vscode.Uri.from({
640+
scheme: "vscode-remote",
641+
authority: devContainerAuthority,
642+
path: devContainerFolder,
643+
}),
644+
newWindow,
645+
)
646+
}

src/extension.ts

+56
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,61 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
130130
await storage.configureCli(toSafeHost(url), url, token)
131131

132132
vscode.commands.executeCommand("coder.open", owner, workspace, agent, folder, openRecent)
133+
} else if (uri.path === "/openDevContainer") {
134+
const workspaceOwner = params.get("owner")
135+
const workspaceName = params.get("workspace")
136+
const workspaceAgent = params.get("agent")
137+
const devContainerName = params.get("devContainerName")
138+
const devContainerFolder = params.get("devContainerFolder")
139+
140+
if (!workspaceOwner) {
141+
throw new Error("workspace owner must be specified as a query parameter")
142+
}
143+
144+
if (!workspaceName) {
145+
throw new Error("workspace name must be specified as a query parameter")
146+
}
147+
148+
if (!devContainerName) {
149+
throw new Error("dev container name must be specified as a query parameter")
150+
}
151+
152+
if (!devContainerFolder) {
153+
throw new Error("dev container folder must be specified as a query parameter")
154+
}
155+
156+
// We are not guaranteed that the URL we currently have is for the URL
157+
// this workspace belongs to, or that we even have a URL at all (the
158+
// queries will default to localhost) so ask for it if missing.
159+
// Pre-populate in case we do have the right URL so the user can just
160+
// hit enter and move on.
161+
const url = await commands.maybeAskUrl(params.get("url"), storage.getUrl())
162+
if (url) {
163+
restClient.setHost(url)
164+
await storage.setUrl(url)
165+
} else {
166+
throw new Error("url must be provided or specified as a query parameter")
167+
}
168+
169+
// If the token is missing we will get a 401 later and the user will be
170+
// prompted to # again, so we do not need to ensure it is set now.
171+
// For non-token auth, we write a blank token since the `vscodessh`
172+
// command currently always requires a token file. However, if there is
173+
// a query parameter for non-token auth go ahead and use it anyway; all
174+
// that really matters is the file is created.
175+
const token = needToken() ? params.get("token") : (params.get("token") ?? "")
176+
177+
// Store on disk to be used by the cli.
178+
await storage.configureCli(toSafeHost(url), url, token)
179+
180+
vscode.commands.executeCommand(
181+
"coder.openDevContainer",
182+
workspaceOwner,
183+
workspaceName,
184+
workspaceAgent,
185+
devContainerName,
186+
devContainerFolder,
187+
)
133188
} else {
134189
throw new Error(`Unknown path ${uri.path}`)
135190
}
@@ -142,6 +197,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
142197
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
143198
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
144199
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
200+
vscode.commands.registerCommand("coder.openDevContainer", commands.openDevContainer.bind(commands))
145201
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
146202
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
147203
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))

src/inbox.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Workspace, GetInboxNotificationResponse } from "coder/site/src/api/type
33
import { ProxyAgent } from "proxy-agent"
44
import * as vscode from "vscode"
55
import { WebSocket } from "ws"
6+
import { coderSessionTokenHeader } from "./api"
67
import { errToStr } from "./api-helper"
78
import { getMemoryLogger } from "./memoryLogger"
89
import { type Storage } from "./storage"
@@ -49,14 +50,15 @@ export class Inbox implements vscode.Disposable {
4950
logger.debug(`Connecting to inbox WebSocket at: ${socketUrl}`)
5051

5152
const coderSessionTokenHeader = "Coder-Session-Token"
53+
const token = restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as string | undefined
5254
this.#socket = new WebSocket(new URL(socketUrl), {
53-
followRedirects: true,
5455
agent: httpAgent,
55-
headers: {
56-
[coderSessionTokenHeader]: restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as
57-
| string
58-
| undefined,
59-
},
56+
followRedirects: true,
57+
headers: token
58+
? {
59+
[coderSessionTokenHeader]: token,
60+
}
61+
: undefined,
6062
})
6163

6264
this.#socket.on("open", () => {

src/util.ts

+13
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
6161
}
6262
}
6363

64+
export function toRemoteAuthority(
65+
baseUrl: string,
66+
workspaceOwner: string,
67+
workspaceName: string,
68+
workspaceAgent: string | undefined,
69+
): string {
70+
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
71+
if (workspaceAgent) {
72+
remoteAuthority += `.${workspaceAgent}`
73+
}
74+
return remoteAuthority
75+
}
76+
6477
/**
6578
* Given a URL, return the host in a format that is safe to write.
6679
*/

0 commit comments

Comments
 (0)