Files
tutti/packages/agent/gui/shared/workspaceAgentSessionDetailViewModel.spec.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

2460 lines
65 KiB
TypeScript

import {
normalizeAgentActivitySession,
type AgentActivitySession
} from "@tutti-os/agent-activity-core";
import type { WorkspaceAgentActivityTimelineItem } from "./workspaceAgentTimelineTypes";
import { afterEach, describe, expect, it } from "vitest";
import { setAgentGuiI18nTestLocale } from "../i18n/testUtils";
import type { WorkspaceAgentActivityCard } from "./workspaceAgentActivityListViewModel";
import { buildWorkspaceAgentSessionDetailViewModel } from "./workspaceAgentSessionDetailViewModel";
function item(
overrides: Partial<WorkspaceAgentActivityTimelineItem> & { id: number }
): WorkspaceAgentActivityTimelineItem {
const { id, ...rest } = overrides;
return {
id,
workspaceId: "room-1",
agentSessionId: "session-1",
eventId: rest.eventId ?? `event-${id}`,
actorType: rest.actorType ?? "agent",
actorId: rest.actorId ?? "actor-1",
itemType: rest.itemType ?? "message",
...rest
};
}
const activity: WorkspaceAgentActivityCard = {
id: "activity-session-1",
sessionId: "session-1",
userId: "user-1",
userName: "Jessica",
userAvatarUrl: "https://cdn.example.com/jessica.png",
agentProvider: "codex",
agentName: "Codex",
title: "帮我根据目前工作区的信息出原型",
latestActivitySummary: "正在整理信息",
status: "working",
changedFiles: [],
sortTimeUnixMs: 10
};
const session: AgentActivitySession = normalizeAgentActivitySession({
...{
activeTurnId: null,
latestTurnInteractions: [],
pendingInteractions: []
},
workspaceId: "workspace-1",
agentSessionId: "session-1",
provider: "codex",
providerSessionId: "provider-session-1",
cwd: "/workspace/app",
title: activity.title
});
describe("buildWorkspaceAgentSessionDetailViewModel", () => {
afterEach(async () => {
setAgentGuiI18nTestLocale("zh-CN");
});
it("groups user, agent messages, and tool calls by turnId in timeline order", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 1,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "请基于 README.md 出一个页面"
}),
item({
id: 2,
turnId: "turn-1",
itemType: "call.tool",
name: "read_file",
status: "completed",
payload: { summary: "Read README.md" }
}),
item({
id: 3,
turnId: "turn-1",
itemType: "message",
role: "assistant",
payload: { content: "已读取 [README.md](README.md),准备设计。" }
}),
item({
id: 4,
turnId: "turn-2",
itemType: "message.user",
payload: { text: "整体更简约一点" }
}),
item({
id: 5,
turnId: "turn-2",
itemType: "call",
name: "edit_file",
status: "failed",
payload: { text: "Update src/App.tsx" }
}),
item({
id: 6,
turnId: "turn-2",
itemType: "message.agent",
content: "我会收敛视觉,并保留 http://127.0.0.1:8765/"
})
],
workspaceRoot: "/workspace"
});
expect(view.activity).toBe(activity);
expect(view.cwd).toBe("/workspace/app");
expect(view.workspaceRoot).toBe("/workspace");
expect(view.turns).toHaveLength(2);
expect(view.turns[0]).toMatchObject({
id: "turn-1",
toolCallCount: 1,
hasFailedToolCall: false,
toolCalls: [
{
id: "event-2",
name: "Read file",
status: "Completed",
summary: "Read README.md",
toolName: "Read"
}
],
userMessage: { body: "请基于 README.md 出一个页面" },
userMessages: [{ body: "请基于 README.md 出一个页面" }],
agentMessages: [{ body: "已读取 [README.md](README.md),准备设计。" }]
});
expect(view.turns[1]).toMatchObject({
id: "turn-2",
toolCallCount: 1,
hasFailedToolCall: true,
toolCalls: [
{
id: "event-5",
name: "Edit file",
status: "Failed",
summary: "Update src/App.tsx",
toolName: "Edit"
}
],
userMessage: { body: "整体更简约一点" },
userMessages: [{ body: "整体更简约一点" }],
agentMessages: [{ body: "我会收敛视觉,并保留 http://127.0.0.1:8765/" }]
});
});
it("falls back to item identity grouping and ignores blank message bodies", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({ id: 10, itemType: "message", role: "assistant", content: " " }),
item({ id: 11, itemType: "message.user", content: "第一轮问题" }),
item({ id: 12, itemType: "call.tool", status: "completed" }),
item({ id: 13, itemType: "message.assistant", content: "第一轮回答" }),
item({ id: 14, itemType: "message.user", content: "第二轮问题" }),
item({ id: 15, itemType: "message.agent", content: "第二轮回答" })
]
});
expect(view.turns).toHaveLength(2);
expect(view.turns[0]?.userMessage?.body).toBe("第一轮问题");
expect(view.turns[0]?.agentMessages.map((message) => message.body)).toEqual(
["第一轮回答"]
);
expect(view.turns[0]?.toolCallCount).toBe(1);
expect(view.turns[1]?.userMessage?.body).toBe("第二轮问题");
expect(view.turns[1]?.agentMessages.map((message) => message.body)).toEqual(
["第二轮回答"]
);
});
it("renders known tool calls as localized type labels and key details", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 20,
itemType: "message.user",
content: "检查项目结构"
}),
item({
id: 21,
itemType: "call.tool",
name: "exec_command",
status: "running",
payload: {
toolName: "Bash",
input: {
cmd: 'pwd && rg --files -g "!node_modules"'
}
}
}),
item({
id: 22,
itemType: "call.tool",
name: "web_search",
status: "completed",
payload: {
toolName: "WebSearch",
input: {
query: "opencode architecture"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "event-21",
name: "Run command",
toolName: "Bash",
callType: null,
status: "Running",
statusKind: "working",
summary: 'pwd && rg --files -g "!node_modules"'
}),
expect.objectContaining({
id: "event-22",
name: "Search web",
toolName: "WebSearch",
callType: null,
status: "Completed",
statusKind: "completed",
summary: "opencode architecture"
})
]);
});
it("recognizes opaque image generation tool aliases from canonical output fields", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
turnId: "turn-image",
itemType: "message.user",
content: "帮我生成一个跳舞小女孩的图片"
}),
item({
id: 31,
turnId: "turn-image",
itemType: "call.completed",
name: "ig_05eb62dbe723c910016a1336ad3de881919216a6f64051a5e2",
status: "completed",
payload: {
toolName: "ig_05eb62dbe723c910016a1336ad3de881919216a6f64051a5e2",
callType: "tool",
input: {
prompt: "Revised prompt: a joyful little girl dancing"
},
output: {
savedPath: "/workspace/output/generated.png",
imageMimeType: "image/png"
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
toolName: "ImageGeneration",
name: "Image Generation"
});
});
it("merges background terminal write_stdin output back into the originating bash tool call", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 200,
turnId: "turn-bg",
itemType: "message.user",
content: "看看有什么文件"
}),
item({
id: 201,
turnId: "turn-bg",
itemType: "call.completed",
callId: "call-bash",
status: "completed",
payload: {
toolName: "Bash",
callID: "call-bash",
input: {
cmd: "ls -la"
},
output: {
output:
"Chunk ID: abc123\nProcess running with session ID 51893\n"
}
}
}),
item({
id: 202,
turnId: "turn-bg",
itemType: "message.assistant_thinking",
content: "Waiting for the background terminal."
}),
item({
id: 203,
turnId: "turn-bg",
itemType: "call.completed",
callId: "call-write-stdin",
status: "completed",
payload: {
toolName: "write_stdin",
callID: "call-write-stdin",
input: {
chars: "",
session_id: "51893"
},
output: {
output: "Chunk ID: out123\nOutput:\ntotal 1\ntoday_time.txt\n"
}
}
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.toolCalls).toHaveLength(1);
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
id: "call:call-bash",
toolName: "Bash",
statusKind: "completed",
payload: {
output: {
output: "Chunk ID: out123\nOutput:\ntotal 1\ntoday_time.txt\n"
}
}
});
expect(view.turns[0]?.agentItems).toEqual(
expect.not.arrayContaining([
expect.objectContaining({
kind: "tool-calls",
toolCalls: expect.arrayContaining([
expect.objectContaining({
id: "call:call-write-stdin"
})
])
})
])
);
});
it("prefers canonical payload tool names over display titles for ACP fetch calls", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 26,
turnId: "turn-web",
itemType: "call.completed",
name: "Searching the Web",
callType: "tool",
status: "completed",
payload: {
toolName: "WebSearch",
kind: "fetch",
input: {
action: {
type: "search",
query: "today top news"
},
query: "today top news"
}
}
}),
item({
id: 27,
turnId: "turn-web",
itemType: "call.completed",
name: "Searching the Web",
callType: "tool",
status: "completed",
payload: {
toolName: "WebFetch",
kind: "fetch",
input: {
action: {
type: "open_page",
url: "https://example.com/news"
},
query: "https://example.com/news"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
name: "Search web",
toolName: "WebSearch",
summary: "today top news"
}),
expect.objectContaining({
name: "Read web page",
toolName: "WebFetch",
summary: "example.com"
})
]);
});
it("builds compact summaries for multi-file edits, web fetch domains, and todo progress", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 28,
turnId: "turn-summary",
itemType: "call.tool",
status: "completed",
payload: {
toolName: "Write",
output: {
structuredPatch: [
{ filePath: "src/app.ts", kind: "add" },
{ filePath: "src/routes.ts", kind: "add" }
]
}
}
}),
item({
id: 29,
turnId: "turn-summary",
itemType: "call.tool",
status: "completed",
payload: {
toolName: "WebFetch",
input: {
url: "https://docs.example.com/renderer"
}
}
}),
item({
id: 30,
turnId: "turn-summary",
itemType: "call.tool",
status: "completed",
payload: {
toolName: "TodoWrite",
input: {
todos: [
{ content: "A", status: "completed" },
{ content: "B", status: "pending" },
{ content: "C", status: "pending" }
]
}
}
})
]
});
expect(view.turns[0]?.toolCalls.map((call) => call.summary)).toEqual([
"2 files",
"docs.example.com",
"1/3 completed"
]);
});
it("builds compact summaries for Codex Edit changes arrays", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 31,
turnId: "turn-codex-array-edit",
itemType: "call.tool",
status: "completed",
payload: {
toolName: "Edit",
input: {
file_path: "/workspace/deck/assets/styles.css",
changes: [
{
path: "/workspace/deck/slides/02-why-now.html",
kind: { type: "add" },
diff: "<section>Why now</section>\n"
},
{
path: "/workspace/deck/slides/01-cover.html",
kind: { type: "update" },
diff: "@@ -1 +1 @@\n-Old\n+New\n"
}
]
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]?.summary).toBe("2 files");
});
it("keeps durable approval items in the transcript as specialized tool calls", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
seq: 30,
turnId: "turn-approval",
itemType: "message.user",
content: "继续执行前先问我"
}),
item({
id: 31,
seq: 31,
turnId: "turn-approval",
itemType: "approval.requested",
name: "Need approval",
callType: "approval",
status: "awaiting_approval",
payload: {
callType: "approval",
input: {
options: [
{ id: "allow_once", label: "Allow once" },
{ id: "deny", label: "Deny" }
]
}
}
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "event-31",
name: "Need approval",
callType: "approval",
status: "Waiting",
statusKind: "waiting"
})
]);
expect(view.turns[0]?.agentItems).toContainEqual(
expect.objectContaining({
kind: "tool-calls",
toolCallCount: 1,
toolCalls: [
expect.objectContaining({
id: "event-31",
callType: "approval"
})
]
})
);
});
it("preserves readable tool call details for second-level expansion", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 24,
itemType: "message.user",
content: "检查项目结构"
}),
item({
id: 25,
itemType: "call.tool",
name: "exec_command",
status: "completed",
payload: {
toolName: "Bash",
input: {
cmd: 'pwd && rg --files -g "!node_modules"'
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
name: "执行命令",
status: "已完成",
summary: 'pwd && rg --files -g "!node_modules"'
});
});
it("shows the tool action without structured output or error payloads", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 26,
itemType: "message.user",
content: "运行测试"
}),
item({
id: 27,
itemType: "call.tool",
name: "exec_command",
status: "failed",
payload: {
input: { cmd: "pnpm test" },
output: { stdout: "1 failed" },
error: { message: "exit code 1" }
}
})
]
});
expect(view.turns[0]?.toolCalls[0]?.summary).toBe("pnpm test");
});
it("preserves raw tool payload and call metadata for specialized renderers", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 40,
seq: 40,
turnId: "turn-plan",
itemType: "message.user",
content: "给我一个实施计划"
}),
item({
id: 41,
seq: 41,
turnId: "turn-plan",
itemType: "call",
callType: "interactive",
name: "ExitPlanMode",
status: "completed",
payload: {
input: {
plan: "# Plan\n\n- Inspect current renderer\n- Add specialized cards"
},
tool_state: {
call_type: "interactive",
name: "ExitPlanMode",
input: {
plan: "# Plan\n\n- Inspect current renderer\n- Add specialized cards"
}
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
toolName: "ExitPlanMode",
callType: "interactive",
payload: expect.objectContaining({
input: expect.objectContaining({
plan: "# Plan\n\n- Inspect current renderer\n- Add specialized cards"
})
})
});
});
it("maps waiting_input interactive calls to waiting status for transcript rendering", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 40,
seq: 40,
turnId: "turn-plan",
itemType: "message.user",
content: "给我一个实施计划"
}),
item({
id: 41,
seq: 41,
turnId: "turn-plan",
itemType: "call.started",
callType: "interactive",
name: "AskUserQuestion",
status: "waiting_input",
payload: {
input: {
questions: [{ id: "scope", question: "Which scope?" }]
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
toolName: "AskUserQuestion",
callType: "interactive",
statusKind: "waiting"
});
});
it("keeps executable Claude AskUserQuestion prompts in the transcript detail view", () => {
const claudeSession: AgentActivitySession = {
...session,
provider: "claude-code"
};
const view = buildWorkspaceAgentSessionDetailViewModel({
activity: {
...activity,
agentProvider: "claude-code",
agentName: "Claude Code"
},
session: claudeSession,
timelineItems: [
item({
id: 42,
seq: 42,
turnId: "turn-claude-ask",
itemType: "message.user",
content: "验证 AskUserQuestion"
}),
item({
id: 43,
seq: 43,
turnId: "turn-claude-ask",
itemType: "call.started",
callId: "call-ask-user",
callType: "interactive",
name: "AskUserQuestion",
status: "waiting_input",
payload: {
callId: "call-ask-user",
toolName: "AskUserQuestion",
input: {
requestId: "request-ask-user",
questions: [{ id: "scope", question: "Which scope?" }]
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]).toMatchObject({
toolName: "AskUserQuestion",
callType: "interactive",
statusKind: "waiting",
payload: expect.objectContaining({
input: expect.objectContaining({
requestId: "request-ask-user"
})
})
});
});
it("hides unavailable AskUserQuestion tool calls by event semantics", () => {
const providerSession: AgentActivitySession = {
...session,
provider: "opencode"
};
const view = buildWorkspaceAgentSessionDetailViewModel({
activity: {
...activity,
agentProvider: "opencode",
agentName: "OpenCode"
},
session: providerSession,
timelineItems: [
item({
id: 42,
seq: 42,
turnId: "turn-claude-ask",
itemType: "message.user",
content: "帮我做一个 todo-list 应用?"
}),
item({
id: 43,
seq: 43,
turnId: "turn-claude-ask",
itemType: "call.started",
callId: "call-ask-user",
callType: "interactive",
name: "AskUserQuestion",
status: "waiting_input",
payload: {
toolName: "AskUserQuestion",
input: {
questions: [{ id: "scope", question: "Which scope?" }]
}
}
}),
item({
id: 44,
seq: 44,
turnId: "turn-claude-ask",
itemType: "call.errored",
callId: "call-ask-user",
callType: "interactive",
name: "AskUserQuestion",
status: "failed",
payload: {
toolName: "AskUserQuestion",
input: {
questions: [{ id: "scope", question: "Which scope?" }]
},
output: {
output:
"Error: No such tool available: AskUserQuestion. AskUserQuestion exists but is not enabled in this context."
},
error: {
error:
"Error: No such tool available: AskUserQuestion. AskUserQuestion exists but is not enabled in this context."
}
}
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.toolCalls).toEqual([]);
expect(view.turns[0]?.toolCallCount).toBe(0);
expect(view.turns[0]?.hasFailedToolCall).toBe(false);
});
it("renders the actual command from failed Codex exec tool payloads instead of raw error JSON", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 28,
itemType: "message.user",
content: "查看 git 状态"
}),
item({
id: 29,
itemType: "call.tool",
name: "exec_command",
status: "failed",
payload: {
error: {
activityKind: "run_command",
callID: "call-1",
command: "git status --short",
exitCode: 128,
status: "failed",
tool: "exec_command"
}
}
})
]
});
expect(view.turns[0]?.toolCalls[0]?.summary).toBe("git status --short");
});
it("keeps distinct Codex calls when legacy top-level callId only contains the tool name", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
turnId: "turn-codex",
itemType: "message.user",
content: "检查仓库"
}),
item({
id: 31,
turnId: "turn-codex",
itemType: "call.started",
callId: "tool.exec_command",
name: "exec_command",
status: "running",
payload: {
activityKind: "run_command",
callID: "call-pwd",
command: "pwd",
tool: "exec_command"
}
}),
item({
id: 32,
turnId: "turn-codex",
itemType: "call.completed",
callId: "tool.exec_command",
name: "exec_command",
status: "completed",
payload: {
activityKind: "run_command",
callID: "call-pwd",
command: "pwd",
tool: "exec_command"
}
}),
item({
id: 33,
turnId: "turn-codex",
itemType: "call.started",
callId: "tool.exec_command",
name: "exec_command",
status: "running",
payload: {
activityKind: "run_command",
callID: "call-rg",
command: "rg --files",
tool: "exec_command"
}
}),
item({
id: 34,
turnId: "turn-codex",
itemType: "call.completed",
callId: "tool.exec_command",
name: "exec_command",
status: "completed",
payload: {
activityKind: "run_command",
callID: "call-rg",
command: "rg --files",
tool: "exec_command"
}
})
]
});
expect(view.turns[0]?.toolCalls.map((call) => call.id)).toEqual([
"call:call-pwd",
"call:call-rg"
]);
expect(view.turns[0]?.toolCalls.map((call) => call.summary)).toEqual([
"pwd",
"rg --files"
]);
});
it("renders concise content for file, search, and patch tool calls", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
itemType: "message.user",
content: "继续检查和修改文件"
}),
item({
id: 31,
itemType: "call.tool",
name: "read_file",
status: "completed",
payload: {
input: { file_path: "src/App.tsx" }
}
}),
item({
id: 32,
itemType: "call.tool",
name: "search_files",
status: "completed",
payload: {
metadata: { activityKind: "search_files", pattern: "useState" }
}
}),
item({
id: 33,
itemType: "call.tool",
name: "web_search",
status: "completed",
payload: {
query: "Codex tool call rendering"
}
}),
item({
id: 34,
itemType: "call.tool",
name: "apply_patch",
status: "completed",
payload: {
input: {
patch:
"*** Begin Patch\n*** Update File: README.md\n*** End Patch"
}
}
})
]
});
expect(view.turns[0]?.toolCalls.map((call) => call.summary)).toEqual([
"src/App.tsx",
"useState",
"Codex tool call rendering",
"*** Begin Patch\n*** Update File: README.md\n*** End Patch"
]);
});
it("keeps distinct user messages across turns even when prompt text repeats", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 20,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "第一条用户消息",
occurredAtUnixMs: 1000
}),
item({
id: 21,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "第二条用户消息",
occurredAtUnixMs: 1100
}),
item({
id: 22,
turnId: "turn-from-transcript",
itemType: "message",
role: "user",
content: "第一条用户消息",
occurredAtUnixMs: 1200
}),
item({
id: 23,
turnId: "turn-1",
itemType: "message",
role: "assistant",
content: "收到。"
})
]
});
expect(view.turns).toHaveLength(2);
expect(view.turns[0]?.userMessages.map((message) => message.body)).toEqual([
"第一条用户消息",
"第二条用户消息"
]);
expect(view.turns[0]?.agentMessages.map((message) => message.body)).toEqual(
["收到。"]
);
expect(view.turns[1]?.userMessages.map((message) => message.body)).toEqual([
"第一条用户消息"
]);
});
it("folds duplicate user prompt projections within the same turn", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 20,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "重复请求",
occurredAtUnixMs: 1000
}),
item({
id: 21,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "重复请求",
occurredAtUnixMs: 1100
}),
item({
id: 22,
turnId: "turn-1",
itemType: "message",
role: "assistant",
content: "收到。",
occurredAtUnixMs: 1200
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.userMessages.map((message) => message.body)).toEqual([
"重复请求"
]);
expect(view.turns[0]?.agentMessages.map((message) => message.body)).toEqual(
["收到。"]
);
});
it("does not render Claude synthetic interrupt messages as user transcript rows", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 20,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "[Request interrupted by user]",
occurredAtUnixMs: 1000
}),
item({
id: 21,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "[Request interrupted by user for tool use]",
occurredAtUnixMs: 1050
}),
item({
id: 23,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "继续正常请求",
occurredAtUnixMs: 1100
}),
item({
id: 22,
turnId: "turn-1",
itemType: "message",
role: "assistant",
content: "收到。",
occurredAtUnixMs: 1200
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.userMessages.map((message) => message.body)).toEqual([
"继续正常请求"
]);
expect(view.turns[0]?.agentMessages.map((message) => message.body)).toEqual(
["收到。"]
);
});
it("keeps agent thinking messages as collapsed agent timeline items", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "分析架构"
}),
item({
id: 31,
turnId: "turn-1",
itemType: "message",
role: "assistant_thinking",
content: "我先检查项目结构和入口文件。"
}),
item({
id: 32,
turnId: "turn-1",
itemType: "message",
role: "assistant",
content: "已经找到主要模块,继续梳理调用链。"
})
]
});
expect(view.turns[0]?.agentMessages.map((message) => message.body)).toEqual(
["已经找到主要模块,继续梳理调用链。"]
);
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "thinking",
thinking: {
id: "event-31",
body: "我先检查项目结构和入口文件。",
turnId: "turn-1",
occurredAtUnixMs: null
}
},
{
kind: "message",
message: {
id: "event-32",
body: "已经找到主要模块,继续梳理调用链。",
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("projects assistant thinking timeline status into the detail view model", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 33,
turnId: "turn-1",
itemType: "message",
role: "assistant_thinking",
status: "streaming",
content: "我正在确认上下文。"
})
]
});
expect(view.turns[0]?.agentItems[0]).toMatchObject({
kind: "thinking",
thinking: {
id: "event-33",
statusKind: "working"
}
});
});
it("deduplicates repeated agent thinking messages within a turn", () => {
const thinkingBody = [
"**Exploring frontend creation**",
"",
"I need to focus on creating a likely frontend for the issue at hand."
].join("\n");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 35,
turnId: "turn-1",
itemType: "message",
role: "user",
content: "帮我写一个登陆页面"
}),
item({
id: 36,
turnId: "turn-1",
itemType: "message",
role: "assistant_thinking",
content: thinkingBody
}),
item({
id: 37,
turnId: "turn-1",
itemType: "message",
role: "assistant_thinking",
content: thinkingBody
})
]
});
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "thinking",
thinking: {
id: "event-36",
body: thinkingBody,
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("keeps a turn when the agent only emits thinking content", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 33,
turnId: "turn-1",
itemType: "message.assistant_thinking",
content: "先确认配置是否已经注入。"
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "thinking",
thinking: {
id: "event-33",
body: "先确认配置是否已经注入。",
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("filters placeholder assistant thinking messages rendered as ellipsis", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 34,
turnId: "turn-1",
itemType: "message.assistant_thinking",
role: "assistant_thinking",
content: "..."
}),
item({
id: 35,
turnId: "turn-1",
itemType: "message.assistant",
role: "assistant",
content: "继续分析这个 session。"
})
]
});
expect(view.turns).toHaveLength(1);
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "message",
message: {
id: "event-35",
body: "继续分析这个 session。",
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("keeps assistant messages before tool calls in the agent-side timeline order", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 60,
turnId: "turn-1",
itemType: "message.user",
content: "分析open code 架构设计"
}),
item({
id: 61,
turnId: "turn-1",
itemType: "message.assistant",
role: "assistant",
payload: {
text: "我先快速扫一下仓库结构和关键配置,确认这是哪类项目,再基于实际代码做架构分析。"
}
}),
item({
id: 62,
turnId: "turn-1",
itemType: "call.started",
name: "exec_command",
status: "running",
payload: {
metadata: { activityKind: "search_files" },
callID: "call-search"
}
}),
item({
id: 63,
turnId: "turn-1",
itemType: "call.completed",
name: "exec_command",
status: "completed",
payload: {
metadata: { activityKind: "search_files" },
callID: "call-search"
}
}),
item({
id: 64,
turnId: "turn-1",
itemType: "message.assistant",
role: "assistant",
content:
"本地工作目录是空的,所以我按当前公开的 OpenCode 项目来分析。"
})
]
});
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "message",
message: {
id: "event-61",
body: "我先快速扫一下仓库结构和关键配置,确认这是哪类项目,再基于实际代码做架构分析。",
turnId: "turn-1",
occurredAtUnixMs: null
}
},
{
kind: "tool-calls",
id: "tools:event-62",
toolCallCount: 1,
hasFailedToolCall: false,
toolCalls: [
expect.objectContaining({
id: "call:call-search",
name: "搜索文件",
toolName: "Grep",
callType: null,
status: "已完成",
statusKind: "completed",
summary: ""
})
]
},
{
kind: "message",
message: {
id: "event-64",
body: "本地工作目录是空的,所以我按当前公开的 OpenCode 项目来分析。",
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("keeps thinking-only assistant items between tool calls at their original position", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session: {
...session
},
timelineItems: [
item({
id: 90,
seq: 90,
turnId: "turn-1",
itemType: "message.user",
content: "完成这个修改"
}),
item({
id: 91,
seq: 91,
turnId: "turn-1",
itemType: "call.completed",
name: "read_file",
status: "completed",
payload: {
input: { file_path: "README.md" }
}
}),
item({
id: 92,
seq: 92,
turnId: "turn-1",
itemType: "message.assistant_thinking",
content: "我先确认文档,再更新实现文件。"
}),
item({
id: 93,
seq: 93,
turnId: "turn-1",
itemType: "call.completed",
name: "edit_file",
status: "completed",
payload: {
input: { file_path: "src/App.tsx" }
}
}),
item({
id: 94,
seq: 94,
turnId: "turn-1",
itemType: "message.assistant",
content: "已经整理好变更。"
})
]
});
expect(view.turns[0]?.agentItems).toEqual([
expect.objectContaining({
kind: "tool-calls",
toolCallCount: 1,
toolCalls: [expect.objectContaining({ toolName: "Read" })]
}),
{
kind: "thinking",
thinking: {
id: "event-92",
body: "我先确认文档,再更新实现文件。",
turnId: "turn-1",
occurredAtUnixMs: null
}
},
expect.objectContaining({
kind: "tool-calls",
toolCallCount: 1,
toolCalls: [expect.objectContaining({ toolName: "Edit" })]
}),
{
kind: "message",
message: {
id: "event-94",
body: "已经整理好变更。",
turnId: "turn-1",
occurredAtUnixMs: null
}
}
]);
});
it("keeps trailing grouped tools split while the session is still processing", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session: {
...session
},
timelineItems: [
item({
id: 95,
seq: 95,
turnId: "turn-1",
itemType: "message.user",
content: "继续执行"
}),
item({
id: 96,
seq: 96,
turnId: "turn-1",
itemType: "call.completed",
name: "read_file",
status: "completed",
payload: {
input: { file_path: "README.md" }
}
}),
item({
id: 97,
seq: 97,
turnId: "turn-1",
itemType: "message.assistant_thinking",
content: "我还在确认下一步改动。"
}),
item({
id: 98,
seq: 98,
turnId: "turn-1",
itemType: "call.completed",
name: "edit_file",
status: "completed",
payload: {
input: { file_path: "src/App.tsx" }
}
})
]
});
expect(view.turns[0]?.agentItems).toEqual([
expect.objectContaining({
kind: "tool-calls",
toolCallCount: 1,
toolCalls: [expect.objectContaining({ id: "event-96" })]
}),
{
kind: "thinking",
thinking: {
id: "event-97",
body: "我还在确认下一步改动。",
turnId: "turn-1",
occurredAtUnixMs: null
}
},
expect.objectContaining({
kind: "tool-calls",
toolCallCount: 1,
toolCalls: [expect.objectContaining({ id: "event-98" })]
})
]);
});
it("renders non-Codex tool activities through the shared tool call disclosure model", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity: {
...activity,
agentProvider: "openclaw",
agentName: "OpenClaw"
},
session: { ...session, provider: "openclaw" },
timelineItems: [
item({
id: 70,
turnId: "turn-1",
itemType: "message.user",
content: "查一下 TODO"
}),
item({
id: 71,
turnId: "turn-1",
itemType: "activity.started",
payload: {
activityKey: "tool.run_shell_command",
metadata: {
activityKind: "run_command",
callID: "openclaw-call-1",
input: { command: "rg TODO" }
}
}
}),
item({
id: 72,
turnId: "turn-1",
itemType: "event",
payload: {
eventKey: "activity.completed",
activityKey: "tool.run_shell_command",
metadata: {
activityKind: "run_command",
callID: "openclaw-call-1",
input: { command: "rg TODO" },
output: { output: "src/App.tsx:12:TODO" }
}
}
})
]
});
expect(view.turns[0]?.toolCallCount).toBe(1);
expect(view.turns[0]?.agentItems).toEqual([
{
kind: "tool-calls",
id: "tools:event-71",
toolCallCount: 1,
hasFailedToolCall: false,
toolCalls: [
expect.objectContaining({
id: "call:openclaw-call-1",
name: "执行命令",
toolName: "Bash",
callType: null,
status: "已完成",
statusKind: "completed",
summary: "rg TODO"
})
]
}
]);
});
it("does not treat agent activity timeline items as tool calls", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity: {
...activity,
agentProvider: "openclaw",
agentName: "OpenClaw"
},
session: { ...session, provider: "openclaw" },
timelineItems: [
item({
id: 80,
turnId: "turn-1",
itemType: "message.user",
content: "总结一下"
}),
item({
id: 81,
turnId: "turn-1",
itemType: "activity.completed",
payload: {
activityKey: "agent.responding",
metadata: { activityKind: "responding" }
}
})
]
});
expect(view.turns[0]?.toolCallCount).toBe(0);
expect(view.turns[0]?.agentItems).toEqual([]);
});
it("does not treat call items carrying agent.responding activity as tool calls", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 82,
turnId: "turn-1",
itemType: "message.user",
content: "总结一下"
}),
item({
id: 83,
turnId: "turn-1",
itemType: "call",
callType: "tool",
name: "agent.responding",
status: "completed",
payload: {
activityKey: "agent.responding",
metadata: { activityKind: "responding" }
}
})
]
});
expect(view.turns[0]?.toolCallCount).toBe(0);
expect(view.turns[0]?.agentItems).toEqual([]);
});
it("uses the canonical tool title for unknown tool calls without compacting payload JSON", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 30,
itemType: "message.user",
content: "运行未知工具"
}),
item({
id: 31,
itemType: "call.tool",
name: "CurrentTask",
status: "completed",
payload: {
nested: {
noisy: true
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "event-31",
name: "当前任务",
toolName: "CurrentTask",
callType: null,
status: "已完成",
statusKind: "completed",
summary: "CurrentTask"
})
]);
});
it("shows a summary for unknown tools when the input includes a useful string", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 32,
itemType: "message.user",
content: "继续当前 Issue"
}),
item({
id: 33,
itemType: "call.tool",
name: "CurrentTask",
status: "completed",
payload: {
metadata: {
activityKind: "use_tool",
tool: "CurrentTask",
input: {
task: "所有文件只写入你指定的 run 输出目录"
}
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "event-33",
name: "当前任务",
toolName: "CurrentTask",
callType: null,
status: "已完成",
statusKind: "completed",
summary: "task: 所有文件只写入你指定的 run 输出目录"
})
]);
});
it("merges repeated timeline items for the same tool call id and uses the latest status", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 40,
itemType: "message.user",
content: "列出文件"
}),
item({
id: 41,
itemType: "call.tool",
callId: "call-ls",
name: "exec_command",
status: "running",
payload: {
input: {
cmd: 'rg --files -g "!node_modules"'
},
metadata: { activityKind: "run_command" }
}
}),
item({
id: 42,
itemType: "call.tool",
callId: "call-ls",
name: "exec_command",
status: "completed",
payload: {
metadata: { activityKind: "list_files" }
}
}),
item({
id: 43,
itemType: "call.tool",
name: "exec_command",
status: "running",
payload: {
callId: "call-pwd",
metadata: { activityKind: "run_command" }
}
})
]
});
expect(view.turns[0]?.toolCallCount).toBe(2);
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-ls",
name: "读取文件",
toolName: "Read",
callType: null,
status: "已完成",
statusKind: "completed",
summary: 'rg --files -g "!node_modules"'
}),
expect.objectContaining({
id: "call:call-pwd",
name: "执行命令",
toolName: "Bash",
callType: null,
status: "进行中",
statusKind: "working",
summary: ""
})
]);
});
it("infers legacy canonical tool names from persisted activity kind and ACP kind metadata", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 50,
turnId: "turn-acp",
itemType: "call.completed",
callType: "tool",
callId: "call-list",
name: "Searching the Web",
status: "completed",
payload: {
kind: "fetch",
input: {
action: {
type: "search",
query: "renderer parity harness"
}
},
output: {
links: [
{
title: "Renderer notes",
url: "https://example.com/renderers"
}
]
}
}
}),
item({
id: 51,
turnId: "turn-acp",
itemType: "call.completed",
callType: "tool",
callId: "call-pwd",
name: "Move file",
status: "completed",
payload: {
kind: "move",
input: {
path: "/workspace/app/src/old.ts",
destination: "/workspace/app/src/new.ts"
},
output: {
success: true
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-list",
toolName: "WebSearch",
summary: "renderer parity harness"
}),
expect.objectContaining({
id: "call:call-pwd",
toolName: "Edit",
summary: "/workspace/app/src/old.ts"
})
]);
});
it("does not infer live ACP tool names when canonical payload toolName is missing", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 52,
turnId: "turn-web-fallback",
itemType: "call.completed",
callType: "tool",
callId: "call-web-fallback",
name: "Searching for: renderer parity harness",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call_update",
kind: "fetch"
},
input: {
action: {
type: "search",
query: "renderer parity harness"
}
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-web-fallback",
toolName: null,
summary: "renderer parity harness"
})
]);
});
it("does not display opaque call ids as unknown tool names", async () => {
setAgentGuiI18nTestLocale("zh-CN");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 152,
turnId: "turn-opaque-call-id",
itemType: "call.completed",
callType: "tool",
callId: "call_D4wKrNKJmMD6hNfBOr4EKpwo",
name: "call_D4wKrNKJmMD6hNfBOr4EKpwo",
status: "completed",
payload: {
callType: "tool"
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call_D4wKrNKJmMD6hNfBOr4EKpwo",
name: "使用工具",
toolName: null,
summary: ""
})
]);
});
it("preserves live canonical mcp tool names instead of nulling them out", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 53,
turnId: "turn-mcp-live",
itemType: "call.completed",
callType: "mcp",
callId: "call-mcp-live",
name: "Search Jira",
status: "completed",
payload: {
toolName: "mcp__Atlassian__searchJiraIssuesUsingJql",
metadata: {
server: "Atlassian"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-mcp-live",
toolName: "mcp__Atlassian__searchJiraIssuesUsingJql"
})
]);
});
it("falls back to legacy ACP kind when payload toolName only repeats the display title", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 54,
turnId: "turn-edit-live-title",
itemType: "call.completed",
callType: "tool",
callId: "call-edit-live-title",
name: "Edit /workspace/app/note.txt",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call",
kind: "edit"
},
kind: "edit",
toolName: "Edit /workspace/app/note.txt",
input: {
file_path: "/workspace/app/note.txt"
},
output: {
changes: {
"/workspace/app/note.txt": {
type: "add",
content: "hello\n"
}
}
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-edit-live-title",
toolName: "Edit",
summary: "/workspace/app/note.txt"
})
]);
});
it("falls back to legacy ACP kind when live ACP toolName looks like an opaque function alias", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 55,
turnId: "turn-edit-live-opaque-alias",
itemType: "call.completed",
callType: "tool",
callId: "call-edit-live-opaque-alias",
name: "Call Function Pjspfy0lsdms 1",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call_update",
kind: "edit"
},
kind: "edit",
toolName: "Pjspfy0lsdms",
input: {
file_path: "/workspace/app/note.txt"
},
output: {
changes: {
"/workspace/app/note.txt": {
type: "add",
content: "hello\n"
}
}
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-edit-live-opaque-alias",
name: "编辑文件",
toolName: "Edit",
summary: "/workspace/app/note.txt"
})
]);
});
it("preserves live canonical provider tool names when they look like stable identifiers", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 54,
turnId: "turn-provider-tool",
itemType: "call.completed",
callType: "tool",
callId: "call-provider-tool",
name: "Use provider tool",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call_update",
kind: "other"
},
toolName: "Context7Lookup",
input: {
query: "agent renderer parity"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-provider-tool",
toolName: "Context7Lookup"
})
]);
});
it("preserves live canonical provider tool names even when they contain spaces", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 56,
turnId: "turn-provider-tool-spaced",
itemType: "call.completed",
callType: "tool",
callId: "call-provider-tool-spaced",
name: "Use provider tool",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call_update",
kind: "other"
},
toolName: "Context7 Lookup",
input: {
query: "agent renderer parity"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-provider-tool-spaced",
toolName: "Context7 Lookup"
})
]);
});
it("preserves unknown live canonical tool names even when they match the display title", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 57,
turnId: "turn-provider-tool-equal-title",
itemType: "call.completed",
callType: "tool",
callId: "call-provider-tool-equal-title",
name: "CurrentTask",
status: "completed",
payload: {
acp: {
sessionUpdate: "tool_call_update",
kind: "other"
},
toolName: "CurrentTask"
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-provider-tool-equal-title",
toolName: "CurrentTask"
})
]);
});
it("uses canonical tool names instead of opaque call_function ids", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 58,
turnId: "turn-claude-code-meta-tool-name",
itemType: "call.completed",
callType: "tool",
callId: "call_function_bxbk8qj6jmk1_1",
name: "call_function_bxbk8qj6jmk1_1",
status: "completed",
payload: {
callType: "tool",
toolName: "Bash",
input: {
kind: "execute",
command: "ls -la",
title: "ls -la"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call_function_bxbk8qj6jmk1_1",
name: "执行命令",
toolName: "Bash",
summary: "ls -la"
})
]);
});
it("nests delegated child tools under the parent agent call using parentToolUseId", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 62,
turnId: "turn-agent-nesting",
itemType: "call.completed",
callType: "tool",
callId: "call_parent_agent",
name: "Task",
status: "completed",
payload: {
toolName: "Agent",
input: {
prompt: "Explore the workspace",
subagent_type: "Explore"
},
output: {
text: "Workspace is empty."
}
}
}),
item({
id: 63,
turnId: "turn-agent-nesting",
itemType: "call.completed",
callType: "tool",
callId: "call_child_glob",
name: "call_child_glob",
status: "completed",
payload: {
toolName: "Glob",
input: {
pattern: "**/*"
},
metadata: {
parentToolUseId: "call_parent_agent"
},
output: {
stdout: "index.html\nCLAUDE.md\n"
}
}
}),
item({
id: 64,
turnId: "turn-agent-nesting",
itemType: "call.completed",
callType: "tool",
callId: "call_child_bash",
name: "call_child_bash",
status: "completed",
payload: {
toolName: "Bash",
input: {
command: "ls -la"
},
metadata: {
parentToolUseId: "call_parent_agent"
},
output: {
stdout: "total 0\n"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toHaveLength(1);
expect(view.turns[0]?.toolCalls[0]).toEqual(
expect.objectContaining({
id: "call:call_parent_agent",
toolName: "Agent"
})
);
expect(view.turns[0]?.toolCalls[0]?.payload).toEqual(
expect.objectContaining({
metadata: expect.objectContaining({
steps: expect.arrayContaining([
expect.objectContaining({
toolUseId: "call_child_glob",
toolName: "Glob"
}),
expect.objectContaining({
toolUseId: "call_child_bash",
toolName: "Bash"
})
])
})
})
);
});
it("nests delegated child tools under the parent agent call across turns when parentToolUseId matches", () => {
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 65,
turnId: "turn-parent-agent",
itemType: "call.completed",
callType: "tool",
callId: "call_parent_cross_turn",
name: "Task",
status: "completed",
payload: {
toolName: "Agent",
input: {
prompt: "Inspect cross-turn child calls"
},
output: {
output: "Done."
}
}
}),
item({
id: 66,
turnId: "turn-child-tool",
itemType: "call.completed",
callType: "tool",
callId: "call_child_cross_turn",
name: "call_child_cross_turn",
status: "completed",
payload: {
toolName: "Glob",
input: {
pattern: "**/*"
},
metadata: {
parentToolUseId: "call_parent_cross_turn"
},
output: {
stdout: "index.html\n"
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toHaveLength(1);
expect(view.turns[0]?.toolCalls[0]?.payload).toEqual(
expect.objectContaining({
metadata: expect.objectContaining({
steps: expect.arrayContaining([
expect.objectContaining({
toolUseId: "call_child_cross_turn",
toolName: "Glob"
})
])
})
})
);
expect(view.turns[1]?.toolCalls ?? []).toHaveLength(0);
});
it("uses canonical search_query arrays for folded web-search summaries", async () => {
setAgentGuiI18nTestLocale("en");
const view = buildWorkspaceAgentSessionDetailViewModel({
activity,
session,
timelineItems: [
item({
id: 55,
turnId: "turn-search-query-array",
itemType: "call.completed",
callType: "tool",
callId: "call-search-query-array",
name: "Search the web",
status: "completed",
payload: {
toolName: "WebSearch",
input: {
search_query: ["renderer parity harness", "codex ui"]
}
}
})
]
});
expect(view.turns[0]?.toolCalls).toEqual([
expect.objectContaining({
id: "call:call-search-query-array",
summary: "renderer parity harness"
})
]);
});
});