* Handle codegraph_node
(args: Record<string, unknown>)
| 5994 | * Handle codegraph_node |
| 5995 | */ |
| 5996 | private async handleNode(args: Record<string, unknown>): Promise<ToolResult> { |
| 5997 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 5998 | // Default to false to minimize context usage |
| 5999 | const includeCode = args.includeCode === true; |
| 6000 | const fileHint = typeof args.file === 'string' && args.file.trim() ? args.file.trim() : undefined; |
| 6001 | const lineHint = typeof args.line === 'number' && args.line > 0 ? args.line : undefined; |
| 6002 | const offset = typeof args.offset === 'number' && args.offset > 0 ? Math.floor(args.offset) : undefined; |
| 6003 | const limit = typeof args.limit === 'number' && args.limit > 0 ? Math.floor(args.limit) : undefined; |
| 6004 | const symbolsOnly = args.symbolsOnly === true; |
| 6005 | const symbolRaw = typeof args.symbol === 'string' ? args.symbol.trim() : ''; |
| 6006 | |
| 6007 | // FILE READ MODE: a `file` with no `symbol` reads that file like the Read |
| 6008 | // tool — its current on-disk source with line numbers, narrowable with |
| 6009 | // `offset`/`limit` exactly as Read does — PLUS a one-line blast-radius |
| 6010 | // header (which files depend on it). `symbolsOnly` returns just the |
| 6011 | // structural map instead. Backed by the index: same bytes Read gives you. |
| 6012 | if (!symbolRaw && fileHint) { |
| 6013 | return this.handleFileView(cg, fileHint, { offset, limit, symbolsOnly }); |
| 6014 | } |
| 6015 | |
| 6016 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 6017 | if (typeof symbol !== 'string') return symbol; |
| 6018 | |
| 6019 | let matches = this.findSymbolMatches(cg, symbol); |
| 6020 | if (matches.length === 0) { |
| 6021 | return this.textResult(`Symbol "${symbol}" not found in the codebase`); |
| 6022 | } |
| 6023 | |
| 6024 | // Disambiguate a heavily-overloaded name to a specific definition the caller |
| 6025 | // pinned by file/line (the `file:line` a trail or another tool showed it) — |
| 6026 | // so it can fetch e.g. `Harness::poll` at harness.rs:153 out of 50+ `poll`s |
| 6027 | // instead of Reading. file matches by path suffix/substring; line prefers the |
| 6028 | // def whose body contains it, else the nearest start. Only narrows (never |
| 6029 | // empties — if a hint matches nothing it's ignored). |
| 6030 | if (matches.length > 1 && (fileHint || lineHint !== undefined)) { |
| 6031 | const norm = (p: string) => p.replace(/\\/g, '/').toLowerCase(); |
| 6032 | let narrowed = matches; |
| 6033 | if (fileHint) { |
| 6034 | const fh = norm(fileHint); |
| 6035 | const byFile = narrowed.filter((n) => norm(n.filePath).endsWith(fh) || norm(n.filePath).includes(fh)); |
| 6036 | if (byFile.length > 0) narrowed = byFile; |
| 6037 | } |
| 6038 | if (lineHint !== undefined && narrowed.length > 1) { |
| 6039 | const containing = narrowed.filter((n) => n.startLine <= lineHint && (n.endLine ?? n.startLine) >= lineHint); |
| 6040 | narrowed = containing.length > 0 |
| 6041 | ? containing |
| 6042 | : [...narrowed].sort((a, b) => Math.abs(a.startLine - lineHint) - Math.abs(b.startLine - lineHint)).slice(0, 1); |
| 6043 | } |
| 6044 | if (narrowed.length > 0) matches = narrowed; |
| 6045 | } |
| 6046 | |
| 6047 | // Single definition — the common case. |
| 6048 | if (matches.length === 1) { |
| 6049 | return this.textResult(this.truncateOutput(await this.renderNodeSection(cg, matches[0]!, includeCode))); |
| 6050 | } |
| 6051 | |
| 6052 | // Multiple definitions share this name — overloads, or same-named methods on |
| 6053 | // different types (Alamofire `didCompleteTask`/`task`/`validate`, gin |
no test coverage detected