diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts
index be5e7b4..17aefd8 100644
--- a/apps/desktop/electron/main.ts
+++ b/apps/desktop/electron/main.ts
@@ -120,8 +120,8 @@ ipcMain.handle('mcp:list', async () => {
ipcMain.handle('skills:list', async () => {
const skills = await loadSkills({ cwd: process.cwd(), home: homedir() });
return skills.map((s) => ({
- name: s.name,
- description: s.description,
+ name: s.qualifiedName,
+ description: s.frontmatter.description,
source: s.source,
path: s.path,
}));
diff --git a/apps/lsp/README.md b/apps/lsp/README.md
new file mode 100644
index 0000000..1a49bb9
--- /dev/null
+++ b/apps/lsp/README.md
@@ -0,0 +1,106 @@
+# @deepcode/lsp — LSP bridge (v1.1)
+
+Exposes DeepCode's agent loop as Language-Server-Protocol commands, so
+any LSP-capable editor (Neovim, Emacs lsp-mode, Sublime, JetBrains via
+LSP plugin) can drive DeepCode via `workspace/executeCommand`.
+
+## Custom commands
+
+| Command | Args | Returns |
+| ---------------------- | ------------------------ | -------------------------------------- |
+| `deepcode.runAgent` | `{ prompt: string }` | `{ turnId: string }` + streams events |
+| `deepcode.abort` | `{ turnId: string }` | `{ aborted: boolean }` |
+| `deepcode.listSkills` | none | `{ skills: SkillRow[] }` |
+
+Streamed events are sent as `deepcode/agentEvent` notifications:
+
+```json
+{ "jsonrpc": "2.0", "method": "deepcode/agentEvent",
+ "params": { "turnId": "lsp-...", "kind": "text_delta", "text": "..." } }
+```
+
+The `kind` field mirrors the AgentStreamEvent union from
+`@deepcode/core/src/ipc/protocol.ts` (started / text_delta / tool_use /
+tool_result / usage / turn_complete / turn_done / error).
+
+## Install & run
+
+```bash
+pnpm install
+pnpm --filter @deepcode/lsp build
+# After publish:
+npx deepcode-lsp
+# Or run from source:
+node apps/lsp/dist/server.js
+```
+
+## Editor configuration
+
+### Neovim (with nvim-lspconfig)
+
+```lua
+local lspconfig = require('lspconfig')
+local configs = require('lspconfig.configs')
+
+if not configs.deepcode then
+ configs.deepcode = {
+ default_config = {
+ cmd = { 'deepcode-lsp' },
+ filetypes = { '*' },
+ root_dir = lspconfig.util.find_git_ancestor,
+ single_file_support = true,
+ },
+ }
+end
+lspconfig.deepcode.setup({})
+
+-- Bind a key to run the agent on the visual selection:
+vim.api.nvim_create_user_command('DeepCodeRun', function(opts)
+ vim.lsp.buf.execute_command({
+ command = 'deepcode.runAgent',
+ arguments = { { prompt = opts.args } },
+ })
+end, { nargs = 1 })
+```
+
+### Emacs (lsp-mode)
+
+```elisp
+(with-eval-after-load 'lsp-mode
+ (lsp-register-client
+ (make-lsp-client
+ :new-connection (lsp-stdio-connection "deepcode-lsp")
+ :activation-fn (lambda (&rest _) t)
+ :server-id 'deepcode-lsp)))
+```
+
+### Sublime Text (LSP package)
+
+In `Preferences → Package Settings → LSP → Settings`:
+
+```json
+{
+ "clients": {
+ "deepcode": {
+ "enabled": true,
+ "command": ["deepcode-lsp"],
+ "selector": "source"
+ }
+ }
+}
+```
+
+## Architecture
+
+- Pure stdio LSP server. Framing: `Content-Length: N\r\n\r\n
`.
+- Notifications (no `id`) silently dropped if unknown.
+- Requests (with `id`) errored with `-32603` if unknown method.
+- Agent loop runs in-process; long turns spawn a child to keep the LSP
+ loop responsive (TODO in v1.1-rest).
+
+## Skeleton vs ready-to-ship
+
+This release ships the protocol skeleton (3 commands, 4 LSP boilerplate
+handlers, stream events). The actual `runAgent` invocation emits a
+placeholder event to confirm the channel — wiring to the real
+`@deepcode/core` agent loop lands with the v1.1 release.
diff --git a/apps/lsp/package.json b/apps/lsp/package.json
new file mode 100644
index 0000000..1caa113
--- /dev/null
+++ b/apps/lsp/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "@deepcode/lsp",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Language-Server-Protocol bridge — exposes DeepCode actions to any LSP-capable IDE.",
+ "license": "MIT",
+ "type": "module",
+ "bin": {
+ "deepcode-lsp": "dist/server.js"
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "vitest run",
+ "clean": "rm -rf dist *.tsbuildinfo"
+ },
+ "dependencies": {
+ "@deepcode/core": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.0",
+ "typescript": "^5.7.0",
+ "vitest": "^2.1.9"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+}
diff --git a/apps/lsp/src/handler.test.ts b/apps/lsp/src/handler.test.ts
new file mode 100644
index 0000000..42c68ca
--- /dev/null
+++ b/apps/lsp/src/handler.test.ts
@@ -0,0 +1,124 @@
+import { describe, expect, it } from 'vitest';
+import { handleMessage, type LspMessage } from './handler.js';
+
+describe('handleMessage — initialize', () => {
+ it('returns capabilities + serverInfo + supported commands', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ {
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'initialize',
+ params: { rootUri: 'file:///tmp/x' },
+ },
+ (m) => out.push(m),
+ );
+ expect(out).toHaveLength(1);
+ const r = out[0]!.result as {
+ capabilities: { executeCommandProvider: { commands: string[] } };
+ serverInfo: { name: string };
+ };
+ expect(r.serverInfo.name).toBe('deepcode-lsp');
+ expect(r.capabilities.executeCommandProvider.commands).toContain('deepcode.runAgent');
+ expect(r.capabilities.executeCommandProvider.commands).toContain('deepcode.abort');
+ expect(r.capabilities.executeCommandProvider.commands).toContain('deepcode.listSkills');
+ });
+});
+
+describe('handleMessage — executeCommand', () => {
+ it('returns a turnId for deepcode.runAgent and streams events', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ {
+ jsonrpc: '2.0',
+ id: 2,
+ method: 'workspace/executeCommand',
+ params: { command: 'deepcode.runAgent', arguments: [{ prompt: 'hi' }] },
+ },
+ (m) => out.push(m),
+ );
+ // Synchronous: started event + reply
+ expect(out.some((m) => m.method === 'deepcode/agentEvent')).toBe(true);
+ const reply = out.find((m) => m.id === 2);
+ expect(reply).toBeDefined();
+ expect((reply!.result as { turnId: string }).turnId).toMatch(/^lsp-/);
+ // Async: wait a tick for the setImmediate-emitted events
+ await new Promise((r) => setImmediate(r));
+ const events = out.filter((m) => m.method === 'deepcode/agentEvent');
+ expect(events.length).toBeGreaterThanOrEqual(3);
+ const kinds = events.map(
+ (e) => (e.params as { kind: string }).kind,
+ );
+ expect(kinds).toContain('text_delta');
+ expect(kinds).toContain('turn_done');
+ });
+
+ it('errors on missing prompt', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ {
+ jsonrpc: '2.0',
+ id: 3,
+ method: 'workspace/executeCommand',
+ params: { command: 'deepcode.runAgent', arguments: [{}] },
+ },
+ (m) => out.push(m),
+ );
+ expect(out[0]!.error).toBeDefined();
+ expect(out[0]!.error!.message).toMatch(/prompt is required/);
+ });
+
+ it('deepcode.abort returns false for unknown turnId', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ {
+ jsonrpc: '2.0',
+ id: 4,
+ method: 'workspace/executeCommand',
+ params: { command: 'deepcode.abort', arguments: [{ turnId: 'no-such' }] },
+ },
+ (m) => out.push(m),
+ );
+ expect((out[0]!.result as { aborted: boolean }).aborted).toBe(false);
+ });
+
+ it('errors on unknown command', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ {
+ jsonrpc: '2.0',
+ id: 5,
+ method: 'workspace/executeCommand',
+ params: { command: 'evil.command', arguments: [] },
+ },
+ (m) => out.push(m),
+ );
+ expect(out[0]!.error).toBeDefined();
+ expect(out[0]!.error!.message).toMatch(/Unknown command/);
+ });
+});
+
+describe('handleMessage — unknown method', () => {
+ it('returns -32603 internal error', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage(
+ { jsonrpc: '2.0', id: 6, method: 'unknown/method' },
+ (m) => out.push(m),
+ );
+ expect(out[0]!.error).toBeDefined();
+ });
+});
+
+describe('handleMessage — notifications', () => {
+ it('silently drops unknown notification', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage({ jsonrpc: '2.0', method: 'unknown/notif' }, (m) => out.push(m));
+ expect(out).toHaveLength(0);
+ });
+
+ it('accepts initialized notification (no reply)', async () => {
+ const out: LspMessage[] = [];
+ await handleMessage({ jsonrpc: '2.0', method: 'initialized' }, (m) => out.push(m));
+ expect(out).toHaveLength(0);
+ });
+});
diff --git a/apps/lsp/src/handler.ts b/apps/lsp/src/handler.ts
new file mode 100644
index 0000000..c740eec
--- /dev/null
+++ b/apps/lsp/src/handler.ts
@@ -0,0 +1,178 @@
+// LSP message handler — dispatches JSON-RPC methods to DeepCode actions.
+// Separated from server.ts for testability.
+
+export interface LspMessage {
+ jsonrpc: '2.0';
+ id?: number | string | null;
+ method?: string;
+ params?: unknown;
+ result?: unknown;
+ error?: { code: number; message: string; data?: unknown };
+}
+
+export type SendFn = (msg: LspMessage) => void;
+
+interface ServerState {
+ initialized: boolean;
+ /** Workspace root URI from initialize. */
+ rootUri?: string;
+ /** In-flight turn IDs so /abort can cancel them. */
+ activeTurns: Set;
+}
+
+const state: ServerState = {
+ initialized: false,
+ activeTurns: new Set(),
+};
+
+const SERVER_INFO = {
+ name: 'deepcode-lsp',
+ version: '0.0.0',
+};
+
+export async function handleMessage(msg: LspMessage, send: SendFn): Promise {
+ // Notifications (no id) — no response expected.
+ if (msg.id === undefined || msg.id === null) {
+ await handleNotification(msg, send);
+ return;
+ }
+
+ try {
+ const result = await dispatch(msg, send);
+ send({ jsonrpc: '2.0', id: msg.id, result });
+ } catch (err) {
+ const e = err as Error;
+ send({
+ jsonrpc: '2.0',
+ id: msg.id,
+ error: { code: -32603, message: e.message },
+ });
+ }
+}
+
+async function handleNotification(msg: LspMessage, _send: SendFn): Promise {
+ switch (msg.method) {
+ case 'initialized':
+ state.initialized = true;
+ return;
+ case 'exit':
+ process.exit(state.initialized ? 0 : 1);
+ return;
+ default:
+ // Silently drop unknown notifications per LSP spec
+ return;
+ }
+}
+
+async function dispatch(msg: LspMessage, send: SendFn): Promise {
+ switch (msg.method) {
+ case 'initialize':
+ return handleInitialize(msg.params as { rootUri?: string });
+ case 'shutdown':
+ return null;
+ case 'workspace/executeCommand':
+ return handleExecuteCommand(msg.params as ExecuteCommandParams, send);
+ default:
+ throw new Error(`Method not supported: ${msg.method ?? ''}`);
+ }
+}
+
+function handleInitialize(params: { rootUri?: string }): unknown {
+ state.rootUri = params?.rootUri;
+ return {
+ capabilities: {
+ // We don't implement any LSP language features; we use the protocol
+ // as a transport for our custom commands.
+ executeCommandProvider: {
+ commands: ['deepcode.runAgent', 'deepcode.abort', 'deepcode.listSkills'],
+ },
+ textDocumentSync: 0,
+ },
+ serverInfo: SERVER_INFO,
+ };
+}
+
+interface ExecuteCommandParams {
+ command: string;
+ arguments?: unknown[];
+}
+
+async function handleExecuteCommand(
+ params: ExecuteCommandParams,
+ send: SendFn,
+): Promise {
+ switch (params.command) {
+ case 'deepcode.runAgent':
+ return handleRunAgent((params.arguments?.[0] ?? {}) as { prompt?: string }, send);
+ case 'deepcode.abort':
+ return handleAbort((params.arguments?.[0] ?? {}) as { turnId?: string });
+ case 'deepcode.listSkills':
+ return handleListSkills();
+ default:
+ throw new Error(`Unknown command: ${params.command}`);
+ }
+}
+
+async function handleRunAgent(
+ args: { prompt?: string },
+ send: SendFn,
+): Promise<{ turnId: string }> {
+ if (!args.prompt) throw new Error('prompt is required');
+ const turnId = `lsp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
+ state.activeTurns.add(turnId);
+
+ // Real impl: spawn @deepcode/core's runAgent + stream events back via
+ // notification. v1.1-rest wires it; here we emit a single ack event
+ // so clients can confirm the channel.
+ send({
+ jsonrpc: '2.0',
+ method: 'deepcode/agentEvent',
+ params: { turnId, kind: 'started', prompt: args.prompt },
+ });
+ // Schedule a fake completion event so the channel is exercised
+ setImmediate(() => {
+ send({
+ jsonrpc: '2.0',
+ method: 'deepcode/agentEvent',
+ params: {
+ turnId,
+ kind: 'text_delta',
+ text: '(LSP-skeleton — wire runAgent in v1.1-rest)',
+ },
+ });
+ send({
+ jsonrpc: '2.0',
+ method: 'deepcode/agentEvent',
+ params: { turnId, kind: 'turn_done', stopReason: 'end_turn' },
+ });
+ state.activeTurns.delete(turnId);
+ });
+
+ return { turnId };
+}
+
+function handleAbort(args: { turnId?: string }): { aborted: boolean } {
+ if (!args.turnId) throw new Error('turnId is required');
+ const had = state.activeTurns.delete(args.turnId);
+ return { aborted: had };
+}
+
+async function handleListSkills(): Promise<{ skills: unknown[] }> {
+ // Lazy import so server.ts type-checks without @deepcode/core resolved.
+ const { loadSkills } = await import('@deepcode/core');
+ const skills = await loadSkills({ cwd: process.cwd() });
+ return {
+ skills: skills.map((s) => ({
+ name: s.qualifiedName,
+ description: s.frontmatter.description,
+ source: s.source,
+ path: s.path,
+ })),
+ };
+}
+
+// Test exports
+export const __test = {
+ state,
+ dispatch,
+};
diff --git a/apps/lsp/src/server.ts b/apps/lsp/src/server.ts
new file mode 100644
index 0000000..2f66660
--- /dev/null
+++ b/apps/lsp/src/server.ts
@@ -0,0 +1,65 @@
+#!/usr/bin/env node
+// DeepCode LSP server — exposes the agent loop to any LSP-capable IDE
+// (Neovim, Emacs lsp-mode, Sublime, JetBrains via the LSP plugin, etc.).
+// Spec: docs/DEVELOPMENT_PLAN.md §v1.1
+//
+// Wire format: LSP base protocol (JSON-RPC 2.0 over stdio, framed with
+// `Content-Length: N\r\n\r\n`). We expose 3 custom commands under
+// `workspace/executeCommand`:
+//
+// · deepcode.runAgent — send a prompt; stream response back via
+// deepcode/agentEvent notification
+// · deepcode.abort — stop the active turn
+// · deepcode.listSkills — return SKILL.md metadata
+//
+// Plus standard LSP boilerplate (initialize / initialized / shutdown /
+// exit) so non-DeepCode-aware clients still handshake cleanly.
+
+import { handleMessage, type LspMessage } from './handler.js';
+
+function readMessages(onMessage: (msg: LspMessage) => void): void {
+ let buffer = Buffer.alloc(0);
+ process.stdin.on('data', (chunk: Buffer) => {
+ buffer = Buffer.concat([buffer, chunk]);
+ while (true) {
+ const headerEnd = buffer.indexOf('\r\n\r\n');
+ if (headerEnd === -1) return;
+ const header = buffer.subarray(0, headerEnd).toString('ascii');
+ const m = /content-length:\s*(\d+)/i.exec(header);
+ if (!m) {
+ process.stderr.write(`malformed LSP header: ${header}\n`);
+ buffer = buffer.subarray(headerEnd + 4);
+ continue;
+ }
+ const bodyLen = parseInt(m[1]!, 10);
+ const totalLen = headerEnd + 4 + bodyLen;
+ if (buffer.length < totalLen) return; // wait for more bytes
+ const body = buffer.subarray(headerEnd + 4, totalLen).toString('utf8');
+ buffer = buffer.subarray(totalLen);
+ try {
+ onMessage(JSON.parse(body) as LspMessage);
+ } catch (err) {
+ process.stderr.write(`malformed LSP body: ${(err as Error).message}\n`);
+ }
+ }
+ });
+}
+
+function sendMessage(msg: LspMessage): void {
+ const body = JSON.stringify(msg);
+ const buf = Buffer.from(body, 'utf8');
+ process.stdout.write(`Content-Length: ${buf.length}\r\n\r\n`);
+ process.stdout.write(buf);
+}
+
+function main(): void {
+ readMessages((msg) => {
+ void handleMessage(msg, sendMessage).catch((err: Error) => {
+ process.stderr.write(`handler crashed: ${err.message}\n${err.stack ?? ''}\n`);
+ });
+ });
+ process.on('SIGTERM', () => process.exit(0));
+ process.on('SIGINT', () => process.exit(0));
+}
+
+if (process.argv[1]?.includes('server')) main();
diff --git a/apps/lsp/tsconfig.json b/apps/lsp/tsconfig.json
new file mode 100644
index 0000000..d717494
--- /dev/null
+++ b/apps/lsp/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "composite": true,
+ "tsBuildInfoFile": "./dist/.tsbuildinfo",
+ "lib": ["ES2022"],
+ "types": ["node"],
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../../packages/core" }]
+}
diff --git a/apps/vscode/README.md b/apps/vscode/README.md
new file mode 100644
index 0000000..514a75e
--- /dev/null
+++ b/apps/vscode/README.md
@@ -0,0 +1,59 @@
+# @deepcode/vscode — DeepCode VS Code extension (v1.1)
+
+DeepSeek-powered coding agent inside VS Code. Same agent loop as the CLI
+and Mac client — Claude-Code parity.
+
+## Current state — v1.1 skeleton
+
+- `package.json` — extension manifest with 3 commands, configuration,
+ activity bar + chat view, default keybinding (`Cmd/Ctrl+Shift+D`).
+- `src/extension.ts` — activate / deactivate + Chat webview + 3 command
+ stubs. Uses lazy `require('vscode')` so the package type-checks without
+ `@types/vscode` installed.
+
+## Activate the extension toolchain
+
+```bash
+pnpm add -D --filter @deepcode/vscode @vscode/vsce @types/vscode
+```
+
+Then:
+
+| Command | Result |
+| --------------------------------------------- | --------------------------------------------------- |
+| `pnpm --filter @deepcode/vscode build` | Compile `src/extension.ts` → `dist/extension.cjs` |
+| `pnpm --filter @deepcode/vscode package` | Produce a `.vsix` file (vsce) |
+| Press F5 in VS Code with this folder open | Launch Extension Development Host |
+
+## Architecture
+
+- The extension runs in the VS Code **extension host** (Node process).
+- Talks directly to `@deepcode/core` — no IPC layer needed (the extension
+ host IS a Node runtime).
+- Long-running agent loops dispatch to a child process to avoid blocking
+ the host (TODO in v1.1-rest).
+
+## Commands
+
+| ID | Default keybinding | What it does |
+| -------------------- | -------------------------- | ------------------------------------------- |
+| `deepcode.openPanel` | `Cmd/Ctrl+Shift+D` | Reveal the DeepCode chat view |
+| `deepcode.run` | (palette) | Run agent on the selected text |
+| `deepcode.review` | (palette) | Run `code-review` skill on current diff |
+
+## Settings
+
+| Key | Type | Default | Notes |
+| ------------------ | -------- | ---------------------- | ----------------------------------------- |
+| `deepcode.apiKey` | string | `""` | Falls back to `~/.deepcode/credentials.json` |
+| `deepcode.model` | enum | `"deepseek-chat"` | Standard alias + concrete model names |
+| `deepcode.effort` | enum | `"medium"` | low / medium / high / xhigh / max |
+
+## Roadmap
+
+- Real `runAgent` invocation in `deepcode.run` (instead of the info popup)
+- Real diff fetch via `vscode.git` API for `deepcode.review`
+- File panel showing live edits as the agent works
+- Inline tool-approval prompts via QuickPick
+- Custom commands via skills (mirror CLI's `/skills` dir)
+- LSP-style command palette integration (see `@deepcode/lsp`)
diff --git a/apps/vscode/package.json b/apps/vscode/package.json
new file mode 100644
index 0000000..ceeb5c1
--- /dev/null
+++ b/apps/vscode/package.json
@@ -0,0 +1,84 @@
+{
+ "name": "@deepcode/vscode",
+ "displayName": "DeepCode",
+ "description": "DeepSeek-powered coding agent — Claude-Code parity inside VS Code.",
+ "version": "0.0.0",
+ "publisher": "deepcode",
+ "private": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=22",
+ "vscode": "^1.85.0"
+ },
+ "categories": [
+ "AI",
+ "Programming Languages",
+ "Other"
+ ],
+ "main": "./dist/extension.cjs",
+ "activationEvents": [
+ "onCommand:deepcode.openPanel",
+ "onCommand:deepcode.run",
+ "onStartupFinished"
+ ],
+ "contributes": {
+ "commands": [
+ { "command": "deepcode.openPanel", "title": "DeepCode: Open Panel" },
+ { "command": "deepcode.run", "title": "DeepCode: Run on selection" },
+ { "command": "deepcode.review", "title": "DeepCode: Review current diff" }
+ ],
+ "configuration": {
+ "title": "DeepCode",
+ "properties": {
+ "deepcode.apiKey": {
+ "type": "string",
+ "default": "",
+ "description": "DeepSeek API key. Leave empty to use ~/.deepcode/credentials.json.",
+ "scope": "machine-overridable"
+ },
+ "deepcode.model": {
+ "type": "string",
+ "default": "deepseek-chat",
+ "enum": ["deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro"],
+ "description": "DeepSeek model to use."
+ },
+ "deepcode.effort": {
+ "type": "string",
+ "default": "medium",
+ "enum": ["low", "medium", "high", "xhigh", "max"],
+ "description": "Effort tier (affects maxTokens + temperature)."
+ }
+ }
+ },
+ "viewsContainers": {
+ "activitybar": [
+ { "id": "deepcode", "title": "DeepCode", "icon": "media/icon.svg" }
+ ]
+ },
+ "views": {
+ "deepcode": [
+ { "id": "deepcode.chat", "name": "Chat" }
+ ]
+ },
+ "keybindings": [
+ { "command": "deepcode.openPanel", "key": "ctrl+shift+d", "mac": "cmd+shift+d" }
+ ]
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.json",
+ "typecheck": "tsc -p tsconfig.json --noEmit",
+ "test": "vitest run --passWithNoTests",
+ "package": "vsce package",
+ "clean": "rm -rf dist *.vsix *.tsbuildinfo"
+ },
+ "dependencies": {
+ "@deepcode/core": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.0",
+ "@types/vscode": "^1.85.0",
+ "typescript": "^5.7.0",
+ "vitest": "^2.1.9"
+ },
+ "//notes": "vsce + @types/vscode pull ~30 MB; install when ready to ship via `pnpm add -D --filter @deepcode/vscode @vscode/vsce @types/vscode`"
+}
diff --git a/apps/vscode/src/extension.ts b/apps/vscode/src/extension.ts
new file mode 100644
index 0000000..f8e3b99
--- /dev/null
+++ b/apps/vscode/src/extension.ts
@@ -0,0 +1,146 @@
+// VS Code extension entry — DeepCode "Chat" view + 3 commands.
+// Spec: docs/DEVELOPMENT_PLAN.md §v1.1 (VS Code extension)
+//
+// Build with: pnpm --filter @deepcode/vscode package (after vsce installed)
+// The extension talks to @deepcode/core directly — no IPC, because the
+// extension host is a Node process. For long-running agent loops we
+// dispatch to a separate child process; this skeleton uses in-process.
+
+import type * as vscode from 'vscode';
+
+// We import lazily so the file type-checks without @types/vscode installed.
+type V = typeof import('vscode');
+
+export async function activate(context: vscode.ExtensionContext): Promise {
+ const vscodeMod = await loadVscode();
+ const { commands, window, workspace } = vscodeMod;
+
+ // ── Commands ────────────────────────────────────────────────────────
+ context.subscriptions.push(
+ commands.registerCommand('deepcode.openPanel', () => {
+ void commands.executeCommand('workbench.view.extension.deepcode');
+ }),
+ commands.registerCommand('deepcode.run', async () => {
+ const editor = window.activeTextEditor;
+ if (!editor) {
+ void window.showInformationMessage('DeepCode: no active editor.');
+ return;
+ }
+ const selection = editor.document.getText(editor.selection);
+ if (!selection.trim()) {
+ void window.showInformationMessage('DeepCode: select some text first.');
+ return;
+ }
+ const prompt = await window.showInputBox({
+ prompt: 'Ask DeepCode about the selection',
+ value: 'Explain this code.',
+ });
+ if (!prompt) return;
+ await runHeadless(prompt, selection, vscodeMod);
+ }),
+ commands.registerCommand('deepcode.review', async () => {
+ // Pipe current diff through code-review skill via headless mode.
+ // Skeleton: just open a doc with the placeholder.
+ const doc = await workspace.openTextDocument({
+ content: '# DeepCode review\n\n(Real wiring lands in v1.1-rest — runs `deepcode -p "review this diff"` on git diff.)',
+ language: 'markdown',
+ });
+ await window.showTextDocument(doc);
+ }),
+ );
+
+ // ── Chat view provider ──────────────────────────────────────────────
+ context.subscriptions.push(
+ window.registerWebviewViewProvider('deepcode.chat', new ChatViewProvider(context)),
+ );
+}
+
+export function deactivate(): void {
+ /* no-op */
+}
+
+// ──────────────────────────────────────────────────────────────────────────
+// Headless run via @deepcode/core — same agent loop the CLI uses
+// ──────────────────────────────────────────────────────────────────────────
+
+async function runHeadless(prompt: string, selection: string, vscodeMod: V): Promise {
+ // Lazy import to avoid bundling @deepcode/core into the extension's
+ // activation path until we know the user actually triggered DeepCode.
+ // (Real implementation lands in v1.1-rest; this is the wire shape.)
+ const composed = `${prompt}\n\n----- Selected code -----\n${selection}`;
+ vscodeMod.window.showInformationMessage(
+ `DeepCode would now run a turn with: "${composed.slice(0, 80)}…"`,
+ );
+}
+
+class ChatViewProvider implements vscode.WebviewViewProvider {
+ constructor(private readonly _context: vscode.ExtensionContext) {}
+
+ resolveWebviewView(view: vscode.WebviewView): void {
+ view.webview.options = { enableScripts: true };
+ view.webview.html = chatHtml();
+ view.webview.onDidReceiveMessage((msg: unknown) => {
+ void this.handleMessage(view, msg as { kind: string; text?: string });
+ });
+ }
+
+ private async handleMessage(
+ view: vscode.WebviewView,
+ msg: { kind: string; text?: string },
+ ): Promise {
+ if (msg.kind === 'send' && msg.text) {
+ // Wire to runAgent in v1.1-rest.
+ view.webview.postMessage({
+ kind: 'assistant',
+ text: '(skeleton — chat IPC lands with the @deepcode/core in-extension-host wiring.)',
+ });
+ }
+ }
+}
+
+function chatHtml(): string {
+ return `
+
+
+
+
+`;
+}
+
+async function loadVscode(): Promise {
+ // VS Code injects this at extension activation time. Type-only import so
+ // the package builds without `@types/vscode` installed during M0 phase.
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ return require('vscode') as V;
+}
diff --git a/apps/vscode/tsconfig.json b/apps/vscode/tsconfig.json
new file mode 100644
index 0000000..1319e84
--- /dev/null
+++ b/apps/vscode/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "composite": true,
+ "tsBuildInfoFile": "./dist/.tsbuildinfo",
+ "lib": ["ES2022"],
+ "module": "CommonJS",
+ "moduleResolution": "node",
+ "types": ["node"],
+ "skipLibCheck": true,
+ "isolatedModules": false,
+ "noEmit": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../../packages/core" }]
+}
diff --git a/apps/vscode/vitest.config.ts b/apps/vscode/vitest.config.ts
new file mode 100644
index 0000000..533d53d
--- /dev/null
+++ b/apps/vscode/vitest.config.ts
@@ -0,0 +1,6 @@
+// No tests yet (extension runs in VS Code host; integration tests need
+// @vscode/test-electron which is heavy).
+export default {
+ test: { include: ['src/**/*.test.ts'] },
+ configFile: false,
+} as const;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e3ee56..3af61fe 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -83,6 +83,41 @@ importers:
specifier: ^2.1.9
version: 2.1.9(@types/node@22.19.19)
+ apps/lsp:
+ dependencies:
+ '@deepcode/core':
+ specifier: workspace:*
+ version: link:../../packages/core
+ devDependencies:
+ '@types/node':
+ specifier: ^22.10.0
+ version: 22.19.19
+ typescript:
+ specifier: ^5.7.0
+ version: 5.9.3
+ vitest:
+ specifier: ^2.1.9
+ version: 2.1.9(@types/node@22.19.19)
+
+ apps/vscode:
+ dependencies:
+ '@deepcode/core':
+ specifier: workspace:*
+ version: link:../../packages/core
+ devDependencies:
+ '@types/node':
+ specifier: ^22.10.0
+ version: 22.19.19
+ '@types/vscode':
+ specifier: ^1.85.0
+ version: 1.120.0
+ typescript:
+ specifier: ^5.7.0
+ version: 5.9.3
+ vitest:
+ specifier: ^2.1.9
+ version: 2.1.9(@types/node@22.19.19)
+
packages/core:
dependencies:
'@modelcontextprotocol/sdk':
@@ -473,6 +508,9 @@ packages:
'@types/react@18.3.29':
resolution: {integrity: sha512-ch0qJdr2JY0r04NXSprbK6TXOgnaJ1Tz23fm5W+z0/CBah6BSBc3n96h7K9GOtwh0HrilNWHIBzE1Ko4Dcw/Wg==}
+ '@types/vscode@1.120.0':
+ resolution: {integrity: sha512-feaT4Rst+FkTch5zz/ZbNCxoIvo55YU80Be2kiL7OJcod4+CUYf2lUBPdIJzozNnSEMq1VRTGrWEcCGFB3fBmA==}
+
'@typescript-eslint/eslint-plugin@8.60.0':
resolution: {integrity: sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1680,6 +1718,8 @@ snapshots:
'@types/prop-types': 15.7.15
csstype: 3.2.3
+ '@types/vscode@1.120.0': {}
+
'@typescript-eslint/eslint-plugin@8.60.0(@typescript-eslint/parser@8.60.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2