* 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 } = {},
)
| 3755 | * through validatePathWithinRoot (#527). |
| 3756 | */ |
| 3757 | private async handleFileView( |
| 3758 | cg: CodeGraph, |
| 3759 | fileArg: string, |
| 3760 | opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {}, |
| 3761 | ): Promise<ToolResult> { |
| 3762 | const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, ''); |
| 3763 | const wantLower = normalize(fileArg).toLowerCase(); |
| 3764 | const allFiles = cg.getFiles(); |
| 3765 | if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.'); |
| 3766 | |
| 3767 | let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower); |
| 3768 | let candidates: typeof allFiles = []; |
| 3769 | if (!resolved) { |
| 3770 | candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower)); |
| 3771 | if (candidates.length === 1) resolved = candidates[0]; |
| 3772 | } |
| 3773 | if (!resolved && candidates.length === 0) { |
| 3774 | candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower)); |
| 3775 | if (candidates.length === 1) resolved = candidates[0]; |
| 3776 | } |
| 3777 | if (!resolved && candidates.length > 1) { |
| 3778 | return this.textResult( |
| 3779 | [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '', |
| 3780 | ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'), |
| 3781 | ); |
| 3782 | } |
| 3783 | if (!resolved) { |
| 3784 | return this.textResult( |
| 3785 | `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`, |
| 3786 | ); |
| 3787 | } |
| 3788 | |
| 3789 | const filePath = resolved.path; |
| 3790 | const nodes = cg.getNodesInFile(filePath) |
| 3791 | .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export') |
| 3792 | .sort((a, b) => a.startLine - b.startLine); |
| 3793 | const dependents = cg.getFileDependents(filePath); |
| 3794 | |
| 3795 | // Compact, one-line blast radius (codegraph's value-add over a plain Read). |
| 3796 | const depSummary = dependents.length |
| 3797 | ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}` |
| 3798 | : 'no other indexed file depends on it'; |
| 3799 | |
| 3800 | // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors. |
| 3801 | const symbolMap = (heading: string, limit = 200): string[] => { |
| 3802 | const lines: string[] = [heading]; |
| 3803 | for (const n of nodes.slice(0, limit)) { |
| 3804 | const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : ''; |
| 3805 | lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`); |
| 3806 | } |
| 3807 | if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`); |
| 3808 | return lines; |
| 3809 | }; |
| 3810 | |
| 3811 | // symbolsOnly → the cheap structural overview, no source. |
| 3812 | if (opts.symbolsOnly) { |
| 3813 | const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, '']; |
| 3814 | if (nodes.length) out.push(...symbolMap('**Symbols**')); |
no test coverage detected