@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
221 lines • 9.71 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { constants } from 'node:fs';
import { access } from 'node:fs/promises';
import { resolve as resolvePath } from 'node:path';
import { Box, Text } from 'ink';
import React from 'react';
import ToolMessage from '../components/tool-message.js';
import { TIMEOUT_LSP_DIAGNOSTICS_MS } from '../constants.js';
import { ThemeContext } from '../hooks/useTheme.js';
import { DiagnosticSeverity, getLSPManager } from '../lsp/index.js';
import { jsonSchema, tool } from '../types/core.js';
import { getVSCodeServer } from '../vscode/index.js';
// Request diagnostics from VS Code with timeout
async function getVSCodeDiagnostics(filePath) {
const server = await getVSCodeServer();
// Convert to absolute path for VS Code
const absPath = filePath ? resolvePath(filePath) : undefined;
return new Promise(resolve => {
const timeout = setTimeout(() => {
resolve(null);
}, TIMEOUT_LSP_DIAGNOSTICS_MS);
// Register callback for this specific request
server.onCallbacks({
onDiagnosticsResponse: diagnostics => {
clearTimeout(timeout);
resolve(diagnostics);
},
});
server.requestDiagnostics(absPath);
});
}
// Format VS Code diagnostics to string
function formatVSCodeDiagnostics(diagnostics, filePath) {
if (diagnostics.length === 0) {
return filePath
? `No diagnostics found for ${filePath}`
: 'No diagnostics found.';
}
// Group by file
const byFile = new Map();
for (const diag of diagnostics) {
const path = diag.filePath;
if (!byFile.has(path)) {
byFile.set(path, []);
}
const fileDiagnostics = byFile.get(path);
if (fileDiagnostics) {
fileDiagnostics.push(diag);
}
}
const lines = [];
if (filePath) {
lines.push(`Diagnostics for ${filePath} (from VS Code):`);
lines.push('');
}
else {
lines.push('Diagnostics from VS Code:');
lines.push('');
}
for (const [file, fileDiags] of byFile) {
if (!filePath) {
lines.push(`\n${file}:`);
}
for (const diag of fileDiags) {
const severity = diag.severity.toUpperCase();
const line = diag.line + 1;
const char = diag.character + 1;
const source = diag.source ? `[${diag.source}] ` : '';
const prefix = filePath ? '' : ' ';
lines.push(`${prefix}${severity} at line ${line}:${char}: ${source}${diag.message}`);
}
}
return lines.join('\n');
}
// Handler function
const executeGetDiagnostics = async (args) => {
// Prefer VS Code diagnostics when connected
const server = await getVSCodeServer();
const hasConnections = server.hasConnections();
if (hasConnections) {
const vscodeDiags = await getVSCodeDiagnostics(args.path);
if (vscodeDiags !== null) {
return formatVSCodeDiagnostics(vscodeDiags, args.path);
}
// Fall through to LSP if VS Code request failed
}
// Fall back to local LSP
const lspManager = await getLSPManager();
if (!lspManager.isInitialized()) {
return 'No diagnostics source available. Either connect VS Code with --vscode flag, or install a language server.';
}
// If path is provided, get diagnostics for that file
if (args.path) {
// Check if we have LSP support for this file type
if (!lspManager.hasLanguageSupport(args.path)) {
return `No language server available for file type: ${args.path}. Try running with --vscode flag to use VS Code's TypeScript diagnostics.`;
}
// Open the document if not already open
await lspManager.openDocument(args.path);
// Get diagnostics
const diagnostics = await lspManager.getDiagnostics(args.path);
if (diagnostics.length === 0) {
return `No diagnostics found for ${args.path}`;
}
// Format diagnostics
const lines = [`Diagnostics for ${args.path}:`, ''];
for (const diag of diagnostics) {
const severity = diag.severity === DiagnosticSeverity.Error
? 'ERROR'
: diag.severity === DiagnosticSeverity.Warning
? 'WARNING'
: diag.severity === DiagnosticSeverity.Information
? 'INFO'
: 'HINT';
const line = diag.range.start.line + 1;
const char = diag.range.start.character + 1;
const source = diag.source ? `[${diag.source}] ` : '';
lines.push(`${severity} at line ${line}:${char}: ${source}${diag.message}`);
}
return lines.join('\n');
}
// Get all diagnostics from all open documents
const allDiagnostics = lspManager.getAllDiagnostics();
if (allDiagnostics.length === 0) {
return 'No diagnostics found in any open documents.';
}
const lines = ['Diagnostics from all open documents:', ''];
for (const { uri, diagnostics } of allDiagnostics) {
// Convert URI to path
const path = uri.startsWith('file://') ? uri.slice(7) : uri;
lines.push(`\n${path}:`);
for (const diag of diagnostics) {
const severity = diag.severity === DiagnosticSeverity.Error
? 'ERROR'
: diag.severity === DiagnosticSeverity.Warning
? 'WARNING'
: diag.severity === DiagnosticSeverity.Information
? 'INFO'
: 'HINT';
const line = diag.range.start.line + 1;
const char = diag.range.start.character + 1;
const source = diag.source ? `[${diag.source}] ` : '';
lines.push(` ${severity} at line ${line}:${char}: ${source}${diag.message}`);
}
}
return lines.join('\n');
};
const getDiagnosticsCoreTool = tool({
description: 'Get errors and warnings for a file or project from the language server. Returns type errors, linting issues, and other diagnostics. Use this to check for problems before or after making code changes.',
inputSchema: jsonSchema({
type: 'object',
properties: {
path: {
type: 'string',
description: 'Optional path to a specific file. If omitted, returns diagnostics for all open documents.',
},
},
required: [],
}),
// Low risk: read-only operation, never requires approval
needsApproval: false,
execute: async (args, _options) => {
// Run validator before execution (for auto-executed tools)
const validationResult = await getDiagnosticsValidator(args);
if (!validationResult.valid) {
return validationResult.error;
}
return await executeGetDiagnostics(args);
},
});
// Formatter component
const GetDiagnosticsFormatter = React.memo(({ args, result }) => {
const themeContext = React.useContext(ThemeContext);
if (!themeContext) {
throw new Error('GetDiagnosticsFormatter must be used within a ThemeProvider');
}
const { colors } = themeContext;
// Count diagnostics from result
const errorCount = (result?.match(/ERROR/g) || []).length;
const warningCount = (result?.match(/WARNING/g) || []).length;
const messageContent = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.tool, children: "\u2692 get_diagnostics" }), args.path ? (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Path: " }), _jsx(Text, { color: colors.text, children: args.path })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Scope: " }), _jsx(Text, { color: colors.text, children: "All open documents" })] })), result && (_jsxs(Box, { children: [_jsx(Text, { color: colors.secondary, children: "Found: " }), _jsxs(Text, { color: errorCount > 0 ? colors.error : colors.text, children: [errorCount, " errors"] }), _jsx(Text, { color: colors.secondary, children: ", " }), _jsxs(Text, { color: warningCount > 0 ? colors.warning : colors.text, children: [warningCount, " warnings"] })] }))] }));
return _jsx(ToolMessage, { message: messageContent, hideBox: true });
});
const getDiagnosticsFormatter = (args, result) => {
return _jsx(GetDiagnosticsFormatter, { args: args, result: result });
};
const getDiagnosticsValidator = async (args) => {
// If no path is provided, we're getting diagnostics for all open documents - always valid
if (!args.path) {
return { valid: true };
}
// Validate that the file exists
const absPath = resolvePath(args.path);
try {
await access(absPath, constants.F_OK);
return { valid: true };
}
catch (error) {
if (error &&
typeof error === 'object' &&
'code' in error &&
error.code === 'ENOENT') {
return {
valid: false,
error: `Error: File "${args.path}" does not exist. Please verify the file path and try again.`,
};
}
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
return {
valid: false,
error: `Error: Cannot access file "${args.path}": ${errorMessage}`,
};
}
};
export const getDiagnosticsTool = {
name: 'lsp_get_diagnostics',
tool: getDiagnosticsCoreTool,
formatter: getDiagnosticsFormatter,
validator: getDiagnosticsValidator,
};
//# sourceMappingURL=lsp-get-diagnostics.js.map