Skip to content

Commit 86028e9

Browse files
1000chclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 51f90a3 commit 86028e9

5 files changed

Lines changed: 130 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ Code v99.99.999
2222

2323
## Unreleased
2424

25+
### Added
26+
27+
- `--agents` starts Code's agent host so agent sessions can run on the server.
28+
Code's dedicated Agents Window is not bundled for the server build yet, so
29+
agents surface in the regular chat UI. See the FAQ for the caveats.
30+
2531
## [4.132.0](https://github.com/coder/code-server/releases/tag/v4.132.0) - 2026-08-10
2632

2733
Code v1.132.0

docs/FAQ.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
- [How do I hide the coder/coder promotion in Help: Getting Started?](#how-do-i-hide-the-codercoder-promotion-in-help-getting-started)
4141
- [How do I disable the proxy?](#how-do-i-disable-the-proxy)
4242
- [How do I disable file download?](#how-do-i-disable-file-download)
43+
- [Can I use the Agents Window?](#can-i-use-the-agents-window)
4344
- [Why do web views not work?](#why-do-web-views-not-work)
4445

4546
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -542,6 +543,40 @@ when using the option.
542543

543544
You can pass the flag `--disable-file-downloads` to `code-server`
544545

546+
## Can I use the Agents Window?
547+
548+
Not yet, but you can enable the agent host it runs on.
549+
550+
VS Code 1.132 added the [Agents
551+
Window](https://code.visualstudio.com/docs/agents/run/agents-window), opened on
552+
the desktop with `code --agents`. It is built from `vs/sessions`, a workbench
553+
that is separate from the regular one, and Code only bundles that workbench into
554+
its `vscode-web` build (the one behind vscode.dev). The server build code-server
555+
ships, `vscode-reh-web`, does not include it, so there is no Agents Window to
556+
serve yet.
557+
558+
What the server build does include is the agent host, the process that actually
559+
runs agent sessions. Code starts it only when it is told where the host should
560+
listen, and code-server does not tell it by default. Pass `--agents` to change
561+
that:
562+
563+
```console
564+
code-server --agents
565+
```
566+
567+
The agent host then listens on `agent-host.sock` inside your user data directory
568+
(a named pipe on Windows), and Code registers the channel the browser uses to
569+
reach it. Agent sessions show up in the regular chat UI rather than in their own
570+
window.
571+
572+
Two caveats:
573+
574+
- Anyone who can read that socket can talk to the agent host, so keep your user
575+
data directory private. It is not exposed over HTTP.
576+
- Code resolves the Claude and Codex SDKs from `product.agentSdks`, which its
577+
build pipeline only writes into the desktop server build. Those agents are
578+
unavailable unless you point Code at a local SDK root yourself.
579+
545580
## Why do web views not work?
546581

547582
Web views rely on service workers, and service workers are only available in a

src/node/cli.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { field, Level, logger } from "@coder/logger"
2+
import * as crypto from "crypto"
23
import { promises as fs } from "fs"
34
import { load } from "js-yaml"
45
import * as path from "path"
@@ -91,6 +92,7 @@ export interface UserProvidedArgs extends UserProvidedCodeArgs {
9192
"reuse-window"?: boolean
9293
"new-window"?: boolean
9394
"ignore-last-opened"?: boolean
95+
agents?: boolean
9496
verbose?: boolean
9597
"app-name"?: string
9698
"welcome-text"?: string
@@ -288,6 +290,16 @@ export const options: Options<Required<UserProvidedArgs>> = {
288290
short: "r",
289291
description: "Force to open a file or folder in an already opened window.",
290292
},
293+
// Named after Code's own --agents so the flags line up once we can serve the
294+
// Agents Window itself. That needs Code's `vs/sessions` bundle, which its
295+
// build only adds to the vscode-web entry points, not to the server build we
296+
// package (see build/gulpfile.reh.ts in the submodule).
297+
agents: {
298+
type: "boolean",
299+
description:
300+
"Start Code's agent host so agent sessions can run on the server. Code's dedicated Agents Window is not \n" +
301+
"bundled for the server yet, so agents surface in the regular chat UI rather than in their own window.",
302+
},
291303

292304
log: { type: LogLevel },
293305
verbose: { type: "boolean", short: "vvv", description: "Enable verbose logging." },
@@ -907,6 +919,22 @@ export interface CodeArgs extends UserProvidedCodeArgs {
907919
"without-browser-env-var"?: boolean
908920
compatibility?: string
909921
log?: string[]
922+
"agent-host-path"?: string
923+
}
924+
925+
/**
926+
* Where Code's agent host should listen when --agents is set.
927+
*
928+
* Windows has no Unix sockets so use a named pipe there instead. The path is
929+
* derived from the user data directory since that is what separates concurrent
930+
* instances from each other.
931+
*/
932+
export function agentHostSocketPath(userDataDir: string): string {
933+
if (process.platform === "win32") {
934+
const hash = crypto.createHash("sha1").update(userDataDir).digest("hex").substring(0, 16)
935+
return `\\\\.\\pipe\\code-server-agent-host-${hash}`
936+
}
937+
return path.join(userDataDir, "agent-host.sock")
910938
}
911939

912940
/**
@@ -921,5 +949,10 @@ export const toCodeArgs = async (args: DefaultedArgs): Promise<CodeArgs> => {
921949
version: !!args.version,
922950
port: args.port?.toString(),
923951
log: args.log ? [args.log] : undefined,
952+
// Telling Code where the agent host should listen is what makes it spawn
953+
// one. It also registers the channel the workbench uses to reach the agent
954+
// host over the remote connection; without a path that channel is
955+
// registered as unavailable and agent sessions cannot connect.
956+
"agent-host-path": args.agents ? agentHostSocketPath(args["user-data-dir"]) : undefined,
924957
}
925958
}

src/node/main.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { field, logger } from "@coder/logger"
2+
import { promises as fs } from "fs"
23
import http from "http"
34
import * as os from "os"
45
import * as path from "path"
56
import { Disposable } from "../common/emitter"
67
import { plural } from "../common/util"
78
import { createApp, ensureAddress } from "./app"
8-
import { AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli"
9+
import { agentHostSocketPath, AuthType, DefaultedArgs, Feature, toCodeArgs, UserProvidedArgs } from "./cli"
910
import { commit, version, vsRootPath } from "./constants"
1011
import { loadCustomStrings } from "./i18n"
1112
import { register } from "./routes"
@@ -119,6 +120,27 @@ export const openInExistingInstance = async (args: DefaultedArgs, socketPath: st
119120
vscode.end()
120121
}
121122

123+
/**
124+
* Remove a leftover agent host socket.
125+
*
126+
* Code binds the socket but never unlinks it, so a socket left behind by a
127+
* killed instance makes the agent host fail to listen on the next start.
128+
* Named pipes on Windows are not files and disappear on their own.
129+
*/
130+
const removeStaleAgentHostSocket = async (socketPath: string): Promise<void> => {
131+
if (process.platform === "win32") {
132+
return
133+
}
134+
try {
135+
await fs.unlink(socketPath)
136+
logger.debug(`Removed stale agent host socket ${socketPath}`)
137+
} catch (error: any) {
138+
if (error.code !== "ENOENT") {
139+
logger.warn(`Could not remove agent host socket ${socketPath}: ${error.message}`)
140+
}
141+
}
142+
}
143+
122144
export const runCodeServer = async (
123145
args: DefaultedArgs,
124146
): Promise<{ dispose: Disposable["dispose"]; server: http.Server }> => {
@@ -139,6 +161,11 @@ export const runCodeServer = async (
139161
)
140162
}
141163

164+
// Must happen before Code can be loaded, which is on the first request.
165+
if (args.agents) {
166+
await removeStaleAgentHostSocket(agentHostSocketPath(args["user-data-dir"]))
167+
}
168+
142169
const app = await createApp(args)
143170
const protocol = args.cert ? "https" : "http"
144171
const serverAddress = ensureAddress(app.server, protocol)
@@ -197,6 +224,10 @@ export const runCodeServer = async (
197224
if (args["skip-auth-preflight"]) {
198225
logger.info(" - Skipping authentication for preflight requests")
199226
}
227+
if (args.agents) {
228+
logger.info(` - Agent host enabled on ${agentHostSocketPath(args["user-data-dir"])}`)
229+
logger.info(" - Code's Agents Window is not bundled for the server; agents appear in the regular chat UI")
230+
}
200231
if (process.env.VSCODE_PROXY_URI) {
201232
logger.info(`Using proxy URI in PORTS tab: ${process.env.VSCODE_PROXY_URI}`)
202233
}

test/unit/node/cli.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
setDefaults,
1111
shouldOpenInExistingInstance,
1212
toCodeArgs,
13+
agentHostSocketPath,
1314
optionDescriptions,
1415
options,
1516
Options,
@@ -114,6 +115,8 @@ describe("parser", () => {
114115

115116
"--skip-auth-preflight",
116117

118+
"--agents",
119+
117120
["--session-socket", "/tmp/override-code-server-ipc-socket"],
118121

119122
["--reconnection-grace-time", "86400"],
@@ -157,6 +160,7 @@ describe("parser", () => {
157160
"reconnection-grace-time": "86400",
158161
"abs-proxy-base-path": "/codeserver/app1",
159162
"skip-auth-preflight": true,
163+
agents: true,
160164
})
161165
})
162166

@@ -984,6 +988,7 @@ describe("toCodeArgs", () => {
984988
port: "8080",
985989
version: false,
986990
log: undefined,
991+
"agent-host-path": undefined,
987992
}
988993

989994
const testName = "vscode-args"
@@ -1006,6 +1011,25 @@ describe("toCodeArgs", () => {
10061011
_: [file],
10071012
})
10081013
})
1014+
1015+
it("should tell Code where to run the agent host", async () => {
1016+
expect(await toCodeArgs(await setDefaults(parse(["--agents"])))).toStrictEqual({
1017+
...vscodeDefaults,
1018+
agents: true,
1019+
"agent-host-path": agentHostSocketPath(paths.data),
1020+
})
1021+
})
1022+
1023+
it("should derive the agent host socket from the user data directory", async () => {
1024+
if (process.platform === "win32") {
1025+
expect(agentHostSocketPath("C:\\one")).toMatch(/^\\\\\.\\pipe\\code-server-agent-host-[0-9a-f]{16}$/)
1026+
} else {
1027+
expect(agentHostSocketPath("/one")).toBe(path.join("/one", "agent-host.sock"))
1028+
}
1029+
// Concurrent instances have separate user data directories, so their agent
1030+
// hosts must not land on the same socket.
1031+
expect(agentHostSocketPath("/one")).not.toBe(agentHostSocketPath("/two"))
1032+
})
10091033
})
10101034

10111035
describe("optionDescriptions", () => {

0 commit comments

Comments
 (0)