Skip to content
Merged
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
4 changes: 2 additions & 2 deletions apps/desktop/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Expand Down
106 changes: 106 additions & 0 deletions apps/lsp/README.md
Original file line number Diff line number Diff line change
@@ -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<body>`.
- 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.
28 changes: 28 additions & 0 deletions apps/lsp/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
124 changes: 124 additions & 0 deletions apps/lsp/src/handler.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading