* FILE READ MODE: resolve `fileArg` (path or basename) to an indexed file and * read it like the Read tool — its current on-disk source with line numbers, * narrowable with `offset`/`limit` exactly as Read's are — preceded by a * one-line blast-radius header (which files depend on it). `sym
(
cg: CodeGraph,
fileArg: string,
opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {},
)
| 6123 | * through validatePathWithinRoot (#527). |
| 6124 | */ |
| 6125 | private async handleFileView( |
| 6126 | cg: CodeGraph, |
| 6127 | fileArg: string, |
| 6128 | opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {}, |
| 6129 | ): Promise<ToolResult> { |
| 6130 | const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, ''); |
| 6131 | const wantLower = normalize(fileArg).toLowerCase(); |
| 6132 | const allFiles = cg.getFiles(); |
| 6133 | if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.'); |
| 6134 | |
| 6135 | let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower); |
| 6136 | let candidates: typeof allFiles = []; |
| 6137 | if (!resolved) { |
| 6138 | candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower)); |
| 6139 | if (candidates.length === 1) resolved = candidates[0]; |
| 6140 | } |
| 6141 | if (!resolved && candidates.length === 0) { |
| 6142 | candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower)); |
| 6143 | if (candidates.length === 1) resolved = candidates[0]; |
| 6144 | } |
| 6145 | if (!resolved && candidates.length > 1) { |
| 6146 | return this.textResult( |
| 6147 | [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '', |
| 6148 | ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'), |
| 6149 | ); |
| 6150 | } |
| 6151 | if (!resolved) { |
| 6152 | return this.textResult( |
| 6153 | `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`, |
| 6154 | ); |
| 6155 | } |
| 6156 | |
| 6157 | const filePath = resolved.path; |
| 6158 | const nodes = cg.getNodesInFile(filePath) |
| 6159 | .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export') |
| 6160 | .sort((a, b) => a.startLine - b.startLine); |
| 6161 | const dependents = cg.getFileDependents(filePath); |
| 6162 | |
| 6163 | // Compact, one-line blast radius (codegraph's value-add over a plain Read). |
| 6164 | const depSummary = dependents.length |
| 6165 | ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}` |
| 6166 | : 'no other indexed file depends on it'; |
| 6167 | |
| 6168 | // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors. |
| 6169 | const symbolMap = (heading: string, limit = 200): string[] => { |
| 6170 | const lines: string[] = [heading]; |
| 6171 | for (const n of nodes.slice(0, limit)) { |
| 6172 | const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : ''; |
| 6173 | lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`); |
| 6174 | } |
| 6175 | if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`); |
| 6176 | return lines; |
| 6177 | }; |
| 6178 | |
| 6179 | // symbolsOnly → the cheap structural overview, no source. |
| 6180 | if (opts.symbolsOnly) { |
| 6181 | const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, '']; |
| 6182 | if (nodes.length) out.push(...symbolMap('**Symbols**')); |
no test coverage detected