Make the plugin composer host pull-based so keystrokes stop re-rendering the thread shell - #2281
Open
vburojevic wants to merge 1 commit into
Open
Make the plugin composer host pull-based so keystrokes stop re-rendering the thread shell#2281vburojevic wants to merge 1 commit into
vburojevic wants to merge 1 commit into
Conversation
Root cause (mobile telemetry: 19.6s frame hang after keydown in the composer): ThreadDetailPromptArea's published PluginComposerHost embedded the live draft as a value field (draft: currentPromptDraft), so every keystroke built a new host object. usePublishPluginComposerHost's identity check notified the pane scope, ThreadDetailSecondaryContentBody (usePluginComposerHost) re-rendered SecondaryPanelLayout -> ThreadTimelinePane (fresh element per render) -> PageShell -> ThreadTimelineSurface per character, and SecondaryPanelLayout republished its hosted-panel model to the workspace host per character. Change: PluginComposerHost drops the draft value field; the live draft is exposed as getCurrent() + subscribeDraft() and read through the new usePluginComposerHostDraft hook, so only actual draft consumers re-render. - usePromptDraftStorage and getPromptDraftAccessor gain a stable per-key subscribe; the thread and new-thread hosts pass it through and become identity-stable per thread/project. - Hosts whose draft lives in React state (inline queued-message and sent-message editors, EmbeddedThreadChat's pair) get subscribeDraft from useComposerHostDraftNotifier, which notifies from a layout effect once a render committed a different draft. In EmbeddedThreadChat the notifiers are declared after ALL ref-sync layout effects, including the active-identity syncs: a thread switch changes the host identity and the draft in one commit, and useSyncExternalStore reads the snapshot inside the notification, so notifying before the identity sync would hand every subscriber the stale pre-switch draft with no later notification to correct it. - The queued-message and sent-message hosts are keyed on their session scalars (editSessionId/operationId), so every host in the app now honors the identity-stable contract; the published value only flips between two stable identities (thread host <-> session host). EmbeddedThreadChat's ...WithDraft wrapper memos are deleted. - Draft readers move to the hook: useComposerView/useComposer (plugin SDK contract unchanged - same values, same update timing) and FollowUpPromptBoxStackOnly. Tests: ThreadDetailPromptArea.keystrokes.test.tsx uses the real draft store and asserts that 21 keystrokes cause zero re-renders of a shell probe holding the published host while a subscribed consumer tracks every character; that submit reads the draft imperatively at event time; that external store writes reach consumers while a pending interaction hides the composer; and that inline queued edits publish one per-session host, stream keystrokes without shell renders, and restore the identical thread host on close. ThreadDetailSecondaryContent.test.tsx mounts the real body/SecondaryPanelLayout with a store-backed publisher in the footer slot and asserts the timeline pane render count stays flat across 20 draft writes. EmbeddedThreadChat.test.tsx switches threads with pre-seeded drafts and asserts subscribers observe the new thread's draft at the notification itself (fails when the notifiers run before the identity sync). All of these fail against the previous per-keystroke host identity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
What was wrong
ThreadDetailPromptArea rebuilt its published PluginComposerHost every keystroke (the draft rode on the host object), so the identity-change notify re-rendered ThreadDetailSecondaryContentBody → SecondaryPanelLayout → ThreadTimelinePane → PageShell per character typed. Production telemetry attributed the worst mobile hang (19.6s after keydown) to this class.
What changed
PluginComposerHost drops the draft field; the contract is an identity-stable host exposing getCurrent() + subscribeDraft(), with usePluginComposerHostDraft as the only reactive read. Store-backed hosts (thread, new-thread) subscribe through the prompt-draft store; state-backed hosts (queued, sent-message, embedded chat) notify from layout effects ordered after the identity-ref sync — a review-caught ordering bug where a thread switch could briefly expose the previous thread's draft is fixed and regression-tested both ways. All hosts (including sent-message) honor the identity-stable contract. Plugin SDK exports (packages/plugin-sdk) are untouched; useComposerView values and timing are unchanged, and the SDK version-lockstep gate confirms no surface files changed.
How verified
336 targeted tests / 17 files: keystroke render-count probes (shell must not re-render while typing; draft consumers must update), the thread-switch ordering regression (fails on the broken ordering), secondary-content probes, plus upstream's escape/collapse tests in the touched areas. Typecheck + oxlint at clean-tree baseline; adversarially reviewed, rebased over the zen-mode removal (#2254) adopting upstream semantics.
🤖 Generated with Claude Code