Skip to Content
Developer GuideDaemon16 · VS Code IDE Daemon Adapter

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 + DaemonSessionClient from a loopback-validated baseUrl passed to connect(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 / sourceIDE callback / action
session_updateonSessionUpdate
Normal permission_requestonPermissionRequest, then respondToPermission()
permission_request where toolCall.kind === 'ask_user_question' and rawInput.questions is an arrayonAskUserQuestion, then forward answers to the daemon
session_died with a payload sessionId matching the current sessiononDisconnected(null, reason)
SSE natural end / stream failure / manual disconnect()onDisconnected(null, 'stream_ended' / 'daemon_error' / 'disconnected')
Other daemon eventsDebug-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 itself uses the shared packages/webui/ component library to render — see Adapter Matrix in 01-architecture.md.

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 (AbortController on the pump) and clears callback registrations.
  • lastEventId is captured from the SDK’s DaemonSessionClient on disconnect and can be re-supplied on the next connect() for resume.

Dependencies

  • packages/sdk-typescript/src/daemon/DaemonClient, DaemonSessionClient (the actual transport).
  • VS Code extension API (vscode.*) — host APIs, quick-pick, webview.
  • packages/webui/src/adapters/ACPAdapter.ts — webview rendering of ACP-shaped messages relayed via postMessage.

Configuration

KnobWhereEffect
baseUrlconnect(options)Daemon URL; must be loopback.
tokenconnect(options)Bearer token (stamped via SDK).
workspaceCwdconnect(options)Used on POST /session; must match the daemon’s bound workspace.
modelServiceIdconnect(options) / setModel()Initial model.
lastEventIdconnect(options)Resume cursor (typically restored from host state).
VS Code setting qwen.ide.daemonUrl (or equivalent)Workspace settingsOperator-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 AcpConnectionState path is still primary in the IDE companion (stdio child). This adapter is the sibling-transport for Mode-B migration; see ../daemon-client-adapters/ide.md for the migration blockers and the planned BridgeFileSystem parity 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.
  • workspaceCwd mismatch with the daemon’s bound workspace returns 400 workspace_mismatch — surface this as a clear setup error rather than retrying.

References

  • packages/vscode-ide-companion/src/services/daemonIdeConnection.ts
  • packages/vscode-ide-companion/src/services/daemonIdeConnection.ts (createSdkDaemonSessionFactory)
  • packages/vscode-ide-companion/src/types/connectionTypes.ts (legacy AcpConnectionState)
  • packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts (webview bridge)
  • packages/webui/src/adapters/ACPAdapter.ts (webview ACP-message adapter)
  • Draft design: ../daemon-client-adapters/ide.md
  • SDK reference: 13-sdk-daemon-client.md
Last updated on