Skip to Content
Native IntegrationWorkspace Controller

Workspace Controller

The phone asks the desktop to switch which workspace Cursor / VS Code has open. Destructive in the sense that it reloads the IDE window — handle accordingly in UI.

Capability

workspace_activation

Without this capability the server rejects open_workspace with errorCode: 'capability_not_negotiated'. Native clients negotiate it only when they actually expose a workspace-switch UI.

Wire shapes

Client → Server

{ "type": "open_workspace", "requestId": "r-1", "traceId": "t-...", "path": "/root/projects/another-repo", "mode": "reuseWindow" | "newWindow", "source": "pwa" | "native-ios" | "native-android" // free-form telemetry hint }

Server → Client

// Acknowledgement BEFORE invoke (see "Ack-before-invoke" below) { "type": "workspace_opening", "requestId": "r-1", "traceId": "t-...", "path": "/root/projects/another-repo", "realPath": "/root/projects/another-repo", // canonical realpath "mode": "reuseWindow", "seqId": N } // Failure (validator rejected, or invoke failed) { "type": "workspace_open_failed", "requestId": "r-1", "traceId": "t-...", "path": "/root/projects/another-repo", // SUBMITTED path, NOT realpath "reason": "outside_allowed_roots" | "invalid_path" | "capability_not_negotiated" | <other>, "detail": "Optional human-readable detail", "seqId": N } // Implicit success signal — the NEXT `connected` payload after Cursor reloads // carries the new workspacePath. There is NO explicit `workspace_opened`. { "type": "connected", "workspacePath": "/root/projects/another-repo", ... }

Two modes — different reliability properties

reuseWindow

  • Server validates the target, sends workspace_opening ACK.
  • Server invokes vscode.openFolder(uri, { forceReuseWindow: true }).
  • The current Cursor window reloads. The extension host dies and reactivates in the new workspace.
  • The phone’s WebSocket disconnects with meaning: 'abnormal_closure'.
  • The phone reconnects. The next connected payload’s workspacePath is the authoritative success signal.

newWindow

  • Server invokes vscode.openFolder(uri, { forceNewWindow: true }).
  • The original window’s extension stays alive — the phone keeps controlling it.
  • A new Cursor window opens, but its extension cannot bind port 18100 (EADDRINUSE) — that new window is NOT controllable from the phone in today’s protocol.
  • The dialog should time out (15 s recommended) and the user should manually focus the new window on the desktop.

This asymmetry is a real limitation. Don’t promise multi-window control on newWindow until the architecture supports it (recent diagnostic findings on multi-window EADDRINUSE in docs/architecture/2026-05-20-system-architecture in the source repo for context).

Ack-before-invoke pattern

The extension’s MessageHandler sends workspace_opening BEFORE invoking vscode.openFolder. This is non-negotiable for reuseWindow because the extension dies within milliseconds of the invoke. If the ack hasn’t been sent yet, the client never knows the operation started.

The native client should:

  1. Send open_workspace.
  2. Wait for workspace_opening OR workspace_open_failed (with a 15 s timeout).
  3. If workspace_opening arrived and mode was reuseWindow, expect a disconnect within 1–2 s — show “Opening…” UI.
  4. After reconnect, watch the next connected.workspacePath. If it matches the realpath from workspace_opening, success. If not, the open didn’t actually take.

What native UI should render

  • Open Workspace dialog:
    • Path input or picker (constrained to entries the server reports as allowed — see list_project_roots / list_projects).
    • Mode selector: reuseWindow / newWindow.
    • “Open” button.
  • Pending state: after sending open_workspace, show “Opening…” with a spinner and a 15 s timeout.
  • Success: dialog closes when the post-reload connected.workspacePath matches. Top of app updates to show the new workspace.
  • Failure: render reason (open errorCode) + detail (human string). Don’t try to recover automatically; let the user retry or cancel.

Path validation

The server enforces projectRoots (an allowlist of directory prefixes) and a system-paths blocklist. The native client should not mirror this allowlist locally — let the server reject, and surface the reason as data. Mirrored allowlists drift; server-side allowlists don’t.

Multi-window risk to surface in UI

Per recent investigation, opening a second Cursor window (intentionally or via newWindow) creates a state where the new window’s extension cannot bind port 18100. If the phone is in local mode and connected to that second window, the phone shows “Offline.”

Native clients should:

  • Surface the environment field from connected so the user can see which workstation they’re controlling.
  • Surface workspacePath in the header so the user can see which workspace.
  • Be tolerant of disconnects after open_workspace — they are expected for reuseWindow, not a bug.

What native UI must NOT do

  • ❌ Open a workspace by manipulating files directly (e.g., writing a .code-workspace file). Only open_workspace is the supported path.
  • ❌ Try to “auto-recover” from workspace_open_failed by retrying with a different mode. Show the error; let the user choose.
  • ❌ Cache realPath and assume it’s stable across server restarts. Always trust the latest connected.workspacePath.

Reference implementation