Skip to content

Push the composer draft subscription below ThreadDetailPromptArea (D1) - #1904

Closed
SawyerHood wants to merge 3 commits into
bb/mobile-perf/thread-bootstrap-includefrom
bb/mobile-perf/composer-draft-pushdown
Closed

Push the composer draft subscription below ThreadDetailPromptArea (D1)#1904
SawyerHood wants to merge 3 commits into
bb/mobile-perf/thread-bootstrap-includefrom
bb/mobile-perf/composer-draft-pushdown

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

ThreadDetailPromptArea (mobile-perf D1) called useActiveComposerDraft, a useSyncExternalStore subscription 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: createPromptDraftAccessor builds the full imperative draft API once per storage key. getPromptDraftAccessor returns it; new usePromptDraftAccessor memoizes it per key. usePromptDraftStorage layers the reactive draft on the same accessor (same return shape). New usePromptDraftHasSubmittableInput flips only when promptDraftToInput(draft).length > 0 changes. New hasPromptDraftSubmittableInput in prompt-draft.ts.
  • useActiveComposerDraftWriters split out of useActiveComposerDraft so the inline-or-stored writers work without a subscription.
  • ThreadDetailPromptArea holds the accessor and the submittable bit. Send and modifier-submit read promptDraft.getCurrent() at event time. The queued-message inline editor uses the inline edit draft directly.
  • New ThreadDetailFollowUpComposer.tsx: ThreadDetailFollowUpComposer (wraps the memoized FollowUpPromptBox) and ThreadDetailPendingInteractionComposer own the draft subscription, build the controlled composer config, attachment items and plugin host draft, and publish the host (or the inline editor host while one is open) to plugin hooks. FollowUpPromptBox's contract is unchanged.
  • New 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.
  • babel-plugin-react-compiler@1.0.0: ThreadDetailPromptArea CompileSuccess (400 slots); all new components/hooks compile.

How you verified

  • New 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 extra useThreadCreationOptions calls; was 20 before) nor ThreadPromptContextBanner, 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).

AGENT GENERATED: by Claude Opus 5

@SawyerHood SawyerHood changed the title bb/mobile perf/composer draft pushdown Push the composer draft subscription below ThreadDetailPromptArea (D1) Aug 19, 2026
@SawyerHood
SawyerHood force-pushed the bb/mobile-perf/composer-draft-pushdown branch from df70118 to 74407b8 Compare August 19, 2026 07:42
@SawyerHood
SawyerHood force-pushed the bb/mobile-perf/thread-bootstrap-include branch from 61e0f29 to 93e1c49 Compare August 19, 2026 07:42
SawyerHood and others added 3 commits August 19, 2026 07:50
…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
SawyerHood force-pushed the bb/mobile-perf/thread-bootstrap-include branch from 93e1c49 to c931531 Compare August 19, 2026 07:50
@SawyerHood
SawyerHood force-pushed the bb/mobile-perf/composer-draft-pushdown branch from 74407b8 to cdf3f13 Compare August 19, 2026 07:50
@SawyerHood
SawyerHood marked this pull request as ready for review August 19, 2026 08:04
@SawyerHood
SawyerHood marked this pull request as draft August 19, 2026 15:09
@SawyerHood

Copy link
Copy Markdown
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.

@SawyerHood SawyerHood closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant