Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions extensions/vscode/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"name": "@coder/vscode",
"displayName": "Coder Agent",
"version": "0.1.0",
"description": "Coder Agent VS Code Extension — AI coding assistant in your editor",
"publisher": "coder",
"private": true,
"license": "Apache-2.0",
"main": "./dist/extension/extension.js",
"engines": {
"vscode": "^1.95.0"
},
"categories": [
"Chat",
"AI",
"Development"
],
"activationEvents": [
"onCommand:coder.chat.start"
],
"contributes": {
"commands": [
{
"command": "coder.chat.start",
"title": "Coder: Start Chat"
},
{
"command": "coder.chat.newSession",
"title": "Coder: New Session"
}
],
"configuration": {
"title": "Coder Agent",
"properties": {
"coder.model": {
"type": "string",
"default": "",
"description": "Model to use for the agent"
},
"coder.provider": {
"type": "string",
"enum": [
"anthropic",
"openai",
"deepseek",
"auto"
],
"default": "auto",
"description": "LLM provider"
},
"coder.permissionMode": {
"type": "string",
"enum": [
"plan",
"ask",
"auto"
],
"default": "ask",
"description": "Permission mode for tool execution"
}
}
}
},
"scripts": {
"build": "webpack --mode production --config webpack.config.mjs",
"dev": "webpack --mode development --watch --config webpack.config.mjs",
"clean": "rm -rf dist *.tsbuildinfo",
"package": "vsce package",
"lint": "eslint src --ext .ts,.tsx"
},
"dependencies": {
"@coder/bridge": "workspace:*",
"@coder/core": "workspace:*",
"@coder/provider": "workspace:*",
"@coder/shared": "workspace:*",
"@coder/tools": "workspace:*",
"katex": "^0.17.0",
"marked": "^18.0.4",
"preact": "^10.26.0",
"tiktoken": "^1.0.22"
},
"devDependencies": {
"@types/vscode": "^1.95.0",
"css-loader": "^7.1.0",
"html-webpack-plugin": "^5.6.0",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.0",
"typescript": "^5.8.0",
"webpack": "^5.97.0",
"webpack-cli": "^6.0.0"
}
}
Binary file added extensions/vscode/resources/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions extensions/vscode/src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { ExtensionContext } from 'vscode';
import { window, commands } from 'vscode';
import { WebviewManager } from './webviewManager';

let webviewManager: WebviewManager | null = null;

function sendTheme(): void {
const kind = window.activeColorTheme.kind === 2 || window.activeColorTheme.kind === 3
? 'dark' : 'light';
webviewManager?.postMessage({ type: 'themeChange', kind });
}

export function activate(context: ExtensionContext): void {
webviewManager = new WebviewManager(context);

context.subscriptions.push(
commands.registerCommand('coder.chat.start', () => {
webviewManager?.show();
sendTheme();
}),
commands.registerCommand('coder.chat.newSession', async () => {
const gw = await webviewManager?.getGateway();
gw?.createSession();
window.showInformationMessage('New Coder session created.');
}),
window.onDidChangeActiveColorTheme(() => sendTheme()),
);
}

export function deactivate(): void {
if (webviewManager) {
webviewManager.dispose();
webviewManager = null;
}
}
195 changes: 195 additions & 0 deletions extensions/vscode/src/extension/webviewManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/**
* webviewManager.ts — Manages the VS Code WebviewPanel lifecycle
*
* Creates and manages the chat webview panel. Handles message routing
* between the webview and the gateway client.
*/

import type { ExtensionContext, WebviewPanel } from 'vscode';
import { window, ViewColumn, Uri, workspace } from 'vscode';
import type { WebviewOutboundMessage, WebviewInboundMessage } from '../types/webviewProtocol';
import type { VSCodeGatewayClient } from '../gateway/vsCodeGateway';

export class WebviewManager {
private panel: WebviewPanel | null = null;
private gateway: VSCodeGatewayClient | null = null;
private context: ExtensionContext;

constructor(context: ExtensionContext) {
this.context = context;
}

show(): void {
if (this.panel) {
this.panel.reveal(ViewColumn.Beside);
return;
}

this.panel = window.createWebviewPanel(
'coderChat',
'Coder Agent',
ViewColumn.Beside,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
Uri.joinPath(this.context.extensionUri, 'dist', 'webview'),
],
},
);

this.panel.iconPath = Uri.joinPath(
this.context.extensionUri,
'resources',
'icon.png',
);

// Load webview HTML
const webviewUri = Uri.joinPath(
this.context.extensionUri,
'dist',
'webview',
'index.html',
);
const html = this.getHtmlContent(webviewUri);
this.panel.webview.html = html;

// Handle messages from webview
this.panel.webview.onDidReceiveMessage(
(msg: WebviewInboundMessage) => {
this.handleWebviewMessage(msg);
},
null,
this.context.subscriptions,
);

// Clean up on dispose
this.panel.onDidDispose(
() => {
this.panel = null;
this.gateway?.dispose();
this.gateway = null;
},
null,
this.context.subscriptions,
);
}

async getGateway(): Promise<VSCodeGatewayClient> {
if (!this.gateway) {
const { VSCodeGatewayClient } = await import('../gateway/vsCodeGateway');
this.gateway = new VSCodeGatewayClient((msg: WebviewOutboundMessage) => this.postMessage(msg));
}
return this.gateway;
}

async createSession(): Promise<void> {
(await this.getGateway()).createSession();
}

postMessage(msg: WebviewOutboundMessage): void {
this.panel?.webview.postMessage(msg);
}

dispose(): void {
this.gateway?.dispose();
this.gateway = null;
this.panel?.dispose();
this.panel = null;
}

private async handleWebviewMessage(msg: WebviewInboundMessage): Promise<void> {
// webviewReady: respond immediately without waiting for gateway
if (msg.type === 'webviewReady') {
this.postMessage({
type: 'statusUpdate',
status: 'ready',
message: 'Connected!',
sessionId: '',
});
return;
}

try {
const gw = await this.getGateway();

switch (msg.type) {
case 'submitPrompt':
gw.submitPrompt(msg.text);
break;
case 'interrupt':
gw.interrupt();
break;
case 'approvalRespond':
gw.handleApproval(msg.requestId, msg.allowed);
break;
case 'newSession':
gw.createSession();
break;
case 'selectSession':
gw.resumeSession(msg.sessionId);
break;
case 'openFile': {
const fileUri = Uri.joinPath(workspace.workspaceFolders?.[0]?.uri ?? Uri.file(this.context.extensionUri.fsPath), msg.path);
workspace.openTextDocument(fileUri).then(
(doc) => window.showTextDocument(doc),
() => window.showErrorMessage(`File not found: ${msg.path}`),
);
break;
}
case 'listSessions': {
const sessions = gw.listSessions();
this.postMessage({
type: 'sessionList',
sessions: sessions.map((s) => ({
id: s.id,
title: s.title,
messageCount: s.turnCount,
startedAt: s.createdAt,
})),
});
break;
}
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.postMessage({
type: 'errorMessage',
message: `Gateway error: ${message}`,
});
}
}

private getHtmlContent(webviewUri: Uri): string {
// In production, webpack generates the HTML via HtmlWebpackPlugin.
// For development, we serve a minimal page that loads the webview bundle.
const scriptUri = Uri.joinPath(
this.context.extensionUri,
'dist',
'webview',
'webview.js',
);
const webviewScript = this.panel!.webview.asWebviewUri(scriptUri);

const csp = `
default-src 'none';
style-src ${this.panel!.webview.cspSource} 'unsafe-inline';
script-src ${this.panel!.webview.cspSource};
font-src ${this.panel!.webview.cspSource};
`.replace(/\s+/g, ' ').trim();

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="${csp}">
<title>Coder Agent</title>
</head>
<body>
<div id="root"></div>
<script src="${webviewScript}"></script>
</body>
</html>`;
}
}
Loading