Conversation
Once a session had usage, every applyMessages minted a new session object with a fresh latestUsage, and SessionViewLoaded keyed its usageData memo on that object identity, so AgentInput (and everything below it) re-rendered per message for as long as the tab stayed open. The same tree also subscribed to the whole messages array just to know whether any exist and are loaded. Key the memo on the usage fields and subscribe to the two booleans only. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
ChatListInternal and NewerEnd used useSession(sessionId) but only read thinking, whether a permission request is pending, and controlledByUser. Every other field change on the open session (activeAt heartbeats, usage, draft saves every 2 s while typing) re-rendered the whole list pipeline and FlashList. A useShallow selector returning exactly those booleans makes the list indifferent to the rest of the object. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…ewest message oldestRenderedId only ever moved older and was reset only on session change, so after scrolling to the top of a long chat the window stayed at thousands of messages for the rest of the tab's life, and every later update of that session ran windowing, grouping, copy-text and list building over all of them. Reset the window in scrollToBottom and whenever a scroll event lands exactly on the newest message; the pin effect re-pins INITIAL_WINDOW and the dropped rows sit past the far end of the inverted list. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
buildAgentTurnCopyTextByMessageId runs on every change to the rendered window — every message of the open session — and re-joined the full text of every completed turn into strings identical to the previous ones, all garbage. Cache the joined text per final message id and reuse it while the turn's message identities are unchanged (store messages are immutable), which also keeps the string identity MessageView's memo compares. Bounded by a wipe at 2000 turns; a wipe costs one rebuild. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
contentContainerStyle, onLayout, ListHeaderComponent and ListFooterComponent were created fresh on every ChatListInternal render, so FlashList saw a prop change on each of them every time the list re-rendered. Memoize the style, the layout handler and the two end components on their real inputs. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Every render of every tool row whose result is a tool-use error logged the full result object; in a long-lived tab with big chats that is a steady stream of console entries (and retained objects in DevTools) for no benefit. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On a web tab that stays open for hours on a chat with thousands of messages, every message applied to the open session re-rendered far more than the row it touched.
SessionViewLoadedkeyed itsusageDatamemo on the session object identity, whichapplyMessagesmints fresh on every batch once the session has usage, soAgentInputand everything below it re-rendered per message (sources/-session/SessionView.tsx).ChatListInternalandNewerEndsubscribed to the whole session object throughuseSessionwhile reading three booleans, soactiveAtheartbeats, usage updates and the 2 s draft saves while typing re-ran the whole list pipeline and FlashList (sources/components/ChatList.tsx). After scrolling to the top once,oldestRenderedIdnever moved back, so windowing, grouping, copy-text and list building ran over the entire history on every later update.buildAgentTurnCopyTextByMessageIdre-joined the text of every completed turn on each of those runs (sources/utils/agentTurnCopy.ts), and FlashList received a freshcontentContainerStyle,onLayout, header and footer on every render.This PR keys the usage memo on primitives, subscribes the list to exactly the three flags it reads (
useShallow), shrinks the render window back toINITIAL_WINDOWwhen the reader is back at the newest message, caches each turn's copy text by final message id (bounded, wiped at 2000 entries), memoizes the FlashList props on their real inputs, and drops a per-renderconsole.loginToolViewthat fired for every errored tool row.Changes
SessionViewLoaded'susageDatamemo on the usage fields and subscribe tohasMessages/isLoadedbooleans instead of the messages arrayChatListInternalandNewerEndtothinking, pending-permission andcontrolledByUservia auseShallowselector instead of the whole sessionINITIAL_WINDOWinscrollToBottomand when a scroll event lands on the newest messagebuildAgentTurnCopyTextByMessageIdoutput per final message id while the turn's message identities are unchangedcontentContainerStyle,onLayout,ListHeaderComponentandListFooterComponentconsole.logof the tool result for errored tool rows inToolViewTest plan
pnpm typecheck(clean)pnpm exec vitest run sources/components/ChatList.test.ts sources/utils/agentTurnCopy.test.ts sources/hooks/useGroupedMessages.test.ts(25 passed;agentTurnCopy.test.ts"reuses the joined text of a turn whose messages are unchanged" fails without the cache)🤖 Generated with Claude Code