Merge branch 'investigate/ui-network-responsive' into main

This commit is contained in:
Mohamed Boudra
2026-02-09 12:25:57 +07:00
29 changed files with 442 additions and 297 deletions
@@ -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}
+36 -16
View File
@@ -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>
+5 -2
View File
@@ -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 =
+19 -14
View File
@@ -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");
}); });
+37 -13
View File
@@ -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}');
+1
View File
@@ -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 {
+1 -1
View File
@@ -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 },
});
}); });
}); });
+9 -4
View File
@@ -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;
+37 -28
View File
@@ -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);
}); });
}); });
+22 -33
View File
@@ -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}');
+28 -26
View File
@@ -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";
} }