diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
index 44ad594ff9b..221fcfd7cbe 100644
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -310,12 +310,14 @@ type ChatViewProps =
| {
environmentId: EnvironmentId;
threadId: ThreadId;
+ onDiffPanelOpen?: () => void;
routeKind: "server";
draftId?: never;
}
| {
environmentId: EnvironmentId;
threadId: ThreadId;
+ onDiffPanelOpen?: () => void;
routeKind: "draft";
draftId: DraftId;
};
@@ -569,7 +571,7 @@ const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDra
});
export default function ChatView(props: ChatViewProps) {
- const { environmentId, threadId, routeKind } = props;
+ const { environmentId, threadId, routeKind, onDiffPanelOpen } = props;
const draftId = routeKind === "draft" ? props.draftId : null;
const routeThreadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
@@ -1470,6 +1472,9 @@ export default function ChatView(props: ChatViewProps) {
if (!isServerThread) {
return;
}
+ if (!diffOpen) {
+ onDiffPanelOpen?.();
+ }
void navigate({
to: "/$environmentId/$threadId",
params: {
@@ -1482,7 +1487,7 @@ export default function ChatView(props: ChatViewProps) {
return diffOpen ? { ...rest, diff: undefined } : { ...rest, diff: "1" };
},
});
- }, [diffOpen, environmentId, isServerThread, navigate, threadId]);
+ }, [diffOpen, environmentId, isServerThread, navigate, onDiffPanelOpen, threadId]);
const envLocked = Boolean(
activeThread &&
@@ -3245,6 +3250,7 @@ export default function ChatView(props: ChatViewProps) {
if (!isServerThread) {
return;
}
+ onDiffPanelOpen?.();
void navigate({
to: "/$environmentId/$threadId",
params: {
@@ -3259,7 +3265,7 @@ export default function ChatView(props: ChatViewProps) {
},
});
},
- [environmentId, isServerThread, navigate, threadId],
+ [environmentId, isServerThread, navigate, onDiffPanelOpen, threadId],
);
const onRevertUserMessage = useCallback(
(messageId: MessageId) => {
diff --git a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx
index 9e75ebe7f73..b62fd6b9c65 100644
--- a/apps/web/src/routes/_chat.$environmentId.$threadId.tsx
+++ b/apps/web/src/routes/_chat.$environmentId.$threadId.tsx
@@ -17,7 +17,7 @@ import {
stripDiffSearchParams,
} from "../diffRouteSearch";
import { useMediaQuery } from "../hooks/useMediaQuery";
-import { selectEnvironmentState, selectThreadByRef, useStore } from "../store";
+import { selectEnvironmentState, selectThreadExistsByRef, useStore } from "../store";
import { createThreadSelectorByRef } from "../storeSelectors";
import { resolveThreadRouteRef, buildThreadRouteParams } from "../threadRoutes";
import { Sheet, SheetPopup } from "../components/ui/sheet";
@@ -172,7 +172,7 @@ function ChatThreadRouteView() {
(store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).bootstrapComplete,
);
const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]));
- const threadExists = useStore((store) => selectThreadByRef(store, threadRef) !== undefined);
+ const threadExists = useStore((store) => selectThreadExistsByRef(store, threadRef));
const environmentHasServerThreads = useStore(
(store) => selectEnvironmentState(store, threadRef?.environmentId ?? null).threadIds.length > 0,
);
@@ -193,7 +193,26 @@ function ChatThreadRouteView() {
const environmentHasAnyThreads = environmentHasServerThreads || environmentHasDraftThreads;
const diffOpen = search.diff === "1";
const shouldUseDiffSheet = useMediaQuery(DIFF_INLINE_LAYOUT_MEDIA_QUERY);
- const [hasOpenedDiff, setHasOpenedDiff] = useState(diffOpen);
+ const currentThreadKey = threadRef ? `${threadRef.environmentId}:${threadRef.threadId}` : null;
+ const [diffPanelMountState, setDiffPanelMountState] = useState(() => ({
+ threadKey: currentThreadKey,
+ hasOpenedDiff: diffOpen,
+ }));
+ const hasOpenedDiff =
+ diffPanelMountState.threadKey === currentThreadKey
+ ? diffPanelMountState.hasOpenedDiff
+ : diffOpen;
+ const markDiffOpened = useCallback(() => {
+ setDiffPanelMountState((previous) => {
+ if (previous.threadKey === currentThreadKey && previous.hasOpenedDiff) {
+ return previous;
+ }
+ return {
+ threadKey: currentThreadKey,
+ hasOpenedDiff: true,
+ };
+ });
+ }, [currentThreadKey]);
const closeDiff = useCallback(() => {
if (!threadRef) {
return;
@@ -208,6 +227,7 @@ function ChatThreadRouteView() {
if (!threadRef) {
return;
}
+ markDiffOpened();
void navigate({
to: "/$environmentId/$threadId",
params: buildThreadRouteParams(threadRef),
@@ -216,13 +236,7 @@ function ChatThreadRouteView() {
return { ...rest, diff: "1" };
},
});
- }, [navigate, threadRef]);
-
- useEffect(() => {
- if (diffOpen) {
- setHasOpenedDiff(true);
- }
- }, [diffOpen]);
+ }, [markDiffOpened, navigate, threadRef]);
useEffect(() => {
if (!threadRef || !bootstrapComplete) {
@@ -254,6 +268,7 @@ function ChatThreadRouteView() {
@@ -273,6 +288,7 @@ function ChatThreadRouteView() {
diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts
index 7b7e786b67d..951830dbbc2 100644
--- a/apps/web/src/store.test.ts
+++ b/apps/web/src/store.test.ts
@@ -18,6 +18,8 @@ import {
applyOrchestrationEvents,
selectEnvironmentState,
selectProjectsAcrossEnvironments,
+ selectThreadByRef,
+ selectThreadExistsByRef,
setThreadBranch,
selectThreadsAcrossEnvironments,
syncServerReadModel,
@@ -245,6 +247,128 @@ function makeEvent(
} as Extract;
}
+describe("thread selection memoization", () => {
+ it("returns stable thread references for repeated reads of the same state", () => {
+ const thread = makeThread({
+ messages: [
+ {
+ id: MessageId.make("message-1"),
+ role: "user",
+ text: "hello",
+ createdAt: "2026-02-13T00:01:00.000Z",
+ streaming: false,
+ },
+ ],
+ activities: [
+ {
+ id: EventId.make("activity-1"),
+ tone: "info",
+ kind: "step",
+ summary: "working",
+ payload: {},
+ turnId: TurnId.make("turn-1"),
+ createdAt: "2026-02-13T00:01:30.000Z",
+ },
+ ],
+ proposedPlans: [
+ {
+ id: "plan-1",
+ turnId: null,
+ planMarkdown: "plan",
+ implementedAt: null,
+ implementationThreadId: null,
+ createdAt: "2026-02-13T00:02:00.000Z",
+ updatedAt: "2026-02-13T00:02:00.000Z",
+ },
+ ],
+ turnDiffSummaries: [
+ {
+ turnId: TurnId.make("turn-1"),
+ completedAt: "2026-02-13T00:03:00.000Z",
+ files: [],
+ },
+ ],
+ });
+ const state = makeState(thread);
+ const ref = scopeThreadRef(thread.environmentId, thread.id);
+
+ const first = selectThreadByRef(state, ref);
+ const second = selectThreadByRef(state, ref);
+
+ expect(first).toBeDefined();
+ expect(second).toBe(first);
+ expect(second?.messages).toBe(first?.messages);
+ expect(second?.activities).toBe(first?.activities);
+ expect(second?.proposedPlans).toBe(first?.proposedPlans);
+ expect(second?.turnDiffSummaries).toBe(first?.turnDiffSummaries);
+ });
+
+ it("reuses the derived thread when the app state wrapper changes but thread data does not", () => {
+ const thread = makeThread({
+ messages: [
+ {
+ id: MessageId.make("message-1"),
+ role: "assistant",
+ text: "done",
+ createdAt: "2026-02-13T00:01:00.000Z",
+ streaming: false,
+ },
+ ],
+ });
+ const state = makeState(thread);
+ const ref = scopeThreadRef(thread.environmentId, thread.id);
+ const wrappedState: AppState = {
+ ...state,
+ environmentStateById: { ...state.environmentStateById },
+ };
+
+ const first = selectThreadByRef(state, ref);
+ const second = selectThreadByRef(wrappedState, ref);
+
+ expect(second).toBe(first);
+ });
+
+ it("updates the derived thread when the underlying thread data changes", () => {
+ const thread = makeThread();
+ const ref = scopeThreadRef(thread.environmentId, thread.id);
+ const firstState = makeState(thread);
+ const secondState = makeState({
+ ...thread,
+ messages: [
+ {
+ id: MessageId.make("message-2"),
+ role: "user",
+ text: "new",
+ createdAt: "2026-02-13T00:04:00.000Z",
+ streaming: false,
+ },
+ ],
+ });
+
+ const first = selectThreadByRef(firstState, ref);
+ const second = selectThreadByRef(secondState, ref);
+
+ expect(second).not.toBe(first);
+ expect(second?.messages).toHaveLength(1);
+ expect(second?.messages[0]?.text).toBe("new");
+ });
+
+ it("checks thread existence without materializing the full thread", () => {
+ const thread = makeThread();
+ const state = makeState(thread);
+ const ref = scopeThreadRef(thread.environmentId, thread.id);
+
+ expect(selectThreadExistsByRef(state, ref)).toBe(true);
+ expect(
+ selectThreadExistsByRef(
+ state,
+ scopeThreadRef(thread.environmentId, ThreadId.make("missing")),
+ ),
+ ).toBe(false);
+ expect(selectThreadExistsByRef(state, null)).toBe(false);
+ });
+});
+
function makeReadModelThread(overrides: Partial) {
return {
id: ThreadId.make("thread-1"),
diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts
index 10f4eac3872..58a0697c7d0 100644
--- a/apps/web/src/store.ts
+++ b/apps/web/src/store.ts
@@ -38,6 +38,7 @@ import {
} from "./types";
import { resolveEnvironmentHttpUrl } from "./environments/runtime";
import { sanitizeThreadErrorMessage } from "./rpc/transportError";
+import { getThreadFromEnvironmentState } from "./threadDerivation";
export interface EnvironmentState {
projectIds: ProjectId[];
@@ -94,19 +95,6 @@ const MAX_THREAD_CHECKPOINTS = 500;
const MAX_THREAD_PROPOSED_PLANS = 200;
const MAX_THREAD_ACTIVITIES = 500;
const EMPTY_THREAD_IDS: ThreadId[] = [];
-const EMPTY_MESSAGE_IDS: MessageId[] = [];
-const EMPTY_ACTIVITY_IDS: string[] = [];
-const EMPTY_PROPOSED_PLAN_IDS: string[] = [];
-const EMPTY_TURN_IDS: TurnId[] = [];
-const EMPTY_MESSAGES: ChatMessage[] = [];
-const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];
-const EMPTY_PROPOSED_PLANS: ProposedPlan[] = [];
-const EMPTY_TURN_DIFF_SUMMARIES: TurnDiffSummary[] = [];
-const EMPTY_MESSAGE_MAP: Record = {};
-const EMPTY_ACTIVITY_MAP: Record = {};
-const EMPTY_PROPOSED_PLAN_MAP: Record = {};
-const EMPTY_TURN_DIFF_MAP: Record = {};
-const EMPTY_THREAD_TURN_STATE: ThreadTurnState = Object.freeze({ latestTurn: null });
function arraysEqual(left: readonly T[], right: readonly T[]): boolean {
return left.length === right.length && left.every((value, index) => value === right[index]);
@@ -403,78 +391,6 @@ function buildTurnDiffSlice(thread: Thread): {
};
}
-function selectThreadMessages(state: EnvironmentState, threadId: ThreadId): ChatMessage[] {
- const ids = state.messageIdsByThreadId[threadId] ?? EMPTY_MESSAGE_IDS;
- const byId = state.messageByThreadId[threadId] ?? EMPTY_MESSAGE_MAP;
- if (ids.length === 0) {
- return EMPTY_MESSAGES;
- }
- return ids.flatMap((id) => {
- const message = byId[id];
- return message ? [message] : [];
- });
-}
-
-function selectThreadActivities(
- state: EnvironmentState,
- threadId: ThreadId,
-): OrchestrationThreadActivity[] {
- const ids = state.activityIdsByThreadId[threadId] ?? EMPTY_ACTIVITY_IDS;
- const byId = state.activityByThreadId[threadId] ?? EMPTY_ACTIVITY_MAP;
- if (ids.length === 0) {
- return EMPTY_ACTIVITIES;
- }
- return ids.flatMap((id) => {
- const activity = byId[id];
- return activity ? [activity] : [];
- });
-}
-
-function selectThreadProposedPlans(state: EnvironmentState, threadId: ThreadId): ProposedPlan[] {
- const ids = state.proposedPlanIdsByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_IDS;
- const byId = state.proposedPlanByThreadId[threadId] ?? EMPTY_PROPOSED_PLAN_MAP;
- if (ids.length === 0) {
- return EMPTY_PROPOSED_PLANS;
- }
- return ids.flatMap((id) => {
- const plan = byId[id];
- return plan ? [plan] : [];
- });
-}
-
-function selectThreadTurnDiffSummaries(
- state: EnvironmentState,
- threadId: ThreadId,
-): TurnDiffSummary[] {
- const ids = state.turnDiffIdsByThreadId[threadId] ?? EMPTY_TURN_IDS;
- const byId = state.turnDiffSummaryByThreadId[threadId] ?? EMPTY_TURN_DIFF_MAP;
- if (ids.length === 0) {
- return EMPTY_TURN_DIFF_SUMMARIES;
- }
- return ids.flatMap((id) => {
- const summary = byId[id];
- return summary ? [summary] : [];
- });
-}
-
-function getThread(state: EnvironmentState, threadId: ThreadId): Thread | undefined {
- const shell = state.threadShellById[threadId];
- if (!shell) {
- return undefined;
- }
- const turnState = state.threadTurnStateById[threadId] ?? EMPTY_THREAD_TURN_STATE;
- return {
- ...shell,
- session: state.threadSessionById[threadId] ?? null,
- latestTurn: turnState.latestTurn,
- pendingSourceProposedPlan: turnState.pendingSourceProposedPlan,
- messages: selectThreadMessages(state, threadId),
- activities: selectThreadActivities(state, threadId),
- proposedPlans: selectThreadProposedPlans(state, threadId),
- turnDiffSummaries: selectThreadTurnDiffSummaries(state, threadId),
- };
-}
-
function getProjects(state: EnvironmentState): Project[] {
return state.projectIds.flatMap((projectId) => {
const project = state.projectById[projectId];
@@ -484,7 +400,7 @@ function getProjects(state: EnvironmentState): Project[] {
function getThreads(state: EnvironmentState): Thread[] {
return state.threadIds.flatMap((threadId) => {
- const thread = getThread(state, threadId);
+ const thread = getThreadFromEnvironmentState(state, threadId);
return thread ? [thread] : [];
});
}
@@ -896,7 +812,7 @@ function updateThreadState(
threadId: ThreadId,
updater: (thread: Thread) => Thread,
): EnvironmentState {
- const currentThread = getThread(state, threadId);
+ const currentThread = getThreadFromEnvironmentState(state, threadId);
if (!currentThread) {
return state;
}
@@ -1163,7 +1079,7 @@ function applyEnvironmentOrchestrationEvent(
}
case "thread.created": {
- const previousThread = getThread(state, event.payload.threadId);
+ const previousThread = getThreadFromEnvironmentState(state, event.payload.threadId);
const nextThread = mapThread(
{
id: event.payload.threadId,
@@ -1669,10 +1585,19 @@ export function selectThreadByRef(
ref: ScopedThreadRef | null | undefined,
): Thread | undefined {
return ref
- ? getThread(selectEnvironmentState(state, ref.environmentId), ref.threadId)
+ ? getThreadFromEnvironmentState(selectEnvironmentState(state, ref.environmentId), ref.threadId)
: undefined;
}
+export function selectThreadExistsByRef(
+ state: AppState,
+ ref: ScopedThreadRef | null | undefined,
+): boolean {
+ return ref
+ ? selectEnvironmentState(state, ref.environmentId).threadShellById[ref.threadId] !== undefined
+ : false;
+}
+
export function selectSidebarThreadSummaryByRef(
state: AppState,
ref: ScopedThreadRef | null | undefined,
diff --git a/apps/web/src/storeSelectors.ts b/apps/web/src/storeSelectors.ts
index 84802ae6d56..02b88f31b18 100644
--- a/apps/web/src/storeSelectors.ts
+++ b/apps/web/src/storeSelectors.ts
@@ -1,40 +1,7 @@
-import {
- type MessageId,
- type ScopedProjectRef,
- type ScopedThreadRef,
- type ThreadId,
- type TurnId,
-} from "@t3tools/contracts";
+import { type ScopedProjectRef, type ScopedThreadRef, type ThreadId } from "@t3tools/contracts";
import { selectEnvironmentState, type AppState, type EnvironmentState } from "./store";
-import {
- type ChatMessage,
- type Project,
- type ProposedPlan,
- type SidebarThreadSummary,
- type Thread,
- type ThreadSession,
- type ThreadTurnState,
- type TurnDiffSummary,
-} from "./types";
-
-const EMPTY_MESSAGES: ChatMessage[] = [];
-const EMPTY_ACTIVITIES: Thread["activities"] = [];
-const EMPTY_PROPOSED_PLANS: ProposedPlan[] = [];
-const EMPTY_TURN_DIFF_SUMMARIES: TurnDiffSummary[] = [];
-
-function collectByIds(
- ids: readonly TKey[] | undefined,
- byId: Record | undefined,
-): TValue[] {
- if (!ids || ids.length === 0 || !byId) {
- return [];
- }
-
- return ids.flatMap((id) => {
- const value = byId[id];
- return value ? [value] : [];
- });
-}
+import { type Project, type SidebarThreadSummary, type Thread } from "./types";
+import { getThreadFromEnvironmentState } from "./threadDerivation";
export function createProjectSelectorByRef(
ref: ScopedProjectRef | null | undefined,
@@ -55,17 +22,8 @@ export function createSidebarThreadSummarySelectorByRef(
function createScopedThreadSelector(
resolveRef: (state: AppState) => ScopedThreadRef | null | undefined,
): (state: AppState) => Thread | undefined {
- let previousShell: EnvironmentState["threadShellById"][ThreadId] | undefined;
- let previousSession: ThreadSession | null | undefined;
- let previousTurnState: ThreadTurnState | undefined;
- let previousMessageIds: MessageId[] | undefined;
- let previousMessagesById: EnvironmentState["messageByThreadId"][ThreadId] | undefined;
- let previousActivityIds: string[] | undefined;
- let previousActivitiesById: EnvironmentState["activityByThreadId"][ThreadId] | undefined;
- let previousProposedPlanIds: string[] | undefined;
- let previousProposedPlansById: EnvironmentState["proposedPlanByThreadId"][ThreadId] | undefined;
- let previousTurnDiffIds: TurnId[] | undefined;
- let previousTurnDiffsById: EnvironmentState["turnDiffSummaryByThreadId"][ThreadId] | undefined;
+ let previousEnvironmentState: EnvironmentState | undefined;
+ let previousThreadId: ThreadId | undefined;
let previousThread: Thread | undefined;
return (state) => {
@@ -75,85 +33,17 @@ function createScopedThreadSelector(
}
const environmentState = selectEnvironmentState(state, ref.environmentId);
- const threadId = ref.threadId;
- const shell = environmentState.threadShellById[threadId];
- if (!shell) {
- return undefined;
- }
-
- const session = environmentState.threadSessionById[threadId] ?? null;
- const turnState = environmentState.threadTurnStateById[threadId];
- const messageIds = environmentState.messageIdsByThreadId[threadId];
- const messageById = environmentState.messageByThreadId[threadId];
- const activityIds = environmentState.activityIdsByThreadId[threadId];
- const activityById = environmentState.activityByThreadId[threadId];
- const proposedPlanIds = environmentState.proposedPlanIdsByThreadId[threadId];
- const proposedPlanById = environmentState.proposedPlanByThreadId[threadId];
- const turnDiffIds = environmentState.turnDiffIdsByThreadId[threadId];
- const turnDiffById = environmentState.turnDiffSummaryByThreadId[threadId];
-
if (
previousThread &&
- previousShell === shell &&
- previousSession === session &&
- previousTurnState === turnState &&
- previousMessageIds === messageIds &&
- previousMessagesById === messageById &&
- previousActivityIds === activityIds &&
- previousActivitiesById === activityById &&
- previousProposedPlanIds === proposedPlanIds &&
- previousProposedPlansById === proposedPlanById &&
- previousTurnDiffIds === turnDiffIds &&
- previousTurnDiffsById === turnDiffById
+ previousEnvironmentState === environmentState &&
+ previousThreadId === ref.threadId
) {
return previousThread;
}
- const nextThread: Thread = {
- ...shell,
- session,
- latestTurn: turnState?.latestTurn ?? null,
- pendingSourceProposedPlan: turnState?.pendingSourceProposedPlan,
- messages: collectByIds(messageIds, messageById) as Thread["messages"] extends ChatMessage[]
- ? ChatMessage[]
- : never,
- activities: collectByIds(activityIds, activityById) as Thread["activities"] extends Array<
- infer _
- >
- ? Thread["activities"]
- : never,
- proposedPlans: collectByIds(
- proposedPlanIds,
- proposedPlanById,
- ) as Thread["proposedPlans"] extends ProposedPlan[] ? ProposedPlan[] : never,
- turnDiffSummaries: collectByIds(
- turnDiffIds,
- turnDiffById,
- ) as Thread["turnDiffSummaries"] extends TurnDiffSummary[] ? TurnDiffSummary[] : never,
- };
-
- previousShell = shell;
- previousSession = session;
- previousTurnState = turnState;
- previousMessageIds = messageIds;
- previousMessagesById = messageById;
- previousActivityIds = activityIds;
- previousActivitiesById = activityById;
- previousProposedPlanIds = proposedPlanIds;
- previousProposedPlansById = proposedPlanById;
- previousTurnDiffIds = turnDiffIds;
- previousTurnDiffsById = turnDiffById;
- previousThread = {
- ...nextThread,
- messages: nextThread.messages.length === 0 ? EMPTY_MESSAGES : nextThread.messages,
- activities: nextThread.activities.length === 0 ? EMPTY_ACTIVITIES : nextThread.activities,
- proposedPlans:
- nextThread.proposedPlans.length === 0 ? EMPTY_PROPOSED_PLANS : nextThread.proposedPlans,
- turnDiffSummaries:
- nextThread.turnDiffSummaries.length === 0
- ? EMPTY_TURN_DIFF_SUMMARIES
- : nextThread.turnDiffSummaries,
- };
+ previousEnvironmentState = environmentState;
+ previousThreadId = ref.threadId;
+ previousThread = getThreadFromEnvironmentState(environmentState, ref.threadId);
return previousThread;
};
}
diff --git a/apps/web/src/threadDerivation.ts b/apps/web/src/threadDerivation.ts
new file mode 100644
index 00000000000..0766f0c8e13
--- /dev/null
+++ b/apps/web/src/threadDerivation.ts
@@ -0,0 +1,152 @@
+import type { MessageId, ThreadId, TurnId } from "@t3tools/contracts";
+import type { EnvironmentState } from "./store";
+import type {
+ ChatMessage,
+ ProposedPlan,
+ Thread,
+ ThreadSession,
+ ThreadShell,
+ ThreadTurnState,
+ TurnDiffSummary,
+} from "./types";
+
+const EMPTY_MESSAGES: ChatMessage[] = [];
+const EMPTY_ACTIVITIES: Thread["activities"] = [];
+const EMPTY_PROPOSED_PLANS: ProposedPlan[] = [];
+const EMPTY_TURN_DIFF_SUMMARIES: TurnDiffSummary[] = [];
+const EMPTY_MESSAGE_MAP: Record = {};
+const EMPTY_ACTIVITY_MAP: Record = {};
+const EMPTY_PROPOSED_PLAN_MAP: Record = {};
+const EMPTY_TURN_DIFF_MAP: Record = {};
+
+const collectedByIdsCache = new WeakMap>();
+const threadCache = new WeakMap<
+ ThreadShell,
+ {
+ session: ThreadSession | null;
+ turnState: ThreadTurnState | undefined;
+ messages: Thread["messages"];
+ activities: Thread["activities"];
+ proposedPlans: Thread["proposedPlans"];
+ turnDiffSummaries: Thread["turnDiffSummaries"];
+ thread: Thread;
+ }
+>();
+
+function collectByIds(
+ ids: readonly TKey[] | undefined,
+ byId: Record | undefined,
+ emptyValue: TValue[],
+): TValue[] {
+ if (!ids || ids.length === 0 || !byId) {
+ return emptyValue;
+ }
+
+ const cachedByRecord = collectedByIdsCache.get(ids);
+ const cached = cachedByRecord?.get(byId);
+ if (cached) {
+ return cached as TValue[];
+ }
+
+ const nextValues = ids.flatMap((id) => {
+ const value = byId[id];
+ return value ? [value] : [];
+ });
+ const nextCachedByRecord = cachedByRecord ?? new WeakMap