Files
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

283 lines
8.2 KiB
TypeScript

import type { WorkspaceAgentActivityTimelineItem } from "../../workspaceAgentTimelineTypes";
import type {
WorkspaceAgentSessionDetailMessage,
WorkspaceAgentSessionDetailThinking,
WorkspaceAgentSessionDetailTurn
} from "../../workspaceAgentSessionDetailViewModel";
import type {
AgentMessageContentVM,
AgentMessageRowVM,
AgentThinkingContentVM
} from "../contracts/agentMessageRowVM";
import type { AgentToolCallVM } from "../contracts/agentToolCallVM";
import { isApprovalToolCall } from "./agentToolRendererKind";
import { resolveAgentTranscriptPresentationKind } from "./agentTranscriptPresentation";
import { projectAgentToolCall } from "./agentToolProjection";
import {
isCollaborationTimelineItem,
projectAgentCollaborationVM
} from "./agentCollaborationProjection";
import { projectConversationUserRow } from "./agentConversationUserProjection";
import { parseTuttiPlanIssueLink } from "../tuttiModePlanIssueLinkMarker";
export type AgentTurnSequenceItemVM =
| {
kind: "user-message";
row: AgentMessageRowVM;
}
| {
kind: "assistant-message";
message: AgentMessageContentVM;
}
| {
kind: "thinking";
thinking: AgentThinkingContentVM;
}
| {
kind: "tool-call";
call: AgentToolCallVM;
};
export function buildAgentTurnSequenceItems(
turn: WorkspaceAgentSessionDetailTurn,
workspaceId?: string | null
): AgentTurnSequenceItemVM[] {
const items = turn.rawAgentItems ?? turn.agentItems;
const sequence: AgentTurnSequenceItemVM[] = turn.userMessages.map(
(message) => ({
kind: "user-message",
row: projectConversationUserRow(message, turn.id, workspaceId)
})
);
items.forEach((item) => {
if (item.kind === "message") {
sequence.push({
kind: "assistant-message",
message: projectMessage(item.message, turn.id)
});
return;
}
if (item.kind === "thinking") {
sequence.push({
kind: "thinking",
thinking: projectThinking(item.thinking, turn.id)
});
return;
}
const sourceEntries =
item.groupEntries ??
item.toolCalls.map((call) => ({ kind: "tool-call", call }) as const);
sourceEntries.forEach((entry) => {
if (entry.kind === "thinking") {
sequence.push({
kind: "thinking",
thinking: projectThinking(entry.thinking, turn.id)
});
return;
}
if (isApprovalToolCall(entry.call)) {
return;
}
sequence.push({
kind: "tool-call",
call: projectAgentToolCall(entry.call)
});
});
});
return sortTurnSequenceItems(sequence);
}
type PositionedTurnSequenceEntry = {
item: AgentTurnSequenceItemVM;
sourceIndex: number;
position: {
seq: number;
occurredAtUnixMs: number;
};
};
function sortTurnSequenceItems(
sequence: readonly AgentTurnSequenceItemVM[]
): AgentTurnSequenceItemVM[] {
const positioned: PositionedTurnSequenceEntry[] = sequence.map(
(item, sourceIndex) => ({
item,
sourceIndex,
position: turnSequencePosition(item)
})
);
const sorted: typeof positioned = [];
let comparableSegment: typeof positioned = [];
const flushComparableSegment = () => {
sorted.push(...comparableSegment.sort(compareTurnSequenceEntries));
comparableSegment = [];
};
for (const entry of positioned) {
if (turnSequencePositionRank(entry.position) === 2) {
// An unpositioned row is a hard boundary. Its relation to either side
// is unknown, so moving known rows across it would change adjacency.
flushComparableSegment();
sorted.push(entry);
continue;
}
comparableSegment.push(entry);
}
flushComparableSegment();
return sorted.map((entry) => entry.item);
}
function compareTurnSequenceEntries(
left: PositionedTurnSequenceEntry,
right: PositionedTurnSequenceEntry
): number {
const leftRank = turnSequencePositionRank(left.position);
const rightRank = turnSequencePositionRank(right.position);
if (leftRank !== rightRank) {
return leftRank - rightRank;
}
if (leftRank === 0 && left.position.seq !== right.position.seq) {
return left.position.seq - right.position.seq;
}
if (
leftRank === 1 &&
left.position.occurredAtUnixMs !== right.position.occurredAtUnixMs
) {
return left.position.occurredAtUnixMs - right.position.occurredAtUnixMs;
}
return left.sourceIndex - right.sourceIndex;
}
function turnSequencePositionRank(position: {
seq: number;
occurredAtUnixMs: number;
}): 0 | 1 | 2 {
if (position.seq > 0) {
return 0;
}
if (position.occurredAtUnixMs > 0) {
return 1;
}
return 2;
}
function turnSequencePosition(item: AgentTurnSequenceItemVM): {
seq: number;
occurredAtUnixMs: number;
} {
const sourceItems = turnSequenceSourceItems(item);
const seq = minimumPositive(
sourceItems.map((sourceItem) => sourceItem.seq ?? 0)
);
const occurredAtUnixMs = minimumPositive(
sourceItems.map(
(sourceItem) =>
sourceItem.occurredAtUnixMs ?? sourceItem.createdAtUnixMs ?? 0
)
);
if (occurredAtUnixMs > 0) {
return { seq, occurredAtUnixMs };
}
switch (item.kind) {
case "user-message":
return { seq, occurredAtUnixMs: item.row.occurredAtUnixMs ?? 0 };
case "assistant-message":
return { seq, occurredAtUnixMs: item.message.occurredAtUnixMs ?? 0 };
case "thinking":
return { seq, occurredAtUnixMs: item.thinking.occurredAtUnixMs ?? 0 };
case "tool-call":
return { seq, occurredAtUnixMs: item.call.occurredAtUnixMs ?? 0 };
}
}
function turnSequenceSourceItems(
item: AgentTurnSequenceItemVM
): WorkspaceAgentActivityTimelineItem[] {
switch (item.kind) {
case "user-message":
return item.row.messages.flatMap(
(message) => message.sourceTimelineItems ?? []
);
case "assistant-message":
return item.message.sourceTimelineItems ?? [];
case "thinking":
return item.thinking.sourceTimelineItems ?? [];
case "tool-call":
return item.call.sourceTimelineItems ?? [];
}
}
function minimumPositive(values: readonly number[]): number {
return values.reduce(
(minimum, value) =>
value > 0 && (minimum === 0 || value < minimum) ? value : minimum,
0
);
}
function projectMessage(
message: WorkspaceAgentSessionDetailMessage,
turnId: string
): AgentMessageContentVM {
const projected: AgentMessageContentVM = {
kind: "message-content",
id: message.id,
turnId: message.turnId ?? turnId,
body: message.body,
presentationKind: resolveAgentTranscriptPresentationKind(
message.systemNotice ?? null
),
statusKind: message.statusKind ?? null,
occurredAtUnixMs: message.occurredAtUnixMs ?? null,
visibleError: message.visibleError ?? null,
systemNotice: message.systemNotice ?? null
};
if (message.sourceTimelineItems) {
projected.sourceTimelineItems = message.sourceTimelineItems;
if (
message.sourceTimelineItems.some(
(item) => item.payload?.messageKind === "plan"
)
) {
projected.contentKind = "plan";
}
const collaborationItem = message.sourceTimelineItems.find(
isCollaborationTimelineItem
);
if (collaborationItem) {
const collaboration = projectAgentCollaborationVM(collaborationItem);
if (collaboration) {
projected.contentKind = "collaboration";
projected.collaboration = collaboration;
}
}
}
// Message id carries the stable "plan-issue:<issueID>" identity (it is the
// daemon messageId via the timeline eventId). A malformed body parses to
// null and stays a plain assistant markdown message.
const planIssueLink = parseTuttiPlanIssueLink(message.id, message.body);
if (planIssueLink) {
projected.contentKind = "tutti-plan-issue-link";
projected.planIssueLink = planIssueLink;
}
return projected;
}
function projectThinking(
thinking: WorkspaceAgentSessionDetailThinking,
turnId: string
): AgentThinkingContentVM {
const projected: AgentThinkingContentVM = {
kind: "thinking-content",
id: thinking.id,
turnId: thinking.turnId ?? turnId,
body: thinking.body,
statusKind: thinking.statusKind ?? null,
occurredAtUnixMs: thinking.occurredAtUnixMs ?? null
};
if (thinking.sourceTimelineItems) {
projected.sourceTimelineItems = thinking.sourceTimelineItems;
}
return projected;
}