From 6fe7061dbe568425b49339e11b3993bada9c5fed Mon Sep 17 00:00:00 2001 From: oratis Date: Thu, 28 May 2026 16:00:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v1.1=20entry=20=E2=80=94=20@deepcode/vs?= =?UTF-8?q?code=20+=20@deepcode/lsp=20scaffolds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new packages opening the v1.1 multi-IDE story. apps/vscode (NEW): · package.json — extension manifest: 3 commands (openPanel/run/review), activity bar + chat view, configuration (apiKey/model/effort), default keybinding Cmd-Shift-D. · src/extension.ts — activate/deactivate + ChatViewProvider (webview with auto-styled VS Code theme tokens). Lazy `require('vscode')` so package builds without @types/vscode installed yet. · tsconfig.json (noEmit for now; flips to emit when @vscode/vsce lands). · vitest.config.ts — opt-out config so vitest doesn't try to load @types/vscode. · README.md — install/build/package commands, architecture, commands table, settings table, roadmap. apps/lsp (NEW): · src/server.ts — stdio LSP server with proper `Content-Length: N` framing. SIGTERM/SIGINT clean exit. · src/handler.ts — JSON-RPC dispatcher exposing 3 custom commands (deepcode.runAgent / deepcode.abort / deepcode.listSkills) via workspace/executeCommand, plus standard LSP boilerplate (initialize/initialized/shutdown/exit). · 8 unit tests cover: initialize capabilities, runAgent ack + event stream, missing prompt error, unknown command, unknown notification. · README.md with editor configs for Neovim (lspconfig), Emacs (lsp-mode), Sublime (LSP package). · bin: deepcode-lsp → dist/server.js Tiny housekeeping: · apps/desktop/electron/main.ts and apps/lsp/src/handler.ts now read `s.qualifiedName` / `s.frontmatter.description` from Skill (the actual core shape — was using stale s.name/s.description). Tests: 514 → 522 passing (+8 LSP). Build clean across 6 packages now: packages/core, packages/shared-ui, apps/cli, apps/desktop, apps/vscode, apps/lsp. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/desktop/electron/main.ts | 4 +- apps/lsp/README.md | 106 ++++++++++++++++++++ apps/lsp/package.json | 28 ++++++ apps/lsp/src/handler.test.ts | 124 +++++++++++++++++++++++ apps/lsp/src/handler.ts | 178 ++++++++++++++++++++++++++++++++++ apps/lsp/src/server.ts | 65 +++++++++++++ apps/lsp/tsconfig.json | 15 +++ apps/vscode/README.md | 59 +++++++++++ apps/vscode/package.json | 84 ++++++++++++++++ apps/vscode/src/extension.ts | 146 ++++++++++++++++++++++++++++ apps/vscode/tsconfig.json | 19 ++++ apps/vscode/vitest.config.ts | 6 ++ pnpm-lock.yaml | 40 ++++++++ 13 files changed, 872 insertions(+), 2 deletions(-) create mode 100644 apps/lsp/README.md create mode 100644 apps/lsp/package.json create mode 100644 apps/lsp/src/handler.test.ts create mode 100644 apps/lsp/src/handler.ts create mode 100644 apps/lsp/src/server.ts create mode 100644 apps/lsp/tsconfig.json create mode 100644 apps/vscode/README.md create mode 100644 apps/vscode/package.json create mode 100644 apps/vscode/src/extension.ts create mode 100644 apps/vscode/tsconfig.json create mode 100644 apps/vscode/vitest.config.ts 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