Skip to content

refactor(chat): publish composer geometry, not a bare measurement - #29608

Merged
chrisnojima merged 2 commits into
masterfrom
nojima/HOTPOT-arch-04-composer-geometry
Sep 25, 2026
Merged

chrisnojima merged 2 commits into
masterfrom
nojima/HOTPOT-arch-04-composer-geometry

Conversation

@chrisnojima

@chrisnojima chrisnojima commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Problem

input-area/normal/max-input-area-context.tsx was three lines — its entire interface was React.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 in normal/index.tsx:147 and suggestors/index.tsx:457, the second with a 4-line comment explaining it must mirror the first.
  • maxInputArea * 0.35 appeared independently in input.tsx:1263 and command-markdown.tsx:20.
  • - inputAreaHeight - 15 appeared twice ~35 lines apart in input.tsx, with 15 a 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 heights singleLineHeight / threeLineHeight are plain exports, not box fields.

ComposerProvider publishes the model as two contexts split along a stable/layout seam, so consumers only re-render for what they read:

  • Anchor (useComposerAnchor()) — {bottomInset, keyboardHeight, keyboardProgress}. Nothing layout-derived, so its identity survives every measurement. ListArea reads 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.

maxInputArea and 0.35 now have zero occurrences outside the geometry module.

Two disagreeing derivations, both preserved

commandMarkdownMaxHeight is 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/portal renders it at a host outside the router, so the provider never reaches it and the height has always been undefined. 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:all clean — 0 bailed out, 0 whole-props deps, tsc clean both projects.
yarn test:unit — 253 suites / 2568 tests.

New composer-geometry.test.ts and composer-viewport-context.test.tsx. The latter drives the real ComposerProvider with 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.

Note on mutation testing here: babel-plugin-react-compiler runs in the jest config and re-derives memoization from what an object literal actually reads, discarding a deliberately-broken useMemo dep. A mutation aimed at a dep array can false-pass; it has to change what the code reads.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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-geometry as the single source of truth for composer box sizing, panel heights, and keyboard/inset-derived helpers.
  • Replaces the old MaxInputAreaContext with ComposerAnchorContext (stable, non-layout-derived) and ComposerBoxContext (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
chrisnojima force-pushed the nojima/HOTPOT-arch-04-composer-geometry branch from 1e08966 to 1e002a4 Compare September 25, 2026 20:33
- 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.
@chrisnojima
chrisnojima marked this pull request as ready for review September 25, 2026 20:54
@chrisnojima
chrisnojima requested a lite review from Copilot September 25, 2026 20:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Address the unnecessary input re-renders and rename the geometry module to follow repository conventions.

Review effort: Lite
Findings: None

@chrisnojima
chrisnojima requested a balanced review from Copilot September 25, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chrisnojima
chrisnojima merged commit 5584146 into master Sep 25, 2026
2 checks passed
@chrisnojima
chrisnojima deleted the nojima/HOTPOT-arch-04-composer-geometry branch September 25, 2026 22:02
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.

2 participants