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

743 lines
22 KiB
TypeScript

import type { WorkspaceAgentActivityTimelineItem } from "./workspaceAgentTimelineTypes";
import { appendWorkspaceAgentGoalControl } from "./workspaceAgentGoalControlProjection";
import { isWorkspaceAgentToolCallItem } from "./workspaceAgentToolCallDisplay";
import type {
BuildWorkspaceAgentSessionDetailInput,
WorkspaceAgentSessionDetailAgentItem,
WorkspaceAgentSessionDetailGoalControl,
WorkspaceAgentSessionDetailToolCall,
WorkspaceAgentSessionDetailToolGroupEntry,
WorkspaceAgentSessionDetailTurn,
WorkspaceAgentSessionDetailViewModel
} from "./workspaceAgentSessionDetailViewModel";
import {
isPlaceholderThinkingBody,
isRecentDuplicateUserMessage,
messageBody,
messageRole,
messageStatusKind,
normalizedMessageBody,
stripReviewProcessSummaryTitle,
thinkingStatusKind,
userMessageProjectionKey
} from "./workspaceAgentTimelineMessageHelpers";
import { isCollaborationTimelineItem } from "./agentConversation/projection/agentCollaborationProjection";
import {
compareToolCallsAscending,
delegatedToolStepFromCall,
firstPresentString,
isTaskLikeToolCall,
mergeSourceTimelineItems,
normalizedPayload,
parentToolUseIdFromCall,
shouldShowProcessingIndicator,
stringRecordValue,
summarizeToolCallGroup,
systemNoticeFromPayload,
toolCallView,
visibleErrorFromPayload,
withSourceTimelineItems
} from "./workspaceAgentTimelineProjectionHelpers";
import { enrichProjectedTurnsWithCanonicalErrors } from "./workspaceAgentTurnErrorProjection";
import {
normalizeToolName,
shouldSuppressToolCall,
suppressedUnavailableAskUserQuestionCallIds
} from "./workspaceAgentTimelineSuppression";
export function buildCanonicalWorkspaceAgentDetailView({
activity,
session,
sessionTurns = [],
timelineItems,
workspaceRoot = null
}: BuildWorkspaceAgentSessionDetailInput): WorkspaceAgentSessionDetailViewModel {
const turns = new Map<string, WorkspaceAgentSessionDetailTurn>();
const goalControls: WorkspaceAgentSessionDetailGoalControl[] = [];
const recentUserMessages = new Map<
string,
WorkspaceAgentActivityTimelineItem
>();
const seenThinkingMessages = new Set<string>();
let activeSequenceTurnId: string | null = null;
const sortedTimelineItems = [...timelineItems].sort(
compareTimelineItemsAscending
);
const suppressedToolCallIds =
suppressedUnavailableAskUserQuestionCallIds(sortedTimelineItems);
for (const item of sortedTimelineItems) {
const role = messageRole(item);
const body = messageBody(item);
const explicitTurnId = item.turnId?.trim();
if (
appendWorkspaceAgentGoalControl(goalControls, item, itemId(item), body)
) {
continue;
}
if (role === "user") {
const turnId = explicitTurnId || `seq:${item.seq || item.id}`;
const projectionKey = userMessageProjectionKey(item, body);
if (!projectionKey) {
continue;
}
if (
isRecentDuplicateUserMessage(
recentUserMessages.get(projectionKey),
item
)
) {
continue;
}
activeSequenceTurnId = turnId;
const turn = getTurn(turns, turnId);
const message = withSourceTimelineItems(
{
id: itemId(item),
body,
turnId,
occurredAtUnixMs:
item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null
},
[item]
);
turn.userMessages.push(message);
turn.userMessage ??= message;
recentUserMessages.set(projectionKey, item);
continue;
}
const turnId =
explicitTurnId || activeSequenceTurnId || `seq:${item.seq || item.id}`;
const turn = getTurn(turns, turnId);
// Collaboration runs render as a dedicated card. Unlike ordinary agent
// messages they must stay visible while their body (resultText) is still
// empty — a running consult has no output yet — so this branch does not
// require a non-empty body.
if (isCollaborationTimelineItem(item)) {
const status = firstPresentString(
item.status,
stringRecordValue(item.payload, "status")
);
const statusKind = messageStatusKind(status);
const message = withSourceTimelineItems(
{
id: itemId(item),
body,
...(status ? { status } : {}),
...(statusKind ? { statusKind } : {}),
turnId,
occurredAtUnixMs:
item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null
},
[item]
);
turn.agentMessages.push(message);
turn.agentItems.push({ kind: "message", message });
continue;
}
if (isWorkspaceAgentToolCallItem(item)) {
if (shouldSuppressToolCall(item, suppressedToolCallIds)) {
continue;
}
upsertToolCall(turn, item);
continue;
}
if (role === "thinking" && body) {
const payload = normalizedPayload(item.payload);
if (payload?.messageKind === "review-process") {
const status = firstPresentString(
item.status,
stringRecordValue(payload, "status")
);
const statusKind = messageStatusKind(status);
const message = withSourceTimelineItems(
{
id: itemId(item),
body: stripReviewProcessSummaryTitle(body),
...(status ? { status } : {}),
...(statusKind ? { statusKind } : {}),
turnId,
occurredAtUnixMs:
item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null
},
[item]
);
turn.agentMessages.push(message);
turn.agentItems.push({ kind: "message", message });
continue;
}
if (isPlaceholderThinkingBody(body)) {
continue;
}
const duplicateKey = `${turnId}\u0000${normalizedMessageBody(body)}`;
if (seenThinkingMessages.has(duplicateKey)) {
continue;
}
seenThinkingMessages.add(duplicateKey);
const statusKind = thinkingStatusKind(item);
const thinking = withSourceTimelineItems(
{
id: itemId(item),
body,
...(statusKind ? { statusKind } : {}),
turnId,
occurredAtUnixMs:
item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null
},
[item]
);
turn.agentItems.push({ kind: "thinking", thinking });
continue;
}
if (role === "agent" && body) {
const payload = normalizedPayload(item.payload);
const visibleError = visibleErrorFromPayload(payload);
const systemNotice = systemNoticeFromPayload(payload, item);
const status = firstPresentString(
item.status,
stringRecordValue(payload, "status")
);
const statusKind = messageStatusKind(status);
const message = withSourceTimelineItems(
{
id: itemId(item),
body,
...(status ? { status } : {}),
...(statusKind ? { statusKind } : {}),
turnId,
occurredAtUnixMs:
item.occurredAtUnixMs ?? item.createdAtUnixMs ?? null,
...(visibleError ? { visibleError } : {}),
...(systemNotice ? { systemNotice } : {})
},
[item]
);
turn.agentMessages.push(message);
turn.agentItems.push({ kind: "message", message });
}
}
enrichProjectedTurnsWithCanonicalErrors({
turns,
latestTurnId: session.latestTurn?.turnId ?? null,
sessionTurns:
sessionTurns.length > 0
? sessionTurns
: session.latestTurn
? [session.latestTurn]
: [],
provider: session.provider,
agentSessionId: session.agentSessionId
});
const visibleTurns = [...turns.values()].filter(
(turn) => turn.userMessages.length > 0 || turn.agentItems.length > 0
);
nestDelegatedToolCallsAcrossTurns(visibleTurns);
visibleTurns.forEach(mergeBackgroundTerminalContinuations);
visibleTurns.forEach((turn) => {
turn.rawAgentItems = [...turn.agentItems];
turn.agentItems = regroupAgentItems(turn.agentItems);
});
return {
activity,
session,
sessionTurns,
cwd: session.cwd.trim(),
workspaceRoot: workspaceRoot?.trim() || null,
goalControls,
turns: visibleTurns,
showProcessingIndicator: shouldShowProcessingIndicator(
session,
visibleTurns
)
};
}
function getTurn(
turns: Map<string, WorkspaceAgentSessionDetailTurn>,
id: string
): WorkspaceAgentSessionDetailTurn {
const existing = turns.get(id);
if (existing) {
return existing;
}
const turn: WorkspaceAgentSessionDetailTurn = {
id,
userMessage: null,
userMessages: [],
agentMessages: [],
toolCalls: [],
toolCallCount: 0,
hasFailedToolCall: false,
agentItems: []
};
turns.set(id, turn);
return turn;
}
function compareTimelineItemsAscending(
left: WorkspaceAgentActivityTimelineItem,
right: WorkspaceAgentActivityTimelineItem
): number {
const leftSeq = left.seq ?? 0;
const rightSeq = right.seq ?? 0;
if (leftSeq > 0 && rightSeq > 0 && leftSeq !== rightSeq) {
return leftSeq - rightSeq;
}
return (
(left.occurredAtUnixMs ?? left.createdAtUnixMs ?? 0) -
(right.occurredAtUnixMs ?? right.createdAtUnixMs ?? 0) ||
leftSeq - rightSeq ||
left.id - right.id
);
}
function itemId(item: WorkspaceAgentActivityTimelineItem): string {
const eventId = item.eventId?.trim();
if (eventId) {
return eventId;
}
const seq = item.seq ?? 0;
return seq > 0 ? `seq:${seq}` : `id:${item.id}`;
}
function upsertToolCall(
turn: WorkspaceAgentSessionDetailTurn,
item: WorkspaceAgentActivityTimelineItem
): void {
const call = toolCallView(item);
const existingIndex = turn.toolCalls.findIndex(
(existing) => existing.id === call.id
);
if (existingIndex >= 0) {
const existing = turn.toolCalls[existingIndex];
if (!existing) {
return;
}
turn.toolCalls[existingIndex] = mergeToolCallDetail(existing, call);
} else {
turn.toolCalls.push(call);
}
turn.toolCallCount = turn.toolCalls.length;
turn.hasFailedToolCall = turn.toolCalls.some(
(existing) => existing.statusKind === "failed"
);
upsertToolCallAgentItem(turn, call, itemId(item));
}
function upsertToolCallAgentItem(
turn: WorkspaceAgentSessionDetailTurn,
call: WorkspaceAgentSessionDetailToolCall,
sourceId: string
): void {
for (const entry of turn.agentItems) {
if (entry.kind !== "tool-calls") {
continue;
}
const existingIndex = entry.toolCalls.findIndex(
(existing) => existing.id === call.id
);
if (existingIndex >= 0) {
const existing = entry.toolCalls[existingIndex];
if (!existing) {
continue;
}
entry.toolCalls[existingIndex] = mergeToolCallDetail(existing, call);
refreshToolCallAgentItem(entry);
return;
}
}
const entry: WorkspaceAgentSessionDetailAgentItem = {
kind: "tool-calls",
id: `tools:${sourceId}`,
toolCalls: [call],
toolCallCount: 1,
hasFailedToolCall: call.statusKind === "failed"
};
turn.agentItems.push(entry);
}
function refreshToolCallAgentItem(
entry: Extract<WorkspaceAgentSessionDetailAgentItem, { kind: "tool-calls" }>
): void {
entry.toolCallCount = entry.toolCalls.length;
entry.hasFailedToolCall = entry.toolCalls.some(
(existing) => existing.statusKind === "failed"
);
const summary = summarizeToolCallGroup(entry.toolCalls);
if (summary) {
entry.summary = summary;
} else {
delete entry.summary;
}
}
function regroupAgentItems(
items: readonly WorkspaceAgentSessionDetailAgentItem[]
): WorkspaceAgentSessionDetailAgentItem[] {
const regrouped: WorkspaceAgentSessionDetailAgentItem[] = [];
let pending: Array<
Extract<WorkspaceAgentSessionDetailAgentItem, { kind: "tool-calls" }>
> = [];
const flushPending = () => {
if (pending.length === 0) {
return;
}
const groupedCalls = pending.flatMap((item) => item.toolCalls);
if (groupedCalls.length >= 2) {
regrouped.push({
kind: "tool-calls",
id: pending.map((item) => item.id).join("+"),
toolCalls: groupedCalls,
toolCallCount: groupedCalls.length,
hasFailedToolCall: groupedCalls.some(
(call) => call.statusKind === "failed"
),
summary: summarizeToolCallGroup(groupedCalls),
groupEntries:
pending.flatMap<WorkspaceAgentSessionDetailToolGroupEntry>((item) =>
item.toolCalls.map((call) => ({ kind: "tool-call", call }))
)
});
} else {
regrouped.push(...pending);
}
pending = [];
};
for (const item of items) {
if (item.kind === "tool-calls" && isGroupableToolCallItem(item)) {
pending.push(item);
continue;
}
flushPending();
regrouped.push(item);
}
flushPending();
return regrouped;
}
function isGroupableToolCallItem(
item: Extract<WorkspaceAgentSessionDetailAgentItem, { kind: "tool-calls" }>
): boolean {
return (
item.toolCalls.length === 1 &&
item.toolCalls.every((call) => isGroupableToolCall(call))
);
}
function mergeToolCallDetail(
previous: WorkspaceAgentSessionDetailToolCall,
next: WorkspaceAgentSessionDetailToolCall
): WorkspaceAgentSessionDetailToolCall {
return withSourceTimelineItems(
{
...next,
name: next.name || previous.name,
toolName: next.toolName || previous.toolName,
callType: next.callType || previous.callType,
summary: next.summary || previous.summary,
payload: next.payload ?? previous.payload
},
mergeSourceTimelineItems(
previous.sourceTimelineItems,
next.sourceTimelineItems
)
);
}
function mergeBackgroundTerminalContinuations(
turn: WorkspaceAgentSessionDetailTurn
): void {
if (turn.agentItems.length === 0 || turn.toolCalls.length === 0) {
return;
}
const removedCallIDs = new Set<string>();
const singleCallItems = turn.agentItems.filter(
(
item
): item is Extract<
WorkspaceAgentSessionDetailAgentItem,
{ kind: "tool-calls" }
> => item.kind === "tool-calls" && item.toolCalls.length === 1
);
const turnToolIndexByID = new Map(
turn.toolCalls.map((call, index) => [call.id, index])
);
for (let index = 0; index < singleCallItems.length - 1; index += 1) {
const item = singleCallItems[index];
const primaryCall = item?.toolCalls[0];
if (!item || !primaryCall) {
continue;
}
const terminalSessionID = backgroundTerminalSessionID(primaryCall);
if (!terminalSessionID) {
continue;
}
const continuationCall = singleCallItems[index + 1]?.toolCalls[0];
if (
!continuationCall ||
!isBackgroundTerminalContinuation(continuationCall, terminalSessionID)
) {
continue;
}
const mergedCall = mergeBackgroundTerminalCall(
primaryCall,
continuationCall
);
item.toolCalls[0] = mergedCall;
const turnToolIndex = turnToolIndexByID.get(primaryCall.id);
if (turnToolIndex !== undefined) {
turn.toolCalls[turnToolIndex] = mergedCall;
}
refreshToolCallAgentItem(item);
removedCallIDs.add(continuationCall.id);
index += 1;
}
if (removedCallIDs.size === 0) {
return;
}
turn.agentItems = turn.agentItems.filter(
(item) =>
item.kind !== "tool-calls" ||
item.toolCalls.every((call) => !removedCallIDs.has(call.id))
);
turn.toolCalls = turn.toolCalls.filter(
(call) => !removedCallIDs.has(call.id)
);
turn.toolCallCount = turn.toolCalls.length;
turn.hasFailedToolCall = turn.toolCalls.some(
(existing) => existing.statusKind === "failed"
);
}
function mergeBackgroundTerminalCall(
primary: WorkspaceAgentSessionDetailToolCall,
continuation: WorkspaceAgentSessionDetailToolCall
): WorkspaceAgentSessionDetailToolCall {
const continuationOutput = normalizedPayload(
continuation.payload
?.output as WorkspaceAgentActivityTimelineItem["payload"]
);
const mergedPayload = primary.payload ? { ...primary.payload } : {};
if (continuationOutput) {
mergedPayload.output = continuationOutput;
}
return {
...primary,
status: continuation.status || primary.status,
statusKind: continuation.statusKind || primary.statusKind,
payload:
Object.keys(mergedPayload).length > 0 ? mergedPayload : primary.payload,
occurredAtUnixMs: continuation.occurredAtUnixMs ?? primary.occurredAtUnixMs
};
}
function nestDelegatedToolCallsAcrossTurns(
turns: readonly WorkspaceAgentSessionDetailTurn[]
): void {
if (turns.length === 0) {
return;
}
const parentCalls = new Map<string, WorkspaceAgentSessionDetailToolCall>();
const callTurnByID = new Map<string, WorkspaceAgentSessionDetailTurn>();
for (const turn of turns) {
for (const call of turn.toolCalls) {
parentCalls.set(call.id, call);
parentCalls.set(call.id.replace(/^call:/, ""), call);
callTurnByID.set(call.id, turn);
callTurnByID.set(call.id.replace(/^call:/, ""), turn);
}
}
const childCallsByParent = new Map<
string,
WorkspaceAgentSessionDetailToolCall[]
>();
for (const turn of turns) {
for (const call of turn.toolCalls) {
const parentToolUseId = parentToolUseIdFromCall(call);
if (!parentToolUseId) {
continue;
}
const parentCall = parentCalls.get(parentToolUseId);
if (!parentCall || !isTaskLikeToolCall(parentCall)) {
continue;
}
const children = childCallsByParent.get(parentToolUseId) ?? [];
children.push(call);
childCallsByParent.set(parentToolUseId, children);
}
}
if (childCallsByParent.size === 0) {
return;
}
const removeIDsByTurn = new Map<
WorkspaceAgentSessionDetailTurn,
Set<string>
>();
for (const [parentID, childCalls] of childCallsByParent.entries()) {
const parentCall = parentCalls.get(parentID);
if (!parentCall) {
continue;
}
appendDelegatedToolSteps(parentCall, childCalls);
for (const childCall of childCalls) {
const childTurn = callTurnByID.get(childCall.id);
if (!childTurn) {
continue;
}
const removeIDs = removeIDsByTurn.get(childTurn) ?? new Set<string>();
removeIDs.add(childCall.id);
removeIDsByTurn.set(childTurn, removeIDs);
}
}
for (const [turn, removeIDs] of removeIDsByTurn.entries()) {
pruneTurnToolCalls(turn, removeIDs);
}
}
function backgroundTerminalSessionID(
call: WorkspaceAgentSessionDetailToolCall
): string | null {
const outputText = stringRecordValue(call.payload?.output, "output");
if (!outputText) {
return null;
}
const match = outputText.match(/Process running with session ID (\d+)/i);
return match?.[1] ?? null;
}
function isBackgroundTerminalContinuation(
call: WorkspaceAgentSessionDetailToolCall,
sessionID: string
): boolean {
const input = normalizedPayload(
call.payload?.input as WorkspaceAgentActivityTimelineItem["payload"]
);
const inputSessionID = firstPresentString(
stringRecordValue(input, "session_id"),
stringRecordValue(input, "sessionId")
);
if (inputSessionID !== sessionID) {
return false;
}
const chars = firstPresentString(stringRecordValue(input, "chars")) ?? "";
if (chars !== "") {
return false;
}
return stringRecordValue(call.payload?.output, "output") !== null;
}
function isGroupableToolCall(
call: WorkspaceAgentSessionDetailToolCall
): boolean {
if (
call.callType === "approval" ||
call.callType === "interactive" ||
call.callType === "subagent"
) {
return false;
}
switch (normalizeToolName(call.toolName)) {
case "askuserquestion":
case "enterplanmode":
case "exitplanmode":
case "task":
case "subagent":
case "delegatetask":
return false;
default:
return true;
}
}
function pruneTurnToolCalls(
turn: WorkspaceAgentSessionDetailTurn,
removeIDs: ReadonlySet<string>
): void {
if (removeIDs.size === 0) {
return;
}
turn.toolCalls = turn.toolCalls.filter((call) => !removeIDs.has(call.id));
turn.toolCallCount = turn.toolCalls.length;
turn.hasFailedToolCall = turn.toolCalls.some(
(existing) => existing.statusKind === "failed"
);
turn.agentItems =
turn.agentItems.flatMap<WorkspaceAgentSessionDetailAgentItem>((item) => {
if (item.kind !== "tool-calls") {
return [item];
}
const toolCalls = item.toolCalls.filter(
(call) => !removeIDs.has(call.id)
);
if (toolCalls.length === 0) {
return [];
}
const nextItem = {
...item,
toolCalls
};
refreshToolCallAgentItem(nextItem);
return [nextItem];
});
}
function appendDelegatedToolSteps(
parentCall: WorkspaceAgentSessionDetailToolCall,
childCalls: readonly WorkspaceAgentSessionDetailToolCall[]
): void {
const nextPayload = parentCall.payload ? { ...parentCall.payload } : {};
const nextMetadata =
normalizedPayload(
nextPayload.metadata as WorkspaceAgentActivityTimelineItem["payload"]
) ?? {};
const existingSteps = Array.isArray(nextMetadata.steps)
? [...nextMetadata.steps]
: [];
const existingStepIDs = new Set(
existingSteps
.map((step) =>
normalizedPayload(step as WorkspaceAgentActivityTimelineItem["payload"])
)
.map(
(step) =>
stringRecordValue(step, "toolUseId") ?? stringRecordValue(step, "id")
)
.filter((value): value is string => Boolean(value))
);
for (const childCall of [...childCalls].sort(compareToolCallsAscending)) {
const step = delegatedToolStepFromCall(childCall);
const stepID =
stringRecordValue(step, "toolUseId") ?? stringRecordValue(step, "id");
if (stepID && existingStepIDs.has(stepID)) {
continue;
}
if (stepID) {
existingStepIDs.add(stepID);
}
existingSteps.push(step);
}
nextMetadata.steps = existingSteps;
nextPayload.metadata = nextMetadata;
parentCall.payload = nextPayload;
}