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

277 lines
8.7 KiB
TypeScript

import type {
AgentActivityMessage,
AgentActivityPresence,
AgentActivitySession
} from "@tutti-os/agent-activity-core";
import { selectRootAgentActivitySessions } from "@tutti-os/agent-activity-core";
import { resolveWorkspaceAgentSessionSortTimeUnixMs } from "./workspaceAgentSessionSortTime";
import { workspaceAgentProviderLabel } from "./workspaceAgentProviderLabel";
import {
type BuildWorkspaceAgentActivityListOptions,
type WorkspaceAgentActivityCard,
type WorkspaceAgentActivityListViewModel,
type WorkspaceAgentActivityListSnapshot,
type WorkspaceAgentActivityStatus
} from "./workspaceAgentActivityListTypes";
import { changedFilesForSession } from "./workspaceAgentGeneratedFiles";
import { resolveWorkspaceAgentActivityStatus } from "./workspaceAgentActivityStatus";
import {
resolveLatestActivity,
resolveWorkspaceAgentActivityTitle
} from "./workspaceAgentActivitySummary";
import {
compareActivities,
shouldHideEmptyRuntimePlaceholderSession,
workspaceAgentSessionMessageAliases
} from "./workspaceAgentActivityListOrdering";
export function buildWorkspaceAgentActivityListViewModel(
snapshot: WorkspaceAgentActivityListSnapshot,
options: BuildWorkspaceAgentActivityListOptions = {}
): WorkspaceAgentActivityListViewModel {
const activities = selectRootAgentActivitySessions(snapshot)
.map((session): WorkspaceAgentActivityCard | null => {
const presence =
snapshot.presences.find(
(candidate) =>
candidate.provider === session.provider &&
Boolean(candidate.userId) &&
candidate.userId === session.userId
) ?? null;
const messages = resolveWorkspaceAgentSessionMessages(
options.sessionMessagesById,
session
);
const status = resolveWorkspaceAgentActivityStatus(session);
if (
shouldHideEmptyRuntimePlaceholderSession(
session,
messages,
status,
options
)
) {
return null;
}
const user = resolveActivityUser(session, presence, options);
const agentProvider = resolveProvider(session, presence);
const agentName = workspaceAgentProviderLabel(agentProvider);
const resolvedSortTimeUnixMs =
resolveWorkspaceAgentSessionSortTimeUnixMs(session);
const latestActivity = resolveLatestActivity(messages, status, {
agentName,
userName: user.userName
});
const canonicalFileChanges = [
...(resolveWorkspaceAgentSessionTurnFileChanges(
options.sessionTurnFileChangesById,
session
) ?? []),
...(session.latestTurn?.fileChanges
? [session.latestTurn.fileChanges]
: [])
];
const activity: WorkspaceAgentActivityCard = {
id: `activity-${session.agentSessionId}`,
sessionId: session.agentSessionId,
userId: user.userId,
userName: user.userName,
...(user.userAvatarUrl ? { userAvatarUrl: user.userAvatarUrl } : {}),
agentProvider,
agentName,
title: resolveWorkspaceAgentActivityTitle(session),
status,
latestActivitySummary: latestActivity.summary,
latestActivityActorName: latestActivity.actorName,
changedFiles: changedFilesForSession(messages, undefined, {
canonicalFileChanges
}),
sortTimeUnixMs: resolvedSortTimeUnixMs,
readTimeUnixMs: readTimeUnixMs(session, status, resolvedSortTimeUnixMs)
};
return activity;
})
.filter(
(activity): activity is WorkspaceAgentActivityCard => activity !== null
)
.sort(compareActivities);
return { activities };
}
function resolveWorkspaceAgentSessionTurnFileChanges(
sessionTurnFileChangesById:
| BuildWorkspaceAgentActivityListOptions["sessionTurnFileChangesById"]
| undefined,
session: AgentActivitySession
): readonly Record<string, unknown>[] | null {
if (!sessionTurnFileChangesById) {
return null;
}
for (const alias of workspaceAgentSessionMessageAliases(session)) {
const fileChanges = sessionTurnFileChangesById[alias];
if (fileChanges) {
return fileChanges;
}
}
return null;
}
export function reuseWorkspaceAgentActivityListViewModelIfUnchanged(
previous: WorkspaceAgentActivityListViewModel | null,
next: WorkspaceAgentActivityListViewModel
): WorkspaceAgentActivityListViewModel {
if (!previous) {
return next;
}
if (next.activities.length === 0) {
return previous.activities.length === 0 ? previous : next;
}
const previousActivitiesById = new Map(
previous.activities.map((activity) => [activity.id, activity])
);
const activities = next.activities.map((activity) => {
const previousActivity = previousActivitiesById.get(activity.id);
if (
!previousActivity ||
!workspaceAgentActivityCardEquals(previousActivity, activity)
) {
return activity;
}
return previousActivity;
});
const reusedEveryActivityInPlace =
previous.activities.length === activities.length &&
previous.activities.every(
(activity, index) => activity === activities[index]
);
return reusedEveryActivityInPlace ? previous : { activities };
}
export { workspaceAgentProviderLabel } from "./workspaceAgentProviderLabel";
function workspaceAgentActivityCardEquals(
left: WorkspaceAgentActivityCard,
right: WorkspaceAgentActivityCard
): boolean {
return (
left.id === right.id &&
left.sessionId === right.sessionId &&
left.userId === right.userId &&
left.userName === right.userName &&
left.userAvatarUrl === right.userAvatarUrl &&
left.agentProvider === right.agentProvider &&
left.agentName === right.agentName &&
left.title === right.title &&
left.status === right.status &&
left.latestActivitySummary === right.latestActivitySummary &&
left.latestActivityActorName === right.latestActivityActorName &&
left.sortTimeUnixMs === right.sortTimeUnixMs &&
JSON.stringify(left.conversationPreview ?? []) ===
JSON.stringify(right.conversationPreview ?? []) &&
JSON.stringify(left.toolCalls ?? []) ===
JSON.stringify(right.toolCalls ?? []) &&
JSON.stringify(left.changedFiles) === JSON.stringify(right.changedFiles)
);
}
function resolveWorkspaceAgentSessionMessages(
sessionMessagesById: Record<string, AgentActivityMessage[]> | undefined,
session: AgentActivitySession
): AgentActivityMessage[] {
if (!sessionMessagesById) {
return [];
}
for (const alias of workspaceAgentSessionMessageAliases(session)) {
const messages = sessionMessagesById[alias];
if (messages) {
return messages;
}
}
return [];
}
function resolveActivityUser(
session: AgentActivitySession,
presence: AgentActivityPresence | null,
options: BuildWorkspaceAgentActivityListOptions
): Pick<WorkspaceAgentActivityCard, "userId" | "userName" | "userAvatarUrl"> {
if (presence) {
return resolveUserFromId(presence.userId ?? "", options);
}
const sessionUserId = session.userId?.trim() ?? "";
if (sessionUserId) {
return resolveUserFromId(sessionUserId, options);
}
return {
userId: null,
userName: "Unknown member"
};
}
function resolveUserFromId(
rawUserId: string,
options: BuildWorkspaceAgentActivityListOptions
): Pick<WorkspaceAgentActivityCard, "userId" | "userName" | "userAvatarUrl"> {
const userId = rawUserId.trim();
const profile = options.userProfilesById?.[userId];
const profileName = compactText(profile?.name ?? "");
const profileAvatar = profile?.avatar?.trim();
const rawName = profileName || userId || "Unknown member";
return {
userId,
userName: stripTrailingParentheticalEmailFromLabel(rawName) || rawName,
...(profileAvatar ? { userAvatarUrl: profileAvatar } : {})
};
}
function resolveProvider(
session: AgentActivitySession,
presence: AgentActivityPresence | null
): string {
const sessionProvider = normalizeProvider(session.provider);
if (sessionProvider) {
return sessionProvider;
}
const presenceProvider = normalizeProvider(presence?.provider);
if (presenceProvider) {
return presenceProvider;
}
return "unknown";
}
function normalizeProvider(provider: string | undefined): string | null {
const trimmed = provider?.trim();
return trimmed ? trimmed.toLowerCase() : null;
}
function readTimeUnixMs(
session: AgentActivitySession,
status: WorkspaceAgentActivityStatus,
fallbackUnixMs: number
): number {
if (status === "completed" || status === "failed") {
return session.endedAtUnixMs ?? session.updatedAtUnixMs ?? fallbackUnixMs;
}
return fallbackUnixMs;
}
function stripTrailingParentheticalEmailFromLabel(label: string): string {
return label
.trim()
.replace(/\s*\([^)]*@[^)]+\)\s*$/u, "")
.trim();
}
function compactText(value: string): string {
return value.replace(/\s+/g, " ").trim();
}