Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/model/agentGuiConversationActivityView.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

266 lines
8.4 KiB
TypeScript

import { resolveAgentGUIConversationSortTimeUnixMs } from "./agentGuiConversationTypes";
import type {
AgentGUIConversationStatus,
AgentGUIConversationSummary
} from "./agentGuiConversationTypes";
const ACTIVITY_RECENT_DAY_COUNT = 7;
const EMPTY_DELETED_SESSION_IDS: Readonly<Record<string, true>> = {};
export type AgentGUIConversationActivityPriorityReason =
| "waiting"
| "unread"
| "active";
export interface AgentGUIConversationActivityRootFact {
needsUserAction: boolean;
status: AgentGUIConversationStatus;
}
export type AgentGUIConversationActivityCandidate = Pick<
AgentGUIConversationSummary,
| "hasUnreadCompletion"
| "id"
| "needsUserAction"
| "sortTimeUnixMs"
| "status"
| "updatedAtUnixMs"
>;
export interface AgentGUIConversationActivityMember {
id: string;
admissionOrder: number;
admittedAtUnixMs: number;
priorityReason: AgentGUIConversationActivityPriorityReason | null;
recentDayStartUnixMs: number | null;
}
export interface AgentGUIConversationActivityActivation {
cutoffDayStartUnixMs: number;
referenceDayStartUnixMs: number;
priority: readonly AgentGUIConversationActivityMember[];
recent: readonly AgentGUIConversationActivityMember[];
}
export interface AgentGUIConversationActivityDateSection {
dayStartUnixMs: number;
ids: readonly string[];
}
export interface AgentGUIConversationActivityProjection {
priorityIds: readonly string[];
priorityReasonsById: ReadonlyMap<
string,
AgentGUIConversationActivityPriorityReason
>;
recentSections: readonly AgentGUIConversationActivityDateSection[];
referenceDayStartUnixMs: number;
}
export function createAgentGUIConversationActivityActivation(
conversations: readonly AgentGUIConversationActivityCandidate[],
openedAtUnixMs: number
): AgentGUIConversationActivityActivation {
const cutoffDate = new Date(localDayStartUnixMs(openedAtUnixMs));
cutoffDate.setDate(cutoffDate.getDate() - (ACTIVITY_RECENT_DAY_COUNT - 1));
const cutoffDayStartUnixMs = cutoffDate.getTime();
const priority: AgentGUIConversationActivityMember[] = [];
const recent: AgentGUIConversationActivityMember[] = [];
const seenIds = new Set<string>();
let admissionOrder = 0;
for (const conversation of conversations) {
if (seenIds.has(conversation.id)) continue;
seenIds.add(conversation.id);
const admittedAtUnixMs =
resolveAgentGUIConversationSortTimeUnixMs(conversation);
const priorityReason = livePriorityReason(conversation);
if (priorityReason) {
priority.push({
admissionOrder: admissionOrder++,
admittedAtUnixMs,
id: conversation.id,
priorityReason,
recentDayStartUnixMs: null
});
continue;
}
const recentDayStartUnixMs = localDayStartUnixMs(admittedAtUnixMs);
if (recentDayStartUnixMs >= cutoffDayStartUnixMs) {
recent.push({
admissionOrder: admissionOrder++,
admittedAtUnixMs,
id: conversation.id,
priorityReason: null,
recentDayStartUnixMs
});
}
}
return {
cutoffDayStartUnixMs,
referenceDayStartUnixMs: localDayStartUnixMs(openedAtUnixMs),
priority: priority.sort(compareActivityMembers),
recent: recent.sort(compareActivityMembers)
};
}
export function reconcileAgentGUIConversationActivityActivation(
activation: AgentGUIConversationActivityActivation,
conversations: readonly AgentGUIConversationActivityCandidate[],
deletedSessionIds: Readonly<Record<string, true>> = EMPTY_DELETED_SESSION_IDS
): AgentGUIConversationActivityActivation {
// Activity membership is an activation snapshot. Keep the member refs even
// when a rail refresh temporarily omits their canonical summary; the view
// layer can render the last known row until the next toggle rebuilds it.
const retainedPriority = activation.priority.filter(
(member) => !deletedSessionIds[member.id]
);
const retainedRecent = activation.recent.filter(
(member) => !deletedSessionIds[member.id]
);
const priorityIds = new Set(retainedPriority.map((member) => member.id));
const recentIds = new Set(retainedRecent.map((member) => member.id));
const currentIds = new Set<string>();
let admissionOrder = Math.max(
-1,
...activation.priority.map((member) => member.admissionOrder),
...activation.recent.map((member) => member.admissionOrder)
);
for (const conversation of conversations) {
if (currentIds.has(conversation.id)) continue;
if (deletedSessionIds[conversation.id]) continue;
currentIds.add(conversation.id);
const currentRecency =
resolveAgentGUIConversationSortTimeUnixMs(conversation);
const liveReason = livePriorityReason(conversation);
if (priorityIds.has(conversation.id)) continue;
if (recentIds.has(conversation.id) && !liveReason) continue;
if (recentIds.has(conversation.id)) {
const recentIndex = retainedRecent.findIndex(
(member) => member.id === conversation.id
);
if (recentIndex >= 0) retainedRecent.splice(recentIndex, 1);
recentIds.delete(conversation.id);
}
// A late canonical snapshot is not an activity signal. It can be a
// selected historical session or a page that arrived after activation;
// only live facts may admit a previously unseen session to Priority.
if (liveReason) {
retainedPriority.push({
admissionOrder: ++admissionOrder,
admittedAtUnixMs: currentRecency,
id: conversation.id,
priorityReason: liveReason,
recentDayStartUnixMs: null
});
priorityIds.add(conversation.id);
continue;
}
}
const nextPriority = retainedPriority.sort(compareActivityMembers);
const nextRecent = retainedRecent.sort(compareActivityMembers);
if (
activityMemberArraysEqual(activation.priority, nextPriority) &&
activityMemberArraysEqual(activation.recent, nextRecent)
) {
return activation;
}
return {
cutoffDayStartUnixMs: activation.cutoffDayStartUnixMs,
referenceDayStartUnixMs: activation.referenceDayStartUnixMs,
priority: nextPriority,
recent: nextRecent
};
}
export function projectAgentGUIConversationActivity(
activation: AgentGUIConversationActivityActivation
): AgentGUIConversationActivityProjection {
const recentSections: AgentGUIConversationActivityDateSection[] = [];
for (const member of activation.recent) {
const dayStartUnixMs = member.recentDayStartUnixMs;
if (dayStartUnixMs === null) continue;
const currentSection = recentSections.at(-1);
if (currentSection?.dayStartUnixMs === dayStartUnixMs) {
currentSection.ids = [...currentSection.ids, member.id];
} else {
recentSections.push({ dayStartUnixMs, ids: [member.id] });
}
}
return {
priorityIds: activation.priority.map((member) => member.id),
priorityReasonsById: new Map(
activation.priority.flatMap((member) =>
member.priorityReason ? [[member.id, member.priorityReason]] : []
)
),
recentSections,
referenceDayStartUnixMs: activation.referenceDayStartUnixMs
};
}
export function localDayStartUnixMs(value: number): number {
const date = new Date(value);
return new Date(
date.getFullYear(),
date.getMonth(),
date.getDate()
).getTime();
}
function livePriorityReason(
conversation: AgentGUIConversationActivityCandidate
): AgentGUIConversationActivityPriorityReason | null {
if (conversation.needsUserAction || conversation.status === "waiting") {
return "waiting";
}
if (conversation.hasUnreadCompletion) return "unread";
if (conversation.status === "working") return "active";
return null;
}
function compareActivityMembers(
left: AgentGUIConversationActivityMember,
right: AgentGUIConversationActivityMember
): number {
const priorityDifference =
priorityRank(left.priorityReason) - priorityRank(right.priorityReason);
if (priorityDifference !== 0) return priorityDifference;
return (
right.admittedAtUnixMs - left.admittedAtUnixMs ||
left.admissionOrder - right.admissionOrder ||
left.id.localeCompare(right.id)
);
}
function priorityRank(
reason: AgentGUIConversationActivityPriorityReason | null
): number {
switch (reason) {
case "waiting":
return 0;
case "unread":
return 1;
case "active":
return 2;
default:
return 3;
}
}
function activityMemberArraysEqual(
left: readonly AgentGUIConversationActivityMember[],
right: readonly AgentGUIConversationActivityMember[]
): boolean {
return (
left.length === right.length &&
left.every((member, index) => member === right[index])
);
}