VS Code IDE Daemon Adapter
Overview
packages/vscode-ide-companion/src/services/daemonIdeConnection.ts is the VS Code extension’s daemon adapter. It lets the IDE companion connect to a running qwen serve daemon over HTTP + SSE instead of launching an in-process qwen --acp stdio child (the legacy AcpConnectionState path). It is the sibling-transport equivalent of 14-cli-tui-adapter.md for VS Code hosts.
The IDE’s chat webview consumes daemon events through this adapter; permission prompts surface as native VS Code quick-pick dialogs.
Responsibilities
- Construct a
DaemonClient+DaemonSessionClientfrom a loopback-validatedbaseUrlpassed toconnect(options). - Pump SSE events from the session client into per-callback dispatch (
onSessionUpdate,onPermissionRequest,onAskUserQuestion,onEndTurn,onDisconnected). - Enforce a loopback-only invariant in
connect(options)(the IDE should only ever connect to a daemon on the same host). - Bridge daemon events into webview
postMessages so the chat panel stays in sync. - Surface permission requests through VS Code’s native quick-pick UI.
- Serialize calls into a queue so a rapid double-
connect()from the host does not race.
Architecture
Public surface
class DaemonIdeConnection {
connect(options: DaemonIdeConnectionOptions): Promise<void>;
disconnect(): Promise<void>;
sendPrompt(prompt: string | ContentBlock[]): Promise<DaemonIdePromptResult>;
cancelSession(): Promise<void>;
setModel(modelId: string): Promise<DaemonIdeSetModelResult>;
onSessionUpdate: (data: SessionNotification) => void;
onPermissionRequest: (
data: RequestPermissionRequest,
) => Promise<{ optionId?: string }>;
onAskUserQuestion: (data: AskUserQuestionRequest) => Promise<{
optionId: string;
answers?: Record<string, string>;
}>;
onEndTurn: (reason?: string) => void;
onDisconnected: (code: number | null, signal: string | null) => void;
}
interface DaemonIdeConnectionOptions {
baseUrl: string; // MUST be loopback (127.0.0.1 / localhost / [::1])
token?: string;
workspaceCwd?: string;
modelServiceId?: string;
lastEventId?: number;
sessionFactory?: DaemonIdeSessionFactory;
}Loopback validation
In connectInternal():
const baseUrl = validateDaemonBaseUrl(options.baseUrl);This is a client-side hard constraint distinct from the daemon’s own hostAllowlist (see 12-auth-security.md). The IDE companion will never connect to a remote daemon — even if the operator configured one. Rationale: VS Code’s threat model assumes the workspace and the daemon share the same host, including filesystem trust and related assumptions.
createSdkDaemonSessionFactory()
createSdkDaemonSessionFactory() constructs DaemonClient and calls
DaemonSessionClient.createOrAttach() from @qwen-code/sdk. The connection
class holds the factory rather than instantiating directly so tests can inject a
fake.
Event dispatch
The connection runs one SSE consumer (for await over session.events()) and routes each event by type:
| Daemon event / source | IDE callback / action |
|---|---|
session_update | onSessionUpdate |
Normal permission_request | onPermissionRequest, then respondToPermission() |
permission_request where toolCall.kind === 'ask_user_question' and rawInput.questions is an array | onAskUserQuestion, then forward answers to the daemon |
session_died with a payload sessionId matching the current session | onDisconnected(null, reason) |
SSE natural end / stream failure / manual disconnect() | onDisconnected(null, 'stream_ended' / 'daemon_error' / 'disconnected') |
| Other daemon events | Debug-level log; no IDE callback today. |
onEndTurn is not produced by SSE dispatch. sendPrompt() waits for the daemon
HTTP prompt response and calls it with response.stopReason; non-abort
exception paths call onEndTurn('error').
Webview bridging
The connection class is transport-only. The actual VS Code integration lives in packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts (and friends). The provider subscribes to the connection’s callbacks and translates them into webview postMessage calls. The webview embeds Web Shell for rendering.
Connect serialization
connect() uses an internal queue so a rapid double call from the host (e.g. user opens the panel twice during an in-flight handshake) does not race. The second call awaits the first; the connection ends up in a single, deterministic state.
Workflow
Initial connect
Permission via quick-pick
Disconnect / recover
State & Lifecycle
- Construction is synchronous; no network I/O until
connect(options). connect()is idempotent through the internal queue; calling twice serializes.disconnect()aborts the SSE iterator (AbortControlleron the pump) and clears callback registrations.lastEventIdis captured from the SDK’sDaemonSessionClienton disconnect and can be re-supplied on the nextconnect()for resume.
Dependencies
packages/sdk-typescript/src/daemon/—DaemonClient,DaemonSessionClient(the actual transport).- VS Code extension API (
vscode.*) — host APIs, quick-pick, webview. packages/web-shell/client/— embedded webview rendering for daemon session events.
Configuration
| Knob | Where | Effect |
|---|---|---|
baseUrl | connect(options) | Daemon URL; must be loopback. |
token | connect(options) | Bearer token (stamped via SDK). |
workspaceCwd | connect(options) | Used on POST /session; must match the daemon’s primary workspace or a registered multi-workspace session runtime. |
modelServiceId | connect(options) / setModel() | Initial model. |
lastEventId | connect(options) | Resume cursor (typically restored from host state). |
VS Code setting qwen.ide.daemonUrl (or equivalent) | Workspace settings | Operator-configured daemon URL. |
Caveats & Known Limits
- Loopback-only — hard refusal in
connect(options). Operators who want to point the IDE at a remote daemon need to use SSH port-forward / local proxy; the adapter will not connect to a non-loopback URL. - The legacy
AcpConnectionStatepath is still primary in the IDE companion (stdio child). This adapter is the sibling-transport for Mode-B migration; see../daemon-client-adapters/ide.mdfor the migration blockers and the plannedBridgeFileSystemparity work. - No reverse RPC or editor-affordance surface yet over HTTP. Features that require the agent to call back into the IDE (e.g. read-only buffer access, diff preview integration) currently live only on the stdio path.
- Webview ↔ connection coupling is host-owned, not in this adapter. Do not push webview-specific logic into
DaemonIdeConnection. workspaceCwdmismatch with the daemon’s registered workspaces returns400 workspace_mismatch— surface this as a clear setup error rather than retrying.
References
packages/vscode-ide-companion/src/services/daemonIdeConnection.tspackages/vscode-ide-companion/src/services/daemonIdeConnection.ts(createSdkDaemonSessionFactory)packages/vscode-ide-companion/src/types/connectionTypes.ts(legacyAcpConnectionState)packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts(webview bridge)packages/web-shell/client/(embedded Web Shell renderer)- Draft design:
../daemon-client-adapters/ide.md - SDK reference:
13-sdk-daemon-client.md