refactor: simplify tool call display and path handling
- Remove displayName from tool calls, use name directly with icon - Add medium font weight for tool call badges - Strip workspace root from file paths at parsing level using Zod schema - Add accentBorder to tool call badges and git diff file blocks - Fix user message alignment and vertical spacing - Deduplicate stripCwdPrefix into tool-call-parsers.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
4f3b2317ab
commit
66881511b6
@@ -283,15 +283,11 @@ export function AgentStreamView({
|
||||
case "tool_call": {
|
||||
const { payload } = item;
|
||||
|
||||
console.log("[TOOL_CALL_DEBUG]", JSON.stringify(payload, null, 2));
|
||||
|
||||
if (payload.source === "agent") {
|
||||
const data = payload.data;
|
||||
const toolLabel = data.displayName ?? `${data.server}/${data.tool}`;
|
||||
content = (
|
||||
<ToolCall
|
||||
toolName={toolLabel}
|
||||
kind={data.kind}
|
||||
toolName={data.name}
|
||||
args={data.input}
|
||||
result={data.result}
|
||||
error={data.error}
|
||||
@@ -299,6 +295,7 @@ export function AgentStreamView({
|
||||
parsedEditEntries={data.parsedEdits}
|
||||
parsedReadEntries={data.parsedReads}
|
||||
parsedCommandDetails={data.parsedCommand ?? null}
|
||||
cwd={agent.cwd}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -315,7 +315,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.colors.muted,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderColor: theme.colors.accentBorder,
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
fileHeader: {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Colors } from "@/constants/theme";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import type { TodoEntry, ThoughtStatus } from "@/types/stream";
|
||||
import type { CommandDetails, EditEntry, ReadEntry } from "@/utils/tool-call-parsers";
|
||||
import { extractPrincipalParam } from "@/utils/tool-call-parsers";
|
||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||
import { useToolCallSheet } from "./tool-call-sheet";
|
||||
|
||||
@@ -41,15 +42,16 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
marginBottom: theme.spacing[3],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
marginBottom: theme.spacing[4],
|
||||
marginTop: theme.spacing[4],
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
},
|
||||
bubble: {
|
||||
backgroundColor: theme.colors.muted,
|
||||
borderRadius: theme.borderRadius["2xl"],
|
||||
borderTopRightRadius: theme.borderRadius.sm,
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[4],
|
||||
maxWidth: "80%",
|
||||
},
|
||||
text: {
|
||||
@@ -187,7 +189,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
pressable: {
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderColor: theme.colors.accentBorder,
|
||||
backgroundColor: theme.colors.secondary,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
@@ -209,13 +211,24 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
label: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
flexShrink: 0,
|
||||
},
|
||||
secondaryLabel: {
|
||||
flex: 1,
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
marginLeft: theme.spacing[2],
|
||||
},
|
||||
spacer: {
|
||||
flex: 1,
|
||||
},
|
||||
chevron: {
|
||||
marginLeft: theme.spacing[1],
|
||||
flexShrink: 0,
|
||||
},
|
||||
detailWrapper: {
|
||||
marginTop: theme.spacing[2],
|
||||
@@ -829,6 +842,7 @@ interface AgentThoughtMessageProps {
|
||||
|
||||
interface ExpandableBadgeProps {
|
||||
label: string;
|
||||
secondaryLabel?: string;
|
||||
icon?: ComponentType<{ size?: number; color?: string }>;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
@@ -839,6 +853,7 @@ interface ExpandableBadgeProps {
|
||||
|
||||
const ExpandableBadge = memo(function ExpandableBadge({
|
||||
label,
|
||||
secondaryLabel,
|
||||
icon,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
@@ -914,6 +929,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
<Text style={expandableBadgeStylesheet.label} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{secondaryLabel ? (
|
||||
<Text style={expandableBadgeStylesheet.secondaryLabel} numberOfLines={1}>
|
||||
{secondaryLabel}
|
||||
</Text>
|
||||
) : (
|
||||
<View style={expandableBadgeStylesheet.spacer} />
|
||||
)}
|
||||
{hasDetails ? (
|
||||
<ChevronRight
|
||||
size={14}
|
||||
@@ -1084,7 +1106,6 @@ export function AgentThoughtMessage({ message, status = "ready" }: AgentThoughtM
|
||||
|
||||
interface ToolCallProps {
|
||||
toolName: string;
|
||||
kind?: string; // Optional kind for ACP tool calls
|
||||
args: any;
|
||||
result?: any;
|
||||
error?: any;
|
||||
@@ -1092,6 +1113,7 @@ interface ToolCallProps {
|
||||
parsedEditEntries?: EditEntry[];
|
||||
parsedReadEntries?: ReadEntry[];
|
||||
parsedCommandDetails?: CommandDetails | null;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
// Icon mapping for tool kinds
|
||||
@@ -1102,9 +1124,19 @@ const toolKindIcons: Record<string, any> = {
|
||||
search: Search,
|
||||
};
|
||||
|
||||
// Derive tool kind from tool name for icon selection
|
||||
function getToolKindFromName(toolName: string): string {
|
||||
const lower = toolName.toLowerCase();
|
||||
if (lower === "read" || lower === "read_file" || lower.startsWith("read")) return "read";
|
||||
if (lower === "edit" || lower === "write" || lower === "apply_patch") return "edit";
|
||||
if (lower === "bash" || lower === "shell") return "execute";
|
||||
if (lower === "grep" || lower === "glob" || lower === "web_search") return "search";
|
||||
return "tool";
|
||||
}
|
||||
|
||||
|
||||
export const ToolCall = memo(function ToolCall({
|
||||
toolName,
|
||||
kind,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
@@ -1112,12 +1144,18 @@ export const ToolCall = memo(function ToolCall({
|
||||
parsedEditEntries,
|
||||
parsedReadEntries,
|
||||
parsedCommandDetails,
|
||||
cwd,
|
||||
}: ToolCallProps) {
|
||||
const { openToolCall } = useToolCallSheet();
|
||||
|
||||
const IconComponent = kind
|
||||
? toolKindIcons[kind.toLowerCase()] || Wrench
|
||||
: Wrench;
|
||||
const kind = getToolKindFromName(toolName);
|
||||
const IconComponent = toolKindIcons[kind] || Wrench;
|
||||
|
||||
// Extract principal param for secondary label (memoized)
|
||||
const principalParam = useMemo(
|
||||
() => extractPrincipalParam(args, cwd),
|
||||
[args, cwd]
|
||||
);
|
||||
|
||||
// Check if there's any content to display in the sheet
|
||||
const hasDetails = args !== undefined || result !== undefined || error !== undefined;
|
||||
@@ -1133,8 +1171,9 @@ export const ToolCall = memo(function ToolCall({
|
||||
parsedEditEntries,
|
||||
parsedReadEntries,
|
||||
parsedCommandDetails,
|
||||
cwd,
|
||||
});
|
||||
}, [openToolCall, toolName, kind, status, args, result, error, parsedEditEntries, parsedReadEntries, parsedCommandDetails]);
|
||||
}, [openToolCall, toolName, kind, status, args, result, error, parsedEditEntries, parsedReadEntries, parsedCommandDetails, cwd]);
|
||||
|
||||
// Dummy renderDetails to make badge tappable - actual content is rendered in the sheet
|
||||
const dummyRenderDetails = useCallback(() => null, []);
|
||||
@@ -1142,6 +1181,7 @@ export const ToolCall = memo(function ToolCall({
|
||||
return (
|
||||
<ExpandableBadge
|
||||
label={toolName}
|
||||
secondaryLabel={principalParam}
|
||||
icon={IconComponent}
|
||||
isExpanded={false}
|
||||
onToggle={handleOpenSheet}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Pencil, Eye, SquareTerminal, Search, Wrench, X } from "lucide-react-nat
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
import {
|
||||
extractKeyValuePairs,
|
||||
stripCwdPrefix,
|
||||
type EditEntry,
|
||||
type ReadEntry,
|
||||
type CommandDetails,
|
||||
@@ -36,6 +37,7 @@ export interface ToolCallSheetData {
|
||||
parsedEditEntries?: EditEntry[];
|
||||
parsedReadEntries?: ReadEntry[];
|
||||
parsedCommandDetails?: CommandDetails | null;
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
interface ToolCallSheetContextValue {
|
||||
@@ -234,6 +236,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
parsedEditEntries,
|
||||
parsedReadEntries,
|
||||
parsedCommandDetails,
|
||||
cwd,
|
||||
} = data;
|
||||
|
||||
const IconComponent = kind
|
||||
@@ -453,7 +456,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
<Text style={styles.sectionTitle}>Diff</Text>
|
||||
{entry.filePath ? (
|
||||
<View style={styles.fileBadge}>
|
||||
<Text style={styles.fileBadgeText}>{entry.filePath}</Text>
|
||||
<Text style={styles.fileBadgeText}>{stripCwdPrefix(entry.filePath, cwd)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={styles.diffContainer}>
|
||||
@@ -478,7 +481,7 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
<Text style={styles.sectionTitle}>Read Result</Text>
|
||||
{entry.filePath ? (
|
||||
<View style={styles.fileBadge}>
|
||||
<Text style={styles.fileBadgeText}>{entry.filePath}</Text>
|
||||
<Text style={styles.fileBadgeText}>{stripCwdPrefix(entry.filePath, cwd)}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<ScrollView
|
||||
|
||||
@@ -179,6 +179,7 @@ const commonTheme = {
|
||||
|
||||
fontWeight: {
|
||||
normal: "normal" as const,
|
||||
medium: "500" as const,
|
||||
semibold: "600" as const,
|
||||
bold: "bold" as const,
|
||||
},
|
||||
|
||||
@@ -19,8 +19,7 @@ const toolCallEvent = (): AgentStreamEventPayload => ({
|
||||
provider: "codex",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server: "local",
|
||||
tool: "run",
|
||||
name: "run",
|
||||
status: "executing",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,8 +33,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
name: "apply_patch",
|
||||
input: {
|
||||
file_path: "README.md",
|
||||
patch: "*** Begin Patch\n*** Update File: README.md\n@@\n-Old line\n+New line\n*** End Patch",
|
||||
@@ -45,8 +44,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
name: "apply_patch",
|
||||
output: {
|
||||
changes: [
|
||||
{
|
||||
@@ -62,8 +60,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
name: "read_file",
|
||||
input: { file_path: "README.md" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:03Z"),
|
||||
@@ -71,8 +68,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
name: "read_file",
|
||||
output: { content: "# README\nNew line\n" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:04Z"),
|
||||
@@ -80,9 +76,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
kind: "execute",
|
||||
name: "shell",
|
||||
input: { command: "ls" },
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:00:05Z"),
|
||||
@@ -90,8 +84,7 @@ const STREAM_HARNESS_LIVE: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolResultEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
name: "shell",
|
||||
output: {
|
||||
result: {
|
||||
command: "ls",
|
||||
@@ -121,8 +114,7 @@ const STREAM_HARNESS_HYDRATED: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.edit,
|
||||
server: "editor",
|
||||
tool: "apply_patch",
|
||||
name: "apply_patch",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:01Z"),
|
||||
@@ -130,8 +122,7 @@ const STREAM_HARNESS_HYDRATED: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.read,
|
||||
server: "editor",
|
||||
tool: "read_file",
|
||||
name: "read_file",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:02Z"),
|
||||
@@ -139,9 +130,7 @@ const STREAM_HARNESS_HYDRATED: HarnessUpdate[] = [
|
||||
{
|
||||
event: buildToolStartEvent({
|
||||
callId: HARNESS_CALL_IDS.command,
|
||||
server: "command",
|
||||
tool: "shell",
|
||||
kind: "execute",
|
||||
name: "shell",
|
||||
status: "completed",
|
||||
}),
|
||||
timestamp: new Date("2025-02-01T10:05:03Z"),
|
||||
@@ -173,17 +162,13 @@ describe("stream harness captures hydrated regression", () => {
|
||||
|
||||
function buildToolStartEvent({
|
||||
callId,
|
||||
server,
|
||||
tool,
|
||||
name,
|
||||
input,
|
||||
kind,
|
||||
status = "executing",
|
||||
}: {
|
||||
callId: string;
|
||||
server: string;
|
||||
tool: string;
|
||||
name: string;
|
||||
input?: Record<string, unknown>;
|
||||
kind?: string;
|
||||
status?: ToolCallStatus;
|
||||
}): AgentStreamEventPayload {
|
||||
return {
|
||||
@@ -191,12 +176,9 @@ function buildToolStartEvent({
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server,
|
||||
tool,
|
||||
name,
|
||||
status,
|
||||
callId,
|
||||
displayName: tool,
|
||||
kind,
|
||||
input,
|
||||
},
|
||||
};
|
||||
@@ -204,13 +186,11 @@ function buildToolStartEvent({
|
||||
|
||||
function buildToolResultEvent({
|
||||
callId,
|
||||
server,
|
||||
tool,
|
||||
name,
|
||||
output,
|
||||
}: {
|
||||
callId: string;
|
||||
server: string;
|
||||
tool: string;
|
||||
name: string;
|
||||
output?: Record<string, unknown>;
|
||||
}): AgentStreamEventPayload {
|
||||
return {
|
||||
@@ -218,10 +198,8 @@ function buildToolResultEvent({
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server,
|
||||
tool,
|
||||
name,
|
||||
callId,
|
||||
displayName: tool,
|
||||
output,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,8 +41,8 @@ function reasoningTimeline(text: string, provider: TestAgentProvider = "claude")
|
||||
function toolTimeline(
|
||||
id: string,
|
||||
status: string,
|
||||
raw?: unknown,
|
||||
options?: { callId?: string | null; provider?: "claude" | "codex"; server?: string; tool?: string; displayName?: string; kind?: string }
|
||||
_raw?: unknown,
|
||||
options?: { callId?: string | null; provider?: "claude" | "codex"; name?: string }
|
||||
): AgentStreamEventPayload {
|
||||
const explicitCallIdProvided =
|
||||
options && Object.prototype.hasOwnProperty.call(options, "callId");
|
||||
@@ -52,17 +52,15 @@ function toolTimeline(
|
||||
: options?.callId
|
||||
: id;
|
||||
const provider = options?.provider ?? "claude";
|
||||
const name = options?.name ?? id;
|
||||
return {
|
||||
type: "timeline",
|
||||
provider,
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server: options?.server ?? "terminal",
|
||||
tool: options?.tool ?? id,
|
||||
name,
|
||||
status,
|
||||
callId: callIdValue,
|
||||
displayName: options?.displayName ?? id,
|
||||
kind: options?.kind ?? "execute",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -73,12 +71,9 @@ function permissionTimeline(id: string, status: string): AgentStreamEventPayload
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "tool_call",
|
||||
server: "permission",
|
||||
tool: "permission_request",
|
||||
name: "permission_request",
|
||||
status,
|
||||
callId: id,
|
||||
displayName: "Permission",
|
||||
kind: "permission",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -188,8 +183,7 @@ function testToolCallStatusInference() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read',
|
||||
name: 'read',
|
||||
status: 'pending',
|
||||
callId: toolCallId,
|
||||
},
|
||||
@@ -200,8 +194,7 @@ function testToolCallStatusInference() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read',
|
||||
name: 'read',
|
||||
callId: toolCallId,
|
||||
output: { content: 'Hello world' },
|
||||
},
|
||||
@@ -234,8 +227,7 @@ function testToolCallStatusInferenceFromRawOnly() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
callId: toolCallId,
|
||||
status: 'completed',
|
||||
output: { metadata: { exit_code: 0 } },
|
||||
@@ -257,8 +249,7 @@ function testToolCallFailureInferenceFromError() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
callId: toolCallId,
|
||||
error: { message: 'Command failed' },
|
||||
},
|
||||
@@ -304,8 +295,7 @@ function testToolCallParsedPayloadHydration() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
name: 'read_file',
|
||||
status: 'pending',
|
||||
callId: readCallId,
|
||||
input: { file_path: 'README.md' },
|
||||
@@ -319,8 +309,7 @@ function testToolCallParsedPayloadHydration() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
name: 'read_file',
|
||||
callId: readCallId,
|
||||
output: { content: 'Hello world' },
|
||||
},
|
||||
@@ -333,12 +322,10 @@ function testToolCallParsedPayloadHydration() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
status: 'pending',
|
||||
callId: commandCallId,
|
||||
input: { command: 'pwd' },
|
||||
kind: 'execute',
|
||||
},
|
||||
},
|
||||
timestamp: timestampStart,
|
||||
@@ -349,8 +336,7 @@ function testToolCallParsedPayloadHydration() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
callId: commandCallId,
|
||||
output: {
|
||||
result: {
|
||||
@@ -449,8 +435,7 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'apply_patch',
|
||||
name: 'apply_patch',
|
||||
status: 'pending',
|
||||
callId: editCallId,
|
||||
input: {
|
||||
@@ -467,8 +452,7 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'apply_patch',
|
||||
name: 'apply_patch',
|
||||
callId: editCallId,
|
||||
output: {
|
||||
changes: [
|
||||
@@ -489,8 +473,7 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
name: 'read_file',
|
||||
status: 'pending',
|
||||
callId: readCallId,
|
||||
input: { file_path: 'README.md' },
|
||||
@@ -504,8 +487,7 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'editor',
|
||||
tool: 'read_file',
|
||||
name: 'read_file',
|
||||
callId: readCallId,
|
||||
output: { content: '# Hydrated test file\nHello Claude!' },
|
||||
},
|
||||
@@ -518,12 +500,10 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
status: 'pending',
|
||||
callId: commandCallId,
|
||||
input: { command: 'ls' },
|
||||
kind: 'execute',
|
||||
},
|
||||
},
|
||||
timestamp: timestampStart,
|
||||
@@ -534,8 +514,7 @@ function testClaudeHydratedToolBodies() {
|
||||
provider: 'claude',
|
||||
item: {
|
||||
type: 'tool_call',
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
name: 'shell',
|
||||
callId: commandCallId,
|
||||
output: {
|
||||
result: {
|
||||
@@ -814,10 +793,7 @@ function buildConcurrentToolCallUpdates(provider: ToolCallProvider) {
|
||||
|
||||
const baseOptions = {
|
||||
provider,
|
||||
server: 'command',
|
||||
tool: 'shell',
|
||||
displayName: 'Run command',
|
||||
kind: 'execute',
|
||||
name: 'shell',
|
||||
} as const;
|
||||
|
||||
return [
|
||||
@@ -924,7 +900,7 @@ function buildOutOfOrderToolCallSequence(provider: ToolCallProvider) {
|
||||
'shell',
|
||||
'completed',
|
||||
{ type: 'tool_result', provider, tool_call_id: callId },
|
||||
{ provider, server: 'command', tool: 'shell', callId }
|
||||
{ provider, name: 'shell', callId }
|
||||
),
|
||||
timestamp: timestamps[0],
|
||||
},
|
||||
@@ -933,7 +909,7 @@ function buildOutOfOrderToolCallSequence(provider: ToolCallProvider) {
|
||||
'shell',
|
||||
'executing',
|
||||
{ type: 'tool_use', provider },
|
||||
{ provider, server: 'command', tool: 'shell', callId: null }
|
||||
{ provider, name: 'shell', callId: null }
|
||||
),
|
||||
timestamp: timestamps[1],
|
||||
},
|
||||
@@ -976,7 +952,7 @@ function buildMetadataReplaySequence(provider: ToolCallProvider) {
|
||||
'shell',
|
||||
'completed',
|
||||
{ type: 'tool_result', provider, tool_call_id: firstCallId },
|
||||
{ provider, server: 'command', tool: 'shell', callId: firstCallId, displayName: 'Run first' }
|
||||
{ provider, name: 'shell', callId: firstCallId }
|
||||
),
|
||||
timestamp: timestamps[0],
|
||||
},
|
||||
@@ -985,7 +961,7 @@ function buildMetadataReplaySequence(provider: ToolCallProvider) {
|
||||
'shell',
|
||||
'completed',
|
||||
{ type: 'tool_result', provider, tool_call_id: secondCallId },
|
||||
{ provider, server: 'command', tool: 'shell', callId: secondCallId, displayName: 'Run second' }
|
||||
{ provider, name: 'shell', callId: secondCallId }
|
||||
),
|
||||
timestamp: timestamps[1],
|
||||
},
|
||||
@@ -994,7 +970,7 @@ function buildMetadataReplaySequence(provider: ToolCallProvider) {
|
||||
'shell',
|
||||
'executing',
|
||||
{ type: 'tool_use', provider },
|
||||
{ provider, server: 'command', tool: 'shell', callId: null, displayName: 'Run first', kind: 'execute' }
|
||||
{ provider, name: 'shell', callId: null }
|
||||
),
|
||||
timestamp: timestamps[2],
|
||||
},
|
||||
@@ -1041,15 +1017,14 @@ function testMetadataReplayDeduplicationHydrated() {
|
||||
|
||||
function testFallbackToolCallIdsStayUnique() {
|
||||
const timestamp = new Date('2025-01-01T14:05:00Z');
|
||||
// Tool calls need different server/tool to remain distinct when lacking callIds
|
||||
// (displayName alone is not sufficient for differentiation)
|
||||
// Tool calls need different name to remain distinct when lacking callIds
|
||||
const updates = [
|
||||
{
|
||||
event: toolTimeline(
|
||||
'fallback-read',
|
||||
'completed',
|
||||
undefined,
|
||||
{ callId: null, server: 'editor', tool: 'read_file', displayName: 'Read file' }
|
||||
{ callId: null, name: 'read_file' }
|
||||
),
|
||||
timestamp,
|
||||
},
|
||||
@@ -1058,7 +1033,7 @@ function testFallbackToolCallIdsStayUnique() {
|
||||
'fallback-shell',
|
||||
'completed',
|
||||
undefined,
|
||||
{ callId: null, server: 'command', tool: 'shell', displayName: 'Run shell' }
|
||||
{ callId: null, name: 'shell' }
|
||||
),
|
||||
timestamp,
|
||||
},
|
||||
@@ -1072,7 +1047,7 @@ function testFallbackToolCallIdsStayUnique() {
|
||||
assert.strictEqual(
|
||||
new Set(ids).size,
|
||||
ids.length,
|
||||
'Fallback-generated tool ids must be unique when server/tool differs'
|
||||
'Fallback-generated tool ids must be unique when name differs'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -106,12 +106,9 @@ interface OrchestratorToolCallData {
|
||||
|
||||
export interface AgentToolCallData {
|
||||
provider: AgentProvider;
|
||||
server: string;
|
||||
tool: string;
|
||||
name: string;
|
||||
status?: ToolCallStatus;
|
||||
callId?: string;
|
||||
displayName?: string;
|
||||
kind?: string;
|
||||
input?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
@@ -355,8 +352,7 @@ function findExistingAgentToolCallIndex(
|
||||
const payload = entry.payload.data;
|
||||
const providerMatches =
|
||||
payload.provider === data.provider &&
|
||||
payload.server === data.server &&
|
||||
payload.tool === data.tool;
|
||||
payload.name === data.name;
|
||||
if (providerMatches) {
|
||||
metadataMatches.push({ index: i, item: entry as AgentToolCallItem });
|
||||
}
|
||||
@@ -371,49 +367,13 @@ function findExistingAgentToolCallIndex(
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedDisplayName = normalizeComparableString(data.displayName);
|
||||
const normalizedKind = normalizeComparableString(data.kind);
|
||||
|
||||
const filterByComparableField = (
|
||||
candidates: Array<{ index: number; item: AgentToolCallItem }>,
|
||||
selector: (entry: AgentToolCallItem) => string | null,
|
||||
value: string | null
|
||||
): Array<{ index: number; item: AgentToolCallItem }> => {
|
||||
if (!value) {
|
||||
return candidates;
|
||||
}
|
||||
const matches = candidates.filter((candidate) => selector(candidate.item) === value);
|
||||
if (matches.length === 1) {
|
||||
return matches;
|
||||
}
|
||||
return matches.length > 0 ? matches : candidates;
|
||||
};
|
||||
|
||||
const selectCandidate = (
|
||||
candidates: Array<{ index: number; item: AgentToolCallItem }>
|
||||
): number => {
|
||||
const byDisplayName = filterByComparableField(
|
||||
candidates,
|
||||
(entry) => normalizeComparableString(entry.payload.data.displayName),
|
||||
normalizedDisplayName
|
||||
);
|
||||
|
||||
const byKind = filterByComparableField(
|
||||
byDisplayName,
|
||||
(entry) => normalizeComparableString(entry.payload.data.kind),
|
||||
normalizedKind
|
||||
);
|
||||
|
||||
return byKind[0]?.index ?? -1;
|
||||
};
|
||||
|
||||
if (fallbackCandidates.length) {
|
||||
return selectCandidate(fallbackCandidates);
|
||||
return fallbackCandidates[0]?.index ?? -1;
|
||||
}
|
||||
|
||||
// If this update still lacks a call id, fall back to metadata matches (e.g. replayed hydration events)
|
||||
if (!normalizedCallId && metadataMatches.length) {
|
||||
return selectCandidate(metadataMatches);
|
||||
return metadataMatches[0]?.index ?? -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
@@ -466,8 +426,6 @@ function appendAgentToolCall(
|
||||
status: mergedStatus,
|
||||
result: mergedResult,
|
||||
error: mergedError,
|
||||
displayName: payloadData.displayName ?? existing.payload.data.displayName,
|
||||
kind: payloadData.kind ?? existing.payload.data.kind,
|
||||
callId: payloadData.callId ?? existing.payload.data.callId,
|
||||
parsedEdits: parsed.parsedEdits ?? existing.payload.data.parsedEdits,
|
||||
parsedReads: parsed.parsedReads ?? existing.payload.data.parsedReads,
|
||||
@@ -483,7 +441,7 @@ function appendAgentToolCall(
|
||||
: createUniqueTimelineId(
|
||||
state,
|
||||
"tool",
|
||||
`${data.provider}:${data.server}:${data.tool}`,
|
||||
`${data.provider}:${data.name}`,
|
||||
timestamp
|
||||
);
|
||||
|
||||
@@ -507,8 +465,8 @@ function isPermissionToolCall(raw: unknown): boolean {
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return false;
|
||||
}
|
||||
const candidate = raw as { server?: string; kind?: string };
|
||||
return candidate.server === "permission" || candidate.kind === "permission";
|
||||
const candidate = raw as { name?: string };
|
||||
return candidate.name === "permission_request";
|
||||
}
|
||||
|
||||
const FAILED_STATUS_PATTERN = /fail|error|deny|reject|cancel|abort|exception|refus/;
|
||||
@@ -779,12 +737,9 @@ export function reduceStreamUpdate(
|
||||
state,
|
||||
{
|
||||
provider: event.provider,
|
||||
server: item.server,
|
||||
tool: item.tool,
|
||||
name: item.name,
|
||||
status: normalizeStatusString(item.status) ?? "executing",
|
||||
callId: item.callId,
|
||||
displayName: item.displayName,
|
||||
kind: item.kind,
|
||||
input: item.input,
|
||||
result: item.output,
|
||||
error: item.error,
|
||||
|
||||
@@ -752,3 +752,41 @@ export function extractKeyValuePairs(result: unknown): KeyValuePair[] {
|
||||
value: stringifyValue(value),
|
||||
}));
|
||||
}
|
||||
|
||||
// ---- Principal Parameter Extraction ----
|
||||
|
||||
const PrincipalParamSchema = z.union([
|
||||
z.object({ file_path: z.string() }).transform((d) => ({ type: "path" as const, value: d.file_path })),
|
||||
z.object({ filePath: z.string() }).transform((d) => ({ type: "path" as const, value: d.filePath })),
|
||||
z.object({ path: z.string() }).transform((d) => ({ type: "path" as const, value: d.path })),
|
||||
z.object({ command: z.string() }).transform((d) => ({ type: "text" as const, value: d.command })),
|
||||
z.object({ pattern: z.string() }).transform((d) => ({ type: "text" as const, value: d.pattern })),
|
||||
z.object({ query: z.string() }).transform((d) => ({ type: "text" as const, value: d.query })),
|
||||
z.object({ url: z.string() }).transform((d) => ({ type: "text" as const, value: d.url })),
|
||||
]);
|
||||
|
||||
export function stripCwdPrefix(filePath: string, cwd?: string): string {
|
||||
if (!cwd || !filePath) return filePath;
|
||||
|
||||
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const normalizedPath = filePath.replace(/\\/g, "/");
|
||||
|
||||
const prefix = `${normalizedCwd}/`;
|
||||
if (normalizedPath.startsWith(prefix)) {
|
||||
return normalizedPath.slice(prefix.length);
|
||||
}
|
||||
if (normalizedPath === normalizedCwd) {
|
||||
return ".";
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
export function extractPrincipalParam(args: unknown, cwd?: string): string | undefined {
|
||||
const parsed = PrincipalParamSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { type, value } = parsed.data;
|
||||
return type === "path" ? stripCwdPrefix(value, cwd) : value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user