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

179 lines
5.2 KiB
TypeScript

import {
createAgentGUIConversationActivityActivation,
reconcileAgentGUIConversationActivityActivation,
type AgentGUIConversationActivityActivation
} from "../model/agentGuiConversationActivityView";
import type { AgentGUIConversationSummary } from "../model/agentGuiConversationTypes";
const EMPTY_DELETED_SESSION_IDS: Readonly<Record<string, true>> = {};
export interface AgentGUIConversationActivityControllerInput {
available: boolean;
conversations: readonly AgentGUIConversationSummary[];
deletedSessionIds?: Readonly<Record<string, true>>;
identityKey: string;
scopeKey: string;
}
export interface AgentGUIConversationActivityControllerSnapshot {
available: boolean;
activation: AgentGUIConversationActivityActivation | null;
conversationCache: ReadonlyMap<string, AgentGUIConversationSummary>;
enabled: boolean;
identityKey: string | null;
scopeKey: string | null;
}
export interface AgentGUIConversationActivityController {
getSnapshot: () => AgentGUIConversationActivityControllerSnapshot;
subscribe: (listener: () => void) => () => void;
configure: (input: AgentGUIConversationActivityControllerInput) => void;
toggle: () => void;
}
const DISABLED_SNAPSHOT: AgentGUIConversationActivityControllerSnapshot = {
available: false,
activation: null,
conversationCache: new Map(),
enabled: false,
identityKey: null,
scopeKey: null
};
const AVAILABLE_OFF_SNAPSHOT: AgentGUIConversationActivityControllerSnapshot = {
...DISABLED_SNAPSHOT,
available: true
};
export function createAgentGUIConversationActivityController(): AgentGUIConversationActivityController {
let snapshot = DISABLED_SNAPSHOT;
let latestInput: AgentGUIConversationActivityControllerInput | null = null;
const listeners = new Set<() => void>();
const publish = (
next: AgentGUIConversationActivityControllerSnapshot
): void => {
if (next === snapshot) return;
snapshot = next;
for (const listener of listeners) listener();
};
const configure = (
input: AgentGUIConversationActivityControllerInput
): void => {
latestInput = input;
if (!input.available) {
if (snapshot !== DISABLED_SNAPSHOT) publish(DISABLED_SNAPSHOT);
return;
}
if (!snapshot.enabled) {
if (snapshot !== AVAILABLE_OFF_SNAPSHOT) {
publish(AVAILABLE_OFF_SNAPSHOT);
}
return;
}
const sameContext =
snapshot.identityKey === input.identityKey &&
snapshot.scopeKey === input.scopeKey;
const deletedSessionIds =
input.deletedSessionIds ?? EMPTY_DELETED_SESSION_IDS;
const candidates = input.conversations.filter(
(conversation) => !deletedSessionIds[conversation.id]
);
const activation =
sameContext && snapshot.activation
? reconcileAgentGUIConversationActivityActivation(
snapshot.activation,
candidates,
deletedSessionIds
)
: createAgentGUIConversationActivityActivation(candidates, Date.now());
const conversationCache = mergeConversationCache(
sameContext ? snapshot.conversationCache : new Map(),
candidates,
deletedSessionIds
);
if (
sameContext &&
activation === snapshot.activation &&
conversationCache === snapshot.conversationCache
) {
return;
}
publish({
available: true,
activation,
conversationCache,
enabled: true,
identityKey: input.identityKey,
scopeKey: input.scopeKey
});
};
const toggle = (): void => {
const input = latestInput;
if (!input) return;
if (!input.available) return;
if (snapshot.enabled) {
publish(AVAILABLE_OFF_SNAPSHOT);
return;
}
const deletedSessionIds =
input.deletedSessionIds ?? EMPTY_DELETED_SESSION_IDS;
const candidates = input.conversations.filter(
(conversation) => !deletedSessionIds[conversation.id]
);
publish({
available: true,
activation: createAgentGUIConversationActivityActivation(
candidates,
Date.now()
),
conversationCache: mergeConversationCache(
new Map(),
candidates,
deletedSessionIds
),
enabled: true,
identityKey: input.identityKey,
scopeKey: input.scopeKey
});
};
return {
getSnapshot: () => snapshot,
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
configure,
toggle
};
}
function mergeConversationCache(
previous: ReadonlyMap<string, AgentGUIConversationSummary>,
conversations: readonly AgentGUIConversationSummary[],
deletedSessionIds: Readonly<Record<string, true>>
): ReadonlyMap<string, AgentGUIConversationSummary> {
const next = new Map(previous);
for (const deletedId of Object.keys(deletedSessionIds)) {
next.delete(deletedId);
}
for (const conversation of conversations) {
next.set(conversation.id, conversation);
}
if (conversationMapsEqual(previous, next)) return previous;
return next;
}
function conversationMapsEqual(
left: ReadonlyMap<string, AgentGUIConversationSummary>,
right: ReadonlyMap<string, AgentGUIConversationSummary>
): boolean {
return (
left.size === right.size &&
[...left].every(([id, conversation]) => right.get(id) === conversation)
);
}