fix: report callers/callees/query truncation (#1674) (#1772)

Land #1647 onto current main: callers/callees/query (CLI + MCP) now say
when --limit hid matches, with totals in JSON and a widening hint. Also
covers #1639.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 09:52:56 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 43271f3cd3
commit 85550eb2ce
5 changed files with 238 additions and 8 deletions
+21 -6
View File
@@ -1164,7 +1164,9 @@ program
const limit = parseInt(options.limit || '10', 10);
const rawResults = cg.searchNodes(search, {
limit,
// Fetch one extra row so the CLI can report a cut without changing the
// long-standing bare-array contract of `query --json` (#1639).
limit: limit + 1,
kinds: options.kind ? [options.kind as any] : undefined,
});
@@ -1172,14 +1174,18 @@ program
// hand-written implementation before protobuf/gRPC scaffolding
// when both share a name. See extraction/generated-detection.ts.
const isGen = cg.generatedFilePredicate(rawResults.map((r) => r.node.filePath));
const results = [...rawResults].sort((a, b) => {
const rankedResults = [...rawResults].sort((a, b) => {
const aGen = isGen(a.node.filePath) ? 1 : 0;
const bGen = isGen(b.node.filePath) ? 1 : 0;
return aGen - bGen;
});
const truncated = rankedResults.length > limit;
const results = rankedResults.slice(0, limit);
const truncationMessage = `Results truncated at ${limit}; pass --limit to widen.`;
if (options.json) {
console.log(JSON.stringify(results, null, 2));
if (truncated) console.error(truncationMessage);
} else {
if (results.length === 0) {
info(`No results found for "${search}"`);
@@ -1205,6 +1211,7 @@ program
}
console.log();
}
if (truncated) console.log(chalk.dim(truncationMessage));
}
}
@@ -2183,13 +2190,16 @@ program
}
const limited = allCallers.slice(0, limit);
const total = allCallers.length;
const truncated = total > limit;
if (options.json) {
console.log(JSON.stringify({ symbol, callers: limited }, null, 2));
console.log(JSON.stringify({ symbol, callers: limited, total, limit, truncated }, null, 2));
} else if (limited.length === 0) {
info(`No callers found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nCallers of "${symbol}" (${limited.length}):\n`));
const count = truncated ? `${limited.length} of ${total}` : String(total);
console.log(chalk.bold(`\nCallers of "${symbol}" (${count}):\n`));
for (const node of limited) {
const loc = node.startLine ? `:${node.startLine}` : '';
console.log(
@@ -2199,6 +2209,7 @@ program
console.log(chalk.dim(` ${node.filePath}${loc}`));
console.log();
}
if (truncated) console.log(chalk.dim(`Showing ${limited.length} of ${total}; pass --limit to widen.`));
}
cg.destroy();
@@ -2261,13 +2272,16 @@ program
}
const limited = allCallees.slice(0, limit);
const total = allCallees.length;
const truncated = total > limit;
if (options.json) {
console.log(JSON.stringify({ symbol, callees: limited }, null, 2));
console.log(JSON.stringify({ symbol, callees: limited, total, limit, truncated }, null, 2));
} else if (limited.length === 0) {
info(`No callees found for "${symbol}"`);
} else {
console.log(chalk.bold(`\nCallees of "${symbol}" (${limited.length}):\n`));
const count = truncated ? `${limited.length} of ${total}` : String(total);
console.log(chalk.bold(`\nCallees of "${symbol}" (${count}):\n`));
for (const node of limited) {
const loc = node.startLine ? `:${node.startLine}` : '';
console.log(
@@ -2277,6 +2291,7 @@ program
console.log(chalk.dim(` ${node.filePath}${loc}`));
console.log();
}
if (truncated) console.log(chalk.dim(`Showing ${limited.length} of ${total}; pass --limit to widen.`));
}
cg.destroy();
+18 -2
View File
@@ -2419,7 +2419,12 @@ export class ToolHandler {
// A successful `file` narrowing makes the multi-symbol aggregation note
// stale — suppress it.
const note = fileFilter && !filteredOut ? '' : allMatches.note;
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + note + filterNote;
// Say when the cap cut the list (#1639, #1674): a truncated answer with
// no marker reads as the complete set, and an agent under-counts from it.
const cut = callers.length > limit
? `\n\n> Showing ${limit} of ${callers.length} callers; pass \`limit\` (up to 100) to widen.`
: '';
const formatted = this.formatNodeList(callers.slice(0, limit), `Callers of ${symbol}`, labels) + cut + note + filterNote;
return this.textResult(this.truncateOutput(formatted));
}
@@ -2441,6 +2446,9 @@ export class ToolHandler {
const label = labels.get(node.id);
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
}
if (callers.length > limit) {
lines.push(`- … +${callers.length - limit} more (pass \`limit\` to widen)`);
}
}
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
}
@@ -2491,7 +2499,12 @@ export class ToolHandler {
// A successful `file` narrowing makes the multi-symbol aggregation note
// stale — suppress it.
const note = fileFilter && !filteredOut ? '' : allMatches.note;
const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels) + note + filterNote;
// Say when the cap cut the list (#1639, #1674): a truncated answer with
// no marker reads as the complete set, and an agent under-counts from it.
const cut = callees.length > limit
? `\n\n> Showing ${limit} of ${callees.length} callees; pass \`limit\` (up to 100) to widen.`
: '';
const formatted = this.formatNodeList(callees.slice(0, limit), `Callees of ${symbol}`, labels) + cut + note + filterNote;
return this.textResult(this.truncateOutput(formatted));
}
@@ -2511,6 +2524,9 @@ export class ToolHandler {
const label = labels.get(node.id);
lines.push(`- ${node.name} (${node.kind}) - ${node.filePath}${location}${label ? ` — via ${label}` : ''}`);
}
if (callees.length > limit) {
lines.push(`- … +${callees.length - limit} more (pass \`limit\` to widen)`);
}
}
return this.textResult(this.truncateOutput(lines.join('\n') + filterNote));
}