Skip to content

feat: Sidecar; mobile bridge for Copilot Chat on-the-go access#5073

Open
Davidobot wants to merge 3 commits intomicrosoft:mainfrom
Davidobot:sidecar
Open

feat: Sidecar; mobile bridge for Copilot Chat on-the-go access#5073
Davidobot wants to merge 3 commits intomicrosoft:mainfrom
Davidobot:sidecar

Conversation

@Davidobot
Copy link
Copy Markdown

Summary

Adds Sidecar, a lightweight orchestration layer that lets a phone connect to and interact with any active Copilot Chat session running on the developer's desktop, without adding relay infrastructure or requiring a native phone app.

The feature works by:

  1. Starting a local HTTP + WebSocket bridge server on localhost.
  2. Bootstrapping a public endpoint over Azure Dev Tunnels (devtunnel host -p <port> --allow-anonymous).
  3. Generating a QR-code pairing URL that opens a static PWA (pwa/) on the phone. The URL is signed with a short-lived session token.
  4. The phone PWA syncs the conversation list, history, streaming assistant chunks, and forwards prompts back to the desktop via the bridge.

The status bar item $(debug-disconnect) Sidecar in the bottom-right corner is the control surface — clicking it starts the bridge and opens the pairing panel.

⚠️ Prototype / RFC — not ready to merge as-is

This PR is a working prototype submitted to start a design conversation. Three things would need to be resolved before it could land:

  1. Personal artefacts — hardcoded davidobot.net URLs and personal copyright headers in new files need to be stripped or transferred to Microsoft ownership.
  2. Fork-only infrastructureREADME.md rewrites, .github/workflows/deploy-pwa.yml, prune.js, TODOs.md, and the pwa/ directory are specific to the fork's release workflow. Upstream would need to decide whether to own the PWA deployment or drop it entirely.
  3. Product / UX scope — Microsoft would need to take ownership of PWA hosting, devtunnel policy, and mobile UX decisions. Given the ~10k line diff, a design discussion / issue before review would be appropriate.

The auth noise-reduction fixes (languageModelAccess.ts, claudeCodeModels.ts, copilotCli.ts, octoKitServiceImpl.ts) are independent of Sidecar and could land in a separate small PR.


Motivation

  • Resume desktop context, not just repository context. Continue the exact repo / branch / chat thread already open on desktop.
  • Local-first state. Interact with unpushed changes, local files, and the real host machine.
  • No relay server. The bridge is a plain HTTP/WebSocket server on localhost; the only cloud hop is the dev-tunnel endpoint that is under the developer's own Azure account.
  • Useful for short pick-up flows. Check agent progress, answer a confirmation, or continue a thread from a phone without switching context.

Changes

New platform layer (src/platform/bridge/)

File Description
bridgeServer.ts In-extension HTTP + WebSocket server. Handles client connections, token authentication, session lifecycle, and typed message dispatch. All message types are defined here as discriminated unions (BridgeMessage).
conversationBridge.ts Connects the bridge server to the rest of the extension. Reads conversation history from IConversationStore, fans out streaming assistant turn events to connected phone clients in real time, and routes inbound phone prompts into the VS Code chat API as first-class requests. Also enumerates Claude / Copilot CLI / Codex / Cloud sessions to build a unified conversation list.
bridge/test/ Unit tests for the chat renderer serialization (chatRenderer.spec.ts) and the full conversation pipeline (conversationBridgePipeline.spec.ts, ~1200 lines).

New extension layer (src/extension/conversation/vscode-node/)

File Description
sidecarContribution.ts Main contribution that owns the status bar item, the webview pairing panel (QR code + URL input), dev-tunnel lifecycle, and the github.copilot.sidecar.showPanel command. Handles both direct-tunnel and VS Code tunnel fallback scenarios.
mobileMirrorContribution.ts Thin contribution that mirrors the current active chat session to the bridge so the phone always shows the live view. Listens to VS Code chat session events and forwards them.

New conversation store (src/extension/conversationStore/node/conversationStore.ts)

Provides IConversationStore — a cross-provider session registry that:

  • Tracks turn history, status, and metadata for Copilot Chat, Claude Code, Copilot CLI, Codex, and Cloud sessions.
  • Fires typed events for every turn artefact (markdown chunks, progress items, tool invocations, confirmations, references, code citations, question carousels, command buttons, etc.).
  • Persists summaries across window reloads via VS Code's global secret + extension storage.
  • Is queried by ConversationBridge to replay history to newly connected phone clients.

New PWA (pwa/)

A self-contained static web app (vanilla JS + CSS, no build step required) designed for GitHub Pages deployment:

  • index.html + manifest.json + sw.js — installable PWA shell.
  • js/ws-client.js — WebSocket client with exponential-backoff reconnect.
  • js/chat-renderer.js — streaming markdown renderer with syntax highlighting, tool-call collapsing, confirmation dialogs, and question-carousel support.
  • js/app.js — application logic: conversation list, history view, prompt composer, model/mode picker.
  • js/qr-scanner.js — QR scan shim for deep-link pairing URLs.

A local dev server (script/pwaDevServer.mjs) is also included for testing without deploying to Pages.

Modifications to existing files

src/extension/prompt/node/chatParticipantRequestHandler.ts

  • ChatParticipantRequestHandler now calls IConversationStore.reportUserTurn / reportAssistantTurn as it processes requests, so the store has a live view of every in-flight turn.
  • Added structural type guards for ChatResponseMultiDiffPart (avoids importing a proposed API type that is not re-exported through vscodeTypes).

src/extension/extension/vscode-node/contributions.ts

  • Registers SidecarContribution in the node-only contribution list.

Auth noise reduction (independent of Sidecar, no functional change in normal use)

Small fixes that reduce log noise when GitHub sign-in is not yet complete (e.g., during cold start):

  • languageModelAccess.ts: deduplicate GitHubLoginFailed log lines; guard _logService.error from non-Error values.
  • claudeCodeModels.ts / copilotCli.ts: catch expected unauthenticated fetch errors and log at debug instead of error.
  • octoKitServiceImpl.ts (getAllSessions, getCopilotAgentModels): return [] instead of re-throwing PermissiveAuthRequiredError when no auth token is available.

Testing

Unit tests

npm run test:unit

New test files:

  • src/platform/bridge/test/node/chatRenderer.spec.ts — serialization round-trip for every response-part type.
  • src/platform/bridge/test/node/conversationBridgePipeline.spec.ts — end-to-end pipeline: user turn → assistant streaming → phone client payload.

Manual testing checklist

  1. Install Azure Dev Tunnels CLI and sign in: devtunnel user login
  2. Run Compile & Launch Extension Host. Sign in to Copilot. Confirm $(debug-disconnect) Sidecar in the status bar.
  3. Click Sidecar → pairing panel opens with QR code.
  4. Scan from a phone (or open the URL in a mobile browser).
  5. Validate bidirectional sync:
    • Desktop prompt → appears on phone.
    • Phone prompt → appears in desktop chat and gets a Copilot response.
    • Streaming assistant chunks render live on phone.
    • Status bar transitions disconnected → active after phone connects.
  6. Validate reconnect: drop and restore phone network → reconnecting then connected; conversation list refreshes.
  7. Validate auth-noise reduction: restart extension host before signing in → no error-level log lines for GitHubLoginFailed.

Architecture

flowchart TD
    subgraph ext["VS Code Extension Host"]
        SC[SidecarContribution]
        BS[BridgeServer]
        CB[ConversationBridge]
        CS[IConversationStore]
        CH[ChatParticipantRequestHandler]

        SC --> BS
        BS --> CB
        CB --> CS
        CH --> CS
    end

    DT["https://*.devtunnels.ms\n(Dev Tunnel)"] -->|public endpoint| BS

    PWA["Phone PWA\n• conversation list\n• history sync\n• streaming UI"]

    PWA <-->|WebSocket| BS
    PWA -->|phone prompt| CH
Loading

Notes for reviewers

  • ws dependency: added as a runtime dep. If the team prefers a different transport, BridgeServer is behind a clean interface.
  • PWA hosting: the pwa/ directory is a zero-build-step static site. It can live here or in a separate repo. A GitHub Actions workflow for Pages deployment is included.
  • Token security: BridgeServer uses a 32-byte cryptographically random token (crypto.randomBytes) embedded in the QR pairing URL. Only the person who scanned the code can connect.
  • VSIX size: prune.js removes pwa/ from the packaged extension bundle; the PWA is served from the Pages deployment, not the VSIX.

Copilot AI review requested due to automatic review settings April 15, 2026 09:09
@Davidobot
Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Introduces a Sidecar prototype that bridges desktop Copilot Chat sessions to a mobile PWA via a local HTTP/WebSocket server and Dev Tunnels, while also reducing auth-related log noise and adding a persisted conversation store.

Changes:

  • Added bridge infrastructure (BridgeServer) and mobile pairing UI (SidecarContribution) plus a static phone PWA (served externally).
  • Introduced IConversationStore with typed turn/artifact events and persisted conversation summaries.
  • Reduced error-level logging for expected unauthenticated states across auth/model/session flows.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 23 comments.

Show a summary per file
File Description
src/platform/bridge/bridgeServer.ts Adds the HTTP + WebSocket bridge server and message contracts used by Sidecar.
src/extension/conversation/vscode-node/sidecarContribution.ts Implements status bar control + pairing webview + devtunnel bootstrap for Sidecar.
src/extension/conversationStore/node/conversationStore.ts Adds a cross-provider conversation store with eventing and persistence for summaries/artifacts.
src/extension/prompt/node/chatParticipantRequestHandler.ts Reports user/assistant streaming events into IConversationStore for mirroring.
pwa/** Adds a static PWA client that connects to the bridge and renders chat + artifacts.
script/pwaDevServer.mjs Adds a simple local dev server for the PWA.
src/platform/github/common/octoKitServiceImpl.ts Returns empty results instead of throwing when permissive auth is required.
src/platform/authentication/vscode-node/copilotTokenManager.ts Deduplicates “GitHub login failed” logging/telemetry.
src/extension/conversation/vscode-node/languageModelAccess.ts Deduplicates missing-auth logs and avoids logging non-Error values as errors.
src/extension/chatSessions/**/ Downgrades expected unauthenticated model fetch failures to debug logs.
.vscode/tasks.json / package.json Adds PWA dev task and packaging/install convenience tasks/scripts.
prune.js Adds a script that rewrites Sidecar source during pruning.
README.md / TODOs.md / .github/workflows/deploy-pwa.yml Adds fork/prototype-specific docs and Pages deployment workflow.

Comment on lines +631 to +634
private handleHttpRequest(_request: IncomingMessage, response: ServerResponse): void {
response.statusCode = 200;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
response.end(`<!doctype html>
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bridge’s HTTP endpoint renders the session token in plaintext. If the dev tunnel endpoint is reachable (or logs proxy responses), this leaks the pairing credential. Recommendation (mandatory): remove the token from the HTTP response (or gate it behind explicit local-only checks / debug flag), and consider returning only a minimal health response (e.g., 200 + signature) without secrets.

Copilot uses AI. Check for mistakes.
<h1>Copilot Sidecar Bridge</h1>
<div class="card">
<p><span class="key">Connected clients:</span> <span class="value">${this.clients.size}</span></p>
<p><span class="key">Session token:</span> <span class="value">${this._sessionToken}</span></p>
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bridge’s HTTP endpoint renders the session token in plaintext. If the dev tunnel endpoint is reachable (or logs proxy responses), this leaks the pairing credential. Recommendation (mandatory): remove the token from the HTTP response (or gate it behind explicit local-only checks / debug flag), and consider returning only a minimal health response (e.g., 200 + signature) without secrets.

Suggested change
<p><span class="key">Session token:</span> <span class="value">${this._sessionToken}</span></p>
<p>Bridge endpoint is running.</p>

Copilot uses AI. Check for mistakes.
</main>
</body>
</html>`);
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bridge’s HTTP endpoint renders the session token in plaintext. If the dev tunnel endpoint is reachable (or logs proxy responses), this leaks the pairing credential. Recommendation (mandatory): remove the token from the HTTP response (or gate it behind explicit local-only checks / debug flag), and consider returning only a minimal health response (e.g., 200 + signature) without secrets.

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +357
const SESSION_TOKEN_BYTES = 16;

function createSessionToken(): string {
return randomBytes(SESSION_TOKEN_BYTES).toString('hex');
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description states a 32-byte token, but the implementation uses 16 bytes (128-bit) producing 32 hex chars. Either update SESSION_TOKEN_BYTES to 32 to match the stated security posture, or adjust the PR/docs to reflect the actual token size.

Copilot uses AI. Check for mistakes.
Comment on lines +616 to +628
const payload = JSON.stringify(message);
for (const client of this.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(payload);
}
}
}

private sendToClient(client: WebSocket, message: BridgeMessage): void {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(JSON.stringify(message));
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single client.send(...) can throw (or error asynchronously) and take down the extension host, especially with flaky mobile connections. Recommendation (mandatory): wrap sends in try/catch, and on failure remove/close the client (or queue error handling) to keep the bridge resilient.

Suggested change
const payload = JSON.stringify(message);
for (const client of this.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(payload);
}
}
}
private sendToClient(client: WebSocket, message: BridgeMessage): void {
if (client.readyState !== WebSocket.OPEN) {
return;
}
client.send(JSON.stringify(message));
for (const client of this.clients) {
this.sendToClient(client, message);
}
}
private handleClientSendFailure(client: WebSocket): void {
if (this.clients.delete(client)) {
this._onDidClientCountChange.fire(this.clients.size);
}
try {
client.close();
} catch {
// Ignore close errors after send failure; the client has already been removed.
}
}
private sendToClient(client: WebSocket, message: BridgeMessage): void {
if (client.readyState !== WebSocket.OPEN) {
return;
}
const payload = JSON.stringify(message);
try {
client.send(payload, error => {
if (error) {
this.handleClientSendFailure(client);
}
});
} catch {
this.handleClientSendFailure(client);
}

Copilot uses AI. Check for mistakes.
private getAssistantTurnArtifactsKey(conversationId: string, turnId: string): string {
return `${conversationId}${TURN_ARTIFACTS_KEY_SEPARATOR}${turnId}`;
}

Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The persistence timer isn’t cleared on dispose, which can keep work scheduled after shutdown and potentially write during teardown. Recommendation (mandatory): clear persistTimer in dispose() (and optionally flush a final persist) to avoid dangling timers and ensure deterministic shutdown.

Suggested change
public override dispose(): void {
if (this.persistTimer !== undefined) {
clearTimeout(this.persistTimer);
this.persistTimer = undefined;
}
super.dispose();
}

Copilot uses AI. Check for mistakes.
Comment on lines +734 to +741
if (this.persistTimer !== undefined) {
clearTimeout(this.persistTimer);
}
this.persistTimer = setTimeout(() => {
this.persistTimer = undefined;
void this.persistSummaries();
}, PERSIST_DEBOUNCE_MS);
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The persistence timer isn’t cleared on dispose, which can keep work scheduled after shutdown and potentially write during teardown. Recommendation (mandatory): clear persistTimer in dispose() (and optionally flush a final persist) to avoid dangling timers and ensure deterministic shutdown.

Copilot uses AI. Check for mistakes.
Comment thread script/pwaDevServer.mjs
Comment on lines +35 to +37
function isPathInsideRoot(filePath) {
return filePath === pwaRoot || filePath.startsWith(`${pwaRoot}/`);
}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path containment check is not cross-platform: on Windows resolve(...) yields backslashes, so startsWith(${pwaRoot}/) will fail and may incorrectly reject valid paths. Recommendation (mandatory): use path.relative(pwaRoot, filePath) and verify it does not start with .. and is not absolute, or normalize separators consistently.

Copilot uses AI. Check for mistakes.
Comment thread prune.js
@@ -0,0 +1,27 @@
const fs = require('fs');
let code = fs.readFileSync('src/extension/conversation/vscode-node/sidecarContribution.ts', 'utf8');
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script rewrites a TypeScript source file in-place using regex transforms. That is brittle (easy to desync with refactors), makes builds non-reproducible, and can silently produce broken TypeScript. Recommendation (mandatory): avoid modifying tracked source files during packaging; implement pruning via packaging config/excludes, build-time transforms that write to an output directory, or feature-flag/conditional compilation at runtime.

Copilot uses AI. Check for mistakes.
Comment thread prune.js
Comment on lines +23 to +27
for (const r of regexes) {
code = code.replace(r, '\n');
}

fs.writeFileSync('src/extension/conversation/vscode-node/sidecarContribution.ts', code);
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script rewrites a TypeScript source file in-place using regex transforms. That is brittle (easy to desync with refactors), makes builds non-reproducible, and can silently produce broken TypeScript. Recommendation (mandatory): avoid modifying tracked source files during packaging; implement pruning via packaging config/excludes, build-time transforms that write to an output directory, or feature-flag/conditional compilation at runtime.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants