diff --git a/.icons/devin.svg b/.icons/devin.svg new file mode 100644 index 000000000..43148d76c --- /dev/null +++ b/.icons/devin.svg @@ -0,0 +1 @@ +Devin \ No newline at end of file diff --git a/registry/coder/modules/devin-desktop/README.md b/registry/coder/modules/devin-desktop/README.md new file mode 100644 index 000000000..85927e8b5 --- /dev/null +++ b/registry/coder/modules/devin-desktop/README.md @@ -0,0 +1,69 @@ +--- +display_name: Devin Desktop +description: Add a one-click button to launch Devin Desktop (formerly Windsurf Editor) +icon: ../../../../.icons/devin.svg +verified: true +tags: [ide, devin, ai] +--- + +# Devin Desktop + +Add a button to open any workspace with a single click in Devin Desktop. + +Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). + +```tf +module "devin-desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/devin-desktop/coder" + version = "1.0.0" + agent_id = coder_agent.main.id +} +``` + +## Examples + +### Open in a specific directory + +```tf +module "devin-desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/devin-desktop/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" +} +``` + +### Configure MCP servers for Devin Desktop + +Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.config/devin/mcp_config.json` using a `coder_script` on workspace start. + +The following example configures Devin Desktop to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource. + +```tf +module "devin-desktop" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/devin-desktop/coder" + version = "1.0.0" + agent_id = coder_agent.main.id + folder = "/home/coder/project" + mcp = jsonencode({ + mcpServers = { + "github" : { + "url" : "https://api.githubcopilot.com/mcp/", + "headers" : { + "Authorization" : "Bearer ${data.coder_external_auth.github.access_token}", + }, + "type" : "http" + } + + + } + }) +} + +data "coder_external_auth" "github" { + id = "github" +} +``` diff --git a/registry/coder/modules/devin-desktop/main.test.ts b/registry/coder/modules/devin-desktop/main.test.ts new file mode 100644 index 000000000..24d4e57e9 --- /dev/null +++ b/registry/coder/modules/devin-desktop/main.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, + runContainer, + execContainer, + removeContainer, + findResourceInstance, + readFileContainer, +} from "~test"; + +describe("devin-desktop", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it("default output", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + expect(state.outputs.devin_desktop_url.value).toBe( + "devin://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + + const coder_app = state.resources.find( + (res) => + res.type === "coder_app" && + res.module === "module.vscode-desktop-core" && + res.name === "vscode-desktop", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBeNull(); + expect(coder_app?.instances[0].attributes.slug).toBe("devin-desktop"); + expect(coder_app?.instances[0].attributes.display_name).toBe( + "Devin Desktop", + ); + }); + + it("adds folder", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + }); + expect(state.outputs.devin_desktop_url.value).toBe( + "devin://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds folder and open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + open_recent: true, + }); + expect(state.outputs.devin_desktop_url.value).toBe( + "devin://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds folder but not open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + folder: "/foo/bar", + open_recent: false, + }); + expect(state.outputs.devin_desktop_url.value).toBe( + "devin://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("adds open_recent", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + open_recent: true, + }); + expect(state.outputs.devin_desktop_url.value).toBe( + "devin://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", + ); + }); + + it("allows overriding slug and display_name", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + slug: "devin", + display_name: "Devin", + }); + const coder_app = state.resources.find( + (res) => + res.type === "coder_app" && + res.module === "module.vscode-desktop-core" && + res.name === "vscode-desktop", + ); + expect(coder_app?.instances[0].attributes.slug).toBe("devin"); + expect(coder_app?.instances[0].attributes.display_name).toBe("Devin"); + }); + + it("writes ~/.config/devin/mcp_config.json when mcp provided", async () => { + const id = await runContainer("alpine"); + try { + const mcp = JSON.stringify({ + servers: { demo: { url: "http://localhost:1234" } }, + }); + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + mcp, + }); + const script = findResourceInstance( + state, + "coder_script", + "devin_desktop_mcp", + ).script; + const resp = await execContainer(id, ["sh", "-c", script]); + if (resp.exitCode !== 0) { + console.log(resp.stdout); + console.log(resp.stderr); + } + expect(resp.exitCode).toBe(0); + const content = await readFileContainer( + id, + "/root/.config/devin/mcp_config.json", + ); + expect(content).toBe(mcp); + } finally { + await removeContainer(id); + } + }, 10000); +}); diff --git a/registry/coder/modules/devin-desktop/main.tf b/registry/coder/modules/devin-desktop/main.tf new file mode 100644 index 000000000..8a76f5436 --- /dev/null +++ b/registry/coder/modules/devin-desktop/main.tf @@ -0,0 +1,108 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "folder" { + type = string + description = "The folder to open in Devin Desktop." + default = "" +} + +variable "open_recent" { + type = bool + description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open." + default = false +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "slug" { + type = string + description = "The slug of the app." + default = "devin-desktop" +} + +variable "display_name" { + type = string + description = "The display name of the app." + default = "Devin Desktop" +} + +variable "mcp" { + type = string + description = "JSON-encoded string to configure MCP servers for Devin Desktop. When set, writes ~/.config/devin/mcp_config.json." + default = "" +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +locals { + mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : "" +} + +# Devin Desktop is Cognition's rebrand of the Windsurf Editor (June 2, 2026), +# which was itself Codeium's rebrand of its original editor. It uses the same +# VS Code Desktop launch mechanism as the windsurf module, so this wraps the +# same shared vscode-desktop-core module with Devin Desktop's branding. +module "vscode-desktop-core" { + source = "registry.coder.com/coder/vscode-desktop-core/coder" + version = "1.0.2" + + agent_id = var.agent_id + + coder_app_icon = "/icon/devin.svg" + coder_app_slug = var.slug + coder_app_display_name = var.display_name + coder_app_order = var.order + coder_app_group = var.group + + folder = var.folder + open_recent = var.open_recent + # devin:// is registered as an external app protocol in coder/coder + # (ALLOWED_EXTERNAL_APP_PROTOCOLS, coder/coder#28214). + protocol = "devin" +} + +resource "coder_script" "devin_desktop_mcp" { + count = var.mcp != "" ? 1 : 0 + agent_id = var.agent_id + display_name = "Devin Desktop MCP" + icon = "/icon/devin.svg" + run_on_start = true + start_blocks_login = false + script = <<-EOT + #!/bin/sh + set -eu + mkdir -p "$HOME/.config/devin" + echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.config/devin/mcp_config.json" + chmod 600 "$HOME/.config/devin/mcp_config.json" + EOT +} + +output "devin_desktop_url" { + value = module.vscode-desktop-core.ide_uri + description = "Devin Desktop URL." +} diff --git a/registry/coder/modules/windsurf/README.md b/registry/coder/modules/windsurf/README.md index 4463552f5..f52cf1795 100644 --- a/registry/coder/modules/windsurf/README.md +++ b/registry/coder/modules/windsurf/README.md @@ -8,6 +8,11 @@ tags: [ide, windsurf, ai] # Windsurf Editor +> [!IMPORTANT] +> Windsurf Editor was rebranded to Devin Desktop by Cognition on June 2, 2026. New templates should use the +> [`devin-desktop`](https://registry.coder.com/modules/coder/devin-desktop) module instead. This module is kept +> published and fully functional for existing templates that reference it; it is not being removed. + Add a button to open any workspace with a single click in Windsurf Editor. Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). @@ -16,7 +21,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.1" + version = "1.3.2" agent_id = coder_agent.main.id } ``` @@ -29,7 +34,7 @@ module "windsurf" { module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.1" + version = "1.3.2" agent_id = coder_agent.main.id folder = "/home/coder/project" } @@ -45,7 +50,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth module "windsurf" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windsurf/coder" - version = "1.3.1" + version = "1.3.2" agent_id = coder_agent.main.id folder = "/home/coder/project" mcp = jsonencode({