Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
12 kB · 397 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398import figures from 'figures'import { logError } from 'src/utils/log.js'import { callIdeRpc } from '../services/mcp/client.js'import type { MCPServerConnection } from '../services/mcp/types.js'import { ClaudeError } from '../utils/errors.js'import { normalizePathForComparison, pathsEqual } from '../utils/file.js'import { getConnectedIdeClient } from '../utils/ide.js'import { jsonParse } from '../utils/slowOperations.js'
class DiagnosticsTrackingError extends ClaudeError {}
const MAX_DIAGNOSTICS_SUMMARY_CHARS = 4000
export interface Diagnostic { message: string severity: 'Error' | 'Warning' | 'Info' | 'Hint' range: { start: { line: number; character: number } end: { line: number; character: number } } source?: string code?: string}
export interface DiagnosticFile { uri: string diagnostics: Diagnostic[]}
export class DiagnosticTrackingService { private static instance: DiagnosticTrackingService | undefined private baseline: Map<string, Diagnostic[]> = new Map()
private initialized = false private mcpClient: MCPServerConnection | undefined
// Track when files were last processed/fetched private lastProcessedTimestamps: Map<string, number> = new Map()
// Track which files have received right file diagnostics and if they've changed // Map<normalizedPath, lastClaudeFsRightDiagnostics> private rightFileDiagnosticsState: Map<string, Diagnostic[]> = new Map()
static getInstance(): DiagnosticTrackingService { if (!DiagnosticTrackingService.instance) { DiagnosticTrackingService.instance = new DiagnosticTrackingService() } return DiagnosticTrackingService.instance }
initialize(mcpClient: MCPServerConnection) { if (this.initialized) { return }
// TODO: Do not cache the connected mcpClient since it can change. this.mcpClient = mcpClient this.initialized = true }
async shutdown(): Promise<void> { this.initialized = false this.baseline.clear() this.rightFileDiagnosticsState.clear() this.lastProcessedTimestamps.clear() }
/** * Reset tracking state while keeping the service initialized. * This clears all tracked files and diagnostics. */ reset() { this.baseline.clear() this.rightFileDiagnosticsState.clear() this.lastProcessedTimestamps.clear() }
private normalizeFileUri(fileUri: string): string { // Remove our protocol prefixes const protocolPrefixes = [ 'file://', '_claude_fs_right:', '_claude_fs_left:', ]
let normalized = fileUri for (const prefix of protocolPrefixes) { if (fileUri.startsWith(prefix)) { normalized = fileUri.slice(prefix.length) break } }
// Use shared utility for platform-aware path normalization // (handles Windows case-insensitivity and path separators) return normalizePathForComparison(normalized) }
/** * Ensure a file is opened in the IDE before processing. * This is important for language services like diagnostics to work properly. */ async ensureFileOpened(fileUri: string): Promise<void> { if ( !this.initialized || !this.mcpClient || this.mcpClient.type !== 'connected' ) { return }
try { // Call the openFile tool to ensure the file is loaded await callIdeRpc( 'openFile', { filePath: fileUri, preview: false, startText: '', endText: '', selectToEndOfLine: false, makeFrontmost: false, }, this.mcpClient, ) } catch (error) { logError(error as Error) } }
/** * Capture baseline diagnostics for a specific file before editing. * This is called before editing a file to ensure we have a baseline to compare against. */ async beforeFileEdited(filePath: string): Promise<void> { if ( !this.initialized || !this.mcpClient || this.mcpClient.type !== 'connected' ) { return }
const timestamp = Date.now()
try { const result = await callIdeRpc( 'getDiagnostics', { uri: `file://${filePath}` }, this.mcpClient, ) const diagnosticFile = this.parseDiagnosticResult(result)[0] if (diagnosticFile) { // Compare normalized paths (handles protocol prefixes and Windows case-insensitivity) if ( !pathsEqual( this.normalizeFileUri(filePath), this.normalizeFileUri(diagnosticFile.uri), ) ) { logError( new DiagnosticsTrackingError( `Diagnostics file path mismatch: expected ${filePath}, got ${diagnosticFile.uri})`, ), ) return }
// Store with normalized path key for consistent lookups on Windows const normalizedPath = this.normalizeFileUri(filePath) this.baseline.set(normalizedPath, diagnosticFile.diagnostics) this.lastProcessedTimestamps.set(normalizedPath, timestamp) } else { // No diagnostic file returned, store an empty baseline const normalizedPath = this.normalizeFileUri(filePath) this.baseline.set(normalizedPath, []) this.lastProcessedTimestamps.set(normalizedPath, timestamp) } } catch (_error) { // Fail silently if IDE doesn't support diagnostics } }
/** * Get new diagnostics from file://, _claude_fs_right, and _claude_fs_ URIs that aren't in the baseline. * Only processes diagnostics for files that have been edited. */ async getNewDiagnostics(): Promise<DiagnosticFile[]> { if ( !this.initialized || !this.mcpClient || this.mcpClient.type !== 'connected' ) { return [] }
// Check if we have any files with diagnostic changes let allDiagnosticFiles: DiagnosticFile[] = [] try { const result = await callIdeRpc( 'getDiagnostics', {}, // Empty params fetches all diagnostics this.mcpClient, ) allDiagnosticFiles = this.parseDiagnosticResult(result) } catch (_error) { // If fetching all diagnostics fails, return empty return [] } const diagnosticsForFileUrisWithBaselines = allDiagnosticFiles .filter(file => this.baseline.has(this.normalizeFileUri(file.uri))) .filter(file => file.uri.startsWith('file://'))
const diagnosticsForClaudeFsRightUrisWithBaselinesMap = new Map< string, DiagnosticFile >() allDiagnosticFiles .filter(file => this.baseline.has(this.normalizeFileUri(file.uri))) .filter(file => file.uri.startsWith('_claude_fs_right:')) .forEach(file => { diagnosticsForClaudeFsRightUrisWithBaselinesMap.set( this.normalizeFileUri(file.uri), file, ) })
const newDiagnosticFiles: DiagnosticFile[] = []
// Process file:// protocol diagnostics for (const file of diagnosticsForFileUrisWithBaselines) { const normalizedPath = this.normalizeFileUri(file.uri) const baselineDiagnostics = this.baseline.get(normalizedPath) || []
// Get the _claude_fs_right file if it exists const claudeFsRightFile = diagnosticsForClaudeFsRightUrisWithBaselinesMap.get(normalizedPath)
// Determine which file to use based on the state of right file diagnostics let fileToUse = file
if (claudeFsRightFile) { const previousRightDiagnostics = this.rightFileDiagnosticsState.get(normalizedPath)
// Use _claude_fs_right if: // 1. We've never gotten right file diagnostics for this file (previousRightDiagnostics === undefined) // 2. OR the right file diagnostics have just changed if ( !previousRightDiagnostics || !this.areDiagnosticArraysEqual( previousRightDiagnostics, claudeFsRightFile.diagnostics, ) ) { fileToUse = claudeFsRightFile }
// Update our tracking of right file diagnostics this.rightFileDiagnosticsState.set( normalizedPath, claudeFsRightFile.diagnostics, ) }
// Find new diagnostics that aren't in the baseline const newDiagnostics = fileToUse.diagnostics.filter( d => !baselineDiagnostics.some(b => this.areDiagnosticsEqual(d, b)), )
if (newDiagnostics.length > 0) { newDiagnosticFiles.push({ uri: file.uri, diagnostics: newDiagnostics, }) }
// Update baseline with current diagnostics this.baseline.set(normalizedPath, fileToUse.diagnostics) }
return newDiagnosticFiles }
private parseDiagnosticResult(result: unknown): DiagnosticFile[] { if (Array.isArray(result)) { const textBlock = result.find(block => block.type === 'text') if (textBlock && 'text' in textBlock) { const parsed = jsonParse(textBlock.text) return parsed } } return [] }
private areDiagnosticsEqual(a: Diagnostic, b: Diagnostic): boolean { return ( a.message === b.message && a.severity === b.severity && a.source === b.source && a.code === b.code && a.range.start.line === b.range.start.line && a.range.start.character === b.range.start.character && a.range.end.line === b.range.end.line && a.range.end.character === b.range.end.character ) }
private areDiagnosticArraysEqual(a: Diagnostic[], b: Diagnostic[]): boolean { if (a.length !== b.length) return false
// Check if every diagnostic in 'a' exists in 'b' return ( a.every(diagA => b.some(diagB => this.areDiagnosticsEqual(diagA, diagB)), ) && b.every(diagB => a.some(diagA => this.areDiagnosticsEqual(diagA, diagB))) ) }
/** * Handle the start of a new query. This method: * - Initializes the diagnostic tracker if not already initialized * - Resets the tracker if already initialized (for new query loops) * - Automatically finds the IDE client from the provided clients list * * @param clients Array of MCP clients that may include an IDE client * @param shouldQuery Whether a query is actually being made (not just a command) */ async handleQueryStart(clients: MCPServerConnection[]): Promise<void> { // Only proceed if we should query and have clients if (!this.initialized) { // Find the connected IDE client const connectedIdeClient = getConnectedIdeClient(clients)
if (connectedIdeClient) { this.initialize(connectedIdeClient) } } else { // Reset diagnostic tracking for new query loops this.reset() } }
/** * Format diagnostics into a human-readable summary string. * This is useful for displaying diagnostics in messages or logs. * * @param files Array of diagnostic files to format * @returns Formatted string representation of the diagnostics */ static formatDiagnosticsSummary(files: DiagnosticFile[]): string { const truncationMarker = '…[truncated]' const result = files .map(file => { const filename = file.uri.split('/').pop() || file.uri const diagnostics = file.diagnostics .map(d => { const severitySymbol = DiagnosticTrackingService.getSeveritySymbol( d.severity, )
return ` ${severitySymbol} [Line ${d.range.start.line + 1}:${d.range.start.character + 1}] ${d.message}${d.code ? ` [${d.code}]` : ''}${d.source ? ` (${d.source})` : ''}` }) .join('\n')
return `${filename}:\n${diagnostics}` }) .join('\n\n')
if (result.length > MAX_DIAGNOSTICS_SUMMARY_CHARS) { return ( result.slice( 0, MAX_DIAGNOSTICS_SUMMARY_CHARS - truncationMarker.length, ) + truncationMarker ) } return result }
/** * Get the severity symbol for a diagnostic */ static getSeveritySymbol(severity: Diagnostic['severity']): string { return ( { Error: figures.cross, Warning: figures.warning, Info: figures.info, Hint: figures.star, }[severity] || figures.bullet ) }}
export const diagnosticTracker = DiagnosticTrackingService.getInstance()