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[] {
|
function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[] {
|
||||||
const out: 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) {
|
if (detail.content) {
|
||||||
out.push(
|
out.push(
|
||||||
<View key="search-content" style={styles.section}>
|
<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 () => {
|
test("completes a foreground run when only system metadata arrives before the first assistant message", async () => {
|
||||||
let step = 0;
|
let step = 0;
|
||||||
sdkQueryFactory.mockImplementation(() =>
|
sdkQueryFactory.mockImplementation(() =>
|
||||||
|
|||||||
@@ -25,6 +25,16 @@ import {
|
|||||||
toolDetailBranchByName,
|
toolDetailBranchByName,
|
||||||
} from "../tool-call-detail-primitives.js";
|
} 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
|
const ClaudeToolEnvelopeSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
@@ -128,17 +138,11 @@ const ClaudeToolDetailPass2Schema = z.union([
|
|||||||
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
toolDetailBranchByName("search", ToolSearchInputSchema, z.unknown(), (input) =>
|
||||||
toSearchToolDetail({ input, toolName: "search" }),
|
toSearchToolDetail({ input, toolName: "search" }),
|
||||||
),
|
),
|
||||||
toolDetailBranchByName(
|
toolDetailBranchByName("Grep", ToolSearchInputSchema, ClaudeGrepOutputSchema, (input, output) =>
|
||||||
"Grep",
|
toSearchToolDetail({ input, output, toolName: "grep" }),
|
||||||
ToolSearchInputSchema,
|
|
||||||
ToolGrepOutputSchema.nullable(),
|
|
||||||
(input, output) => toSearchToolDetail({ input, output, toolName: "grep" }),
|
|
||||||
),
|
),
|
||||||
toolDetailBranchByName(
|
toolDetailBranchByName("grep", ToolSearchInputSchema, ClaudeGrepOutputSchema, (input, output) =>
|
||||||
"grep",
|
toSearchToolDetail({ input, output, toolName: "grep" }),
|
||||||
ToolSearchInputSchema,
|
|
||||||
ToolGrepOutputSchema.nullable(),
|
|
||||||
(input, output) => toSearchToolDetail({ input, output, toolName: "grep" }),
|
|
||||||
),
|
),
|
||||||
toolDetailBranchByName(
|
toolDetailBranchByName(
|
||||||
"Glob",
|
"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", () => {
|
it("maps WebSearch calls with structured results", () => {
|
||||||
const item = expectMapped(
|
const item = expectMapped(
|
||||||
mapClaudeCompletedToolCall({
|
mapClaudeCompletedToolCall({
|
||||||
|
|||||||
Reference in New Issue
Block a user