* 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 } = {},
)
| 3863 | * through validatePathWithinRoot (#527). |
| 3864 | */ |
| 3865 | private async handleFileView( |
| 3866 | cg: CodeGraph, |
| 3867 | fileArg: string, |
| 3868 | opts: { offset?: number; limit?: number; symbolsOnly?: boolean } = {}, |
| 3869 | ): Promise<ToolResult> { |
| 3870 | const normalize = (p: string) => p.replace(/\\/g, '/').replace(/^(?:\.?\/+)+/, '').replace(/\/+$/, ''); |
| 3871 | const wantLower = normalize(fileArg).toLowerCase(); |
| 3872 | const allFiles = cg.getFiles(); |
| 3873 | if (allFiles.length === 0) return this.textResult('No files indexed. Run `codegraph index` first.'); |
| 3874 | |
| 3875 | let resolved = allFiles.find((f) => f.path.toLowerCase() === wantLower); |
| 3876 | let candidates: typeof allFiles = []; |
| 3877 | if (!resolved) { |
| 3878 | candidates = allFiles.filter((f) => f.path.toLowerCase().endsWith('/' + wantLower)); |
| 3879 | if (candidates.length === 1) resolved = candidates[0]; |
| 3880 | } |
| 3881 | if (!resolved && candidates.length === 0) { |
| 3882 | candidates = allFiles.filter((f) => f.path.toLowerCase().includes(wantLower)); |
| 3883 | if (candidates.length === 1) resolved = candidates[0]; |
| 3884 | } |
| 3885 | if (!resolved && candidates.length > 1) { |
| 3886 | return this.textResult( |
| 3887 | [`"${fileArg}" matches ${candidates.length} indexed files — pass a longer path:`, '', |
| 3888 | ...candidates.slice(0, 25).map((f) => `- ${f.path}`)].join('\n'), |
| 3889 | ); |
| 3890 | } |
| 3891 | if (!resolved) { |
| 3892 | return this.textResult( |
| 3893 | `No indexed file matches "${fileArg}". Codegraph indexes source files; configs/docs it doesn't parse won't appear — Read those directly.`, |
| 3894 | ); |
| 3895 | } |
| 3896 | |
| 3897 | const filePath = resolved.path; |
| 3898 | const nodes = cg.getNodesInFile(filePath) |
| 3899 | .filter((n) => n.kind !== 'file' && n.kind !== 'import' && n.kind !== 'export') |
| 3900 | .sort((a, b) => a.startLine - b.startLine); |
| 3901 | const dependents = cg.getFileDependents(filePath); |
| 3902 | |
| 3903 | // Compact, one-line blast radius (codegraph's value-add over a plain Read). |
| 3904 | const depSummary = dependents.length |
| 3905 | ? `used by ${dependents.length} file${dependents.length === 1 ? '' : 's'}: ${dependents.slice(0, 8).join(', ')}${dependents.length > 8 ? `, +${dependents.length - 8} more` : ''}` |
| 3906 | : 'no other indexed file depends on it'; |
| 3907 | |
| 3908 | // Symbol-map renderer — for symbolsOnly, the config fallback, and read errors. |
| 3909 | const symbolMap = (heading: string, limit = 200): string[] => { |
| 3910 | const lines: string[] = [heading]; |
| 3911 | for (const n of nodes.slice(0, limit)) { |
| 3912 | const sig = n.signature ? ` ${n.signature.replace(/\s+/g, ' ').trim()}` : ''; |
| 3913 | lines.push(`- \`${n.name}\` (${n.kind})${sig} — :${n.startLine}`); |
| 3914 | } |
| 3915 | if (nodes.length > limit) lines.push(`- … +${nodes.length - limit} more`); |
| 3916 | return lines; |
| 3917 | }; |
| 3918 | |
| 3919 | // symbolsOnly → the cheap structural overview, no source. |
| 3920 | if (opts.symbolsOnly) { |
| 3921 | const out = [`**${filePath}** — ${nodes.length} symbol${nodes.length === 1 ? '' : 's'}, ${depSummary}`, '']; |
| 3922 | if (nodes.length) out.push(...symbolMap('**Symbols**')); |
no test coverage detected