Repository navigation
perf(desktop): subscribe components to the session fields they use - #169
Merged
Merged
Conversation
GoDiao
force-pushed
the
perf/fine-grained-selectors
branch
from
October 23, 2026 14:27
1752461 to
0cbde96
Compare
ActiveSession, ChatInput, ModelSelector, StreamingIndicator, MessageList, PermissionDialog and AskUserQuestion select only the fields or derived values they render instead of the whole session object or store. Each tool group gets only its own results, children and agent notifications, with references kept while they are unchanged, so finished groups no longer re-render while a reply streams. Part of #153
GoDiao
force-pushed
the
perf/fine-grained-selectors
branch
from
October 23, 2026 14:29
0cbde96 to
d5b024e
Compare
GoDiao
added a commit
that referenced
this pull request
Oct 24, 2026
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
Components that used to subscribe to the whole session object (or the whole chat store) now subscribe only to the fields, or derived values, they render. Each tool group gets only its own results, with stable references, so finished groups stop re-rendering while a reply streams.
Part of #153. Thinking-delta batching (#155 / #160) is untouched.
What changed
Store (
chatStore.ts) – two small hooks shared by several components:useSessionField(sessionId, key)– one field of one session.useSessionRuntimeSelection(sessionId)– the desired runtime viauseShallow, so the reference survives mirror snapshots that don't change it.Per component
ActiveSession– a boolean "is running" selector instead ofchatState, a boolean for "any background task running", a boolean for "has messages or streaming text" (the only thingisEmptyneeded), andactiveGoal.ChatInput– actions through their own selectors instead ofuseChatStore(); per-fieldchatState,slashCommands(stable empty fallback),composerPrefill,composerInsertion,permissionMode, the effective permission mode, the runtime selection, andmessages.length(the only use ofmessages).ModelSelector(child of the composer, same whole-session pattern) – usesuseSessionRuntimeSelection.StreamingIndicator–chatState,statusVerb,apiRetry, the turn'sstartedAtandoutput_tokensas primitives.PermissionDialog–respondToPermissionselector and a boolean "is my request pending".AskUserQuestion–respondToPermissionselector and the pending request id when it matches itstoolUseId.MessageList– per-field selectors, plus derivedactiveAskUserQuestionToolUseIdandhasActiveTool. It still re-renders on every text flush because it rendersstreamingText; splitting that out is Render streaming replies incrementally instead of re-parsing the whole reply #154.Tool results per group –
buildToolGroupResultsgives each tool group only its own tool calls, results, child calls and agent notifications, and reuses the previous group's arrays/maps when their entries are unchanged.ToolCallGroupwas alreadymemo, so finished groups now skip rendering. The derivedtoolResultByToolUseIdmap (new{content, isError}objects on every rebuild) is gone;MessageBlockgets the tool result message itself, which has a stable reference.Measurement
How: a render-count harness in the tests (
streamingRenderScenario.ts) plays one streaming reply as 31 store updates shaped like the real ones: a user message and thinking state, 20 text flushes, 5 token-usage updates, a tool call, a permission request and its resolution, the tool result, and the final message. Each component is rendered under its ownReact.Profiler, and the count is the number of commits for that subtree during the reply.ActiveSessionis measured with its heavy children stubbed (its own test file). Tool groups are counted with a counting wrapper aroundToolCallGroup. "Before" is the same harness run onmain(6f07ea9) before any source change.The remaining renders are the ones where a shown field changed: ChatInput on the 8
chatStatechanges, StreamingIndicator on status/verb/token changes, the live group on its appearance,isStreamingflips around the permission prompt, and its result. MessageList is #154.Tests
streamingRenderCounts.test.tsx/ActiveSession.renderCounts.test.tsx– the harness above, with the after numbers asserted (MessageList pinned at 62, so a regression to a whole-store subscription fails at 65).permissionMode, the effective mode,composerPrefill,slashCommandsandchatState; StreamingIndicator onstatusVerb,tokenUsageandapiRetry; PermissionDialog and AskUserQuestion on their pending request; ActiveSession on running state, background tasks, the goal and the first message; a tool group re-rendering with its own result while the history group does not; the runtime selection keeping its reference until the desired runtime changes.buildToolGroupResultsunit tests: per-group results, children and grandchildren, per-group agent notifications, and reused references for unchanged groups (a changed notification gets a new reference, an unchanged one keeps it).cd desktop && bun run test– 117 files, 1209 passed.bun run lint(tsc --noEmit) – clean.