From c2f56076b5f1e6516eb74f084462e275ea7a2869 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 24 Mar 2026 10:53:28 +0000 Subject: [PATCH 1/2] perf(ui): eliminate N+1 reactive subscriptions in SessionTurn Add optional messages prop to SessionTurn so the parent MessageTimeline can pass the computed allMessages array. This replaces N store subscriptions (one per rendered turn) with a single subscription in the parent. Falls back to the existing store subscription when the prop is absent (backwards compatible with enterprise share page). Also replaces JSON.stringify equality check in the comments memo with structural field comparison. --- packages/app/src/pages/session/message-timeline.tsx | 11 ++++++++++- packages/ui/src/components/session-turn.tsx | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index fe61f16854cc..5fef41a55056 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -923,7 +923,15 @@ export function MessageTimeline(props: { {(messageID) => { const active = createMemo(() => activeMessageID() === messageID) const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { - equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + equals: (a, b) => + a.length === b.length && + a.every( + (c, i) => + c.path === b[i].path && + c.comment === b[i].comment && + c.selection?.startLine === b[i].selection?.startLine && + c.selection?.endLine === b[i].selection?.endLine, + ), }) const commentCount = createMemo(() => comments().length) return ( @@ -979,6 +987,7 @@ export function MessageTimeline(props: { list(data.store.message?.[props.sessionID], emptyMessages)) + const allMessages = createMemo(() => props.messages ?? list(data.store.message?.[props.sessionID], emptyMessages)) const messageIndex = createMemo(() => { const messages = allMessages() ?? emptyMessages From d7340a9f5f65770a1d5385c2f01cdcaf6314c98b Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 24 Mar 2026 13:34:13 +0000 Subject: [PATCH 2/2] fix(ui): consolidate triple part iteration in SessionTurn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace three independent memos (assistantVisible, assistantTailVisible, reasoningHeading) that each iterate all assistant messages and parts with a single-pass computation. One turn had 106 assistant messages / 411 parts — three memos created 3x106 store subscriptions and ~1200 part evaluations per reactive cycle. --- packages/ui/src/components/session-turn.tsx | 46 ++++++++++----------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 065eb0575894..f7ba20af5796 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -341,30 +341,28 @@ export function SessionTurn( if (end < start) return undefined return end - start }) - const assistantVisible = createMemo(() => - assistantMessages().reduce((count, message) => { - const parts = list(data.store.part?.[message.id], emptyParts) - return count + parts.filter((part) => partState(part, showReasoningSummaries()) === "visible").length - }, 0), - ) - const assistantTailVisible = createMemo(() => - assistantMessages() - .flatMap((message) => list(data.store.part?.[message.id], emptyParts)) - .flatMap((part) => { - if (partState(part, showReasoningSummaries()) !== "visible") return [] - if (part.type === "text") return ["text" as const] - return ["other" as const] - }) - .at(-1), - ) - const reasoningHeading = createMemo(() => - assistantMessages() - .flatMap((message) => list(data.store.part?.[message.id], emptyParts)) - .filter((part): part is PartType & { type: "reasoning"; text: string } => part.type === "reasoning") - .map((part) => heading(part.text)) - .filter((text): text is string => !!text) - .at(-1), - ) + const assistantDerived = createMemo(() => { + let visible = 0 + let tail: "text" | "other" | undefined + let reason: string | undefined + const show = showReasoningSummaries() + for (const message of assistantMessages()) { + for (const part of list(data.store.part?.[message.id], emptyParts)) { + if (partState(part, show) === "visible") { + visible++ + tail = part.type === "text" ? "text" : "other" + } + if (part.type === "reasoning" && part.text) { + const h = heading(part.text) + if (h) reason = h + } + } + } + return { visible, tail, reason } + }) + const assistantVisible = createMemo(() => assistantDerived().visible) + const assistantTailVisible = createMemo(() => assistantDerived().tail) + const reasoningHeading = createMemo(() => assistantDerived().reason) const showThinking = createMemo(() => { if (!working() || !!error()) return false if (status().type === "retry") return false