[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
131
domain/aiSessionsForScope.ts
Normal file
131
domain/aiSessionsForScope.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user