Files
NetMesh/domain/aiSessionsForScope.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

132 lines
4.6 KiB
TypeScript

/**
* Exact-scope AI session helpers for multi-panel memo isolation.
*
* History still needs the full sessions list for fuzzy host-match ranking
* (`getScopedHistorySessions`). Stream thrash is blocked by comparing only
* exact-scope session object refs in panel are equal — not by pre-filtering
* the history universe away.
*/
export type AISessionScopeLike = {
type: string;
targetId?: string;
};
export type AISessionLike = {
id: string;
scope: AISessionScopeLike;
/** Optional chrome for history list equality (title renames without message thrash). */
title?: string | null;
/** Optional chrome for history sort / relative-time display (`getScopedHistorySessions`). */
updatedAt?: number;
};
export function buildAIScopeKey(scopeType: string, scopeTargetId?: string): string {
return `${scopeType}:${scopeTargetId ?? ''}`;
}
export function sessionMatchesAIScope(
session: AISessionLike,
scopeType: string,
scopeTargetId?: string,
): boolean {
return session.scope.type === scopeType
&& (session.scope.targetId ?? '') === (scopeTargetId ?? '');
}
export function filterAISessionsForScope<T extends AISessionLike>(
sessions: readonly T[],
scopeType: string,
scopeTargetId?: string,
): T[] {
return sessions.filter((session) => sessionMatchesAIScope(session, scopeType, scopeTargetId));
}
/**
* True when the given session id's object identity is unchanged across arrays
* (or both sides lack that session).
*/
export function aiSessionByIdEqual<T extends AISessionLike>(
prev: readonly T[] | null | undefined,
next: readonly T[] | null | undefined,
sessionId: string | null | undefined,
): boolean {
if (!sessionId) return true;
if (prev === next) return true;
if (!prev || !next) return false;
const prevSession = prev.find((session) => session.id === sessionId);
const nextSession = next.find((session) => session.id === sessionId);
return prevSession === nextSession;
}
/**
* True when both arrays contain the same session ids (order-insensitive) and
* matching history chrome (title + updatedAt). Detects create/delete/rename and
* timestamp/order changes without treating message-body object replacement alone
* as a reason to re-render when chrome is unchanged.
*/
export function aiSessionIdSetEqual<T extends AISessionLike>(
prev: readonly T[] | null | undefined,
next: readonly T[] | null | undefined,
): boolean {
if (prev === next) return true;
if (!prev || !next) return false;
if (prev.length !== next.length) return false;
if (prev.length === 0) return true;
const prevById = new Map(prev.map((session) => [session.id, session]));
for (const session of next) {
const prevSession = prevById.get(session.id);
if (!prevSession) return false;
if ((prevSession.title ?? '') !== (session.title ?? '')) return false;
if ((prevSession.updatedAt ?? 0) !== (session.updatedAt ?? 0)) return false;
}
return true;
}
/**
* True when exact-scope session object identities match (order-insensitive by id).
* Sibling stream updates replace only their own session objects, so other panels
* see the same exact-scope refs and can skip re-render.
*
* When `selectedSessionId` is set (e.g. a history chat resumed under a newer
* terminal whose stored scope still points at an older target), that session is
* also compared by identity so stream updates still re-render the visible panel.
*/
export function exactScopeAISessionsEqual<T extends AISessionLike>(
prev: readonly T[] | null | undefined,
next: readonly T[] | null | undefined,
scopeType: string,
scopeTargetId?: string,
selectedSessionId?: string | null,
): boolean {
if (prev === next) return true;
if (!prev || !next) return false;
if (!aiSessionByIdEqual(prev, next, selectedSessionId)) return false;
const prevExact = filterAISessionsForScope(prev, scopeType, scopeTargetId);
const nextExact = filterAISessionsForScope(next, scopeType, scopeTargetId);
if (prevExact.length !== nextExact.length) return false;
if (prevExact.length === 0) return true;
const prevById = new Map(prevExact.map((session) => [session.id, session]));
for (const session of nextExact) {
if (prevById.get(session.id) !== session) return false;
}
return true;
}
/**
* Keep previous filtered array identity when every matching session ref is the same.
*/
export function retainStableAISessionsForScope<T extends AISessionLike>(
previous: readonly T[] | null | undefined,
next: readonly T[],
): readonly T[] {
if (
previous
&& previous.length === next.length
&& previous.every((session, index) => session === next[index])
) {
return previous;
}
return next;
}