diff --git a/__tests__/explore-blast-radius.test.ts b/__tests__/explore-blast-radius.test.ts
index e85b0738e..1cf02263a 100644
--- a/__tests__/explore-blast-radius.test.ts
+++ b/__tests__/explore-blast-radius.test.ts
@@ -71,3 +71,48 @@ describe('codegraph_explore — blast radius', () => {
expect(text).not.toMatch(/Blast radius[\s\S]*`lonelyLeaf`/);
});
});
+
+describe('codegraph_explore — template-view callers in blast radius', () => {
+ let testDir: string;
+ let cg: CodeGraph;
+ let handler: ToolHandler;
+
+ beforeEach(async () => {
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-blast-view-'));
+ const src = path.join(testDir, 'src');
+ fs.mkdirSync(src, { recursive: true });
+
+ // A shared helper depended on by MANY same-language code callers (> FILE_CAP)
+ // plus a template view. Under a single flat cap the lone view would be
+ // pushed into "+N more"; it must instead surface in its own dedicated slot.
+ fs.writeFileSync(path.join(src, 'helper.ts'), `export function fmt(x: number) { return x + 1; }\n`);
+ for (const n of ['a', 'b', 'c', 'd', 'e']) {
+ fs.writeFileSync(
+ path.join(src, `${n}.ts`),
+ `import { fmt } from './helper';\nexport function use_${n}() { return fmt(1); }\n`,
+ );
+ }
+ fs.writeFileSync(
+ path.join(src, 'Widget.vue'),
+ `\n\n`,
+ );
+
+ cg = CodeGraph.initSync(testDir, { config: { include: ['**/*.ts', '**/*.vue'], exclude: [] } });
+ await cg.indexAll();
+ handler = new ToolHandler(cg);
+ });
+
+ afterEach(() => {
+ if (cg) cg.destroy();
+ if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
+ });
+
+ it('surfaces template-view callers in their own slot, not buried under the code "+N more" cap', async () => {
+ const res = await handler.execute('codegraph_explore', { query: 'fmt' });
+ const text = res.content[0].text;
+ expect(text).toContain('`fmt`');
+ // The .vue view appears in a dedicated "view(s):" slot rather than being
+ // hidden behind the >FILE_CAP code-caller "+N more".
+ expect(text).toMatch(/views?:[^\n]*Widget\.vue/);
+ });
+});
diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts
index b33abbdd4..6bd5de780 100644
--- a/src/mcp/tools.ts
+++ b/src/mcp/tools.ts
@@ -2193,9 +2193,23 @@ export class ToolHandler {
const testFiles = callerFiles.filter((f) => isTestFile(f));
const nonTest = callerFiles.filter((f) => !isTestFile(f));
- const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
- const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : '';
- const where = nonTest.length > 0 ? ` in ${shown}${more}` : '';
+ // Template views (.cshtml/.razor/.vue/.svelte/.astro) are cross-layer
+ // callers — a JS/TS change ripples into the view. Under a single flat cap
+ // they're easily drowned out by the far more numerous same-language code
+ // callers and vanish into "+N more" (a JS helper used by 4 .js files and
+ // 22 views would show 0 views). Surface them in their own slot so the
+ // "which views depend on this?" answer never gets hidden.
+ const isView = (f: string) => /\.(cshtml|razor|vue|svelte|astro)$/i.test(f);
+ const viewFiles = nonTest.filter(isView);
+ const codeFiles = nonTest.filter((f) => !isView(f));
+
+ const shownCode = codeFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ');
+ const moreCode = codeFiles.length > FILE_CAP ? ` +${codeFiles.length - FILE_CAP} more` : '';
+ const codePart = codeFiles.length > 0 ? ` in ${shownCode}${moreCode}` : '';
+ const viewPart = viewFiles.length > 0
+ ? `; ${viewFiles.length} view${viewFiles.length === 1 ? '' : 's'}: ${viewFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${viewFiles.length > FILE_CAP ? ` +${viewFiles.length - FILE_CAP}` : ''}`
+ : '';
+ const where = codePart + viewPart;
const tests = testFiles.length > 0
? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}`
: '; ⚠️ no covering tests found';