Files
tutti/packages/agent/gui/agent-message-center/workspaceAgentMessageCenterModelStability.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

333 lines
9.5 KiB
TypeScript

import type {
WorkspaceAgentMessageCenterCounts,
WorkspaceAgentMessageCenterIdentity,
WorkspaceAgentMessageCenterItem,
WorkspaceAgentMessageCenterModel,
WorkspaceAgentMessageCenterTurnOutcome
} from "./workspaceAgentMessageCenterModel";
import type { WorkspaceAgentMessageCenterDigest } from "./workspaceAgentMessageCenterDigest";
type WorkspaceAgentMessageCenterPrompt =
WorkspaceAgentMessageCenterItem["pendingPrompt"];
type WorkspaceAgentMessageCenterAskUserQuestions = Extract<
NonNullable<WorkspaceAgentMessageCenterPrompt>,
{ kind: "ask-user" }
>["questions"];
export function stabilizeWorkspaceAgentMessageCenterModel(
previous: WorkspaceAgentMessageCenterModel | null,
next: WorkspaceAgentMessageCenterModel
): WorkspaceAgentMessageCenterModel {
if (!previous) {
return next;
}
const previousItemsById = new Map(
previous.items.map((item) => [item.id, item])
);
let itemsChanged = previous.items.length !== next.items.length;
const items = next.items.map((nextItem, index) => {
const previousItem = previousItemsById.get(nextItem.id);
const stableItem =
previousItem &&
workspaceAgentMessageCenterItemEqual(previousItem, nextItem)
? previousItem
: nextItem;
if (stableItem !== previous.items[index]) {
itemsChanged = true;
}
return stableItem;
});
const stableItems = itemsChanged ? items : previous.items;
const stableCounts = messageCenterCountsEqual(previous.counts, next.counts)
? previous.counts
: next.counts;
if (
previous.waitingCount === next.waitingCount &&
stableItems === previous.items &&
stableCounts === previous.counts
) {
return previous;
}
return {
...next,
counts: stableCounts,
items: stableItems
};
}
export function workspaceAgentMessageCenterItemEqual(
left: WorkspaceAgentMessageCenterItem,
right: WorkspaceAgentMessageCenterItem
): boolean {
return (
left.id === right.id &&
left.agentSessionId === right.agentSessionId &&
left.agentTargetId === right.agentTargetId &&
left.agentName === right.agentName &&
left.agentAvatarUrl === right.agentAvatarUrl &&
left.provider === right.provider &&
left.userId === right.userId &&
left.title === right.title &&
left.imported === right.imported &&
left.cwd === right.cwd &&
left.status === right.status &&
left.lastAgentMessageSummary === right.lastAgentMessageSummary &&
left.lastAgentMessageAtUnixMs === right.lastAgentMessageAtUnixMs &&
messageCenterInteractionTargetEqual(
left.pendingInteractionTarget,
right.pendingInteractionTarget
) &&
left.needsAttentionKind === right.needsAttentionKind &&
left.needsAttentionSummary === right.needsAttentionSummary &&
left.sortTimeUnixMs === right.sortTimeUnixMs &&
messageCenterIdentityEqual(left.identity, right.identity) &&
messageCenterDigestEqual(left.digest, right.digest) &&
messageCenterPromptEqual(left.pendingPrompt, right.pendingPrompt) &&
messageCenterTurnOutcomeEqual(
left.latestTurnOutcome ?? null,
right.latestTurnOutcome ?? null
)
);
}
function messageCenterInteractionTargetEqual(
left: WorkspaceAgentMessageCenterItem["pendingInteractionTarget"],
right: WorkspaceAgentMessageCenterItem["pendingInteractionTarget"]
): boolean {
return (
left === right ||
(left !== null &&
right !== null &&
left.agentSessionId === right.agentSessionId &&
left.turnId === right.turnId &&
left.requestId === right.requestId)
);
}
function messageCenterCountsEqual(
left: WorkspaceAgentMessageCenterCounts,
right: WorkspaceAgentMessageCenterCounts
): boolean {
return (
left.all === right.all &&
left.working === right.working &&
left.waiting === right.waiting &&
left.completed === right.completed &&
left.failed === right.failed
);
}
function messageCenterIdentityEqual(
left: WorkspaceAgentMessageCenterIdentity | null,
right: WorkspaceAgentMessageCenterIdentity | null
): boolean {
if (left === right) {
return true;
}
if (!left || !right) {
return false;
}
return (
left.userName === right.userName &&
left.userAvatarUrl === right.userAvatarUrl &&
left.agentName === right.agentName &&
left.agentAvatarUrl === right.agentAvatarUrl
);
}
function messageCenterDigestEqual(
left: WorkspaceAgentMessageCenterDigest,
right: WorkspaceAgentMessageCenterDigest
): boolean {
return (
left.primary.kind === right.primary.kind &&
left.primary.summary === right.primary.summary &&
left.primary.occurredAtUnixMs === right.primary.occurredAtUnixMs
);
}
function messageCenterTurnOutcomeEqual(
left: WorkspaceAgentMessageCenterTurnOutcome | null,
right: WorkspaceAgentMessageCenterTurnOutcome | null
): boolean {
if (left === right) {
return true;
}
if (!left || !right) {
return false;
}
return (
left.notificationKey === right.notificationKey &&
left.status === right.status &&
left.turnId === right.turnId
);
}
function messageCenterPromptEqual(
left: WorkspaceAgentMessageCenterPrompt,
right: WorkspaceAgentMessageCenterPrompt
): boolean {
if (left === right) {
return true;
}
if (!left || !right || left.kind !== right.kind) {
return false;
}
switch (left.kind) {
case "approval":
return (
right.kind === "approval" &&
left.id === right.id &&
left.turnId === right.turnId &&
left.requestId === right.requestId &&
left.callId === right.callId &&
left.title === right.title &&
left.toolName === right.toolName &&
left.status === right.status &&
left.occurredAtUnixMs === right.occurredAtUnixMs &&
messageCenterJsonValueEqual(left.input, right.input) &&
messageCenterJsonValueEqual(
left.output ?? null,
right.output ?? null
) &&
messageCenterApprovalOptionsEqual(left.options, right.options)
);
case "ask-user":
return (
right.kind === "ask-user" &&
left.requestId === right.requestId &&
left.title === right.title &&
messageCenterAskUserQuestionsEqual(left.questions, right.questions)
);
case "exit-plan":
return (
right.kind === "exit-plan" &&
left.requestId === right.requestId &&
left.title === right.title &&
left.keepPlanningOptionId === right.keepPlanningOptionId &&
messageCenterApprovalOptionsEqual(left.options, right.options)
);
case "plan-implementation":
return (
right.kind === "plan-implementation" &&
left.requestId === right.requestId &&
left.title === right.title
);
}
}
function messageCenterApprovalOptionsEqual(
left: readonly {
description?: string;
id: string;
kind: string;
label: string;
}[],
right: readonly {
description?: string;
id: string;
kind: string;
label: string;
}[]
): boolean {
if (left.length !== right.length) {
return false;
}
return left.every((option, index) => {
const rightOption = right[index];
return (
rightOption &&
option.id === rightOption.id &&
option.label === rightOption.label &&
option.kind === rightOption.kind &&
option.description === rightOption.description
);
});
}
function messageCenterAskUserQuestionsEqual(
left: WorkspaceAgentMessageCenterAskUserQuestions,
right: WorkspaceAgentMessageCenterAskUserQuestions
): boolean {
if (left.length !== right.length) {
return false;
}
return left.every((question, index) => {
const rightQuestion = right[index];
return (
rightQuestion &&
question.id === rightQuestion.id &&
question.header === rightQuestion.header &&
question.question === rightQuestion.question &&
question.multiSelect === rightQuestion.multiSelect &&
question.allowFreeText === rightQuestion.allowFreeText &&
messageCenterAnswerEqual(
question.answer ?? null,
rightQuestion.answer ?? null
) &&
question.options.length === rightQuestion.options.length &&
question.options.every((option, optionIndex) => {
const rightOption = rightQuestion.options[optionIndex];
return (
rightOption &&
option.label === rightOption.label &&
option.description === rightOption.description
);
})
);
});
}
function messageCenterAnswerEqual(
left: string | string[] | null,
right: string | string[] | null
): boolean {
if (left === right) {
return true;
}
if (!Array.isArray(left) || !Array.isArray(right)) {
return false;
}
return (
left.length === right.length &&
left.every((value, index) => value === right[index])
);
}
function messageCenterJsonValueEqual(left: unknown, right: unknown): boolean {
if (left === right) {
return true;
}
if (
!left ||
!right ||
typeof left !== "object" ||
typeof right !== "object"
) {
return false;
}
if (Array.isArray(left) || Array.isArray(right)) {
if (!Array.isArray(left) || !Array.isArray(right)) {
return false;
}
return (
left.length === right.length &&
left.every((value, index) =>
messageCenterJsonValueEqual(value, right[index])
)
);
}
const leftRecord = left as Record<string, unknown>;
const rightRecord = right as Record<string, unknown>;
const leftKeys = Object.keys(leftRecord);
if (leftKeys.length !== Object.keys(rightRecord).length) {
return false;
}
return leftKeys.every((key) =>
messageCenterJsonValueEqual(leftRecord[key], rightRecord[key])
);
}