Files
tutti/packages/agent/gui/shared/agentConversation/projection/workspaceAgentMessageProjection.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

1694 lines
49 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
normalizeAgentActivitySession,
type AgentActivityMessage,
type AgentActivitySnapshot,
type AgentActivitySession
} from "@tutti-os/agent-activity-core";
import type { WorkspaceAgentActivityCard } from "../../workspaceAgentActivityListViewModel";
import { mergeWorkspaceAgentActivityDurableAndOverlayMessages } from "../../workspaceAgentMessageOverlay";
import {
projectAgentActivitySessionToConversationVM,
projectWorkspaceAgentMessagesToConversationVM,
projectWorkspaceAgentMessagesToTimelineItems
} from "./workspaceAgentMessageProjection";
describe("projectWorkspaceAgentMessagesToConversationVM", () => {
it("derives the selected Session and its messages from one activity snapshot", () => {
const selectedSession = session();
const selectedMessage = message({
messageId: "snapshot-message",
version: 1,
role: "user",
kind: "text",
payload: { text: "Snapshot-owned message" }
});
const activitySnapshot: AgentActivitySnapshot = {
workspaceId: "workspace-1",
sessions: [selectedSession],
presences: [],
sessionMessagesById: {
[selectedSession.agentSessionId]: [selectedMessage]
}
};
const conversation = projectAgentActivitySessionToConversationVM({
activitySnapshot,
agentSessionId: selectedSession.agentSessionId
});
const userRow = conversation?.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(userRow?.kind === "message" ? userRow.messages[0]?.body : null).toBe(
"Snapshot-owned message"
);
expect(
projectAgentActivitySessionToConversationVM({
activitySnapshot,
agentSessionId: "missing-session"
})
).toBeNull();
});
it("preserves message whitespace through the Mobile conversation projection", () => {
const selectedSession = session();
const selectedMessage = message({
messageId: "streaming-whitespace",
version: 1,
role: "assistant",
kind: "text",
payload: { text: "\nHello \n" }
});
const activitySnapshot: AgentActivitySnapshot = {
workspaceId: "workspace-1",
sessions: [selectedSession],
presences: [],
sessionMessagesById: {
[selectedSession.agentSessionId]: [selectedMessage]
}
};
const conversation = projectAgentActivitySessionToConversationVM({
activitySnapshot,
agentSessionId: selectedSession.agentSessionId
});
const assistantRow = conversation?.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
expect(
assistantRow?.kind === "message" ? assistantRow.messages[0]?.body : null
).toBe("\nHello \n");
});
it("filters supplied Turns to the selected Session", () => {
const selectedSession = session();
const selectedMessage = message({
messageId: "selected-message",
payload: { text: "Selected Session message" },
role: "user"
});
const activitySnapshot: AgentActivitySnapshot = {
workspaceId: "workspace-1",
sessions: [selectedSession],
presences: [],
sessionMessagesById: {
[selectedSession.agentSessionId]: [selectedMessage]
}
};
const conversation = projectAgentActivitySessionToConversationVM({
activitySnapshot,
agentSessionId: selectedSession.agentSessionId,
sessionTurns: [
{
agentSessionId: "session-2",
error: { message: "Foreign Turn failure" },
origin: "user_prompt",
outcome: "failed",
phase: "settled",
settledAtUnixMs: 10,
startedAtUnixMs: 1,
turnId: "foreign-turn",
updatedAtUnixMs: 10
}
]
});
expect(conversation?.sourceDetail.sessionTurns).toEqual([]);
expect(
conversation?.rows.flatMap((row) =>
row.kind === "message"
? row.messages.map((message) => message.body)
: []
)
).not.toContain("Foreign Turn failure");
});
it("prefers canonical browser element prompt content over a lossy provider echo", () => {
const prompt =
"[@a](mention://browser-element/browser-element%3A1?path=%2Ftmp%2Fa.txt&tag=a&workspaceId=workspace-1) " +
"[@div](mention://browser-element/browser-element%3A2?path=%2Ftmp%2Fdiv.txt&tag=div&workspaceId=workspace-1) " +
"这里说的什么";
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "user-browser-elements",
version: 1,
role: "user",
kind: "text",
payload: {
text: "@<a> @<div> 这里说的什么",
content: [{ type: "text", text: prompt }]
}
})
]
});
const userRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(userRow?.kind === "message" ? userRow.messages[0]?.body : null).toBe(
prompt
);
});
it("renders the browser element display prompt instead of staged snapshot files", () => {
const displayPrompt =
"[@a](mention://browser-element/browser-element%3A1?path=%2Ftmp%2Fa.txt&tag=a&workspaceId=workspace-1) " +
"[@div](mention://browser-element/browser-element%3A2?path=%2Ftmp%2Fdiv.txt&tag=div&workspaceId=workspace-1) 22222222";
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "user-browser-element-files",
version: 1,
role: "user",
kind: "text",
payload: {
displayPrompt,
text: "pasted-text-1.txt",
content: [
{
type: "file",
kind: "pasted-text",
name: "pasted-text-1.txt",
path: "/tmp/a.txt"
},
{
type: "file",
kind: "pasted-text",
name: "pasted-text-2.txt",
path: "/tmp/div.txt"
}
]
}
})
]
});
const userRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(userRow?.kind === "message" ? userRow.messages[0]?.body : null).toBe(
displayPrompt
);
});
it("sorts message rows by stable message version instead of source-local ids", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "assistant-1",
version: 2,
role: "assistant",
kind: "text",
payload: { text: "Second by id" }
}),
message({
messageId: "user-1",
version: 1,
role: "user",
kind: "text",
payload: { text: "First by id" }
})
]
});
const rows = conversation.rows.filter((row) => row.kind === "message");
expect(rows[0]?.kind === "message" ? rows[0].speaker : null).toBe("user");
expect(rows[0]?.kind === "message" ? rows[0].messages[0]?.body : null).toBe(
"First by id"
);
expect(rows[1]?.kind === "message" ? rows[1].speaker : null).toBe(
"assistant"
);
expect(rows[1]?.kind === "message" ? rows[1].messages[0]?.body : null).toBe(
"Second by id"
);
});
it("keeps status panel order aligned with stream order when snapshot ids differ", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "working" }),
messages: [
message({
messageId: "assistant-intro",
version: 1,
role: "assistant",
kind: "text",
payload: { text: "I will inspect the repo." }
}),
message({
messageId: "reasoning-1",
version: 5,
role: "assistant",
kind: "reasoning",
status: "completed",
payload: { text: "Thinking after the active tool update." }
}),
message({
messageId: "tool-1",
version: 2,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: { callId: "list-1", title: "List" }
}),
message({
messageId: "tool-2",
version: 3,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: { callId: "read-1", title: "Read" }
}),
message({
messageId: "tool-3",
version: 4,
role: "assistant",
kind: "tool_call",
status: "running",
payload: { callId: "fetch-1", title: "读取网页" }
})
]
});
expect(
conversation.rows.map((row) => {
if (row.kind === "message") {
if (row.messages.length > 0) {
return row.messages[0]?.body;
}
return row.thinking[0]?.body;
}
if (row.kind === "tool-group") {
return row.calls.map((call) => call.name).join(",");
}
return row.kind;
})
).toEqual([
"I will inspect the repo.",
"List,Read file,读取网页",
"Thinking after the active tool update."
]);
});
it("orders late completed tool calls by start time before the final assistant answer", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
messages: [
message({
messageId: "user-1",
version: 1,
role: "user",
kind: "text",
payload: { text: "Inspect AI Canvas" },
occurredAtUnixMs: 100,
startedAtUnixMs: 100
}),
message({
messageId: "toolcall:search-1",
version: 4,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: {
callId: "search-1",
title: "Bash",
toolName: "Bash",
input: { command: "rg 陶瓷家具与冲浪 /Users/Sun" }
},
startedAtUnixMs: 110,
occurredAtUnixMs: 400,
completedAtUnixMs: 400
}),
message({
messageId: "assistant-final",
version: 3,
role: "assistant",
kind: "text",
payload: { text: "项目里有图片和视频。" },
occurredAtUnixMs: 300,
startedAtUnixMs: 300
})
]
});
expect(
conversation.rows.map((row) => {
if (row.kind === "message") {
return `${row.speaker}:${row.messages[0]?.body}`;
}
if (row.kind === "tool-group") {
return `tool:${row.calls[0]?.toolName}`;
}
return row.kind;
})
).toEqual([
"user:Inspect AI Canvas",
"tool:Bash",
"assistant:项目里有图片和视频。"
]);
});
it("keeps a streaming assistant message before a later tool call by durable sequence", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
messages: [
message({
messageId: "assistant-intro",
sequence: 2,
version: 8,
role: "assistant",
kind: "text",
status: "completed",
payload: { text: "I will start the agents." },
createdAtUnixMs: 100,
occurredAtUnixMs: 201
}),
message({
messageId: "tool-1",
sequence: 3,
version: 11,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: {
callId: "task-1",
title: "Task",
toolName: "Task"
},
createdAtUnixMs: 202,
startedAtUnixMs: 200,
occurredAtUnixMs: 300,
completedAtUnixMs: 300
})
]
});
expect(
conversation.rows.map((row) => {
if (row.kind === "message") {
return `assistant:${row.messages[0]?.body}`;
}
if (row.kind === "tool-group") {
return `tool:${row.calls[0]?.toolName}`;
}
return row.kind;
})
).toEqual(["assistant:I will start the agents.", "tool:Agent"]);
});
it("keeps an optimistic prompt after durable messages with later timestamps", () => {
const durableAssistant = message({
messageId: "assistant-a",
occurredAtUnixMs: 300,
payload: { text: "Assistant A" },
role: "assistant",
sequence: 2,
turnId: "turn-a",
version: 2
});
const optimisticImagePrompt = message({
messageId: "client-submit:user:submit-b",
occurredAtUnixMs: 200,
payload: {
__agentGuiOptimisticPrompt: true,
clientSubmitId: "submit-b",
content: [
{
type: "image",
data: "aW1hZ2UtYQ==",
mimeType: "image/png",
name: "image-a.png"
}
],
text: "[Image]"
},
role: "user",
startedAtUnixMs: 200,
turnId: "pending:submit-b",
version: 0
});
const mergedMessages = mergeWorkspaceAgentActivityDurableAndOverlayMessages(
{
durableMessages: [durableAssistant],
localMessages: [optimisticImagePrompt]
}
);
expect(mergedMessages.map((item) => item.messageId)).toEqual([
"assistant-a",
"client-submit:user:submit-b"
]);
expect(
projectWorkspaceAgentMessagesToTimelineItems(mergedMessages).map(
(item) => item.eventId
)
).toEqual(["assistant-a", "client-submit:user:submit-b"]);
});
it("projects text, reasoning, errors, and unknown kinds conservatively", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "failed", turnPhase: "failed" }),
messages: [
message({
messageId: "user-1",
role: "user",
kind: "text",
payload: { text: "Inspect this" }
}),
message({
messageId: "thinking-1",
role: "assistant",
kind: "reasoning",
payload: { text: "Need to inspect first." }
}),
message({
messageId: "assistant-1",
role: "assistant",
kind: "text",
payload: { text: "I found the issue." }
}),
message({
messageId: "error-1",
role: "assistant",
kind: "error",
status: "failed",
payload: { title: "Agent failed", text: "Config invalid" }
}),
message({
messageId: "unknown-1",
role: "assistant",
kind: "provider_notice",
payload: { title: "Provider notice", text: "Notice text" }
})
]
});
const messageRows = conversation.rows.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message"
);
expect(messageRows.map((row) => row.speaker)).toEqual([
"user",
"assistant",
"assistant",
"assistant"
]);
expect(
messageRows.flatMap((row) => row.thinking.map((item) => item.body))
).toContain("Need to inspect first.");
expect(
messageRows.flatMap((row) => row.messages.map((item) => item.body))
).toContain("Agent failed\n\nConfig invalid");
expect(
messageRows.flatMap((row) => row.messages.map((item) => item.body))
).toContain("Provider notice\n\nNotice text");
});
it("projects the plan Issue reverse link as a friendly assistant mention, not an error banner", () => {
const mention =
"[@修复登录问题](mention://workspace-issue/tutti-mode-plan-wf-1?topicId=topic-1&workspaceId=workspace-1)";
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "completed",
turnPhase: "completed"
}),
messages: [
message({
messageId: "plan-issue:tutti-mode-plan-wf-1",
version: 1,
turnId: undefined,
role: "assistant",
kind: "session_audit",
status: "completed",
payload: { content: mention },
occurredAtUnixMs: 100
})
]
});
const assistantRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
const item =
assistantRow?.kind === "message" ? assistantRow.messages[0] : null;
expect(item?.contentKind).toBe("tutti-plan-issue-link");
expect(item?.planIssueLink).toEqual({
issueId: "tutti-mode-plan-wf-1",
title: "修复登录问题",
mentionMarkdown: mention
});
expect(item?.body).toBe(mention);
expect(item?.systemNotice ?? null).toBeNull();
expect(item?.visibleError ?? null).toBeNull();
});
it("degrades a malformed plan Issue link body to plain assistant markdown", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "completed",
turnPhase: "completed"
}),
messages: [
message({
messageId: "plan-issue:tutti-mode-plan-wf-2",
version: 1,
turnId: undefined,
role: "assistant",
kind: "session_audit",
status: "completed",
payload: { content: "Issue created without a mention link" },
occurredAtUnixMs: 100
})
]
});
const assistantRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
const item =
assistantRow?.kind === "message" ? assistantRow.messages[0] : null;
expect(item?.body).toBe("Issue created without a mention link");
expect(item?.contentKind).toBeUndefined();
expect(item?.planIssueLink ?? null).toBeNull();
expect(item?.systemNotice ?? null).toBeNull();
expect(item?.visibleError ?? null).toBeNull();
});
it("keeps non plan-issue assistant session audits on the notice fallback", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "completed",
turnPhase: "completed"
}),
messages: [
message({
messageId: "session-audit:other",
version: 1,
turnId: undefined,
role: "assistant",
kind: "session_audit",
status: "completed",
payload: { content: "Some other session annotation" },
occurredAtUnixMs: 100
})
]
});
const assistantRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
const item =
assistantRow?.kind === "message" ? assistantRow.messages[0] : null;
expect(item?.systemNotice).toEqual(
expect.objectContaining({
noticeKind: "session_audit",
severity: "info"
})
);
});
it("preserves a turnless runtime failure audit as a visible error", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "failed", turnPhase: "failed" }),
messages: [
message({
messageId: "visible-error:session-failed-1",
version: 1,
turnId: undefined,
role: "assistant",
kind: "session_audit",
status: "failed",
payload: {
kind: "agent_visible_error",
severity: "error",
phase: "start",
code: "provider_stream_disconnected",
provider: "claude-code",
retryable: true,
content: "Claude Code connection was interrupted.",
text: "Claude Code connection was interrupted.",
detail: "sidecar stream disconnected"
},
occurredAtUnixMs: 100
})
]
});
const assistantRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
const item =
assistantRow?.kind === "message" ? assistantRow.messages[0] : null;
expect(item?.visibleError).toEqual({
code: "provider_stream_disconnected",
phase: "start",
provider: "claude-code",
detail: "sidecar stream disconnected",
retryable: true
});
expect(item?.systemNotice ?? null).toBeNull();
});
it("keeps turnless session audits in chronological conversation order", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "completed",
turnPhase: "completed"
}),
messages: [
message({
messageId: "goal-control:first",
version: 1,
turnId: undefined,
role: "user",
kind: "session_audit",
payload: { text: "/goal first" },
occurredAtUnixMs: 100
}),
message({
messageId: "assistant-first",
version: 2,
turnId: "goal-turn-1",
role: "assistant",
kind: "text",
payload: { text: "First goal result" },
occurredAtUnixMs: 200
}),
message({
messageId: "goal-control:second",
version: 3,
turnId: undefined,
role: "user",
kind: "session_audit",
payload: { text: "/goal second" },
occurredAtUnixMs: 300
}),
message({
messageId: "assistant-second",
version: 4,
turnId: "goal-turn-2",
role: "assistant",
kind: "text",
payload: { text: "Second goal result" },
occurredAtUnixMs: 400
})
]
});
expect(
conversation.rows
.filter((row) => row.kind === "message")
.map((row) => `${row.speaker}:${row.messages[0]?.body}`)
).toEqual([
"user:/goal first",
"assistant:First goal result",
"user:/goal second",
"assistant:Second goal result"
]);
});
it("projects goal control audits as dedicated non-turn rows", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "completed",
turnPhase: "completed"
}),
messages: [
message({
messageId: "user-message",
version: 1,
role: "user",
kind: "text",
payload: { text: "Pause the goal" },
occurredAtUnixMs: 100
}),
message({
messageId: "goal-control:pause",
version: 2,
turnId: undefined,
role: "user",
kind: "session_audit",
payload: {
action: "pause",
goalControl: true,
text: "/goal pause"
},
occurredAtUnixMs: 200
})
]
});
expect(conversation.rows.map((row) => row.kind)).toEqual([
"message",
"goal-control"
]);
expect(
conversation.rows.find((row) => row.kind === "goal-control")
).toMatchObject({
action: "pause",
body: "/goal pause",
turnId: null
});
expect(conversation.sourceDetail.turns).toHaveLength(1);
});
it("keeps the goal control row identity stable when the durable audit replaces its optimistic echo", () => {
const optimistic = message({
messageId: "client-submit:user:submit-goal-1",
version: 0,
turnId: "pending:submit-goal-1",
role: "user",
kind: "session_audit",
payload: {
__agentGuiOptimisticPrompt: true,
action: "set",
clientSubmitId: "submit-goal-1",
goalControl: true,
messageId: "client-submit:user:submit-goal-1",
text: "/goal ship it"
},
occurredAtUnixMs: 100
});
const durable = message({
messageId: "goal-control:operation-1",
version: 1,
turnId: undefined,
role: "user",
kind: "session_audit",
payload: {
action: "set",
clientSubmitId: "submit-goal-1",
goalControl: true,
messageId: "client-submit:user:submit-goal-1",
operationId: "operation-1",
text: "/goal ship it"
},
occurredAtUnixMs: 100
});
const optimisticConversation =
projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "completed" }),
messages: [optimistic]
});
const mergedMessages = mergeWorkspaceAgentActivityDurableAndOverlayMessages(
{
durableMessages: [durable],
localMessages: [optimistic]
}
);
const durableConversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "completed" }),
messages: mergedMessages
});
const optimisticRow = optimisticConversation.rows.find(
(row) => row.kind === "goal-control"
);
const durableRow = durableConversation.rows.find(
(row) => row.kind === "goal-control"
);
expect(mergedMessages).toEqual([durable]);
expect(optimisticRow?.id).toBe(
"goal-control:client-submit:user:submit-goal-1"
);
expect(durableRow?.id).toBe(optimisticRow?.id);
expect(optimisticConversation.sourceDetail.turns).toHaveLength(0);
expect(durableConversation.sourceDetail.turns).toHaveLength(0);
});
it("projects only the latest text snapshot for a stable message id", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "assistant-stream-1",
version: 1,
role: "assistant",
kind: "text",
payload: { text: "I'll " }
}),
message({
messageId: "assistant-stream-1",
id: 1,
version: 2,
role: "assistant",
kind: "text",
payload: { text: "I'll check " }
}),
message({
messageId: "assistant-stream-1",
id: 1,
version: 3,
role: "assistant",
kind: "text",
payload: { text: "I'll check the repo." }
}),
message({
messageId: "assistant-stream-2",
id: 2,
version: 4,
role: "assistant",
kind: "text",
payload: { text: "Distinct message." }
})
]
});
const bodies = conversation.rows
.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message"
)
.flatMap((row) => row.messages.map((item) => item.body));
expect(bodies).toEqual(["I'll check the repo.\nDistinct message."]);
});
it("projects only the latest reasoning snapshot for a stable message id", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "reasoning-stream-1",
id: 1,
version: 1,
role: "assistant",
kind: "reasoning",
payload: { text: "Looking " }
}),
message({
messageId: "reasoning-stream-1",
id: 1,
version: 2,
role: "assistant",
kind: "reasoning",
payload: { text: "Looking through files." }
})
]
});
const thinkingBodies = conversation.rows
.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message"
)
.flatMap((row) => row.thinking.map((item) => item.body));
expect(thinkingBodies).toEqual(["Looking through files."]);
});
it("projects matching tool_call updates into one stable action row", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
messages: [
message({
messageId: "tool-1",
id: 1,
version: 1,
role: "assistant",
kind: "tool_call",
status: "running",
payload: {
callId: "read-1",
title: "Read",
toolName: "Read",
input: { path: "/workspace/demo/README.md" }
}
}),
message({
messageId: "tool-1",
id: 1,
version: 2,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: {
callId: "read-1",
title: "Read",
output: { text: "README contents" }
}
})
]
});
const toolRows = conversation.rows.filter(
(row) => row.kind === "tool-group"
);
expect(toolRows).toHaveLength(1);
const call = toolRows[0]?.calls[0];
expect(call?.id).toBe("call:read-1");
expect(call?.statusKind).toBe("completed");
expect(call?.input).toEqual({ path: "/workspace/demo/README.md" });
expect(call?.output).toEqual({ text: "README contents" });
});
it("projects durable AskUserQuestion tool_call messages as pending interactive prompts", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({
effectiveStatus: "working",
turnPhase: "working"
}),
messages: [
message({
messageId: "toolcall:call-ask",
id: 1,
version: 1,
role: "assistant",
kind: "tool_call",
status: "running",
payload: {
callId: "call-ask",
callType: "interactive",
title: "AskUserQuestion",
toolName: "AskUserQuestion",
status: "streaming",
input: {
requestId: "request-ask",
toolName: "AskUserQuestion",
questions: [
{
id: "favorite-color",
header: "Color",
question: "What's your favorite color?",
options: [
{
label: "Green",
description: "Pick green"
}
]
}
]
},
metadata: {
adapter: "claude-agent-sdk",
callType: "interactive",
interactiveKind: "ask-user",
toolName: "AskUserQuestion"
}
}
})
]
});
expect("pendingInteractivePrompt" in conversation).toBe(false);
});
it("does not use opaque call ids as tool names", () => {
const opaqueCallId = "call_SMAI3q45S9s5TwOqO8R7ZMdU";
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
messages: [
message({
messageId: `toolcall:${opaqueCallId}`,
id: 1,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: {
callId: opaqueCallId,
title: opaqueCallId,
callType: "tool",
toolName: "Bash",
input: { command: "pwd" }
}
})
]
});
const toolRows = conversation.rows.filter(
(row) => row.kind === "tool-group"
);
const call = toolRows[0]?.calls[0];
expect(call?.toolName).toBe("Bash");
expect(call?.name).not.toContain("SMAI3q45");
});
it("normalizes subagent tool variants into agent tool names", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
messages: [
message({
messageId: "toolcall:delegate-1",
id: 1,
role: "assistant",
kind: "tool_call",
status: "completed",
payload: {
callId: "delegate-1",
title: "delegate_task",
callType: "tool",
toolName: "Task",
input: { task: "review this change" }
}
})
]
});
const toolRows = conversation.rows.filter(
(row) => row.kind === "tool-group"
);
expect(toolRows[0]?.calls[0]?.toolName).toBe("Agent");
});
it("drops source-less skills warnings but preserves explicit non-runtime matches", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "user-1",
id: 1,
version: 1,
role: "user",
kind: "text",
payload: { text: "你好" }
}),
message({
messageId: "notice-1",
id: 2,
version: 2,
role: "assistant",
kind: "text",
payload: {
kind: "agent_system_notice",
noticeKind: "warning",
severity: "warning",
title: "Codex warning",
detail:
"Skill descriptions were shortened to fit the skills context budget. Codex can still see every skill, but some descriptions are shorter.",
text: "Codex warning",
content: "Codex warning",
contentMode: "snapshot"
}
}),
message({
messageId: "notice-2",
id: 3,
version: 3,
role: "assistant",
kind: "text",
payload: {
kind: "agent_system_notice",
noticeKind: "warning",
severity: "warning",
title: "Codex warning",
source: "user",
detail:
"Skill descriptions were shortened to fit the skills context budget.",
text: "Codex warning",
content: "Codex warning",
contentMode: "snapshot"
}
}),
message({
messageId: "assistant-1",
id: 4,
version: 4,
role: "assistant",
kind: "text",
payload: {
text: "你好。有什么需要我在这个 workspace 里处理?"
}
})
]
});
const assistantRows = conversation.rows.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message" && row.speaker === "assistant"
);
expect(assistantRows).toHaveLength(2);
expect(assistantRows[0]?.messages[0]?.systemNotice).toEqual({
noticeKind: "warning",
severity: "warning",
title: "Codex warning",
source: "user",
detail:
"Skill descriptions were shortened to fit the skills context budget.",
retryable: null
});
expect(assistantRows[1]?.messages[0]?.systemNotice).toBeNull();
expect(assistantRows[1]?.messages[0]?.body).toBe(
"你好。有什么需要我在这个 workspace 里处理?"
);
});
it("keeps fallback metadata warnings when runtime metadata is absent", () => {
const metadataWarning =
"Model metadata for `minimax/minimax-m2.5` not found. Defaulting to fallback metadata; this can degrade performance and cause issues.";
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "notice-1",
id: 1,
version: 1,
role: "assistant",
kind: "text",
payload: {
kind: "agent_system_notice",
noticeKind: "warning",
severity: "warning",
title: metadataWarning,
detail: metadataWarning,
text: "Codex warning",
content: "Codex warning",
contentMode: "snapshot"
}
})
]
});
const warning = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "assistant"
);
expect(warning).toMatchObject({
kind: "message",
messages: [
{
systemNotice: expect.objectContaining({
noticeKind: "warning",
detail: metadataWarning
})
}
]
});
});
it("renders an image-only optimistic prompt without synthetic text", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "client-submit:user:submit-image-1",
id: 0,
role: "user",
kind: "text",
payload: {
__agentGuiOptimisticPrompt: true,
clientSubmitId: "submit-image-1",
text: "",
content: [
{
type: "image",
mimeType: "image/png",
path: "/prompt-assets/screen.png",
name: "screen.png"
}
]
}
})
]
});
const userRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(userRow?.kind === "message" ? userRow.messages : null).toEqual([
expect.objectContaining({
body: "",
contentKind: "image-grid",
images: [
expect.objectContaining({
id: "client-submit:user:submit-image-1:image:0",
mimeType: "image/png",
name: "screen.png",
path: "/prompt-assets/screen.png"
})
]
})
]);
const durableConversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "client-submit:user:submit-image-1",
id: 1,
version: 1,
role: "user",
kind: "text",
payload: {
clientSubmitId: "submit-image-1",
text: "[Image]",
content: [
{
type: "image",
mimeType: "image/png",
attachmentId: "attachment-1",
name: "screen.png"
}
]
}
})
]
});
const durableUserRow = durableConversation.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(
durableUserRow?.kind === "message"
? durableUserRow.messages[0]?.images?.[0]
: null
).toMatchObject({
id: "client-submit:user:submit-image-1:image:0",
attachmentId: "attachment-1"
});
});
it("projects legacy compact text as a semantic notice and removes only its matching echo", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "compact-1",
version: 1,
status: "failed",
payload: {
source: "compact",
text: "Compacting failed: Not enough messages to compact."
}
}),
message({
messageId: "assistant-echo",
version: 2,
payload: { text: "Not enough messages to compact." }
}),
message({
messageId: "assistant-guidance",
version: 3,
payload: { text: "Add more messages and try again." }
})
]
});
const assistantRows = conversation.rows.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message" && row.speaker === "assistant"
);
expect(assistantRows).toHaveLength(2);
expect(assistantRows[0]?.messages[0]?.systemNotice).toEqual({
noticeKind: "system_notice",
severity: null,
source: "compact",
command: "compact",
commandStatus: "failed",
title: "Context compaction interrupted.",
detail: "Not enough messages to compact.",
retryable: null
});
expect(assistantRows[0]?.messages[0]?.presentationKind).toBe(
"turn-boundary"
);
expect(assistantRows[1]?.messages[0]?.body).toBe(
"Add more messages and try again."
);
});
it("preserves matching assistant guidance after canceled compaction", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "compact-canceled",
version: 1,
status: "canceled",
semantics: {
noticeCommand: "compact",
noticeCommandStatus: "canceled"
},
payload: {
kind: "agent_system_notice",
noticeKind: "system_notice",
title: "Context compaction interrupted.",
detail: "Try again after adding more context.",
text: "Context compaction interrupted."
}
}),
message({
messageId: "assistant-guidance",
version: 2,
payload: { text: "Try again after adding more context." }
})
]
});
const assistantRows = conversation.rows.filter(
(
row
): row is Extract<
(typeof conversation.rows)[number],
{ kind: "message" }
> => row.kind === "message" && row.speaker === "assistant"
);
expect(assistantRows).toHaveLength(2);
expect(assistantRows[0]?.messages[0]?.systemNotice).toEqual(
expect.objectContaining({
command: "compact",
commandStatus: "canceled"
})
);
expect(assistantRows[1]?.messages[0]?.body).toBe(
"Try again after adding more context."
);
});
it("normalizes persisted Codex compaction notices before presentation", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "compaction:turn-compact",
turnId: "turn-compact",
version: 1,
status: "completed",
payload: {
kind: "agent_system_notice",
noticeKind: "system_notice",
source: "runtime",
title: "Context compacted.",
text: "Context compacted."
}
})
]
});
const compactMessage = conversation.rows.flatMap((row) =>
row.kind === "message" ? row.messages : []
)[0];
expect(compactMessage?.systemNotice).toEqual(
expect.objectContaining({
command: "compact",
commandStatus: "completed"
})
);
expect(compactMessage?.presentationKind).toBe("turn-boundary");
});
it("projects context overflow as a typed handoff-required notice", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "compact-handoff-required",
turnId: "turn-compact",
status: "failed",
semantics: {
noticeCommand: "compact",
noticeCommandStatus: "failed"
},
payload: {
kind: "agent_system_notice",
noticeKind: "context_handoff_required",
severity: "error",
title: "Context compaction interrupted.",
detail: "Maximum context length exceeded."
}
})
]
});
const projected = conversation.rows.flatMap((row) =>
row.kind === "message" ? row.messages : []
)[0];
expect(projected?.systemNotice).toEqual(
expect.objectContaining({
semanticKind: "context-handoff-required",
severity: "error"
})
);
});
it("prefers canonical message semantics over duplicated notice payload fields", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session({ effectiveStatus: "working" }),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "compact-running",
version: 1,
status: "working",
semantics: {
noticeCommand: "compact",
noticeCommandStatus: "running"
},
payload: {
kind: "agent_system_notice",
noticeKind: "system_notice",
noticeCommand: "review",
noticeCommandStatus: "completed",
title: "Compacting context.",
text: "Compacting context."
}
})
]
});
const compactMessage = conversation.rows.flatMap((row) =>
row.kind === "message" ? row.messages : []
)[0];
expect(compactMessage?.systemNotice).toEqual(
expect.objectContaining({
command: "compact",
commandStatus: "running"
})
);
expect(compactMessage?.presentationKind).toBe("specific-progress");
expect(conversation.rows.some((row) => row.kind === "processing")).toBe(
false
);
});
it("renders displayPrompt instead of rich content text while preserving prompt images", () => {
const conversation = projectWorkspaceAgentMessagesToConversationVM({
activity: activity(),
session: session(),
workspaceRoot: "/workspace/demo",
messages: [
message({
messageId: "user-1",
id: 1,
role: "user",
kind: "text",
payload: {
displayPrompt: "Run Automation",
text: "Run Automation",
content: [
{ type: "text", text: "long automation prompt" },
{
type: "image",
mimeType: "image/png",
attachmentId: "attachment-1",
name: "screen.png"
}
]
}
})
]
});
const userRow = conversation.rows.find(
(row) => row.kind === "message" && row.speaker === "user"
);
expect(
userRow?.kind === "message" ? userRow.messages[0]?.contentKind : null
).toBe("image-grid");
expect(
userRow?.kind === "message" ? userRow.messages[0]?.images?.[0] : null
).toMatchObject({
attachmentId: "attachment-1",
mimeType: "image/png"
});
expect(userRow?.kind === "message" ? userRow.messages[1]?.body : null).toBe(
"Run Automation"
);
});
});
function activity(
overrides: Partial<WorkspaceAgentActivityCard> = {}
): WorkspaceAgentActivityCard {
return {
id: "activity-1",
sessionId: "session-1",
agentName: "Codex",
agentProvider: "codex",
status: "working",
title: "Codex",
latestActivitySummary: "Working",
sortTimeUnixMs: 10,
changedFiles: [],
userId: "user-1",
userName: "Taylor",
userAvatarUrl: "",
...overrides
};
}
function session(
overrides: Partial<AgentActivitySession> & {
effectiveStatus?: string;
turnPhase?: string;
} = {}
): AgentActivitySession {
const { effectiveStatus, turnPhase, ...canonical } = overrides;
const phase = turnPhase ?? effectiveStatus;
const hasActiveTurn =
phase === "submitted" ||
phase === "running" ||
phase === "working" ||
phase === "waiting" ||
phase === "settling";
return normalizeAgentActivitySession({
...{
activeTurnId: null,
latestTurnInteractions: [],
pendingInteractions: []
},
workspaceId: "workspace-1",
agentSessionId: "session-1",
userId: "user-1",
provider: "codex",
providerSessionId: "provider-session-1",
cwd: "/workspace/demo",
title: "Codex",
createdAtUnixMs: 1,
updatedAtUnixMs: 10,
...(hasActiveTurn
? {
activeTurn: {
agentSessionId: "session-1",
outcome: null,
origin: "user_prompt",
phase: phase === "working" ? "running" : phase,
settledAtUnixMs: null,
startedAtUnixMs: 1,
turnId: "turn-1",
updatedAtUnixMs: 10
},
activeTurnId: "turn-1"
}
: {}),
...canonical
});
}
it("hides tutti-plan issue dispatch delegate cards but keeps other collaborations", () => {
const items = projectWorkspaceAgentMessagesToTimelineItems([
message({
messageId: "collab:cr-plan",
version: 1,
kind: "collaboration",
turnId: "",
payload: {
mode: "delegate",
triggerReason: "issue:tutti-mode-plan-wf-1/task:t1/run:r1"
}
}),
message({
messageId: "collab:cr-manual",
version: 2,
kind: "collaboration",
turnId: "",
payload: { mode: "consult", triggerReason: "user" }
})
]);
const collaborationItems = items.filter(
(item) => item.itemType === "collaboration"
);
expect(collaborationItems).toHaveLength(1);
expect(collaborationItems[0]?.eventId).toBe("collab:cr-manual");
});
function message(
overrides: Partial<AgentActivityMessage> & { id?: number }
): AgentActivityMessage {
const { id: _legacyStorageId, ...canonical } = overrides;
const version = overrides.version ?? 1;
return {
agentSessionId: "session-1",
messageId: "message-1",
version,
turnId: "turn-1",
role: "assistant",
kind: "text",
payload: {},
occurredAtUnixMs: 1,
...canonical
};
}