Merge branch 'investigate/ui-network-responsive' into main
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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}');
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user