Merge branch 'investigate/ui-network-responsive' into main
This commit is contained in:
@@ -362,8 +362,6 @@ export function AgentStreamView({
|
|||||||
return (
|
return (
|
||||||
<ToolCall
|
<ToolCall
|
||||||
toolName={data.name}
|
toolName={data.name}
|
||||||
args={data.input}
|
|
||||||
result={data.result}
|
|
||||||
error={data.error}
|
error={data.error}
|
||||||
status={data.status}
|
status={data.status}
|
||||||
detail={data.detail}
|
detail={data.detail}
|
||||||
@@ -1123,8 +1121,11 @@ function PermissionRequestCard({
|
|||||||
|
|
||||||
{!isPlanRequest ? (
|
{!isPlanRequest ? (
|
||||||
<ToolCallDetailsContent
|
<ToolCallDetailsContent
|
||||||
input={request.input ?? null}
|
detail={{
|
||||||
output={null}
|
type: "unknown",
|
||||||
|
rawInput: request.input ?? null,
|
||||||
|
rawOutput: null,
|
||||||
|
}}
|
||||||
maxHeight={200}
|
maxHeight={200}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1151,7 +1151,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
|||||||
|
|
||||||
interface ToolCallProps {
|
interface ToolCallProps {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
args: unknown | null;
|
args?: unknown | null;
|
||||||
result?: unknown | null;
|
result?: unknown | null;
|
||||||
error?: unknown | null;
|
error?: unknown | null;
|
||||||
status: "executing" | "running" | "completed" | "failed" | "canceled";
|
status: "executing" | "running" | "completed" | "failed" | "canceled";
|
||||||
@@ -1191,29 +1191,53 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
UnistylesRuntime.breakpoint === "xs" ||
|
UnistylesRuntime.breakpoint === "xs" ||
|
||||||
UnistylesRuntime.breakpoint === "sm";
|
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(
|
const displayModel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
buildToolCallDisplayModel({
|
buildToolCallDisplayModel({
|
||||||
name: toolName,
|
name: toolName,
|
||||||
status: status === "executing" ? "running" : status,
|
status: status === "executing" ? "running" : status,
|
||||||
input: args ?? null,
|
|
||||||
output: result ?? null,
|
|
||||||
error: error ?? null,
|
error: error ?? null,
|
||||||
detail,
|
detail: displayDetail,
|
||||||
metadata,
|
metadata,
|
||||||
cwd,
|
cwd,
|
||||||
}),
|
}),
|
||||||
[toolName, status, args, result, error, detail, metadata, cwd]
|
[toolName, status, error, displayDetail, metadata, cwd]
|
||||||
);
|
);
|
||||||
const displayName = displayModel.displayName;
|
const displayName = displayModel.displayName;
|
||||||
const summary = displayModel.summary;
|
const summary = displayModel.summary;
|
||||||
const errorText = displayModel.errorText;
|
const errorText = displayModel.errorText;
|
||||||
const iconCategory = detail?.type ?? toolName.trim().toLowerCase();
|
const iconCategory = effectiveDetail?.type ?? toolName.trim().toLowerCase();
|
||||||
const IconComponent = resolveToolCallIcon(toolName, detail);
|
const IconComponent = resolveToolCallIcon(toolName, effectiveDetail);
|
||||||
|
|
||||||
// Check if there's any content to display
|
// Check if there's any content to display
|
||||||
const hasDetails =
|
const hasDetails =
|
||||||
args !== undefined || result !== undefined || error !== undefined;
|
Boolean(error) ||
|
||||||
|
(effectiveDetail
|
||||||
|
? effectiveDetail.type !== "unknown" ||
|
||||||
|
effectiveDetail.rawInput !== null ||
|
||||||
|
effectiveDetail.rawOutput !== null
|
||||||
|
: false);
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggle = useCallback(() => {
|
||||||
if (!isMobile && isPerfLoggingEnabled()) {
|
if (!isMobile && isPerfLoggingEnabled()) {
|
||||||
@@ -1224,15 +1248,13 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
toolName,
|
toolName,
|
||||||
displayName,
|
displayName,
|
||||||
summary,
|
summary,
|
||||||
detail,
|
detail: effectiveDetail,
|
||||||
input: args,
|
|
||||||
output: result,
|
|
||||||
errorText,
|
errorText,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setIsExpanded((prev) => !prev);
|
setIsExpanded((prev) => !prev);
|
||||||
}
|
}
|
||||||
}, [isMobile, openToolCall, toolName, displayName, summary, detail, args, result, errorText]);
|
}, [isMobile, openToolCall, toolName, displayName, summary, effectiveDetail, errorText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile || !isPerfLoggingEnabled()) {
|
if (isMobile || !isPerfLoggingEnabled()) {
|
||||||
@@ -1293,14 +1315,12 @@ export const ToolCall = memo(function ToolCall({
|
|||||||
if (isMobile) return null;
|
if (isMobile) return null;
|
||||||
return (
|
return (
|
||||||
<ToolCallDetailsContent
|
<ToolCallDetailsContent
|
||||||
detail={detail}
|
detail={effectiveDetail}
|
||||||
input={args}
|
|
||||||
output={result}
|
|
||||||
errorText={errorText}
|
errorText={errorText}
|
||||||
maxHeight={400}
|
maxHeight={400}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, [isMobile, detail, args, result, errorText]);
|
}, [isMobile, effectiveDetail, errorText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableBadge
|
<ExpandableBadge
|
||||||
|
|||||||
@@ -17,16 +17,12 @@ const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
|
|||||||
|
|
||||||
interface ToolCallDetailsContentProps {
|
interface ToolCallDetailsContentProps {
|
||||||
detail?: ToolCallDetail;
|
detail?: ToolCallDetail;
|
||||||
input?: unknown | null;
|
|
||||||
output?: unknown | null;
|
|
||||||
errorText?: string;
|
errorText?: string;
|
||||||
maxHeight?: number;
|
maxHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallDetailsContent({
|
export function ToolCallDetailsContent({
|
||||||
detail,
|
detail,
|
||||||
input,
|
|
||||||
output,
|
|
||||||
errorText,
|
errorText,
|
||||||
maxHeight = 300,
|
maxHeight = 300,
|
||||||
}: ToolCallDetailsContentProps) {
|
}: ToolCallDetailsContentProps) {
|
||||||
@@ -141,10 +137,10 @@ export function ToolCallDetailsContent({
|
|||||||
<Text selectable style={styles.scrollText}>{detail.query}</Text>
|
<Text selectable style={styles.scrollText}>{detail.query}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
} else {
|
} else if (detail?.type === "unknown") {
|
||||||
const sectionsFromTopLevel = [
|
const sectionsFromTopLevel = [
|
||||||
{ title: "Input", value: input },
|
{ title: "Input", value: detail.rawInput },
|
||||||
{ title: "Output", value: output },
|
{ title: "Output", value: detail.rawOutput },
|
||||||
].filter((entry) => entry.value !== null && entry.value !== undefined);
|
].filter((entry) => entry.value !== null && entry.value !== undefined);
|
||||||
|
|
||||||
for (const section of sectionsFromTopLevel) {
|
for (const section of sectionsFromTopLevel) {
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export type ToolCallSheetData = {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
detail?: ToolCallDetail;
|
detail?: ToolCallDetail;
|
||||||
input?: unknown | null;
|
|
||||||
output?: unknown | null;
|
|
||||||
errorText?: string;
|
errorText?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ interface ToolCallSheetContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ToolCallSheetContent({ data, onClose }: 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);
|
const IconComponent = resolveToolCallIcon(toolName, detail);
|
||||||
|
|
||||||
@@ -158,8 +156,6 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|||||||
>
|
>
|
||||||
<ToolCallDetailsContent
|
<ToolCallDetailsContent
|
||||||
detail={detail}
|
detail={detail}
|
||||||
input={input}
|
|
||||||
output={output}
|
|
||||||
errorText={errorText}
|
errorText={errorText}
|
||||||
/>
|
/>
|
||||||
</BottomSheetScrollView>
|
</BottomSheetScrollView>
|
||||||
|
|||||||
@@ -26,8 +26,11 @@ const toolCallEvent = (): AgentStreamEventPayload => ({
|
|||||||
callId: "buffer-tool-call",
|
callId: "buffer-tool-call",
|
||||||
name: "run",
|
name: "run",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "echo hi" },
|
detail: {
|
||||||
output: null,
|
type: "unknown",
|
||||||
|
rawInput: { command: "echo hi" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -213,14 +213,18 @@ function buildToolEvent({
|
|||||||
error?: unknown;
|
error?: unknown;
|
||||||
detail?: ToolCallDetail;
|
detail?: ToolCallDetail;
|
||||||
}): AgentStreamEventPayload {
|
}): AgentStreamEventPayload {
|
||||||
|
const canonicalDetail: ToolCallDetail = detail ?? {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: input,
|
||||||
|
rawOutput: output,
|
||||||
|
};
|
||||||
|
|
||||||
const baseItem = {
|
const baseItem = {
|
||||||
type: "tool_call" as const,
|
type: "tool_call" as const,
|
||||||
name,
|
name,
|
||||||
status,
|
status,
|
||||||
callId,
|
callId,
|
||||||
input,
|
detail: canonicalDetail,
|
||||||
output,
|
|
||||||
...(detail ? { detail } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const item =
|
const item =
|
||||||
|
|||||||
@@ -40,15 +40,19 @@ function canonicalToolTimeline(params: {
|
|||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
detail?: ToolCallDetail;
|
detail?: ToolCallDetail;
|
||||||
}): AgentStreamEventPayload {
|
}): AgentStreamEventPayload {
|
||||||
|
const detail: ToolCallDetail = params.detail ?? {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: params.input ?? null,
|
||||||
|
rawOutput: params.output ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
const baseItem = {
|
const baseItem = {
|
||||||
type: "tool_call" as const,
|
type: "tool_call" as const,
|
||||||
callId: params.callId,
|
callId: params.callId,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
status: params.status,
|
status: params.status,
|
||||||
input: params.input ?? null,
|
detail,
|
||||||
output: params.output ?? null,
|
|
||||||
metadata: params.metadata,
|
metadata: params.metadata,
|
||||||
...(params.detail ? { detail: params.detail } : {}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const item =
|
const item =
|
||||||
@@ -144,10 +148,13 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
|
|
||||||
assert.strictEqual(tools.length, 1);
|
assert.strictEqual(tools.length, 1);
|
||||||
assert.strictEqual(tools[0].payload.data.status, "completed");
|
assert.strictEqual(tools[0].payload.data.status, "completed");
|
||||||
assert.deepStrictEqual(tools[0].payload.data.input, { command: "pwd" });
|
assert.deepStrictEqual(tools[0].payload.data.detail, {
|
||||||
assert.deepStrictEqual(tools[0].payload.data.result, {
|
type: "unknown",
|
||||||
output: "/tmp/repo\n",
|
rawInput: { command: "pwd" },
|
||||||
exitCode: 0,
|
rawOutput: {
|
||||||
|
output: "/tmp/repo\n",
|
||||||
|
exitCode: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,8 +183,6 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
const summary = buildToolCallDisplayModel({
|
const summary = buildToolCallDisplayModel({
|
||||||
name: tool.payload.data.name,
|
name: tool.payload.data.name,
|
||||||
status: tool.payload.data.status,
|
status: tool.payload.data.status,
|
||||||
input: tool.payload.data.input,
|
|
||||||
output: tool.payload.data.result,
|
|
||||||
error: tool.payload.data.error,
|
error: tool.payload.data.error,
|
||||||
detail: tool.payload.data.detail,
|
detail: tool.payload.data.detail,
|
||||||
}).summary;
|
}).summary;
|
||||||
@@ -209,8 +214,6 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
const summary = buildToolCallDisplayModel({
|
const summary = buildToolCallDisplayModel({
|
||||||
name: tool.payload.data.name,
|
name: tool.payload.data.name,
|
||||||
status: tool.payload.data.status,
|
status: tool.payload.data.status,
|
||||||
input: tool.payload.data.input,
|
|
||||||
output: tool.payload.data.result,
|
|
||||||
error: tool.payload.data.error,
|
error: tool.payload.data.error,
|
||||||
detail: tool.payload.data.detail,
|
detail: tool.payload.data.detail,
|
||||||
cwd: "/tmp/repo",
|
cwd: "/tmp/repo",
|
||||||
@@ -240,8 +243,6 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: tool.payload.data.name,
|
name: tool.payload.data.name,
|
||||||
status: tool.payload.data.status,
|
status: tool.payload.data.status,
|
||||||
input: tool.payload.data.input,
|
|
||||||
output: tool.payload.data.result,
|
|
||||||
error: tool.payload.data.error,
|
error: tool.payload.data.error,
|
||||||
detail: tool.payload.data.detail,
|
detail: tool.payload.data.detail,
|
||||||
});
|
});
|
||||||
@@ -279,7 +280,11 @@ describe("stream reducer canonical tool calls", () => {
|
|||||||
const tool = findToolByCallId(state, callId);
|
const tool = findToolByCallId(state, callId);
|
||||||
|
|
||||||
assert.ok(tool);
|
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");
|
assert.strictEqual(tool.payload.data.status, "completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,8 @@ export interface AgentToolCallData {
|
|||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: AgentToolCallStatus;
|
status: AgentToolCallStatus;
|
||||||
input: unknown | null;
|
|
||||||
result: unknown | null;
|
|
||||||
error: unknown | null;
|
error: unknown | null;
|
||||||
detail?: ToolCallDetail;
|
detail: ToolCallDetail;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +326,7 @@ function hasNonEmptyObject(value: unknown): boolean {
|
|||||||
return isRecord(value) && Object.keys(value).length > 0;
|
return isRecord(value) && Object.keys(value).length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCanonicalValue(
|
function mergeUnknownValue(
|
||||||
existing: unknown | null,
|
existing: unknown | null,
|
||||||
incoming: unknown | null
|
incoming: unknown | null
|
||||||
): unknown | null {
|
): unknown | null {
|
||||||
@@ -343,6 +341,34 @@ function mergeCanonicalValue(
|
|||||||
return incoming;
|
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(
|
function mergeAgentToolCallStatus(
|
||||||
existing: AgentToolCallStatus,
|
existing: AgentToolCallStatus,
|
||||||
incoming: AgentToolCallStatus
|
incoming: AgentToolCallStatus
|
||||||
@@ -375,8 +401,6 @@ function appendAgentToolCall(
|
|||||||
if (!existing || !isAgentToolCallItem(existing)) {
|
if (!existing || !isAgentToolCallItem(existing)) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
const mergedInput = mergeCanonicalValue(existing.payload.data.input, data.input);
|
|
||||||
const mergedResult = mergeCanonicalValue(existing.payload.data.result, data.result);
|
|
||||||
const mergedStatus = mergeAgentToolCallStatus(
|
const mergedStatus = mergeAgentToolCallStatus(
|
||||||
existing.payload.data.status,
|
existing.payload.data.status,
|
||||||
data.status
|
data.status
|
||||||
@@ -389,6 +413,7 @@ function appendAgentToolCall(
|
|||||||
data.metadata || existing.payload.data.metadata
|
data.metadata || existing.payload.data.metadata
|
||||||
? { ...existing.payload.data.metadata, ...data.metadata }
|
? { ...existing.payload.data.metadata, ...data.metadata }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const mergedDetail = mergeToolCallDetail(existing.payload.data.detail, data.detail);
|
||||||
|
|
||||||
next[existingIndex] = {
|
next[existingIndex] = {
|
||||||
...existing,
|
...existing,
|
||||||
@@ -399,10 +424,8 @@ function appendAgentToolCall(
|
|||||||
...existing.payload.data,
|
...existing.payload.data,
|
||||||
...data,
|
...data,
|
||||||
status: mergedStatus,
|
status: mergedStatus,
|
||||||
input: mergedInput,
|
|
||||||
result: mergedResult,
|
|
||||||
error: mergedError,
|
error: mergedError,
|
||||||
detail: data.detail ?? existing.payload.data.detail,
|
detail: mergedDetail,
|
||||||
metadata: mergedMetadata,
|
metadata: mergedMetadata,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -527,7 +550,10 @@ export function reduceStreamUpdate(
|
|||||||
) {
|
) {
|
||||||
// For Claude: TodoWrite often appears as a tool call that never resolves. Always render it
|
// 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.
|
// 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) {
|
if (tasks) {
|
||||||
nextState = appendTodoList(
|
nextState = appendTodoList(
|
||||||
state,
|
state,
|
||||||
@@ -544,7 +570,7 @@ export function reduceStreamUpdate(
|
|||||||
|
|
||||||
const tasks = extractTaskEntriesFromToolCall(
|
const tasks = extractTaskEntriesFromToolCall(
|
||||||
item.name,
|
item.name,
|
||||||
item.input
|
rawInputFromDetail(item.detail)
|
||||||
);
|
);
|
||||||
if (tasks) {
|
if (tasks) {
|
||||||
nextState = appendTodoList(
|
nextState = appendTodoList(
|
||||||
@@ -566,8 +592,6 @@ export function reduceStreamUpdate(
|
|||||||
callId: item.callId,
|
callId: item.callId,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
input: item.input,
|
|
||||||
result: item.output,
|
|
||||||
error: item.error,
|
error: item.error,
|
||||||
detail: item.detail,
|
detail: item.detail,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ describe("tool-call-display", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "npm test" },
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "shell",
|
type: "shell",
|
||||||
@@ -26,8 +24,6 @@ describe("tool-call-display", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: { path: "/tmp/repo/src/index.ts" },
|
|
||||||
output: { content: "hello" },
|
|
||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "read",
|
type: "read",
|
||||||
@@ -46,9 +42,12 @@ describe("tool-call-display", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "task",
|
name: "task",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: null,
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
subAgentActivity: "Running tests",
|
subAgentActivity: "Running tests",
|
||||||
},
|
},
|
||||||
@@ -64,9 +63,12 @@ describe("tool-call-display", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "custom_tool_name",
|
name: "custom_tool_name",
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: null,
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(display).toEqual({
|
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({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "exec_command",
|
name: "exec_command",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "npm run test" },
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { command: "npm run test" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(display).toEqual({
|
expect(display).toEqual({
|
||||||
@@ -92,9 +97,12 @@ describe("tool-call-display", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
input: { command: "false" },
|
|
||||||
output: null,
|
|
||||||
error: { message: "boom" },
|
error: { message: "boom" },
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { command: "false" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(display.errorText).toBe('{\n "message": "boom"\n}');
|
expect(display.errorText).toBe('{\n "message": "boom"\n}');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const TOOL_DETAIL_ICONS: Record<ToolCallDetail["type"], ToolCallIconComponent> =
|
|||||||
edit: Pencil,
|
edit: Pencil,
|
||||||
write: Pencil,
|
write: Pencil,
|
||||||
search: Search,
|
search: Search,
|
||||||
|
unknown: Wrench,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail): ToolCallIconComponent {
|
export function resolveToolCallIcon(toolName: string, detail?: ToolCallDetail): ToolCallIconComponent {
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ function printStreamEvent(event: AgentStreamEventPayload): void {
|
|||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Other event types (thread_started, provider_event, etc.) are internal
|
// Other event types are internal
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -221,8 +221,10 @@ describe("DaemonClient", () => {
|
|||||||
callId: "call_cli_stream",
|
callId: "call_cli_stream",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
output: null,
|
type: "shell",
|
||||||
|
command: "pwd",
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -241,7 +243,9 @@ describe("DaemonClient", () => {
|
|||||||
type: "tool_call";
|
type: "tool_call";
|
||||||
status: string;
|
status: string;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
output: unknown;
|
detail: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -249,7 +253,7 @@ describe("DaemonClient", () => {
|
|||||||
|
|
||||||
expect(streamMsg.payload.event.item.status).toBe("running");
|
expect(streamMsg.payload.event.item.status).toBe("running");
|
||||||
expect(streamMsg.payload.event.item.error).toBeNull();
|
expect(streamMsg.payload.event.item.error).toBeNull();
|
||||||
expect(streamMsg.payload.event.item.output).toBeNull();
|
expect(streamMsg.payload.event.item.detail.type).toBe("shell");
|
||||||
expect(logger.warn).not.toHaveBeenCalled();
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -288,7 +292,11 @@ describe("DaemonClient", () => {
|
|||||||
callId: "call_cli_stream_legacy",
|
callId: "call_cli_stream_legacy",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "inProgress",
|
status: "inProgress",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { command: "pwd" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -338,8 +346,10 @@ describe("DaemonClient", () => {
|
|||||||
callId: "call_cli_snapshot",
|
callId: "call_cli_snapshot",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
output: null,
|
type: "shell",
|
||||||
|
command: "pwd",
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -361,7 +371,9 @@ describe("DaemonClient", () => {
|
|||||||
type: "tool_call";
|
type: "tool_call";
|
||||||
status: string;
|
status: string;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
output: unknown;
|
detail: {
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
@@ -373,7 +385,7 @@ describe("DaemonClient", () => {
|
|||||||
if (firstTimeline?.type === "timeline" && firstTimeline.item.type === "tool_call") {
|
if (firstTimeline?.type === "timeline" && firstTimeline.item.type === "tool_call") {
|
||||||
expect(firstTimeline.item.status).toBe("running");
|
expect(firstTimeline.item.status).toBe("running");
|
||||||
expect(firstTimeline.item.error).toBeNull();
|
expect(firstTimeline.item.error).toBeNull();
|
||||||
expect(firstTimeline.item.output).toBeNull();
|
expect(firstTimeline.item.detail.type).toBe("shell");
|
||||||
}
|
}
|
||||||
expect(logger.warn).not.toHaveBeenCalled();
|
expect(logger.warn).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,19 +10,23 @@ function toolCallItem(params: {
|
|||||||
output?: unknown | null;
|
output?: unknown | null;
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
detail?: Extract<AgentTimelineItem, { type: "tool_call" }>['detail'];
|
detail?: Extract<AgentTimelineItem, { type: "tool_call" }>["detail"];
|
||||||
}): Extract<AgentTimelineItem, { type: "tool_call" }> {
|
}): Extract<AgentTimelineItem, { type: "tool_call" }> {
|
||||||
const status = params.status ?? "completed";
|
const status = params.status ?? "completed";
|
||||||
|
const detail =
|
||||||
|
params.detail ?? {
|
||||||
|
type: "unknown" as const,
|
||||||
|
rawInput: params.input ?? null,
|
||||||
|
rawOutput: params.output ?? null,
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
callId: params.callId,
|
callId: params.callId,
|
||||||
name: params.name,
|
name: params.name,
|
||||||
status,
|
status,
|
||||||
input: params.input ?? null,
|
detail,
|
||||||
output: params.output ?? null,
|
|
||||||
error: status === "failed" ? params.error ?? { message: "failed" } : null,
|
error: status === "failed" ? params.error ?? { message: "failed" } : null,
|
||||||
metadata: params.metadata,
|
metadata: params.metadata,
|
||||||
detail: params.detail,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AgentTimelineItem } from "./agent-sdk-types.js";
|
import type { AgentTimelineItem, ToolCallDetail } from "./agent-sdk-types.js";
|
||||||
import { isLikelyExternalToolName } from "./tool-name-normalization.js";
|
import { isLikelyExternalToolName } from "./tool-name-normalization.js";
|
||||||
import { buildToolCallDisplayModel } from "../../shared/tool-call-display.js";
|
import { buildToolCallDisplayModel } from "../../shared/tool-call-display.js";
|
||||||
|
|
||||||
@@ -45,6 +45,46 @@ function formatToolInputJson(input: unknown): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasNonEmptyObject(value: unknown): boolean {
|
||||||
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeUnknownValue(existing: unknown | null, incoming: unknown | null): unknown | null {
|
||||||
|
if (incoming === null) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNonEmptyObject(incoming) && hasNonEmptyObject(existing)) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return incoming;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeToolDetail(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 {
|
||||||
|
return detail.type === "unknown" ? detail.rawInput : null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collapse timeline items:
|
* Collapse timeline items:
|
||||||
* - Dedupe tool calls by callId (pending/completed -> single)
|
* - Dedupe tool calls by callId (pending/completed -> single)
|
||||||
@@ -95,9 +135,7 @@ function collapseTimeline(items: AgentTimelineItem[]): AgentTimelineItem[] {
|
|||||||
toolCallMap.set(item.callId, {
|
toolCallMap.set(item.callId, {
|
||||||
...existing,
|
...existing,
|
||||||
...item,
|
...item,
|
||||||
input: item.input ?? existing.input,
|
detail: mergeToolDetail(existing.detail, item.detail),
|
||||||
output: item.output ?? existing.output,
|
|
||||||
detail: item.detail ?? existing.detail,
|
|
||||||
error: item.error,
|
error: item.error,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
});
|
});
|
||||||
@@ -105,9 +143,7 @@ function collapseTimeline(items: AgentTimelineItem[]): AgentTimelineItem[] {
|
|||||||
toolCallMap.set(item.callId, {
|
toolCallMap.set(item.callId, {
|
||||||
...existing,
|
...existing,
|
||||||
...item,
|
...item,
|
||||||
input: item.input ?? existing.input,
|
detail: mergeToolDetail(existing.detail, item.detail),
|
||||||
output: item.output ?? existing.output,
|
|
||||||
detail: item.detail ?? existing.detail,
|
|
||||||
error: null,
|
error: null,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
});
|
});
|
||||||
@@ -167,12 +203,10 @@ export function curateAgentActivity(
|
|||||||
break;
|
break;
|
||||||
case "tool_call": {
|
case "tool_call": {
|
||||||
flushBuffers(lines, buffers);
|
flushBuffers(lines, buffers);
|
||||||
const inputJson = formatToolInputJson(item.input);
|
const inputJson = formatToolInputJson(rawInputFromDetail(item.detail));
|
||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
status: item.status,
|
status: item.status,
|
||||||
input: item.input,
|
|
||||||
output: item.output,
|
|
||||||
error: item.error,
|
error: item.error,
|
||||||
detail: item.detail,
|
detail: item.detail,
|
||||||
metadata: item.metadata,
|
metadata: item.metadata,
|
||||||
|
|||||||
@@ -133,15 +133,18 @@ export type ToolCallDetail =
|
|||||||
| {
|
| {
|
||||||
type: "search";
|
type: "search";
|
||||||
query: string;
|
query: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "unknown";
|
||||||
|
rawInput: unknown | null;
|
||||||
|
rawOutput: unknown | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolCallBase = {
|
type ToolCallBase = {
|
||||||
type: "tool_call";
|
type: "tool_call";
|
||||||
callId: string;
|
callId: string;
|
||||||
name: string;
|
name: string;
|
||||||
input: unknown | null;
|
detail: ToolCallDetail;
|
||||||
output: unknown | null;
|
|
||||||
detail?: ToolCallDetail;
|
|
||||||
metadata?: Record<string, unknown>;
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -194,7 +197,6 @@ export type AgentStreamEvent =
|
|||||||
| { type: "turn_failed"; provider: AgentProvider; error: string }
|
| { type: "turn_failed"; provider: AgentProvider; error: string }
|
||||||
| { type: "turn_canceled"; provider: AgentProvider; reason: string }
|
| { type: "turn_canceled"; provider: AgentProvider; reason: string }
|
||||||
| { type: "timeline"; item: AgentTimelineItem; provider: AgentProvider }
|
| { type: "timeline"; item: AgentTimelineItem; provider: AgentProvider }
|
||||||
| { type: "provider_event"; provider: AgentProvider; raw: unknown }
|
|
||||||
| { type: "permission_requested"; provider: AgentProvider; request: AgentPermissionRequest }
|
| { type: "permission_requested"; provider: AgentProvider; request: AgentPermissionRequest }
|
||||||
| {
|
| {
|
||||||
type: "permission_resolved";
|
type: "permission_resolved";
|
||||||
|
|||||||
@@ -89,8 +89,21 @@ function extractCommandText(input: unknown): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractToolCommand(detail: unknown): string | null {
|
||||||
|
if (!isKeyValueObject(detail) || typeof detail.type !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (detail.type === "shell" && typeof detail.command === "string") {
|
||||||
|
return detail.command;
|
||||||
|
}
|
||||||
|
if (detail.type === "unknown") {
|
||||||
|
return extractCommandText(detail.rawInput);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function isSleepCommandToolCall(item: ToolCallItem): boolean {
|
function isSleepCommandToolCall(item: ToolCallItem): boolean {
|
||||||
const inputCommand = extractCommandText(item.input)?.toLowerCase() ?? "";
|
const inputCommand = extractToolCommand(item.detail)?.toLowerCase() ?? "";
|
||||||
return inputCommand.includes("sleep 60");
|
return inputCommand.includes("sleep 60");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +111,7 @@ function isPermissionCommandToolCall(item: ToolCallItem): boolean {
|
|||||||
if (item.name === "permission_request") {
|
if (item.name === "permission_request") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const inputCommand = extractCommandText(item.input)?.toLowerCase() ?? "";
|
const inputCommand = extractToolCommand(item.detail)?.toLowerCase() ?? "";
|
||||||
return inputCommand.includes("permission.txt");
|
return inputCommand.includes("permission.txt");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,7 +389,7 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
event.item.name.toLowerCase().includes("bash") &&
|
event.item.name.toLowerCase().includes("bash") &&
|
||||||
event.item.status === "pending"
|
event.item.status === "pending"
|
||||||
) {
|
) {
|
||||||
pendingCommand = extractCommandText(event.item.input);
|
pendingCommand = extractToolCommand(event.item.detail);
|
||||||
}
|
}
|
||||||
if (event.type === "turn_completed" || event.type === "turn_failed") {
|
if (event.type === "turn_completed" || event.type === "turn_failed") {
|
||||||
break;
|
break;
|
||||||
@@ -432,28 +445,20 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
item.name !== "permission_request"
|
item.name !== "permission_request"
|
||||||
);
|
);
|
||||||
const fileChangeEvent = toolCalls.find((item) => {
|
const fileChangeEvent = toolCalls.find((item) => {
|
||||||
// Check for file changes in structured output.files array
|
if (item.detail.type === "write" || item.detail.type === "edit") {
|
||||||
if (isKeyValueObject(item.output)) {
|
return item.detail.filePath.includes("tool-test.txt");
|
||||||
const files = item.output.files;
|
|
||||||
if (Array.isArray(files) && files.some((file) => typeof file?.path === "string" && file.path.includes("tool-test.txt"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Also check for file path in output structure (write/edit tools)
|
if (item.detail.type === "unknown") {
|
||||||
if (isKeyValueObject(item.output)) {
|
return (
|
||||||
const output = item.output;
|
rawContainsText(item.detail.rawInput, "tool-test.txt") ||
|
||||||
if (output.type === "file_write" || output.type === "file_edit") {
|
rawContainsText(item.detail.rawOutput, "tool-test.txt")
|
||||||
const filePath = output.filePath;
|
);
|
||||||
if (typeof filePath === "string" && filePath.includes("tool-test.txt")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return rawContainsText(item.detail, "tool-test.txt");
|
||||||
});
|
});
|
||||||
|
|
||||||
const sawPwdCommand = commandEvents.some(
|
const sawPwdCommand = commandEvents.some(
|
||||||
(item) => (extractCommandText(item.input) ?? "").toLowerCase().includes("pwd") && item.status === "completed"
|
(item) => (extractToolCommand(item.detail) ?? "").toLowerCase().includes("pwd") && item.status === "completed"
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(completed).toBe(true);
|
expect(completed).toBe(true);
|
||||||
@@ -950,13 +955,13 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
const liveSnapshots = extractAgentToolSnapshots(liveState);
|
const liveSnapshots = extractAgentToolSnapshots(liveState);
|
||||||
const commandTool = liveSnapshots.find((snapshot) =>
|
const commandTool = liveSnapshots.find((snapshot) =>
|
||||||
snapshot.data.name.toLowerCase().includes("bash") &&
|
snapshot.data.name.toLowerCase().includes("bash") &&
|
||||||
(extractCommandText(snapshot.data.input) ?? "").toLowerCase().includes("pwd")
|
(extractToolCommand(snapshot.data.detail) ?? "").toLowerCase().includes("pwd")
|
||||||
);
|
);
|
||||||
const editTool = liveSnapshots.find((snapshot) =>
|
const editTool = liveSnapshots.find((snapshot) =>
|
||||||
rawContainsText(snapshot.data.result, "hydrate-proof.txt")
|
rawContainsText(snapshot.data.detail, "hydrate-proof.txt")
|
||||||
);
|
);
|
||||||
const readTool = liveSnapshots.find((snapshot) =>
|
const readTool = liveSnapshots.find((snapshot) =>
|
||||||
rawContainsText(snapshot.data.result, "HYDRATION_PROOF_LINE_TWO")
|
rawContainsText(snapshot.data.detail, "HYDRATION_PROOF_LINE_TWO")
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(commandTool).toBeTruthy();
|
expect(commandTool).toBeTruthy();
|
||||||
@@ -993,23 +998,22 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
commandTool!,
|
commandTool!,
|
||||||
hydratedMap,
|
hydratedMap,
|
||||||
(data) =>
|
(data) =>
|
||||||
rawContainsText(data.result, cwd),
|
rawContainsText(data.detail, cwd),
|
||||||
({ live, hydrated }) => {
|
({ live, hydrated }) => {
|
||||||
expect(rawContainsText(live.result, cwd)).toBe(true);
|
expect(rawContainsText(live.detail, cwd)).toBe(true);
|
||||||
expect(rawContainsText(hydrated.result, cwd)).toBe(true);
|
expect(rawContainsText(hydrated.detail, cwd)).toBe(true);
|
||||||
expect((extractCommandText(live.input) ?? "").toLowerCase()).toContain("pwd");
|
expect((extractToolCommand(live.detail) ?? "").toLowerCase()).toContain("pwd");
|
||||||
expect((extractCommandText(hydrated.input) ?? "").toLowerCase()).toContain("pwd");
|
expect((extractToolCommand(hydrated.detail) ?? "").toLowerCase()).toContain("pwd");
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assertHydratedReplica(
|
assertHydratedReplica(
|
||||||
editTool!,
|
editTool!,
|
||||||
hydratedMap,
|
hydratedMap,
|
||||||
(data) =>
|
(data) =>
|
||||||
isFileWriteResult(data.result) &&
|
rawContainsText(data.detail, "hydrate-proof.txt"),
|
||||||
data.result.filePath.includes("hydrate-proof.txt"),
|
|
||||||
({ live, hydrated }) => {
|
({ live, hydrated }) => {
|
||||||
const liveDiff = JSON.stringify(live.result ?? {});
|
const liveDiff = JSON.stringify(live.detail ?? {});
|
||||||
const hydratedDiff = JSON.stringify(hydrated.result ?? {});
|
const hydratedDiff = JSON.stringify(hydrated.detail ?? {});
|
||||||
expect(liveDiff).toContain("hydrate-proof.txt");
|
expect(liveDiff).toContain("hydrate-proof.txt");
|
||||||
expect(hydratedDiff).toContain("hydrate-proof.txt");
|
expect(hydratedDiff).toContain("hydrate-proof.txt");
|
||||||
}
|
}
|
||||||
@@ -1018,11 +1022,11 @@ async function startAgentMcpServer(): Promise<AgentMcpServerHandle> {
|
|||||||
readTool!,
|
readTool!,
|
||||||
hydratedMap,
|
hydratedMap,
|
||||||
(data) =>
|
(data) =>
|
||||||
rawContainsText(data.result, "HYDRATION_PROOF_LINE_ONE") &&
|
rawContainsText(data.detail, "HYDRATION_PROOF_LINE_ONE") &&
|
||||||
rawContainsText(data.result, "HYDRATION_PROOF_LINE_TWO"),
|
rawContainsText(data.detail, "HYDRATION_PROOF_LINE_TWO"),
|
||||||
({ live, hydrated }) => {
|
({ live, hydrated }) => {
|
||||||
const liveReads = JSON.stringify(live.result ?? {});
|
const liveReads = JSON.stringify(live.detail ?? {});
|
||||||
const hydratedReads = JSON.stringify(hydrated.result ?? {});
|
const hydratedReads = JSON.stringify(hydrated.detail ?? {});
|
||||||
expect(liveReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
expect(liveReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||||
expect(hydratedReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
expect(hydratedReads).toContain("HYDRATION_PROOF_LINE_ONE");
|
||||||
expect(liveReads).toContain("HYDRATION_PROOF_LINE_TWO");
|
expect(liveReads).toContain("HYDRATION_PROOF_LINE_TWO");
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ describe("claude tool-call mapper", () => {
|
|||||||
expect(item.status).toBe("running");
|
expect(item.status).toBe("running");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.callId).toBe("claude-call-1");
|
expect(item.callId).toBe("claude-call-1");
|
||||||
expect(item.input).toEqual({ command: "pwd", cwd: "/tmp/repo" });
|
|
||||||
expect(item.output).toBeNull();
|
|
||||||
expect(item.detail?.type).toBe("shell");
|
expect(item.detail?.type).toBe("shell");
|
||||||
if (item.detail?.type === "shell") {
|
if (item.detail?.type === "shell") {
|
||||||
expect(item.detail.command).toBe("pwd");
|
expect(item.detail.command).toBe("pwd");
|
||||||
@@ -85,8 +83,6 @@ describe("claude tool-call mapper", () => {
|
|||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.callId).toBe("claude-call-2");
|
expect(item.callId).toBe("claude-call-2");
|
||||||
expect(item.input).toEqual({ file_path: "README.md" });
|
|
||||||
expect(item.output).toEqual({ content: "hello" });
|
|
||||||
expect(item.detail?.type).toBe("read");
|
expect(item.detail?.type).toBe("read");
|
||||||
if (item.detail?.type === "read") {
|
if (item.detail?.type === "read") {
|
||||||
expect(item.detail.filePath).toBe("README.md");
|
expect(item.detail.filePath).toBe("README.md");
|
||||||
@@ -141,8 +137,6 @@ describe("claude tool-call mapper", () => {
|
|||||||
expect(item.status).toBe("failed");
|
expect(item.status).toBe("failed");
|
||||||
expect(item.error).toEqual({ message: "Command failed" });
|
expect(item.error).toEqual({ message: "Command failed" });
|
||||||
expect(item.callId).toBe("claude-call-3");
|
expect(item.callId).toBe("claude-call-3");
|
||||||
expect(item.input).toEqual({ command: "false" });
|
|
||||||
expect(item.output).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps write/edit/search known shapes with distinct detail types", () => {
|
it("maps write/edit/search known shapes with distinct detail types", () => {
|
||||||
@@ -181,7 +175,7 @@ describe("claude tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps unknown tools canonical without detail", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapClaudeCompletedToolCall({
|
const item = mapClaudeCompletedToolCall({
|
||||||
callId: "claude-call-4",
|
callId: "claude-call-4",
|
||||||
name: "my_custom_tool",
|
name: "my_custom_tool",
|
||||||
@@ -191,8 +185,10 @@ describe("claude tool-call mapper", () => {
|
|||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toBeUndefined();
|
expect(item.detail).toEqual({
|
||||||
expect(item.input).toEqual({ foo: "bar" });
|
type: "unknown",
|
||||||
expect(item.output).toEqual({ ok: true });
|
rawInput: { foo: "bar" },
|
||||||
|
rawOutput: { ok: true },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,20 +49,33 @@ function useTempCodexSessionDir(): () => void {
|
|||||||
|
|
||||||
function hasShellCommand(item: AgentTimelineItem, commandFragment: string): boolean {
|
function hasShellCommand(item: AgentTimelineItem, commandFragment: string): boolean {
|
||||||
if (item.type !== "tool_call" || item.name !== "shell") return false;
|
if (item.type !== "tool_call" || item.name !== "shell") return false;
|
||||||
const input = item.input as { command?: string } | undefined;
|
if (item.detail.type === "shell") {
|
||||||
const command = input?.command ?? "";
|
return item.detail.command.includes(commandFragment);
|
||||||
|
}
|
||||||
|
const unknownInput =
|
||||||
|
item.detail.type === "unknown" && typeof item.detail.rawInput === "object" && item.detail.rawInput
|
||||||
|
? (item.detail.rawInput as { command?: string })
|
||||||
|
: undefined;
|
||||||
|
const command = unknownInput?.command ?? "";
|
||||||
return command.includes(commandFragment);
|
return command.includes(commandFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
|
function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
|
||||||
if (item.type !== "tool_call" || item.name !== "apply_patch") return false;
|
if (item.type !== "tool_call" || item.name !== "apply_patch") return false;
|
||||||
const input = item.input as { files?: Array<{ path?: string }> } | undefined;
|
if (item.detail.type === "edit") {
|
||||||
const output = item.output as
|
return item.detail.filePath === fileName || (item.detail.unifiedDiff?.includes(fileName) ?? false);
|
||||||
| { files?: Array<{ path?: string; patch?: string }>; diff?: string }
|
}
|
||||||
| undefined;
|
const unknownInput =
|
||||||
const inInput = (input?.files ?? []).some((file) => file?.path === fileName);
|
item.detail.type === "unknown" && typeof item.detail.rawInput === "object" && item.detail.rawInput
|
||||||
const inOutput = (output?.files ?? []).some((file) => file?.path === fileName);
|
? (item.detail.rawInput as { files?: Array<{ path?: string }> })
|
||||||
const inDiff = typeof output?.diff === "string" && output.diff.includes(fileName);
|
: undefined;
|
||||||
|
const unknownOutput =
|
||||||
|
item.detail.type === "unknown" && typeof item.detail.rawOutput === "object" && item.detail.rawOutput
|
||||||
|
? (item.detail.rawOutput as { files?: Array<{ path?: string; patch?: string }>; diff?: string })
|
||||||
|
: undefined;
|
||||||
|
const inInput = (unknownInput?.files ?? []).some((file) => file?.path === fileName);
|
||||||
|
const inOutput = (unknownOutput?.files ?? []).some((file) => file?.path === fileName);
|
||||||
|
const inDiff = typeof unknownOutput?.diff === "string" && unknownOutput.diff.includes(fileName);
|
||||||
return inInput || inOutput || inDiff;
|
return inInput || inOutput || inDiff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1671,7 +1671,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleNotification(method: string, params: unknown): void {
|
private handleNotification(method: string, params: unknown): void {
|
||||||
this.emitEvent({ type: "provider_event", provider: CODEX_PROVIDER, raw: { method, params } });
|
|
||||||
const parsed = CodexNotificationSchema.parse({ method, params });
|
const parsed = CodexNotificationSchema.parse({ method, params });
|
||||||
|
|
||||||
if (parsed.kind === "thread_started") {
|
if (parsed.kind === "thread_started") {
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ describe("codex tool-call mapper", () => {
|
|||||||
expect(item?.error).toBeNull();
|
expect(item?.error).toBeNull();
|
||||||
expect(item?.callId).toBe("codex-call-1");
|
expect(item?.callId).toBe("codex-call-1");
|
||||||
expect(item?.name).toBe("shell");
|
expect(item?.name).toBe("shell");
|
||||||
expect(item?.input).toEqual({ command: "pwd", cwd: "/tmp/repo" });
|
expect(item?.detail).toEqual({
|
||||||
|
type: "shell",
|
||||||
|
command: "pwd",
|
||||||
|
cwd: "/tmp/repo",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps running known tool variants with detail for early summaries", () => {
|
it("maps running known tool variants with detail for early summaries", () => {
|
||||||
@@ -133,7 +137,7 @@ describe("codex tool-call mapper", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("truncates large diff payloads deterministically in canonical output", () => {
|
it("truncates large diff payloads deterministically in canonical detail", () => {
|
||||||
const hugeDiff = `@@\\n-${"a".repeat(14_000)}\\n+${"b".repeat(14_000)}\\n`;
|
const hugeDiff = `@@\\n-${"a".repeat(14_000)}\\n+${"b".repeat(14_000)}\\n`;
|
||||||
const item = mapCodexToolCallFromThreadItem(
|
const item = mapCodexToolCallFromThreadItem(
|
||||||
{
|
{
|
||||||
@@ -147,11 +151,12 @@ describe("codex tool-call mapper", () => {
|
|||||||
|
|
||||||
expect(item).toBeTruthy();
|
expect(item).toBeTruthy();
|
||||||
expect(item?.status).toBe("completed");
|
expect(item?.status).toBe("completed");
|
||||||
expect(item?.output).toBeTruthy();
|
expect(item?.detail?.type).toBe("edit");
|
||||||
const output = item?.output as { files?: Array<{ patch?: string }> };
|
if (item?.detail?.type === "edit") {
|
||||||
expect(output.files?.[0]?.patch).toBeDefined();
|
expect(item.detail.unifiedDiff).toBeDefined();
|
||||||
expect(output.files?.[0]?.patch?.includes("...[truncated ")).toBe(true);
|
expect(item.detail.unifiedDiff?.includes("...[truncated ")).toBe(true);
|
||||||
expect((output.files?.[0]?.patch?.length ?? 0) < hugeDiff.length).toBe(true);
|
expect((item.detail.unifiedDiff?.length ?? 0) < hugeDiff.length).toBe(true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps write/edit/search known variants with distinct detail types", () => {
|
it("maps write/edit/search known variants with distinct detail types", () => {
|
||||||
@@ -218,7 +223,7 @@ describe("codex tool-call mapper", () => {
|
|||||||
expect(item?.callId).toBe("codex-call-3");
|
expect(item?.callId).toBe("codex-call-3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps unknown tools canonical without detail", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapCodexRolloutToolCall({
|
const item = mapCodexRolloutToolCall({
|
||||||
callId: "codex-call-4",
|
callId: "codex-call-4",
|
||||||
name: "my_custom_tool",
|
name: "my_custom_tool",
|
||||||
@@ -228,9 +233,11 @@ describe("codex tool-call mapper", () => {
|
|||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toBeUndefined();
|
expect(item.detail).toEqual({
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { foo: "bar" },
|
||||||
|
rawOutput: { ok: true },
|
||||||
|
});
|
||||||
expect(item.callId).toBe("codex-call-4");
|
expect(item.callId).toBe("codex-call-4");
|
||||||
expect(item.input).toEqual({ foo: "bar" });
|
|
||||||
expect(item.output).toEqual({ ok: true });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ describe("opencode tool-call mapper", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps unknown tools canonical without detail", () => {
|
it("maps unknown tools to unknown detail with raw payloads", () => {
|
||||||
const item = mapOpencodeToolCall({
|
const item = mapOpencodeToolCall({
|
||||||
toolName: "my_custom_tool",
|
toolName: "my_custom_tool",
|
||||||
callId: "opencode-call-4",
|
callId: "opencode-call-4",
|
||||||
@@ -192,8 +192,10 @@ describe("opencode tool-call mapper", () => {
|
|||||||
|
|
||||||
expect(item.status).toBe("completed");
|
expect(item.status).toBe("completed");
|
||||||
expect(item.error).toBeNull();
|
expect(item.error).toBeNull();
|
||||||
expect(item.detail).toBeUndefined();
|
expect(item.detail).toEqual({
|
||||||
expect(item.input).toEqual({ foo: "bar" });
|
type: "unknown",
|
||||||
expect(item.output).toEqual({ ok: true });
|
rawInput: { foo: "bar" },
|
||||||
|
rawOutput: { ok: true },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,8 +33,10 @@ describe("serializeAgentStreamEvent", () => {
|
|||||||
callId: "call_1",
|
callId: "call_1",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
output: null,
|
type: "shell",
|
||||||
|
command: "pwd",
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -45,7 +47,6 @@ describe("serializeAgentStreamEvent", () => {
|
|||||||
throw new Error("Expected timeline.tool_call event");
|
throw new Error("Expected timeline.tool_call event");
|
||||||
}
|
}
|
||||||
expect(serialized.item.status).toBe("running");
|
expect(serialized.item.status).toBe("running");
|
||||||
expect(serialized.item.output).toBeNull();
|
|
||||||
expect(serialized.item.error).toBeNull();
|
expect(serialized.item.error).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +59,11 @@ describe("serializeAgentStreamEvent", () => {
|
|||||||
callId: "call_legacy",
|
callId: "call_legacy",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "inProgress",
|
status: "inProgress",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { command: "pwd" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} satisfies unknown;
|
} satisfies unknown;
|
||||||
|
|
||||||
|
|||||||
@@ -1930,11 +1930,14 @@ export class Session {
|
|||||||
name: "paseo_worktree_setup",
|
name: "paseo_worktree_setup",
|
||||||
callId,
|
callId,
|
||||||
status: "running",
|
status: "running",
|
||||||
input: {
|
detail: {
|
||||||
worktreePath: worktree.worktreePath,
|
type: "unknown",
|
||||||
branchName: worktree.branchName,
|
rawInput: {
|
||||||
|
worktreePath: worktree.worktreePath,
|
||||||
|
branchName: worktree.branchName,
|
||||||
|
},
|
||||||
|
rawOutput: null,
|
||||||
},
|
},
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
if (!started) {
|
if (!started) {
|
||||||
@@ -1952,18 +1955,21 @@ export class Session {
|
|||||||
name: "paseo_worktree_setup",
|
name: "paseo_worktree_setup",
|
||||||
callId,
|
callId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: {
|
detail: {
|
||||||
worktreePath: worktree.worktreePath,
|
type: "unknown",
|
||||||
branchName: worktree.branchName,
|
rawInput: {
|
||||||
},
|
worktreePath: worktree.worktreePath,
|
||||||
output: {
|
branchName: worktree.branchName,
|
||||||
worktreePath: worktree.worktreePath,
|
},
|
||||||
commands: results.map((result) => ({
|
rawOutput: {
|
||||||
command: result.command,
|
worktreePath: worktree.worktreePath,
|
||||||
cwd: result.cwd,
|
commands: results.map((result) => ({
|
||||||
exitCode: result.exitCode,
|
command: result.command,
|
||||||
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
cwd: result.cwd,
|
||||||
})),
|
exitCode: result.exitCode,
|
||||||
|
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
@@ -1977,18 +1983,21 @@ export class Session {
|
|||||||
name: "paseo_worktree_setup",
|
name: "paseo_worktree_setup",
|
||||||
callId,
|
callId,
|
||||||
status: "failed",
|
status: "failed",
|
||||||
input: {
|
detail: {
|
||||||
worktreePath: worktree.worktreePath,
|
type: "unknown",
|
||||||
branchName: worktree.branchName,
|
rawInput: {
|
||||||
},
|
worktreePath: worktree.worktreePath,
|
||||||
output: {
|
branchName: worktree.branchName,
|
||||||
worktreePath: worktree.worktreePath,
|
},
|
||||||
commands: results.map((result) => ({
|
rawOutput: {
|
||||||
command: result.command,
|
worktreePath: worktree.worktreePath,
|
||||||
cwd: result.cwd,
|
commands: results.map((result) => ({
|
||||||
exitCode: result.exitCode,
|
command: result.command,
|
||||||
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
cwd: result.cwd,
|
||||||
})),
|
exitCode: result.exitCode,
|
||||||
|
output: `${result.stdout ?? ""}${result.stderr ? `\n${result.stderr}` : ""}`.trim(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
error: { message },
|
error: { message },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -246,8 +246,11 @@ class FakeAgentSession implements AgentSession {
|
|||||||
name: tool.name,
|
name: tool.name,
|
||||||
callId,
|
callId,
|
||||||
status: "running",
|
status: "running",
|
||||||
input: tool.input ?? null,
|
detail: {
|
||||||
output: null,
|
type: "unknown",
|
||||||
|
rawInput: tool.input ?? null,
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -327,8 +330,11 @@ class FakeAgentSession implements AgentSession {
|
|||||||
name: tool.name,
|
name: tool.name,
|
||||||
callId,
|
callId,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
input: tool.input ?? null,
|
detail: {
|
||||||
output: toolOutput ?? { ok: true },
|
type: "unknown",
|
||||||
|
rawInput: tool.input ?? null,
|
||||||
|
rawOutput: toolOutput ?? { ok: true },
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,13 +33,11 @@ describe("shared messages stream parsing", () => {
|
|||||||
callId: "call_live",
|
callId: "call_live",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "ls" },
|
|
||||||
output: null,
|
|
||||||
error: null,
|
|
||||||
detail: {
|
detail: {
|
||||||
type: "shell",
|
type: "shell",
|
||||||
command: "ls",
|
command: "ls",
|
||||||
},
|
},
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ function canonicalBase() {
|
|||||||
type: "tool_call" as const,
|
type: "tool_call" as const,
|
||||||
callId: "call_123",
|
callId: "call_123",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
output: null,
|
type: "shell" as const,
|
||||||
|
command: "pwd",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,17 +20,12 @@ describe("shared messages tool_call schema", () => {
|
|||||||
...canonicalBase(),
|
...canonicalBase(),
|
||||||
status: "running",
|
status: "running",
|
||||||
error: null,
|
error: null,
|
||||||
detail: {
|
|
||||||
type: "shell",
|
|
||||||
command: "pwd",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const completed = AgentTimelineItemPayloadSchema.parse({
|
const completed = AgentTimelineItemPayloadSchema.parse({
|
||||||
...canonicalBase(),
|
...canonicalBase(),
|
||||||
status: "completed",
|
status: "completed",
|
||||||
error: null,
|
error: null,
|
||||||
output: { output: "/tmp/repo" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const failed = AgentTimelineItemPayloadSchema.parse({
|
const failed = AgentTimelineItemPayloadSchema.parse({
|
||||||
@@ -54,8 +51,10 @@ describe("shared messages tool_call schema", () => {
|
|||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "pwd" },
|
detail: {
|
||||||
output: null,
|
type: "shell",
|
||||||
|
command: "pwd",
|
||||||
|
},
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,12 +81,11 @@ describe("shared messages tool_call schema", () => {
|
|||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const missingOutput = AgentTimelineItemPayloadSchema.safeParse({
|
const missingDetail = AgentTimelineItemPayloadSchema.safeParse({
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
callId: "call_missing_output",
|
callId: "call_missing_detail",
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "pwd" },
|
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,7 +97,7 @@ describe("shared messages tool_call schema", () => {
|
|||||||
|
|
||||||
expect(completedWithError.success).toBe(false);
|
expect(completedWithError.success).toBe(false);
|
||||||
expect(failedWithoutError.success).toBe(false);
|
expect(failedWithoutError.success).toBe(false);
|
||||||
expect(missingOutput.success).toBe(false);
|
expect(missingDetail.success).toBe(false);
|
||||||
expect(legacyStatus.success).toBe(false);
|
expect(legacyStatus.success).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,14 +140,22 @@ export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionReque
|
|||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Structured tool result types for better client rendering
|
const UnknownValueSchema = z.union([
|
||||||
// These types define the structure of the `output` field in tool_call timeline items
|
z.null(),
|
||||||
export type StructuredToolResult =
|
z.boolean(),
|
||||||
| { type: "command"; command: string; output: string; exitCode?: number; cwd?: string }
|
z.number(),
|
||||||
| { type: "file_write"; filePath: string; oldContent: string; newContent: string }
|
z.string(),
|
||||||
| { type: "file_edit"; filePath: string; diff?: string; oldContent?: string; newContent?: string }
|
z.array(z.unknown()),
|
||||||
| { type: "file_read"; filePath: string; content: string }
|
z.object({}).passthrough(),
|
||||||
| { type: "generic"; data: unknown };
|
]);
|
||||||
|
|
||||||
|
const NonNullUnknownSchema = z.union([
|
||||||
|
z.boolean(),
|
||||||
|
z.number(),
|
||||||
|
z.string(),
|
||||||
|
z.array(z.unknown()),
|
||||||
|
z.object({}).passthrough(),
|
||||||
|
]);
|
||||||
|
|
||||||
const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUnion("type", [
|
const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUnion("type", [
|
||||||
z.object({
|
z.object({
|
||||||
@@ -180,32 +188,18 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail> = z.discriminatedUn
|
|||||||
type: z.literal("search"),
|
type: z.literal("search"),
|
||||||
query: z.string(),
|
query: z.string(),
|
||||||
}),
|
}),
|
||||||
]);
|
z.object({
|
||||||
|
type: z.literal("unknown"),
|
||||||
const NonUndefinedUnknownSchema = z.union([
|
rawInput: UnknownValueSchema,
|
||||||
z.null(),
|
rawOutput: UnknownValueSchema,
|
||||||
z.boolean(),
|
}),
|
||||||
z.number(),
|
|
||||||
z.string(),
|
|
||||||
z.array(z.unknown()),
|
|
||||||
z.object({}).passthrough(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const NonNullUnknownSchema = z.union([
|
|
||||||
z.boolean(),
|
|
||||||
z.number(),
|
|
||||||
z.string(),
|
|
||||||
z.array(z.unknown()),
|
|
||||||
z.object({}).passthrough(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ToolCallBasePayloadSchema = z.object({
|
const ToolCallBasePayloadSchema = z.object({
|
||||||
type: z.literal("tool_call"),
|
type: z.literal("tool_call"),
|
||||||
callId: z.string(),
|
callId: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
input: NonUndefinedUnknownSchema,
|
detail: ToolCallDetailPayloadSchema,
|
||||||
output: NonUndefinedUnknownSchema,
|
|
||||||
detail: ToolCallDetailPayloadSchema.optional(),
|
|
||||||
metadata: z.record(z.unknown()).optional(),
|
metadata: z.record(z.unknown()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,11 +274,6 @@ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
|
|||||||
sessionId: z.string(),
|
sessionId: z.string(),
|
||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
}),
|
}),
|
||||||
z.object({
|
|
||||||
type: z.literal("provider_event"),
|
|
||||||
provider: AgentProviderSchema,
|
|
||||||
raw: z.unknown(),
|
|
||||||
}),
|
|
||||||
z.object({
|
z.object({
|
||||||
type: z.literal("turn_started"),
|
type: z.literal("turn_started"),
|
||||||
provider: AgentProviderSchema,
|
provider: AgentProviderSchema,
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ describe("shared tool-call display mapping", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { path: "/tmp/repo/src/index.ts" },
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
detail: {
|
detail: {
|
||||||
type: "read",
|
type: "read",
|
||||||
@@ -23,13 +21,16 @@ describe("shared tool-call display mapping", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not infer summaries from raw input when detail is missing", () => {
|
it("does not infer summaries from unknown raw detail", () => {
|
||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "exec_command",
|
name: "exec_command",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: { command: "npm test" },
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: { command: "npm test" },
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(display).toEqual({
|
expect(display).toEqual({
|
||||||
@@ -37,13 +38,16 @@ describe("shared tool-call display mapping", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps task metadata summary without detail", () => {
|
it("keeps task metadata summary on unknown detail", () => {
|
||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "task",
|
name: "task",
|
||||||
status: "running",
|
status: "running",
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
error: null,
|
error: null,
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: null,
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
subAgentActivity: "Running tests",
|
subAgentActivity: "Running tests",
|
||||||
},
|
},
|
||||||
@@ -59,9 +63,12 @@ describe("shared tool-call display mapping", () => {
|
|||||||
const display = buildToolCallDisplayModel({
|
const display = buildToolCallDisplayModel({
|
||||||
name: "shell",
|
name: "shell",
|
||||||
status: "failed",
|
status: "failed",
|
||||||
input: null,
|
|
||||||
output: null,
|
|
||||||
error: { message: "boom" },
|
error: { message: "boom" },
|
||||||
|
detail: {
|
||||||
|
type: "unknown",
|
||||||
|
rawInput: null,
|
||||||
|
rawOutput: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(display.errorText).toBe('{\n "message": "boom"\n}');
|
expect(display.errorText).toBe('{\n "message": "boom"\n}');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { stripCwdPrefix } from "./path-utils.js";
|
|||||||
|
|
||||||
export type ToolCallDisplayInput = Pick<
|
export type ToolCallDisplayInput = Pick<
|
||||||
ToolCallTimelineItem,
|
ToolCallTimelineItem,
|
||||||
"name" | "status" | "input" | "output" | "error" | "metadata" | "detail"
|
"name" | "status" | "error" | "metadata" | "detail"
|
||||||
> & {
|
> & {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
};
|
};
|
||||||
@@ -62,33 +62,35 @@ export function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCall
|
|||||||
let displayName = humanizeToolName(input.name);
|
let displayName = humanizeToolName(input.name);
|
||||||
let summary: string | undefined;
|
let summary: string | undefined;
|
||||||
|
|
||||||
if (input.detail) {
|
switch (input.detail.type) {
|
||||||
switch (input.detail.type) {
|
case "shell":
|
||||||
case "shell":
|
displayName = "Shell";
|
||||||
displayName = "Shell";
|
summary = input.detail.command;
|
||||||
summary = input.detail.command;
|
break;
|
||||||
break;
|
case "read":
|
||||||
case "read":
|
displayName = "Read";
|
||||||
displayName = "Read";
|
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
||||||
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
break;
|
||||||
break;
|
case "edit":
|
||||||
case "edit":
|
displayName = "Edit";
|
||||||
displayName = "Edit";
|
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
||||||
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
break;
|
||||||
break;
|
case "write":
|
||||||
case "write":
|
displayName = "Write";
|
||||||
displayName = "Write";
|
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
||||||
summary = stripCwdPrefix(input.detail.filePath, input.cwd);
|
break;
|
||||||
break;
|
case "search":
|
||||||
case "search":
|
displayName = "Search";
|
||||||
displayName = "Search";
|
summary = input.detail.query;
|
||||||
summary = input.detail.query;
|
break;
|
||||||
break;
|
case "unknown":
|
||||||
}
|
break;
|
||||||
} else if (lowerName === "task") {
|
}
|
||||||
|
|
||||||
|
if (lowerName === "task" && input.detail.type === "unknown") {
|
||||||
displayName = "Task";
|
displayName = "Task";
|
||||||
summary = isRecord(input.metadata) ? readString(input.metadata.subAgentActivity) : undefined;
|
summary = isRecord(input.metadata) ? readString(input.metadata.subAgentActivity) : undefined;
|
||||||
} else if (lowerName === "thinking") {
|
} else if (lowerName === "thinking" && input.detail.type === "unknown") {
|
||||||
displayName = "Thinking";
|
displayName = "Thinking";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user