refactor(chat): publish composer geometry, not a bare measurement - #29608
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The refactor is cohesive, removes duplicated arithmetic safely, preserves documented behavioral differences, and adds targeted tests to lock in the geometry and render invariants.
Pull request overview
Refactors the mobile chat composer sizing logic to publish a structured “composer geometry” model (instead of a single raw measurement), centralizing all derivations/constants and splitting the published values across two contexts to avoid unnecessary message-list re-renders on layout measurement/rotation.
Changes:
- Introduces
composer-geometryas the single source of truth for composer box sizing, panel heights, and keyboard/inset-derived helpers. - Replaces the old
MaxInputAreaContextwithComposerAnchorContext(stable, non-layout-derived) andComposerBoxContext(layout-derived sizes). - Adds focused unit/component tests covering geometry arithmetic and the “anchor doesn’t re-render on measurement” invariant.
File summaries
| File | Description |
|---|---|
| shared/chat/conversation/normal/index.tsx | Provides the new anchor/box contexts and uses computeComposerBox for measured layout-derived sizing. |
| shared/chat/conversation/list-area/index.tsx | Switches list keyboard/inset math to read from ComposerAnchorContext and uses geometry helpers. |
| shared/chat/conversation/input-area/suggestors/index.tsx | Uses ComposerBoxContext/geometry helpers for suggestion area sizing while preserving portal fallback behavior. |
| shared/chat/conversation/input-area/normal/max-input-area-context.tsx | Removes the now-obsolete raw measurement context. |
| shared/chat/conversation/input-area/normal/input.tsx | Uses ComposerBoxContext + expandedInputMaxHeight for expanded input and suggestion list sizing. |
| shared/chat/conversation/composer-viewport-context.tsx | Adds the two new contexts and defines their non-conversation fallbacks. |
| shared/chat/conversation/composer-viewport-context.test.tsx | Adds render-probe tests validating context split and render behavior. |
| shared/chat/conversation/composer-geometry.ts | Adds centralized geometry model/constants and worklet-safe helpers. |
| shared/chat/conversation/composer-geometry.test.ts | Adds unit tests locking in the shipped arithmetic and edge cases. |
| shared/chat/conversation/command-markdown.tsx | Switches command-markdown max-height to the new geometry-derived value. |
Review details
- Files reviewed: 10/10 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
MaxInputAreaContext published a single number, so five modules each re-derived the composer's geometry from it plus their own insets and keyboard animation. The sticky offset existed twice byte-identically, the `- inputAreaHeight - 15` term twice in one file, and the 0.35 panel ratio twice across two files with different clamping. Re-syncing those copies is what the last two suggestion-popup fixes were doing. computeComposerBox now owns every derivation and every constant. The model ships as two contexts, split on whether a value comes from layout: the box changes identity when the conversation is measured, while the bottom inset, the sticky offset and the keyboard shared values do not. The message list reads only the latter, so measuring the box no longer re-renders it. Keep that seam — merging the two costs a list render on every mount and rotation, and there is a test that fails if you do. The keyboard-driven parts stay animated: they are worklet helpers taking the shared value's current frame, never React state. Behaviour is unchanged. Where two call sites disagreed today they still do: the command-markdown panel keeps its unclamped 35% and its 250pt pre-layout backstop, while the suggestion list keeps its [120, 240] clamp, its leftover-reserve cap and its 0. The suggestion popup is portaled to a host outside the router, so the conversation's contexts cannot reach it; it keeps its local hooks and its height clamp stays inert exactly as before, now documented. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015rccpV5nLxxC5opF5xzrz7
chrisnojima
force-pushed
the
nojima/HOTPOT-arch-04-composer-geometry
branch
from
September 25, 2026 20:33
1e08966 to
1e002a4
Compare
- The anchor context has no default any more: its keyboard values are real shared values only a conversation owns, so a consumer outside the provider now throws instead of silently reading a zero inset and a frozen keyboard. - The box depends on the measured height alone. The container height and the sticky offset are local to NativeConversation, so a header or window change no longer rebuilds the box and re-renders its consumers twice. - The fixed input heights are plain exports instead of box fields. - MobileSuggestionArea drops its context read and height style: the portal keeps it outside the provider, so the clamp never applied, as on master. - The memo split lives in ComposerProvider, and the re-render test drives that provider instead of a hand-copied mirror of it.
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.
Problem
input-area/normal/max-input-area-context.tsxwas three lines — its entire interface wasReact.createContext(0).It published a raw measurement where callers needed a geometry model, so five modules re-derived that model from the integer plus their own inset and keyboard hooks:
{closed: -insets.bottom, opened: 0}appeared byte-identical innormal/index.tsx:147andsuggestors/index.tsx:457, the second with a 4-line comment explaining it must mirror the first.maxInputArea * 0.35appeared independently ininput.tsx:1263andcommand-markdown.tsx:20.- inputAreaHeight - 15appeared twice ~35 lines apart ininput.tsx, with15a bare literal both times.Two shipped fixes three weeks apart edited the same file to re-sync these derivations. Zero tests covered any of the arithmetic.
Change
One pure
computeComposerBox(measuredHeight)owns every derivation and every constant, now named:composerPanelHeightRatio,expandedInputTopGap,commandMarkdownFallbackMaxHeight. The fixed input heightssingleLineHeight/threeLineHeightare plain exports, not box fields.ComposerProviderpublishes the model as two contexts split along a stable/layout seam, so consumers only re-render for what they read:useComposerAnchor()) —{bottomInset, keyboardHeight, keyboardProgress}. Nothing layout-derived, so its identity survives every measurement.ListAreareads this and only this. It has no default: the keyboard values are real shared values only a conversation owns, so reading it outside the provider throws rather than silently pinning the list and input to a zero inset and a closed keyboard.ComposerBoxContext— the measured box and sizes derived from it, keyed on the measured height alone. Read only by the composer's own panels. Its default is the pre-layout box, which is what the desktop command-markdown panel gets.The container height (window minus header) and the sticky offset stay local to
NativeConversation, so a header or window change does not rebuild the box.The invariant is stated on the type: "Do not add layout-derived fields." Keyboard values stay reanimated shared values — nothing became React state.
maxInputAreaand0.35now have zero occurrences outside the geometry module.Two disagreeing derivations, both preserved
commandMarkdownMaxHeightis unclamped with a 250pt backstop; the suggestion list is clamped to[120, 240]and reserve-capped with a 0 fallback. At h=200 these give 70 vs 16. Preserved as-is rather than silently unified.Dead clamp removed
MobileSuggestionArea's height clamp has never executed —@gorhom/portalrenders it at a host outside the router, so the provider never reaches it and the height has always beenundefined. Its context read and animated height style are deleted; behaviour matches master. Making the clamp bite would be a real behaviour change and is left for a separate PR.Validation
lint:allclean —0 bailed out, 0 whole-props deps, tsc clean both projects.yarn test:unit— 253 suites / 2568 tests.New
composer-geometry.test.tsandcomposer-viewport-context.test.tsx. The latter drives the realComposerProviderwith render-counting probes proving measuring the box does not re-render the list and an inset change does not re-render the panels; both directions of merging the two memos fail it. It also checks that reading the anchor outside a provider throws.