Skip to content

Commit 9f918b8

Browse files
feat: add support for coder inbox (#444)
1 parent a356a31 commit 9f918b8

File tree

4 files changed

+92
-3
lines changed

4 files changed

+92
-3
lines changed

src/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function needToken(): boolean {
2828
/**
2929
* Create a new agent based off the current settings.
3030
*/
31-
async function createHttpAgent(): Promise<ProxyAgent> {
31+
export async function createHttpAgent(): Promise<ProxyAgent> {
3232
const cfg = vscode.workspace.getConfiguration()
3333
const insecure = Boolean(cfg.get("coder.insecure"))
3434
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())

src/inbox.ts

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Api } from "coder/site/src/api/api"
2+
import { Workspace, GetInboxNotificationResponse } from "coder/site/src/api/typesGenerated"
3+
import { ProxyAgent } from "proxy-agent"
4+
import * as vscode from "vscode"
5+
import { WebSocket } from "ws"
6+
import { errToStr } from "./api-helper"
7+
import { type Storage } from "./storage"
8+
9+
// These are the template IDs of our notifications.
10+
// Maybe in the future we should avoid hardcoding
11+
// these in both coderd and here.
12+
const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a"
13+
const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a"
14+
15+
export class Inbox implements vscode.Disposable {
16+
readonly #storage: Storage
17+
#disposed = false
18+
#socket: WebSocket
19+
20+
constructor(workspace: Workspace, httpAgent: ProxyAgent, restClient: Api, storage: Storage) {
21+
this.#storage = storage
22+
23+
const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL
24+
if (!baseUrlRaw) {
25+
throw new Error("No base URL set on REST client")
26+
}
27+
28+
const watchTemplates = [TEMPLATE_WORKSPACE_OUT_OF_DISK, TEMPLATE_WORKSPACE_OUT_OF_MEMORY]
29+
const watchTemplatesParam = encodeURIComponent(watchTemplates.join(","))
30+
31+
const watchTargets = [workspace.id]
32+
const watchTargetsParam = encodeURIComponent(watchTargets.join(","))
33+
34+
// We shouldn't need to worry about this throwing. Whilst `baseURL` could
35+
// be an invalid URL, that would've caused issues before we got to here.
36+
const baseUrl = new URL(baseUrlRaw)
37+
const socketProto = baseUrl.protocol === "https:" ? "wss:" : "ws:"
38+
const socketUrl = `${socketProto}//${baseUrl.host}/api/v2/notifications/inbox/watch?format=plaintext&templates=${watchTemplatesParam}&targets=${watchTargetsParam}`
39+
40+
const coderSessionTokenHeader = "Coder-Session-Token"
41+
this.#socket = new WebSocket(new URL(socketUrl), {
42+
followRedirects: true,
43+
agent: httpAgent,
44+
headers: {
45+
[coderSessionTokenHeader]: restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as
46+
| string
47+
| undefined,
48+
},
49+
})
50+
51+
this.#socket.on("open", () => {
52+
this.#storage.writeToCoderOutputChannel("Listening to Coder Inbox")
53+
})
54+
55+
this.#socket.on("error", (error) => {
56+
this.notifyError(error)
57+
this.dispose()
58+
})
59+
60+
this.#socket.on("message", (data) => {
61+
try {
62+
const inboxMessage = JSON.parse(data.toString()) as GetInboxNotificationResponse
63+
64+
vscode.window.showInformationMessage(inboxMessage.notification.title)
65+
} catch (error) {
66+
this.notifyError(error)
67+
}
68+
})
69+
}
70+
71+
dispose() {
72+
if (!this.#disposed) {
73+
this.#storage.writeToCoderOutputChannel("No longer listening to Coder Inbox")
74+
this.#socket.close()
75+
this.#disposed = true
76+
}
77+
}
78+
79+
private notifyError(error: unknown) {
80+
const message = errToStr(error, "Got empty error while monitoring Coder Inbox")
81+
this.#storage.writeToCoderOutputChannel(message)
82+
}
83+
}

src/remote.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import * as path from "path"
99
import prettyBytes from "pretty-bytes"
1010
import * as semver from "semver"
1111
import * as vscode from "vscode"
12-
import { makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
12+
import { createHttpAgent, makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
1313
import { extractAgents } from "./api-helper"
1414
import * as cli from "./cliManager"
1515
import { Commands } from "./commands"
1616
import { featureSetForVersion, FeatureSet } from "./featureSet"
1717
import { getHeaderCommand } from "./headers"
18+
import { Inbox } from "./inbox"
1819
import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"
1920
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
2021
import { Storage } from "./storage"
@@ -403,6 +404,11 @@ export class Remote {
403404
disposables.push(monitor)
404405
disposables.push(monitor.onChange.event((w) => (this.commands.workspace = w)))
405406

407+
// Watch coder inbox for messages
408+
const httpAgent = await createHttpAgent()
409+
const inbox = new Inbox(workspace, httpAgent, workspaceRestClient, this.storage)
410+
disposables.push(inbox)
411+
406412
// Wait for the agent to connect.
407413
if (agent.status === "connecting") {
408414
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}/${agent.name}...`)

yarn.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -1594,7 +1594,7 @@ [email protected]:
15941594

15951595
"coder@https://github.com/coder/coder#main":
15961596
version "0.0.0"
1597-
resolved "https://github.com/coder/coder#975ea23d6f49a4043131f79036d1bf5166eb9140"
1597+
resolved "https://github.com/coder/coder#3ac844ad3d341d2910542b83d4f33df7bd0be85e"
15981598

15991599
collapse-white-space@^1.0.2:
16001600
version "1.0.6"

0 commit comments

Comments
 (0)