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
23 changes: 15 additions & 8 deletions apps/lsp/src/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,23 @@ describe('handleMessage — executeCommand', () => {
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));
// Async: wait for the agent run to finish (will error in test env
// because no DEEPSEEK_API_KEY is set — that's the expected path).
// Poll for turn_done with a timeout.
for (let i = 0; i < 50; i++) {
const done = out.find(
(m) =>
m.method === 'deepcode/agentEvent' &&
(m.params as { kind: string }).kind === 'turn_done',
);
if (done) break;
await new Promise((r) => setTimeout(r, 20));
}
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');
const kinds = events.map((e) => (e.params as { kind: string }).kind);
expect(kinds).toContain('started');
expect(kinds).toContain('turn_done');
});
}, 5000);

it('errors on missing prompt', async () => {
const out: LspMessage[] = [];
Expand Down
102 changes: 80 additions & 22 deletions apps/lsp/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,39 +114,97 @@ async function handleExecuteCommand(
}

async function handleRunAgent(
args: { prompt?: string },
args: { prompt?: string; model?: 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.
// Stream events back via JSON-RPC notifications.
// Wired to the real agent loop — same code that drives the CLI / Mac client.
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);
});

// Run async; we return turnId immediately so the LSP client can
// call deepcode.abort while it's in-flight.
void (async () => {
try {
const [
{ runAgent },
{ DeepSeekProvider },
{ ToolRegistry, BUILTIN_TOOLS },
{ resolveCredentials, CredentialsStore },
] = await Promise.all([
import('@deepcode/core').then((m) => ({ runAgent: m.runAgent })),
import('@deepcode/core').then((m) => ({ DeepSeekProvider: m.DeepSeekProvider })),
import('@deepcode/core').then((m) => ({
ToolRegistry: m.ToolRegistry,
BUILTIN_TOOLS: m.BUILTIN_TOOLS,
})),
import('@deepcode/core').then((m) => ({
resolveCredentials: m.resolveCredentials,
CredentialsStore: m.CredentialsStore,
})),
]);

const creds = await resolveCredentials({ store: new CredentialsStore() });
if (!creds.apiKey && !creds.authToken) {
throw new Error(
'No DeepSeek credentials. Run `deepcode` once to onboard, or set DEEPSEEK_API_KEY.',
);
}

const provider = new DeepSeekProvider({
apiKey: creds.apiKey ?? '',
authToken: creds.authToken,
baseURL: creds.baseURL,
});

const result = await runAgent({
provider,
tools: new ToolRegistry(BUILTIN_TOOLS),
systemPrompt:
'You are DeepCode, an AI coding assistant powered by DeepSeek. Be concise.',
userMessage: args.prompt!,
model: args.model ?? 'deepseek-chat',
cwd: state.rootUri ? new URL(state.rootUri).pathname : process.cwd(),
onEvent: (e) => {
send({
jsonrpc: '2.0',
method: 'deepcode/agentEvent',
params: { turnId, kind: e.type, ...e },
});
},
});

send({
jsonrpc: '2.0',
method: 'deepcode/agentEvent',
params: { turnId, kind: 'turn_done', stopReason: result.stopReason },
});
} catch (err) {
send({
jsonrpc: '2.0',
method: 'deepcode/agentEvent',
params: {
turnId,
kind: 'error',
error: (err as Error).message ?? String(err),
},
});
send({
jsonrpc: '2.0',
method: 'deepcode/agentEvent',
params: { turnId, kind: 'turn_done', stopReason: 'error' },
});
} finally {
state.activeTurns.delete(turnId);
}
})();

return { turnId };
}
Expand Down
Loading
Loading