Continue.dev GUI Integration Guide

Document Version: 1.0
Based on Continue.dev: v1.4.46 (October 2025) / main branch
Repository: https://github.com/continuedev/continue
Last Updated: October 9, 2025


Document Overview

This guide explains how to reuse Continue.dev's production-quality chat GUI in your own VS Code extension by implementing their message-passing protocol architecture. Continue.dev was specifically designed with a modular architecture where the GUI communicates with the backend purely through well-defined message protocols, making the GUI genuinely reusable across different implementations.

Key insight: Continue.dev already supports both VS Code and JetBrains IDEs using the same GUI codebase, proving the architecture's portability.


Architecture Overview

Continue.dev uses a three-layer message-passing architecture:

┌─────────────────────────────────────┐
│  GUI (React + Redux)                │
│  - Runs in VS Code webview          │
│  - Built separately with Vite       │
│  - Located in gui/ folder           │
└──────────────┬──────────────────────┘
               │ Webview postMessage
┌──────────────┴──────────────────────┐
│  Extension (VS Code API)            │
│  - Hosts webview                    │
│  - Routes messages                  │
│  - Implements IDE interface         │
└──────────────┬──────────────────────┘
               │ JSON-RPC / stdio
┌──────────────┴──────────────────────┐
│  Core (Business Logic)              │
│  - LLM interactions                 │
│  - Configuration                    │
│  - Context providers                │
└─────────────────────────────────────┘

For your use case, you'd replace Core with your ACP agent:

┌─────────────────────────────────────┐
│  Continue GUI (unchanged)           │
└──────────────┬──────────────────────┘
               │ Continue protocols
┌──────────────┴──────────────────────┐
│  YOUR Extension (adapter layer)     │
└──────────────┬──────────────────────┘
               │ Your interface
┌──────────────┴──────────────────────┐
│  YOUR Core → ACP Agent              │
└─────────────────────────────────────┘

Architecture Source Files


Protocol Messages

The protocols are defined in core/protocol/ and consist of four message types:

1. ToWebviewFromIdeProtocol (Extension → GUI)

Messages the extension sends to the GUI.

Key protocol file: core/protocol/ide.ts

// Common messages you'll need to handle:
{
  messageType: "configUpdate",
  data: { config: SerializedContinueConfig }
}

{
  messageType: "configError", 
  data: { message: string }
}

{
  messageType: "newSessionWithPrompt",
  data: { prompt: string }
}

{
  messageType: "addContextItem",
  data: {
    item: ContextItem,
    historyIndex: number
  }
}

// Streaming LLM response
{
  messageType: "llmStreamChunk",
  data: {
    chunk: ChatMessage,
    index: number
  }
}

References:

2. ToIdeFromWebviewProtocol (GUI → Extension)

Messages the GUI sends that you need to handle.

Key protocol file: core/protocol/webview.ts

// User sends a message
{
  messageType: "userInput",
  data: {
    input: string,
    contextItems: ContextItem[]
  }
}

// User selects a context provider (@file, @code, etc.)
{
  messageType: "loadContextProvider",
  data: {
    name: string,
    params: any
  }
}

// User stops generation
{
  messageType: "stopGeneration",
  data: {}
}

// User changes model
{
  messageType: "setModel",
  data: {
    model: string
  }
}

// Request for file contents
{
  messageType: "readRangeInFile",
  data: {
    filepath: string,
    range: { start: number, end: number }
  }
}

References:

3. ToCoreFromWebviewProtocol (GUI → Core)

These go through the extension first, so you'd intercept and handle them:

{
  messageType: "sendChatMessage",
  data: {
    message: string,
    modelTitle: string
  }
}

References:

4. ToWebviewFromCoreProtocol (Core → GUI)

Messages you'd send from your ACP adapter back to the GUI:

// Stream LLM tokens
{
  messageType: "llmStream",
  data: {
    content: string,
    done: boolean,
    index: number
  }
}

// Tool/function call
{
  messageType: "toolCall",
  data: {
    toolCallId: string,
    name: string,
    arguments: object
  }
}

Key Data Types

ContextItem

Type definition: core/index.d.ts

interface ContextItem {
  name: string;
  description: string;
  content: string;
  id?: string;
  editing?: boolean;
  editable?: boolean;
}

ChatMessage

Type definition: core/index.d.ts

interface ChatMessage {
  role: "user" | "assistant" | "system";
  content: string | Array<{ type: string; text?: string; imageUrl?: string }>;
  toolCalls?: ToolCall[];
}

SerializedContinueConfig

Type definition: core/index.d.ts

interface SerializedContinueConfig {
  models: Array<{
    title: string;
    provider: string;
    model: string;
    apiKey?: string;
  }>;
  contextProviders?: ContextProviderWithParams[];
  slashCommands?: SlashCommandDescription[];
  // ... many other optional fields
}

Full type definitions: core/index.d.ts


Webview Integration - The Nitty Gritty

Step 1: Create Webview Panel

Reference implementation: extensions/vscode/src/ContinueGUIWebviewViewProvider.ts

import * as vscode from 'vscode';
import * as path from 'path';

export class ContinueGuiProvider implements vscode.WebviewViewProvider {
  private _view?: vscode.WebviewView;

  constructor(
    private readonly _extensionUri: vscode.Uri,
    private readonly _guiDistPath: string
  ) {}

  public resolveWebviewView(
    webviewView: vscode.WebviewView,
    context: vscode.WebviewViewResolveContext,
    _token: vscode.CancellationToken
  ) {
    this._view = webviewView;

    webviewView.webview.options = {
      enableScripts: true,
      localResourceRoots: [
        vscode.Uri.file(this._guiDistPath)
      ]
    };

    // Load the Continue GUI HTML
    webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);

    // Set up message handlers
    this._setupMessageHandlers(webviewView.webview);
  }

  private _getHtmlForWebview(webview: vscode.Webview): string {
    // Path to the Continue GUI dist folder
    const scriptUri = webview.asWebviewUri(
      vscode.Uri.file(path.join(this._guiDistPath, 'assets', 'index.js'))
    );
    const styleUri = webview.asWebviewUri(
      vscode.Uri.file(path.join(this._guiDistPath, 'assets', 'index.css'))
    );

    // Generate nonce for CSP
    const nonce = getNonce();

    return `<!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="Content-Security-Policy" content="
        default-src 'none'; 
        style-src ${webview.cspSource} 'unsafe-inline';
        script-src 'nonce-${nonce}';
        connect-src https:;
      ">
      <link href="${styleUri}" rel="stylesheet">
      <title>Continue</title>
    </head>
    <body>
      <div id="root"></div>
      <script nonce="${nonce}">
        window.windowId = "${getNonce()}";
        window.serverUrl = "http://localhost:3000"; // Your server if needed
        window.vscMediaUrl = "${webview.asWebviewUri(vscode.Uri.file(this._guiDistPath))}";
        window.ide = "vscode";
      </script>
      <script nonce="${nonce}" src="${scriptUri}"></script>
    </body>
    </html>`;
  }

  private _setupMessageHandlers(webview: vscode.Webview) {
    // Listen for messages FROM the webview
    webview.onDidReceiveMessage(
      async (message) => {
        await this.handleWebviewMessage(message);
      }
    );
  }

  // Send messages TO the webview
  public postMessage(message: any): void {
    this._view?.webview.postMessage(message);
  }

  private async handleWebviewMessage(message: any): Promise<void> {
    const { messageType, data } = message;

    switch (messageType) {
      case "userInput":
        await this.handleUserInput(data);
        break;
        
      case "loadContextProvider":
        await this.handleContextProvider(data);
        break;
        
      case "stopGeneration":
        this.stopCurrentGeneration();
        break;
        
      case "setModel":
        this.setModel(data.model);
        break;

      // ... handle other message types
    }
  }

  private async handleUserInput(data: any): Promise<void> {
    const { input, contextItems } = data;
    
    // This is where you'd call YOUR ACP agent
    // Send the prompt to your ACP agent and stream responses back
    
    // Example: Send to ACP agent
    const response = await this.yourAcpClient.sendMessage({
      prompt: input,
      context: contextItems
    });

    // Stream tokens back to GUI
    for await (const chunk of response) {
      this.postMessage({
        messageType: "llmStreamChunk",
        data: {
          chunk: { role: "assistant", content: chunk },
          index: 0
        }
      });
    }

    // Signal completion
    this.postMessage({
      messageType: "llmStreamChunk",
      data: {
        chunk: { role: "assistant", content: "" },
        index: 0,
        done: true
      }
    });
  }
}

function getNonce(): string {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < 32; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

Reference files:

Step 2: Register in activate()

Reference implementation: extensions/vscode/src/activation/activate.ts

export function activate(context: vscode.ExtensionContext) {
  // Path to Continue's GUI build output
  const guiDistPath = path.join(context.extensionPath, 'continue-gui', 'dist');

  const provider = new ContinueGuiProvider(
    context.extensionUri,
    guiDistPath
  );

  // Register the webview provider
  context.subscriptions.push(
    vscode.window.registerWebviewViewProvider(
      'continue.continueGUIView',
      provider,
      {
        webviewOptions: {
          retainContextWhenHidden: true
        }
      }
    )
  );
}

Step 3: package.json Configuration

Reference: extensions/vscode/package.json

{
  "contributes": {
    "viewsContainers": {
      "activitybar": [
        {
          "id": "continue",
          "title": "Continue",
          "icon": "media/icon.svg"
        }
      ]
    },
    "views": {
      "continue": [
        {
          "type": "webview",
          "id": "continue.continueGUIView",
          "name": "Continue"
        }
      ]
    }
  }
}

Building the Continue GUI

Continue's GUI is built separately with Vite.

Build configuration: gui/vite.config.ts

# In the Continue repo
cd gui
npm install
npm run build

# Output goes to gui/dist
# Copy this to your extension:
cp -r gui/dist your-extension/continue-gui/dist

Build scripts: gui/package.json


Protocol Adapter Pattern

The key abstraction is creating a thin adapter:

class AcpToContinueAdapter {
  constructor(
    private acpClient: YourAcpClient,
    private webviewProvider: ContinueGuiProvider
  ) {}

  async sendChatMessage(message: string, contextItems: ContextItem[]) {
    // Translate Continue format → ACP format
    const acpRequest = {
      prompt: message,
      context: contextItems.map(item => ({
        content: item.content,
        name: item.name
      }))
    };

    // Send to ACP agent
    const stream = await this.acpClient.streamChat(acpRequest);

    // Translate ACP responses → Continue format
    for await (const chunk of stream) {
      this.webviewProvider.postMessage({
        messageType: "llmStreamChunk",
        data: {
          chunk: { role: "assistant", content: chunk.content },
          index: 0
        }
      });
    }
  }
}

Source Code Reference Map

Core Architecture

GUI Layer

VS Code Extension

Communication Flow

Context Providers


No Helper Libraries (Yet)

Continue doesn't currently provide a standalone library for hosting their GUI - you need to:

  1. Clone their repo and build the GUI
  2. Copy the GUI dist to your extension
  3. Implement the protocol handlers yourself
  4. Host the webview using VS Code's standard webview API

The good news: their protocol is well-defined and message-passing makes this clean.


Summary

What you get:

  • Production-quality React chat UI
  • Message history
  • Context provider system (@file, @code, etc.)
  • Model selection UI
  • Stop generation button
  • Streaming support

What you build:

  • ~300-400 lines of adapter code
  • Protocol message handlers
  • Integration with your ACP client
  • Webview hosting boilerplate

Reusability: Very high - the GUI is truly decoupled via message passing, just as advertised!


Additional Resources

  • Main Repository: https://github.com/continuedev/continue
  • Documentation: https://docs.continue.dev
  • Discord Community: https://discord.gg/vapESyrFmJ
  • Contributing Guide: CONTRIBUTING.md
  • Architecture Deep Dive: DeepWiki Documentation

Version History

VersionDateChanges
1.0Oct 9, 2025Initial document based on Continue.dev v1.4.46

License: This guide is provided for educational purposes. Continue.dev is licensed under Apache 2.0. See their LICENSE file for details.