Skip to content
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

feat: add support for coder inbox #444

Merged
merged 15 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
2 changes: 1 addition & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function needToken(): boolean {
/**
* Create a new agent based off the current settings.
*/
async function createHttpAgent(): Promise<ProxyAgent> {
export async function createHttpAgent(): Promise<ProxyAgent> {
const cfg = vscode.workspace.getConfiguration()
const insecure = Boolean(cfg.get("coder.insecure"))
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
Expand Down
93 changes: 93 additions & 0 deletions src/inbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Api } from "coder/site/src/api/api"
import { ProxyAgent } from "proxy-agent"
import * as vscode from "vscode"
import { WebSocket } from "ws"
import { errToStr } from "./api-helper"
import { type Storage } from "./storage"

type InboxMessage = {
unread_count: number
notification: {
id: string
user_id: string
template_id: string
targets: string[]
title: string
content: string
actions: Record<string, string>
read_at: string
created_at: string
}
}

// These are the template IDs of our notifications.
// Maybe in the future we should avoid hardcoding
// these in both coderd and here.
const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a"
const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a"

export class Inbox implements vscode.Disposable {
private readonly storage: Storage
private disposed = false
private socket: WebSocket

constructor(httpAgent: ProxyAgent, restClient: Api, storage: Storage) {
this.storage = storage

const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL
if (!baseUrlRaw) {
throw new Error("No base URL set on REST client")
}

const baseUrl = new URL(baseUrlRaw)
const socketProto = baseUrl.protocol === "https:" ? "wss:" : "ws:"
const socketUrlRaw = `${socketProto}//${baseUrl.host}/api/v2/notifications/watch`

const coderSessionTokenHeader = "Coder-Session-Token"
this.socket = new WebSocket(new URL(socketUrlRaw), {
followRedirects: true,
agent: httpAgent,
headers: {
[coderSessionTokenHeader]: restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as
| string
| undefined,
},
})

this.socket.on("open", () => {
this.storage.writeToCoderOutputChannel("Listening to Coder Inbox")
})

this.socket.on("error", (error) => {
this.notifyError(error)
})

this.socket.on("message", (data) => {
try {
const inboxMessage = JSON.parse(data.toString()) as InboxMessage

if (
inboxMessage.notification.template_id === TEMPLATE_WORKSPACE_OUT_OF_DISK ||
inboxMessage.notification.template_id === TEMPLATE_WORKSPACE_OUT_OF_MEMORY
) {
vscode.window.showWarningMessage(inboxMessage.notification.title)
}
} catch (error) {
this.notifyError(error)
}
})
}

dispose() {
if (!this.disposed) {
this.storage.writeToCoderOutputChannel("No longer listening to Coder Inbox")
this.socket.close()
this.disposed = true
}
}

private notifyError(error: unknown) {
const message = errToStr(error, "Got empty error while monitoring Coder Inbox")
this.storage.writeToCoderOutputChannel(message)
}
}
8 changes: 7 additions & 1 deletion src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import * as path from "path"
import prettyBytes from "pretty-bytes"
import * as semver from "semver"
import * as vscode from "vscode"
import { makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
import { createHttpAgent, makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
import { extractAgents } from "./api-helper"
import * as cli from "./cliManager"
import { Commands } from "./commands"
import { featureSetForVersion, FeatureSet } from "./featureSet"
import { getHeaderCommand } from "./headers"
import { Inbox } from "./inbox"
import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
import { Storage } from "./storage"
Expand Down Expand Up @@ -403,6 +404,11 @@ export class Remote {
disposables.push(monitor)
disposables.push(monitor.onChange.event((w) => (this.commands.workspace = w)))

// Watch coder inbox for messages
const httpAgent = await createHttpAgent()
const inbox = new Inbox(httpAgent, workspaceRestClient, this.storage)
disposables.push(inbox)

// Wait for the agent to connect.
if (agent.status === "connecting") {
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}/${agent.name}...`)
Expand Down