feat(coder): add LSP code intelligence tools
- lsp/ module: types, config, JSON-RPC client, server-manager, operations - lsp_diagnostics: TypeScript/JavaScript diagnostics for a file - lsp_goto_definition: find symbol definition at position - lsp_find_references: find all references to a symbol - Registered as READ_TOOLS in tool index
This commit is contained in:
@@ -7,6 +7,9 @@ import { rewindTool } from './rewind.js';
|
||||
import { newTaskTool } from './new_task.js';
|
||||
import { listTasksTool } from './list_tasks.js';
|
||||
import { checkTaskStatusTool } from './check_task_status.js';
|
||||
import { lspDiagnosticsTool } from './lsp_diagnostics.js';
|
||||
import { lspGotoDefinitionTool } from './lsp_goto_definition.js';
|
||||
import { lspFindReferencesTool } from './lsp_find_references.js';
|
||||
|
||||
export type { ToolDef, ToolContext, ToolJsonSchema } from './types.js';
|
||||
|
||||
@@ -26,4 +29,16 @@ export const WRITE_TOOLS: readonly ToolDef<any>[] = [
|
||||
checkTaskStatusTool,
|
||||
];
|
||||
|
||||
export { editFileTool, createFileTool, deleteFileTool, applyPendingTool, rewindTool, newTaskTool, listTasksTool, checkTaskStatusTool };
|
||||
// Read-only agent tools for code intelligence.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const READ_TOOLS: readonly ToolDef<any>[] = [
|
||||
lspDiagnosticsTool,
|
||||
lspGotoDefinitionTool,
|
||||
lspFindReferencesTool,
|
||||
];
|
||||
|
||||
export {
|
||||
editFileTool, createFileTool, deleteFileTool, applyPendingTool, rewindTool,
|
||||
newTaskTool, listTasksTool, checkTaskStatusTool,
|
||||
lspDiagnosticsTool, lspGotoDefinitionTool, lspFindReferencesTool,
|
||||
};
|
||||
|
||||
48
apps/coder/src/services/tools/lsp_diagnostics.ts
Normal file
48
apps/coder/src/services/tools/lsp_diagnostics.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { ToolDef, ToolContext } from './types.js';
|
||||
import { resolveWritePath } from '../write_guard.js';
|
||||
import { lspManager } from '../lsp/server-manager.js';
|
||||
import { getDiagnostics } from '../lsp/operations.js';
|
||||
|
||||
const LspDiagnosticsInput = z.object({
|
||||
file_path: z.string().describe('Path to the file to check for diagnostics'),
|
||||
});
|
||||
|
||||
type InputT = z.infer<typeof LspDiagnosticsInput>;
|
||||
|
||||
export const lspDiagnosticsTool: ToolDef<InputT> = {
|
||||
name: 'lsp_diagnostics',
|
||||
description: 'Get TypeScript/JavaScript diagnostics (errors, warnings) for a file. Returns diagnostic messages with severity and location.',
|
||||
inputSchema: LspDiagnosticsInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'lsp_diagnostics',
|
||||
description: 'Get TypeScript/JavaScript diagnostics for a file',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: { type: 'string', description: 'Path to the file' },
|
||||
},
|
||||
required: ['file_path'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async execute(input: InputT, projectRoot: string, _context: ToolContext): Promise<unknown> {
|
||||
const resolved = await resolveWritePath(projectRoot, input.file_path);
|
||||
const content = await readFile(resolved, 'utf8');
|
||||
const client = await lspManager.getClient(resolved);
|
||||
if (!client) return { error: 'Unsupported file type for LSP diagnostics' };
|
||||
|
||||
const diagnostics = await getDiagnostics(client, resolved, content);
|
||||
if (diagnostics.length === 0) return { result: 'No diagnostics found.' };
|
||||
|
||||
const lines = diagnostics.map((d) => {
|
||||
const sev = ['', 'error', 'warning', 'info', 'hint'][d.severity] ?? 'unknown';
|
||||
return `[${sev}] line ${d.range.start.line + 1}:${d.range.start.character + 1} - ${d.message}`;
|
||||
});
|
||||
return { result: lines.join('\n') };
|
||||
},
|
||||
};
|
||||
49
apps/coder/src/services/tools/lsp_find_references.ts
Normal file
49
apps/coder/src/services/tools/lsp_find_references.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { z } from 'zod';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { ToolDef, ToolContext } from './types.js';
|
||||
import { resolveWritePath } from '../write_guard.js';
|
||||
import { lspManager } from '../lsp/server-manager.js';
|
||||
import { findReferences } from '../lsp/operations.js';
|
||||
|
||||
const LspFindReferencesInput = z.object({
|
||||
file_path: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().nonnegative().describe('0-based line number'),
|
||||
character: z.number().int().nonnegative().describe('0-based character offset'),
|
||||
});
|
||||
|
||||
type InputT = z.infer<typeof LspFindReferencesInput>;
|
||||
|
||||
export const lspFindReferencesTool: ToolDef<InputT> = {
|
||||
name: 'lsp_find_references',
|
||||
description: 'Find all references to a symbol at a given position in a file.',
|
||||
inputSchema: LspFindReferencesInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'lsp_find_references',
|
||||
description: 'Find all references to symbol at position',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: { type: 'string' },
|
||||
line: { type: 'number' },
|
||||
character: { type: 'number' },
|
||||
},
|
||||
required: ['file_path', 'line', 'character'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async execute(input: InputT, projectRoot: string, _context: ToolContext): Promise<unknown> {
|
||||
const resolved = await resolveWritePath(projectRoot, input.file_path);
|
||||
const content = await readFile(resolved, 'utf8');
|
||||
const client = await lspManager.getClient(resolved);
|
||||
if (!client) return { error: 'Unsupported file type' };
|
||||
|
||||
const refs = await findReferences(client, resolved, content, input.line, input.character);
|
||||
if (refs.length === 0) return { result: 'No references found.' };
|
||||
|
||||
const lines = refs.map((r) => `${r.uri}:${r.range.start.line + 1}:${r.range.start.character + 1}`);
|
||||
return { result: `Found ${refs.length} reference(s):\n${lines.join('\n')}` };
|
||||
},
|
||||
};
|
||||
48
apps/coder/src/services/tools/lsp_goto_definition.ts
Normal file
48
apps/coder/src/services/tools/lsp_goto_definition.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { z } from 'zod';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { ToolDef, ToolContext } from './types.js';
|
||||
import { resolveWritePath } from '../write_guard.js';
|
||||
import { lspManager } from '../lsp/server-manager.js';
|
||||
import { gotoDefinition } from '../lsp/operations.js';
|
||||
|
||||
const LspGotoDefinitionInput = z.object({
|
||||
file_path: z.string().describe('Path to the source file'),
|
||||
line: z.number().int().nonnegative().describe('0-based line number'),
|
||||
character: z.number().int().nonnegative().describe('0-based character offset'),
|
||||
});
|
||||
|
||||
type InputT = z.infer<typeof LspGotoDefinitionInput>;
|
||||
|
||||
export const lspGotoDefinitionTool: ToolDef<InputT> = {
|
||||
name: 'lsp_goto_definition',
|
||||
description: 'Find the definition of a symbol at a given position in a file.',
|
||||
inputSchema: LspGotoDefinitionInput,
|
||||
jsonSchema: {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'lsp_goto_definition',
|
||||
description: 'Find definition of symbol at position',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
file_path: { type: 'string' },
|
||||
line: { type: 'number' },
|
||||
character: { type: 'number' },
|
||||
},
|
||||
required: ['file_path', 'line', 'character'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
async execute(input: InputT, projectRoot: string, _context: ToolContext): Promise<unknown> {
|
||||
const resolved = await resolveWritePath(projectRoot, input.file_path);
|
||||
const content = await readFile(resolved, 'utf8');
|
||||
const client = await lspManager.getClient(resolved);
|
||||
if (!client) return { error: 'Unsupported file type' };
|
||||
|
||||
const loc = await gotoDefinition(client, resolved, content, input.line, input.character);
|
||||
if (!loc) return { result: 'No definition found.' };
|
||||
|
||||
return { result: `Defined at ${loc.uri}:${loc.range.start.line + 1}:${loc.range.start.character + 1}` };
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user