Update files

This commit is contained in:
Mohamed Boudra
2026-02-09 12:00:49 +07:00
parent 6d115074e9
commit 34fff59658
30 changed files with 500 additions and 321 deletions
@@ -362,8 +362,6 @@ export function AgentStreamView({
return (
<ToolCall
toolName={data.name}
args={data.input}
result={data.result}
error={data.error}
status={data.status}
detail={data.detail}
@@ -1123,8 +1121,11 @@ function PermissionRequestCard({
{!isPlanRequest ? (
<ToolCallDetailsContent
input={request.input ?? null}
output={null}
detail={{
type: "unknown",
rawInput: request.input ?? null,
rawOutput: null,
}}
maxHeight={200}
/>
) : null}
+36 -16
View File
@@ -1151,7 +1151,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
interface ToolCallProps {
toolName: string;
args: unknown | null;
args?: unknown | null;
result?: unknown | null;
error?: unknown | null;
status: "executing" | "running" | "completed" | "failed" | "canceled";
@@ -1191,29 +1191,53 @@ export const ToolCall = memo(function ToolCall({
UnistylesRuntime.breakpoint === "xs" ||
UnistylesRuntime.breakpoint === "sm";
const effectiveDetail = useMemo<ToolCallDetail | undefined>(() => {
if (detail) {
return detail;
}
if (args !== undefined || result !== undefined) {
return {
type: "unknown",
rawInput: args ?? null,
rawOutput: result ?? null,
};
}
return undefined;
}, [detail, args, result]);
const displayDetail =
effectiveDetail ?? {
type: "unknown",
rawInput: null,
rawOutput: null,
};
const displayModel = useMemo(
() =>
buildToolCallDisplayModel({
name: toolName,
status: status === "executing" ? "running" : status,
input: args ?? null,
output: result ?? null,
error: error ?? null,
detail,
detail: displayDetail,
metadata,
cwd,
}),
[toolName, status, args, result, error, detail, metadata, cwd]
[toolName, status, error, displayDetail, metadata, cwd]
);
const displayName = displayModel.displayName;
const summary = displayModel.summary;
const errorText = displayModel.errorText;
const iconCategory = detail?.type ?? toolName.trim().toLowerCase();
const IconComponent = resolveToolCallIcon(toolName, detail);
const iconCategory = effectiveDetail?.type ?? toolName.trim().toLowerCase();
const IconComponent = resolveToolCallIcon(toolName, effectiveDetail);
// Check if there's any content to display
const hasDetails =
args !== undefined || result !== undefined || error !== undefined;
Boolean(error) ||
(effectiveDetail
? effectiveDetail.type !== "unknown" ||
effectiveDetail.rawInput !== null ||
effectiveDetail.rawOutput !== null
: false);
const handleToggle = useCallback(() => {
if (!isMobile && isPerfLoggingEnabled()) {
@@ -1224,15 +1248,13 @@ export const ToolCall = memo(function ToolCall({
toolName,
displayName,
summary,
detail,
input: args,
output: result,
detail: effectiveDetail,
errorText,
});
} else {
setIsExpanded((prev) => !prev);
}
}, [isMobile, openToolCall, toolName, displayName, summary, detail, args, result, errorText]);
}, [isMobile, openToolCall, toolName, displayName, summary, effectiveDetail, errorText]);
useEffect(() => {
if (isMobile || !isPerfLoggingEnabled()) {
@@ -1293,14 +1315,12 @@ export const ToolCall = memo(function ToolCall({
if (isMobile) return null;
return (
<ToolCallDetailsContent
detail={detail}
input={args}
output={result}
detail={effectiveDetail}
errorText={errorText}
maxHeight={400}
/>
);
}, [isMobile, detail, args, result, errorText]);
}, [isMobile, effectiveDetail, errorText]);
return (
<ExpandableBadge
@@ -17,16 +17,12 @@ const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
interface ToolCallDetailsContentProps {
detail?: ToolCallDetail;
input?: unknown | null;
output?: unknown | null;
errorText?: string;
maxHeight?: number;
}
export function ToolCallDetailsContent({
detail,
input,
output,
errorText,
maxHeight = 300,
}: ToolCallDetailsContentProps) {
@@ -141,10 +137,10 @@ export function ToolCallDetailsContent({
<Text selectable style={styles.scrollText}>{detail.query}</Text>
</View>
);
} else {
} else if (detail?.type === "unknown") {
const sectionsFromTopLevel = [
{ title: "Input", value: input },
{ title: "Output", value: output },
{ title: "Input", value: detail.rawInput },
{ title: "Output", value: detail.rawOutput },
].filter((entry) => entry.value !== null && entry.value !== undefined);
for (const section of sectionsFromTopLevel) {
@@ -27,8 +27,6 @@ export type ToolCallSheetData = {
displayName: string;
summary?: string;
detail?: ToolCallDetail;
input?: unknown | null;
output?: unknown | null;
errorText?: string;
};
@@ -132,7 +130,7 @@ interface ToolCallSheetContentProps {
}
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
const { toolName, displayName, detail, input, output, errorText } = data;
const { toolName, displayName, detail, errorText } = data;
const IconComponent = resolveToolCallIcon(toolName, detail);
@@ -158,8 +156,6 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
>
<ToolCallDetailsContent
detail={detail}
input={input}
output={output}
errorText={errorText}
/>
</BottomSheetScrollView>
+5 -2
View File
@@ -26,8 +26,11 @@ const toolCallEvent = (): AgentStreamEventPayload => ({
callId: "buffer-tool-call",
name: "run",
status: "running",
input: { command: "echo hi" },
output: null,
detail: {
type: "unknown",
rawInput: { command: "echo hi" },
rawOutput: null,
},
error: null,
},
});
@@ -213,14 +213,18 @@ function buildToolEvent({
error?: unknown;
detail?: ToolCallDetail;
}): AgentStreamEventPayload {
const canonicalDetail: ToolCallDetail = detail ?? {
type: "unknown",
rawInput: input,
rawOutput: output,
};
const baseItem = {
type: "tool_call" as const,
name,
status,
callId,
input,
output,
...(detail ? { detail } : {}),
detail: canonicalDetail,
};
const item =
+19 -14
View File
@@ -40,15 +40,19 @@ function canonicalToolTimeline(params: {
metadata?: Record<string, unknown>;
detail?: ToolCallDetail;
}): AgentStreamEventPayload {
const detail: ToolCallDetail = params.detail ?? {
type: "unknown",
rawInput: params.input ?? null,
rawOutput: params.output ?? null,
};
const baseItem = {
type: "tool_call" as const,
callId: params.callId,
name: params.name,
status: params.status,
input: params.input ?? null,
output: params.output ?? null,
detail,
metadata: params.metadata,
...(params.detail ? { detail: params.detail } : {}),
};
const item =
@@ -144,10 +148,13 @@ describe("stream reducer canonical tool calls", () => {
assert.strictEqual(tools.length, 1);
assert.strictEqual(tools[0].payload.data.status, "completed");
assert.deepStrictEqual(tools[0].payload.data.input, { command: "pwd" });
assert.deepStrictEqual(tools[0].payload.data.result, {
output: "/tmp/repo\n",
exitCode: 0,
assert.deepStrictEqual(tools[0].payload.data.detail, {
type: "unknown",
rawInput: { command: "pwd" },
rawOutput: {
output: "/tmp/repo\n",
exitCode: 0,
},
});
});
@@ -176,8 +183,6 @@ describe("stream reducer canonical tool calls", () => {
const summary = buildToolCallDisplayModel({
name: tool.payload.data.name,
status: tool.payload.data.status,
input: tool.payload.data.input,
output: tool.payload.data.result,
error: tool.payload.data.error,
detail: tool.payload.data.detail,
}).summary;
@@ -209,8 +214,6 @@ describe("stream reducer canonical tool calls", () => {
const summary = buildToolCallDisplayModel({
name: tool.payload.data.name,
status: tool.payload.data.status,
input: tool.payload.data.input,
output: tool.payload.data.result,
error: tool.payload.data.error,
detail: tool.payload.data.detail,
cwd: "/tmp/repo",
@@ -240,8 +243,6 @@ describe("stream reducer canonical tool calls", () => {
const display = buildToolCallDisplayModel({
name: tool.payload.data.name,
status: tool.payload.data.status,
input: tool.payload.data.input,
output: tool.payload.data.result,
error: tool.payload.data.error,
detail: tool.payload.data.detail,
});
@@ -279,7 +280,11 @@ describe("stream reducer canonical tool calls", () => {
const tool = findToolByCallId(state, callId);
assert.ok(tool);
assert.deepStrictEqual(tool.payload.data.input, { path: "README.md" });
assert.deepStrictEqual(tool.payload.data.detail, {
type: "unknown",
rawInput: { path: "README.md" },
rawOutput: { content: "hello" },
});
assert.strictEqual(tool.payload.data.status, "completed");
});
+37 -13
View File
@@ -100,10 +100,8 @@ export interface AgentToolCallData {
callId: string;
name: string;
status: AgentToolCallStatus;
input: unknown | null;
result: unknown | null;
error: unknown | null;
detail?: ToolCallDetail;
detail: ToolCallDetail;
metadata?: Record<string, unknown>;
}
@@ -328,7 +326,7 @@ function hasNonEmptyObject(value: unknown): boolean {
return isRecord(value) && Object.keys(value).length > 0;
}
function mergeCanonicalValue(
function mergeUnknownValue(
existing: unknown | null,
incoming: unknown | null
): unknown | null {
@@ -343,6 +341,34 @@ function mergeCanonicalValue(
return incoming;
}
function mergeToolCallDetail(existing: ToolCallDetail, incoming: ToolCallDetail): ToolCallDetail {
if (existing.type === "unknown" && incoming.type !== "unknown") {
return incoming;
}
if (incoming.type === "unknown" && existing.type !== "unknown") {
return existing;
}
if (existing.type === "unknown" && incoming.type === "unknown") {
return {
type: "unknown",
rawInput: mergeUnknownValue(existing.rawInput, incoming.rawInput),
rawOutput: mergeUnknownValue(existing.rawOutput, incoming.rawOutput),
};
}
if (existing.type === incoming.type) {
return { ...existing, ...incoming };
}
return incoming;
}
function rawInputFromDetail(detail: ToolCallDetail): unknown | null {
return detail.type === "unknown" ? detail.rawInput : null;
}
function mergeAgentToolCallStatus(
existing: AgentToolCallStatus,
incoming: AgentToolCallStatus
@@ -375,8 +401,6 @@ function appendAgentToolCall(
if (!existing || !isAgentToolCallItem(existing)) {
return state;
}
const mergedInput = mergeCanonicalValue(existing.payload.data.input, data.input);
const mergedResult = mergeCanonicalValue(existing.payload.data.result, data.result);
const mergedStatus = mergeAgentToolCallStatus(
existing.payload.data.status,
data.status
@@ -389,6 +413,7 @@ function appendAgentToolCall(
data.metadata || existing.payload.data.metadata
? { ...existing.payload.data.metadata, ...data.metadata }
: undefined;
const mergedDetail = mergeToolCallDetail(existing.payload.data.detail, data.detail);
next[existingIndex] = {
...existing,
@@ -399,10 +424,8 @@ function appendAgentToolCall(
...existing.payload.data,
...data,
status: mergedStatus,
input: mergedInput,
result: mergedResult,
error: mergedError,
detail: data.detail ?? existing.payload.data.detail,
detail: mergedDetail,
metadata: mergedMetadata,
},
},
@@ -527,7 +550,10 @@ export function reduceStreamUpdate(
) {
// For Claude: TodoWrite often appears as a tool call that never resolves. Always render it
// as Tasks when possible and otherwise hide it to avoid a stuck loading tool call.
const tasks = extractTaskEntriesFromToolCall(item.name, item.input);
const tasks = extractTaskEntriesFromToolCall(
item.name,
rawInputFromDetail(item.detail)
);
if (tasks) {
nextState = appendTodoList(
state,
@@ -544,7 +570,7 @@ export function reduceStreamUpdate(
const tasks = extractTaskEntriesFromToolCall(
item.name,
item.input
rawInputFromDetail(item.detail)
);
if (tasks) {
nextState = appendTodoList(
@@ -566,8 +592,6 @@ export function reduceStreamUpdate(
callId: item.callId,
name: item.name,
status: item.status,
input: item.input,
result: item.output,
error: item.error,
detail: item.detail,
metadata: item.metadata,
@@ -7,8 +7,6 @@ describe("tool-call-display", () => {
const display = buildToolCallDisplayModel({
name: "shell",
status: "running",
input: { command: "npm test" },
output: null,
error: null,
detail: {
type: "shell",
@@ -26,8 +24,6 @@ describe("tool-call-display", () => {
const display = buildToolCallDisplayModel({
name: "read_file",
status: "completed",
input: { path: "/tmp/repo/src/index.ts" },
output: { content: "hello" },
error: null,
detail: {
type: "read",
@@ -46,9 +42,12 @@ describe("tool-call-display", () => {
const display = buildToolCallDisplayModel({
name: "task",
status: "running",
input: null,
output: null,
error: null,
detail: {
type: "unknown",
rawInput: null,
rawOutput: null,
},
metadata: {
subAgentActivity: "Running tests",
},
@@ -64,9 +63,12 @@ describe("tool-call-display", () => {
const display = buildToolCallDisplayModel({
name: "custom_tool_name",
status: "completed",
input: null,
output: null,
error: null,
detail: {
type: "unknown",
rawInput: null,
rawOutput: null,
},
});
expect(display).toEqual({
@@ -74,13 +76,16 @@ describe("tool-call-display", () => {
});
});
it("does not derive command summary from raw input when detail is missing", () => {
it("does not derive command summary from unknown raw detail", () => {
const display = buildToolCallDisplayModel({
name: "exec_command",
status: "running",
input: { command: "npm run test" },
output: null,
error: null,
detail: {
type: "unknown",
rawInput: { command: "npm run test" },
rawOutput: null,
},
});
expect(display).toEqual({
@@ -92,9 +97,12 @@ describe("tool-call-display", () => {
const display = buildToolCallDisplayModel({
name: "shell",
status: "failed",
input: { command: "false" },
output: null,
error: { message: "boom" },
detail: {
type: "unknown",
rawInput: { command: "false" },
rawOutput: null,
},
});
expect(display.errorText).toBe('{\n "message": "boom"\n}');
+1
View File
@@ -10,6 +10,7 @@ const TOOL_DETAIL_ICONS: Record<ToolCallDetail["type"], ToolCallIconComponent> =
edit: Pencil,
write: Pencil,
search: Search,
unknown: Wrench,
};
export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail): ToolCallIconComponent {