fix: surface Claude Grep results in search detail body

The Claude SDK delivers Grep tool_result content as a plain string, which
claude-agent.ts wraps as { output: <string> } for unrecognized tools. The
canonical ToolGrepOutputSchema requires structured fields, so parsing fell
through to an unknown detail and the UI rendered only the redundant query.
This commit is contained in:
Mohamed Boudra
2026-04-26 11:56:01 +07:00
parent 18903bcecb
commit 712cb8fc86
4 changed files with 83 additions and 19 deletions
@@ -382,15 +382,6 @@ interface SearchDetail {
function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[] {
const out: ReactNode[] = [];
if (detail.query) {
out.push(
<View key="search-query" style={styles.section}>
<Text selectable style={styles.scrollText}>
{detail.query}
</Text>
</View>,
);
}
if (detail.content) {
out.push(
<View key="search-content" style={styles.section}>
@@ -620,6 +620,56 @@ test("maps tool_result content shapes into deterministic string output", async (
}
});
test("Grep tool_result string content flows to a search detail with content", async () => {
const session = await createSession();
const internal = session as unknown as {
buildToolOutput: (
block: Record<string, unknown>,
entry: Record<string, unknown> | undefined,
) => Record<string, unknown> | undefined;
};
const grepEntry = {
id: "tool-grep-1",
name: "Grep",
server: "Grep",
classification: "search",
started: true,
input: { pattern: "MaskedView", output_mode: "files_with_matches" },
};
try {
const output = internal.buildToolOutput(
{
type: "tool_result",
tool_use_id: "tool-grep-1",
tool_name: "Grep",
content: "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx",
is_error: false,
},
grepEntry,
);
const { mapClaudeCompletedToolCall } = await import("./claude/tool-call-mapper.js");
const item = mapClaudeCompletedToolCall({
callId: "tool-grep-1",
name: "Grep",
input: grepEntry.input,
output: output ?? null,
});
expect(item?.detail).toEqual({
type: "search",
query: "MaskedView",
toolName: "grep",
content: "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx",
numFiles: 0,
});
} finally {
await session.close();
}
});
test("completes a foreground run when only system metadata arrives before the first assistant message", async () => {
let step = 0;
sdkQueryFactory.mockImplementation(() =>
@@ -25,6 +25,16 @@ import {
toolDetailBranchByName,
} from "../tool-call-detail-primitives.js";
const ClaudeGrepOutputSchema = z
.union([
ToolGrepOutputSchema,
z
.object({ output: z.string() })
.passthrough()
.transform(({ output }) => ({ numFiles: 0, filenames: [], content: output })),
])
.nullable();
const ClaudeToolEnvelopeSchema = z
.object({
name: z.string().min(1),
@@ -128,17 +138,11 @@ const ClaudeToolDetailPass2Schema = z.union([
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
toSearchToolDetail({ input, toolName: "search" }),
),
toolDetailBranchByName(
"Grep",
ToolSearchInputSchema,
ToolGrepOutputSchema.nullable(),
(input, output) => toSearchToolDetail({ input, output, toolName: "grep" }),
toolDetailBranchByName("Grep", ToolSearchInputSchema, ClaudeGrepOutputSchema, (input, output) =>
toSearchToolDetail({ input, output, toolName: "grep" }),
),
toolDetailBranchByName(
"grep",
ToolSearchInputSchema,
ToolGrepOutputSchema.nullable(),
(input, output) => toSearchToolDetail({ input, output, toolName: "grep" }),
toolDetailBranchByName("grep", ToolSearchInputSchema, ClaudeGrepOutputSchema, (input, output) =>
toSearchToolDetail({ input, output, toolName: "grep" }),
),
toolDetailBranchByName(
"Glob",
@@ -310,6 +310,25 @@ describe("claude tool-call mapper", () => {
});
});
it("maps Grep calls when output arrives as the claude-agent string wrapper", () => {
const item = expectMapped(
mapClaudeCompletedToolCall({
callId: "claude-grep-string-1",
name: "Grep",
input: { pattern: "MaskedView", output_mode: "files_with_matches" },
output: { output: "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx" },
}),
);
expect(item.detail).toEqual({
type: "search",
query: "MaskedView",
toolName: "grep",
content: "Found 2 files\nsrc/foo.tsx\nsrc/bar.tsx",
numFiles: 0,
});
});
it("maps WebSearch calls with structured results", () => {
const item = expectMapped(
mapClaudeCompletedToolCall({