Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/node/cli.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -288,6 +290,16 @@ export const options: Options<Required<UserProvidedArgs>> = {
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." },
Expand Down Expand Up @@ -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")
}

/**
Expand All @@ -921,5 +949,10 @@ export const toCodeArgs = async (args: DefaultedArgs): Promise<CodeArgs> => {
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,
}
}
33 changes: 32 additions & 1 deletion src/node/main.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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<void> => {
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 }> => {
Expand All @@ -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)
Expand Down Expand Up @@ -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}`)
}
Expand Down
24 changes: 24 additions & 0 deletions test/unit/node/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
setDefaults,
shouldOpenInExistingInstance,
toCodeArgs,
agentHostSocketPath,
optionDescriptions,
options,
Options,
Expand Down Expand Up @@ -114,6 +115,8 @@ describe("parser", () => {

"--skip-auth-preflight",

"--agents",

["--session-socket", "/tmp/override-code-server-ipc-socket"],

["--reconnection-grace-time", "86400"],
Expand Down Expand Up @@ -157,6 +160,7 @@ describe("parser", () => {
"reconnection-grace-time": "86400",
"abs-proxy-base-path": "/codeserver/app1",
"skip-auth-preflight": true,
agents: true,
})
})

Expand Down Expand Up @@ -984,6 +988,7 @@ describe("toCodeArgs", () => {
port: "8080",
version: false,
log: undefined,
"agent-host-path": undefined,
}

const testName = "vscode-args"
Expand All @@ -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", () => {
Expand Down