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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user