Push the composer draft subscription below ThreadDetailPromptArea (D1) - #1904
Closed
SawyerHood wants to merge 3 commits into
Closed
Push the composer draft subscription below ThreadDetailPromptArea (D1)#1904SawyerHood wants to merge 3 commits into
SawyerHood wants to merge 3 commits into
Conversation
SawyerHood
force-pushed
the
bb/mobile-perf/composer-draft-pushdown
branch
from
August 19, 2026 07:42
df70118 to
74407b8
Compare
SawyerHood
force-pushed
the
bb/mobile-perf/thread-bootstrap-include
branch
from
August 19, 2026 07:42
61e0f29 to
93e1c49
Compare
…draft writers Root cause: `usePromptDraftStorage` is the only way to get the draft's imperative API (setTextAndMentions, addAttachment, clearIfCurrentMatches, restoreIfEmpty, ...), and it is a useSyncExternalStore subscription, so any component that needs those methods re-renders on every keystroke even when it never renders the draft. `getPromptDraftAccessor` existed but only exposed getCurrent/setDraft/addQuote and returned fresh closures per call. Change: `createPromptDraftAccessor` builds the full imperative API once per storage key; `getPromptDraftAccessor` returns it and a new `usePromptDraftAccessor` memoizes it per key so it can sit in dependency lists and memoized child props. `usePromptDraftStorage` now layers the reactive draft on top of the same accessor (same return shape). Add `usePromptDraftHasSubmittableInput` (flips only when `promptDraftToInput(draft).length > 0` changes, unlike `usePromptDraftHasInput` which also counts whitespace) and `hasPromptDraftSubmittableInput`. Split `useActiveComposerDraftWriters` out of `useActiveComposerDraft` so the inline-or-stored draft writers can be used without the draft subscription. Co-Authored-By: Claude <noreply@anthropic.com>
Root cause (mobile-perf D1): `ThreadDetailPromptArea` called `useActiveComposerDraft`, a useSyncExternalStore subscription to the prompt-draft store, so every keystroke re-ran its ~1600-line body and ~70 hooks (thread creation options, typeahead, queued-message actions, attachment uploads, three queries and five mutations) and rebuilt the composer/attachments/plugin-host objects. Only the controlled composer and the plugin composer host actually need the live draft. Change: - The area now holds an imperative `usePromptDraftAccessor` plus a `usePromptDraftHasSubmittableInput` bit (needed for the modifier-submit shortcut and escape-to-hide, flips only on empty <-> non-empty). Send and modifier-submit read `promptDraft.getCurrent()` at event time. The queued-message inline editor uses the inline edit draft directly. - New `ThreadDetailFollowUpComposer` (wraps the memoized `FollowUpPromptBox`) owns the `usePromptDraftStorage` subscription: it builds the controlled composer config, the attachment items and the plugin composer host `draft`, and publishes the host (or the inline editor's host while one is open) to plugin hooks, exactly where the area used to. A pending permission/question is passed through as `FollowUpPromptBox`'s `pendingInteraction` with the reduced stack (D3), so the same `FollowUpPromptBox` instance stays mounted across approvals. `FollowUpPromptBox`'s contract is unchanged, so inline editors, side chats and EmbeddedThreadChat are untouched. - The stack/queue/banner assembly moves into a memoized `ThreadDetailPromptStack` child that also owns the banner-section, todo, workflow and background-command expansion state and the unarchive mutation, so toggling a card re-renders that subtree only. Goal and prompt-mode card state stays in the area because both branches show them. babel-plugin-react-compiler@1.0.0 still reports CompileSuccess for `ThreadDetailPromptArea` (400 slots) and for every new component/hook. Tests: `ThreadDetailPromptArea.keystrokes.test.tsx` uses the real draft store and asserts that after the first (empty -> non-empty) keystroke, 20 more keystrokes re-render neither the prompt area nor its stack children while the composer value and the published plugin host draft track the text; that submit sends the draft as typed; and that the live draft keeps reaching plugin hooks while a pending interaction hides the composer. Co-Authored-By: Claude <noreply@anthropic.com>
… swaps ThreadDetailPromptStack owned the todo/workflow/background-command/banner expand state, but the stack unmounts whenever the composer slot swaps (a pending permission or question, the composer hiding, a plugin composer taking over), so an expanded card collapsed after every approval. Before the D1 push-down that state lived in ThreadDetailPromptArea and persisted. Move it back into the area behind a memoized `useThreadDetailPromptStackExpansion` hook and pass it to the memoized stack; add a test that an expanded todo card stays expanded across a pending interaction round trip (fails on the previous commit). Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood
force-pushed
the
bb/mobile-perf/thread-bootstrap-include
branch
from
August 19, 2026 07:50
93e1c49 to
c931531
Compare
SawyerHood
force-pushed
the
bb/mobile-perf/composer-draft-pushdown
branch
from
August 19, 2026 07:50
74407b8 to
cdf3f13
Compare
SawyerHood
marked this pull request as ready for review
August 19, 2026 08:04
SawyerHood
marked this pull request as draft
August 19, 2026 15:09
Collaborator
Author
|
Closing: the maintainer decided to land only layers 1–22 of the mobile-perf stack (#1880–#1901). The branch stays on the remote for reference; it can be reopened and rebased later if the remaining big rocks (sidebar-bootstrap patching, thread bootstrap include=, composer draft push-down, timeline windowing, persisted cache, service worker) are picked up again. |
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(mobile-perf D1) calleduseActiveComposerDraft, auseSyncExternalStoresubscription to the prompt-draft store. Every keystroke re-ran its ~1600-line body and ~70 hooks (thread creation options, typeahead, queued-message actions, attachment uploads, three queries, five mutations) and rebuilt the composer, attachment and plugin-host objects. Only the controlled composer and the plugin composer host need the live draft.What changed
usePromptDraftStorage.ts:createPromptDraftAccessorbuilds the full imperative draft API once per storage key.getPromptDraftAccessorreturns it; newusePromptDraftAccessormemoizes it per key.usePromptDraftStoragelayers the reactive draft on the same accessor (same return shape). NewusePromptDraftHasSubmittableInputflips only whenpromptDraftToInput(draft).length > 0changes. NewhasPromptDraftSubmittableInputinprompt-draft.ts.useActiveComposerDraftWriterssplit out ofuseActiveComposerDraftso the inline-or-stored writers work without a subscription.ThreadDetailPromptAreaholds the accessor and the submittable bit. Send and modifier-submit readpromptDraft.getCurrent()at event time. The queued-message inline editor uses the inline edit draft directly.ThreadDetailFollowUpComposer.tsx:ThreadDetailFollowUpComposer(wraps the memoizedFollowUpPromptBox) andThreadDetailPendingInteractionComposerown the draft subscription, build the controlled composer config, attachment items and plugin hostdraft, and publish the host (or the inline editor host while one is open) to plugin hooks.FollowUpPromptBox's contract is unchanged.ThreadDetailPromptStack.tsx: memoized stack (workflow, background-command, todo cards, context banner, model fallback card, queued list) with its expansion state and the unarchive mutation. Goal and prompt-mode card state stays in the area because both branches show them.ThreadDetailPromptAreaCompileSuccess (400 slots); all new components/hooks compile.How you verified
apps/app/src/views/thread-detail/ThreadDetailPromptArea.keystrokes.test.tsx(real draft store): after the empty -> non-empty keystroke, 20 more keystrokes do not re-render the prompt area (0 extrauseThreadCreationOptionscalls; was 20 before) norThreadPromptContextBanner,ThreadTodoCard,QueuedMessagesList, while the composer value and the published plugin host draft track the text; submit sends the draft as typed and clears it; the live draft keeps reaching plugin hooks while a pending interaction replaces the composer. First test fails on the parent branch (22 renders vs 2).pnpm exec turbo run typecheck --filter=@bb/app: pass.pnpm exec turbo run lint --filter=@bb/app: 0 errors, warning count unchanged from base (pre-existing).pnpm exec turbo run test --filter=@bb/app --force: 371 files, 2913 passed, 3 skipped.Fixes: part of the mobile / iOS Safari performance program (verified sweep report in the bb thread; no single issue).
Stack context
Layer 25 of 28 in the
bb/mobile-perf/*stack (bottom → top: quick wins first, big rocks last).bb/mobile-perf/thread-bootstrap-include(Bundle per-thread reads into GET /threads/:id?include= and seed the caches (B5) #1903).bb/mobile-perf/timeline-windowing(Window the top-level timeline on phones and evict far pages (C1 step 2) #1905).