Update files
This commit is contained in:
@@ -363,6 +363,7 @@ export function AgentStreamView({
|
||||
return (
|
||||
<ToolCall
|
||||
toolName={data.name}
|
||||
provider={data.provider}
|
||||
args={data.input}
|
||||
result={data.result}
|
||||
error={data.error}
|
||||
@@ -907,8 +908,12 @@ function PermissionRequestCard({
|
||||
if (isPlanRequest) {
|
||||
return null;
|
||||
}
|
||||
return parseToolCallDisplay({ name: request.name ?? "unknown", input: request.input });
|
||||
}, [isPlanRequest, request.name, request.input]);
|
||||
return parseToolCallDisplay({
|
||||
name: request.name ?? "unknown",
|
||||
provider: request.provider,
|
||||
input: request.input,
|
||||
});
|
||||
}, [isPlanRequest, request.name, request.provider, request.input]);
|
||||
|
||||
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
|
||||
|
||||
|
||||
@@ -420,7 +420,6 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 100,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1017,7 +1016,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
if (isLoading) {
|
||||
shimmer.value = -1;
|
||||
shimmer.value = withRepeat(
|
||||
withTiming(1, { duration: 2400, easing: Easing.bezier(0.4, 0, 0.6, 1) }),
|
||||
withTiming(1, { duration: 3200, easing: Easing.linear }),
|
||||
-1
|
||||
);
|
||||
} else {
|
||||
@@ -1026,7 +1025,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
const shimmerBandWidth = 100;
|
||||
const shimmerBandWidth = 14;
|
||||
const shimmerStyle = useAnimatedStyle(() => {
|
||||
const travel = badgeWidth + shimmerBandWidth;
|
||||
return {
|
||||
@@ -1098,7 +1097,11 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
{isLoading && badgeWidth > 0 ? (
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
style={[expandableBadgeStylesheet.shimmerOverlay, shimmerStyle]}
|
||||
style={[
|
||||
expandableBadgeStylesheet.shimmerOverlay,
|
||||
{ width: shimmerBandWidth },
|
||||
shimmerStyle,
|
||||
]}
|
||||
>
|
||||
<Svg width="100%" height="100%" preserveAspectRatio="none">
|
||||
<Defs>
|
||||
@@ -1109,10 +1112,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
x2="1"
|
||||
y2="0"
|
||||
>
|
||||
<Stop offset="0" stopColor={theme.colors.surface1} stopOpacity="0" />
|
||||
<Stop offset="0.35" stopColor={theme.colors.surface1} stopOpacity="1" />
|
||||
<Stop offset="0.65" stopColor={theme.colors.surface1} stopOpacity="1" />
|
||||
<Stop offset="1" stopColor={theme.colors.surface1} stopOpacity="0" />
|
||||
<Stop offset="0" stopColor={baseColors.white} stopOpacity="0" />
|
||||
<Stop offset="0.34" stopColor={baseColors.white} stopOpacity="0" />
|
||||
<Stop offset="0.46" stopColor={baseColors.white} stopOpacity="0.12" />
|
||||
<Stop offset="0.5" stopColor={baseColors.white} stopOpacity="0.34" />
|
||||
<Stop offset="0.54" stopColor={baseColors.white} stopOpacity="0.12" />
|
||||
<Stop offset="0.66" stopColor={baseColors.white} stopOpacity="0" />
|
||||
<Stop offset="1" stopColor={baseColors.white} stopOpacity="0" />
|
||||
</LinearGradient>
|
||||
</Defs>
|
||||
<Rect width="100%" height="100%" fill="url(#shimmerGrad)" />
|
||||
@@ -1149,6 +1155,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
|
||||
interface ToolCallProps {
|
||||
toolName: string;
|
||||
provider?: string;
|
||||
args: any;
|
||||
result?: any;
|
||||
error?: any;
|
||||
@@ -1176,6 +1183,7 @@ const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
|
||||
|
||||
export const ToolCall = memo(function ToolCall({
|
||||
toolName,
|
||||
provider,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
@@ -1197,8 +1205,17 @@ export const ToolCall = memo(function ToolCall({
|
||||
UnistylesRuntime.breakpoint === "sm";
|
||||
|
||||
const displayInfo = useMemo(
|
||||
() => parseToolCallDisplay({ name: toolName, input: args, output: result, error, metadata, cwd }),
|
||||
[toolName, args, result, error, metadata, cwd]
|
||||
() =>
|
||||
parseToolCallDisplay({
|
||||
name: toolName,
|
||||
provider,
|
||||
input: args,
|
||||
output: result,
|
||||
error,
|
||||
metadata,
|
||||
cwd,
|
||||
}),
|
||||
[toolName, provider, args, result, error, metadata, cwd]
|
||||
);
|
||||
const { kind, displayName, summary, detail, errorText } = displayInfo;
|
||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||
|
||||
@@ -384,6 +384,54 @@ function testToolCallParsedPayloadHydration() {
|
||||
assert.ok(commandPass, 'Command payload should persist across hydration');
|
||||
}
|
||||
|
||||
function testNullToolPayloadDoesNotEraseKnownInput() {
|
||||
const timestampStart = new Date("2025-01-01T10:36:00Z");
|
||||
const timestampFinish = new Date("2025-01-01T10:36:05Z");
|
||||
const callId = "null-input-preserve";
|
||||
|
||||
const updates: Array<{ event: AgentStreamEventPayload; timestamp: Date }> = [
|
||||
{
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
name: "shell",
|
||||
status: "pending",
|
||||
callId,
|
||||
input: { command: "pwd" },
|
||||
},
|
||||
},
|
||||
timestamp: timestampStart,
|
||||
},
|
||||
{
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
name: "shell",
|
||||
status: "completed",
|
||||
callId,
|
||||
input: null,
|
||||
output: { type: "command", output: "/tmp" },
|
||||
},
|
||||
},
|
||||
timestamp: timestampFinish,
|
||||
},
|
||||
];
|
||||
|
||||
const state = hydrateStreamState(updates);
|
||||
const commandEntry = state.find(
|
||||
(item): item is AgentToolCallItem =>
|
||||
isAgentToolCallItem(item) && item.payload.data.callId === callId
|
||||
);
|
||||
|
||||
assert.ok(commandEntry, "Tool call should exist");
|
||||
assert.strictEqual(commandEntry.payload.data.status, "completed");
|
||||
assert.deepStrictEqual(commandEntry.payload.data.input, { command: "pwd" });
|
||||
}
|
||||
|
||||
function buildClaudeToolUseBlock({
|
||||
id,
|
||||
name,
|
||||
@@ -1116,6 +1164,7 @@ describe('stream timeline reducers', () => {
|
||||
it('infers failure from error payloads', testToolCallFailureInferenceFromError);
|
||||
it('reconciles late call IDs against pending entries', testToolCallLateCallIdReconciliation);
|
||||
it('persists parsed read/edit/command payloads after hydration', testToolCallParsedPayloadHydration);
|
||||
it('preserves known input when later tool updates send null input', testNullToolPayloadDoesNotEraseKnownInput);
|
||||
it('hydrates Claude tool bodies with parsed content', testClaudeHydratedToolBodies);
|
||||
it('preserves whitespace in assistant chunk concatenation', testAssistantWhitespacePreservation);
|
||||
it('hydrates user messages and deduplicates optimistic/live entries', testUserMessageHydration);
|
||||
|
||||
@@ -431,15 +431,15 @@ function appendAgentToolCall(
|
||||
const next = [...state];
|
||||
const existing = next[existingIndex] as AgentToolCallItem;
|
||||
const mergedInput =
|
||||
payloadData.input !== undefined
|
||||
hasValue(payloadData.input)
|
||||
? payloadData.input
|
||||
: existing.payload.data.input;
|
||||
const mergedResult =
|
||||
payloadData.result !== undefined
|
||||
hasValue(payloadData.result)
|
||||
? payloadData.result
|
||||
: existing.payload.data.result;
|
||||
const mergedError =
|
||||
payloadData.error !== undefined
|
||||
hasValue(payloadData.error)
|
||||
? payloadData.error
|
||||
: existing.payload.data.error;
|
||||
const mergedStatus = mergeToolCallStatus(
|
||||
|
||||
@@ -64,6 +64,35 @@ describe("parseToolCallDisplay", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("falls back to output command when shell input is missing", () => {
|
||||
const output = { type: "command", command: "pwd", output: "/some/path" };
|
||||
|
||||
const info: ToolCallDisplayInfo = parseToolCallDisplay({ name: "Bash", output });
|
||||
expect(info.summary).toBe("pwd");
|
||||
expect(info.detail.type).toBe("shell");
|
||||
if (info.detail.type === "shell") {
|
||||
expect(info.detail.command).toBe("pwd");
|
||||
expect(info.detail.output).toBe("/some/path");
|
||||
}
|
||||
});
|
||||
|
||||
test("falls back to read output path when input is missing", () => {
|
||||
const info: ToolCallDisplayInfo = parseToolCallDisplay({
|
||||
name: "Read",
|
||||
output: {
|
||||
type: "file_read",
|
||||
filePath: "/some/file.txt",
|
||||
content: "hello",
|
||||
},
|
||||
});
|
||||
expect(info.summary).toBe("/some/file.txt");
|
||||
expect(info.detail.type).toBe("read");
|
||||
if (info.detail.type === "read") {
|
||||
expect(info.detail.filePath).toBe("/some/file.txt");
|
||||
expect(info.detail.content).toBe("hello");
|
||||
}
|
||||
});
|
||||
|
||||
test("strips shell + cd wrapper from command (Codex exec_command style)", () => {
|
||||
const input = {
|
||||
command:
|
||||
@@ -110,6 +139,14 @@ describe("parseToolCallDisplay", () => {
|
||||
expect(info.displayName).toBe("Read");
|
||||
});
|
||||
|
||||
test("uses stable label for Edit in frontend", () => {
|
||||
const input = { file_path: "/some/file.txt", old_string: "a", new_string: "b" };
|
||||
const first = parseToolCallDisplay({ name: "Edit", input });
|
||||
const second = parseToolCallDisplay({ name: "Edit", input });
|
||||
expect(first.displayName).toBe("Edit");
|
||||
expect(second.displayName).toBe("Edit");
|
||||
});
|
||||
|
||||
test("normalizes tool names - paseo_voice.speak to Speak", () => {
|
||||
const input = { text: "hello from namespaced speak" };
|
||||
const info = parseToolCallDisplay({ name: "paseo_voice.speak", input });
|
||||
@@ -128,6 +165,15 @@ describe("parseToolCallDisplay", () => {
|
||||
expect(info.displayName).toBe("MyCustomTool");
|
||||
});
|
||||
|
||||
test("does not let Task metadata override non-Task summary", () => {
|
||||
const info = parseToolCallDisplay({
|
||||
name: "shell",
|
||||
input: { command: "pwd" },
|
||||
metadata: { subAgentActivity: "Read" },
|
||||
});
|
||||
expect(info.summary).toBe("pwd");
|
||||
});
|
||||
|
||||
test("parses non-command tool call into generic detail", () => {
|
||||
const input = { file_path: "/some/file.txt" };
|
||||
const output = { content: "file contents here", lineCount: 42 };
|
||||
@@ -141,12 +187,16 @@ describe("parseToolCallDisplay", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("handles file_write output as generic", () => {
|
||||
test("handles file_write output as edit", () => {
|
||||
const input = { file_path: "/some/file.txt", content: "new content" };
|
||||
const output = { type: "file_write", filePath: "/some/file.txt" };
|
||||
|
||||
const info: ToolCallDisplayInfo = parseToolCallDisplay({ name: "Write", input, output });
|
||||
expect(info.detail.type).toBe("generic");
|
||||
expect(info.detail.type).toBe("edit");
|
||||
expect(info.summary).toBe("/some/file.txt");
|
||||
if (info.detail.type === "edit") {
|
||||
expect(info.detail.filePath).toBe("/some/file.txt");
|
||||
}
|
||||
});
|
||||
|
||||
test("handles undefined input and output gracefully", () => {
|
||||
@@ -366,14 +416,13 @@ describe("parseToolCallDisplay - read_file (Codex)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("Codex read_file falls through to generic when result is missing", () => {
|
||||
test("Codex read_file stays read when result is missing", () => {
|
||||
const input = {
|
||||
path: "/some/file.txt",
|
||||
};
|
||||
|
||||
const info = parseToolCallDisplay({ name: "read_file", input });
|
||||
// Without result, it can't match the schema so falls through to generic
|
||||
expect(info.detail.type).toBe("generic");
|
||||
expect(info.detail.type).toBe("read");
|
||||
expect(info.displayName).toBe("Read");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user