From 86028e9b4ac8dfe4ad3624df563d5b721dbf0de6 Mon Sep 17 00:00:00 2001 From: 1000ch Date: Thu, 13 Aug 2026 09:50:20 +0900 Subject: [PATCH] feat: add --agents to run Code's agent host VS Code 1.132 added the Agents Window, opened on the desktop with `code --agents`. Its workbench lives in `vs/sessions`, which Code only bundles into its vscode-web build, not into the vscode-reh-web server build we package, so there is no Agents Window for us to serve yet. The agent host that runs agent sessions is in the server build, though. Code spawns it only when told where it should listen, and registers the channel the browser uses to reach it over the remote connection at the same time. Without a path that channel is registered as unavailable, so agent sessions cannot connect at all today. `--agents` supplies that path, pointing the agent host at a socket in the user data directory (a named pipe on Windows). Agent sessions then work in the regular chat UI. Code never unlinks the socket, so a leftover one from a killed instance is removed before startup. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 6 ++++++ docs/FAQ.md | 35 +++++++++++++++++++++++++++++++++++ src/node/cli.ts | 33 +++++++++++++++++++++++++++++++++ src/node/main.ts | 33 ++++++++++++++++++++++++++++++++- test/unit/node/cli.test.ts | 24 ++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb962e813d8e..1bb117fc11c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ Code v99.99.999 ## Unreleased +### Added + +- `--agents` starts Code's agent host so agent sessions can run on the server. + Code's dedicated Agents Window is not bundled for the server build yet, so + agents surface in the regular chat UI. See the FAQ for the caveats. + ## [4.132.0](https://github.com/coder/code-server/releases/tag/v4.132.0) - 2026-08-10 Code v1.132.0 diff --git a/docs/FAQ.md b/docs/FAQ.md index 656b45978fe3..183d95d17c5d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -40,6 +40,7 @@ - [How do I hide the coder/coder promotion in Help: Getting Started?](#how-do-i-hide-the-codercoder-promotion-in-help-getting-started) - [How do I disable the proxy?](#how-do-i-disable-the-proxy) - [How do I disable file download?](#how-do-i-disable-file-download) +- [Can I use the Agents Window?](#can-i-use-the-agents-window) - [Why do web views not work?](#why-do-web-views-not-work) @@ -542,6 +543,40 @@ when using the option. You can pass the flag `--disable-file-downloads` to `code-server` +## Can I use the Agents Window? + +Not yet, but you can enable the agent host it runs on. + +VS Code 1.132 added the [Agents +Window](https://code.visualstudio.com/docs/agents/run/agents-window), opened on +the desktop with `code --agents`. It is built from `vs/sessions`, a workbench +that is separate from the regular one, and Code only bundles that workbench into +its `vscode-web` build (the one behind vscode.dev). The server build code-server +ships, `vscode-reh-web`, does not include it, so there is no Agents Window to +serve yet. + +What the server build does include is the agent host, the process that actually +runs agent sessions. Code starts it only when it is told where the host should +listen, and code-server does not tell it by default. Pass `--agents` to change +that: + +```console +code-server --agents +``` + +The agent host then listens on `agent-host.sock` inside your user data directory +(a named pipe on Windows), and Code registers the channel the browser uses to +reach it. Agent sessions show up in the regular chat UI rather than in their own +window. + +Two caveats: + +- Anyone who can read that socket can talk to the agent host, so keep your user + data directory private. It is not exposed over HTTP. +- Code resolves the Claude and Codex SDKs from `product.agentSdks`, which its + build pipeline only writes into the desktop server build. Those agents are + unavailable unless you point Code at a local SDK root yourself. + ## Why do web views not work? Web views rely on service workers, and service workers are only available in a diff --git a/src/node/cli.ts b/src/node/cli.ts index 0946c8e04344..543e8a921e7f 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -1,4 +1,5 @@ import { field, Level, logger } from "@coder/logger" +import * as crypto from "crypto" import { promises as fs } from "fs" import { load } from "js-yaml" import * as path from "path" @@ -91,6 +92,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs { "reuse-window"?: boolean "new-window"?: boolean "ignore-last-opened"?: boolean + agents?: boolean verbose?: boolean "app-name"?: string "welcome-text"?: string @@ -288,6 +290,16 @@ export const options: Options> = { short: "r", description: "Force to open a file or folder in an already opened window.", }, + // Named after Code's own --agents so the flags line up once we can serve the + // Agents Window itself. That needs Code's `vs/sessions` bundle, which its + // build only adds to the vscode-web entry points, not to the server build we + // package (see build/gulpfile.reh.ts in the submodule). + agents: { + type: "boolean", + description: + "Start Code's agent host so agent sessions can run on the server. Code's dedicated Agents Window is not \n" + + "bundled for the server yet, so agents surface in the regular chat UI rather than in their own window.", + }, log: { type: LogLevel }, verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." }, @@ -907,6 +919,22 @@ export interface CodeArgs extends UserProvidedCodeArgs { "without-browser-env-var"?: boolean compatibility?: string log?: string[] + "agent-host-path"?: string +} + +/** + * Where Code's agent host should listen when --agents is set. + * + * Windows has no Unix sockets so use a named pipe there instead. The path is + * derived from the user data directory since that is what separates concurrent + * instances from each other. + */ +export function agentHostSocketPath(userDataDir: string): string { + if (process.platform === "win32") { + const hash = crypto.createHash("sha1").update(userDataDir).digest("hex").substring(0, 16) + return `\\\\.\\pipe\\code-server-agent-host-${hash}` + } + return path.join(userDataDir, "agent-host.sock") } /** @@ -921,5 +949,10 @@ export const toCodeArgs = async (args: DefaultedArgs): Promise => { version: !!args.version, port: args.port?.toString(), log: args.log ? [args.log] : undefined, + // Telling Code where the agent host should listen is what makes it spawn + // one. It also registers the channel the workbench uses to reach the agent + // host over the remote connection; without a path that channel is + // registered as unavailable and agent sessions cannot connect. + "agent-host-path": args.agents ? agentHostSocketPath(args["user-data-dir"]) : undefined, } } diff --git a/src/node/main.ts b/src/node/main.ts index c2d3bd57852b..1b14f7c6ac62 100644 --- a/src/node/main.ts +++ b/src/node/main.ts @@ -1,11 +1,12 @@ import { field, logger } from "@coder/logger" +import { promises as fs } from "fs" import http from "http" import * as os from "os" import * as path from "path" import { Disposable } from "../common/emitter" import { plural } from "../common/util" import { createApp, ensureAddress } from "./app" -import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" +import { agentHostSocketPath, AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli" import { commit, version, vsRootPath } from "./constants" import { loadCustomStrings } from "./i18n" import { register } from "./routes" @@ -119,6 +120,27 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st vscode.end() } +/** + * Remove a leftover agent host socket. + * + * Code binds the socket but never unlinks it, so a socket left behind by a + * killed instance makes the agent host fail to listen on the next start. + * Named pipes on Windows are not files and disappear on their own. + */ +const removeStaleAgentHostSocket = async (socketPath: string): Promise => { + if (process.platform === "win32") { + return + } + try { + await fs.unlink(socketPath) + logger.debug(`Removed stale agent host socket ${socketPath}`) + } catch (error: any) { + if (error.code !== "ENOENT") { + logger.warn(`Could not remove agent host socket ${socketPath}: ${error.message}`) + } + } +} + export const runCodeServer = async ( args: DefaultedArgs, ): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => { @@ -139,6 +161,11 @@ export const runCodeServer = async ( ) } + // Must happen before Code can be loaded, which is on the first request. + if (args.agents) { + await removeStaleAgentHostSocket(agentHostSocketPath(args["user-data-dir"])) + } + const app = await createApp(args) const protocol = args.cert ? "https" : "http" const serverAddress = ensureAddress(app.server, protocol) @@ -197,6 +224,10 @@ export const runCodeServer = async ( if (args["skip-auth-preflight"]) { logger.info(" - Skipping authentication for preflight requests") } + if (args.agents) { + logger.info(` - Agent host enabled on ${agentHostSocketPath(args["user-data-dir"])}`) + logger.info(" - Code's Agents Window is not bundled for the server; agents appear in the regular chat UI") + } if (process.env.VSCODE_PROXY_URI) { logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`) } diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 85e16f38ca99..f5e02fa7dea9 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -10,6 +10,7 @@ import { setDefaults, shouldOpenInExistingInstance, toCodeArgs, + agentHostSocketPath, optionDescriptions, options, Options, @@ -114,6 +115,8 @@ describe("parser", () => { "--skip-auth-preflight", + "--agents", + ["--session-socket", "/tmp/override-code-server-ipc-socket"], ["--reconnection-grace-time", "86400"], @@ -157,6 +160,7 @@ describe("parser", () => { "reconnection-grace-time": "86400", "abs-proxy-base-path": "/codeserver/app1", "skip-auth-preflight": true, + agents: true, }) }) @@ -984,6 +988,7 @@ describe("toCodeArgs", () => { port: "8080", version: false, log: undefined, + "agent-host-path": undefined, } const testName = "vscode-args" @@ -1006,6 +1011,25 @@ describe("toCodeArgs", () => { _: [file], }) }) + + it("should tell Code where to run the agent host", async () => { + expect(await toCodeArgs(await setDefaults(parse(["--agents"])))).toStrictEqual({ + ...vscodeDefaults, + agents: true, + "agent-host-path": agentHostSocketPath(paths.data), + }) + }) + + it("should derive the agent host socket from the user data directory", async () => { + if (process.platform === "win32") { + expect(agentHostSocketPath("C:\\one")).toMatch(/^\\\\\.\\pipe\\code-server-agent-host-[0-9a-f]{16}$/) + } else { + expect(agentHostSocketPath("/one")).toBe(path.join("/one", "agent-host.sock")) + } + // Concurrent instances have separate user data directories, so their agent + // hosts must not land on the same socket. + expect(agentHostSocketPath("/one")).not.toBe(agentHostSocketPath("/two")) + }) }) describe("optionDescriptions", () => {