Skip to content

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

Draft
chrisnojima wants to merge 1 commit into
masterfrom
nojima/HOTPOT-arch-04-composer-geometry
Draft

refactor(chat): publish composer geometry, not a bare measurement#29608
chrisnojima wants to merge 1 commit 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() owns every derivation and every constant, now named: composerPanelHeightRatio, expandedInputTopGap, commandMarkdownFallbackMaxHeight, singleLineHeight, threeLineHeight.

The context is split along a stable/layout seam, so consumers only re-render for what they read:

  • ComposerAnchorContext{bottomInset, stickyOffset, keyboardHeight, keyboardProgress}. Nothing layout-derived, so its identity survives every measurement. ListArea reads this and only this.
  • ComposerBoxContext — the measured box and sizes derived from it. Read only by the composer's own panels.

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.

Dormant code, deliberately left dormant

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. Preserved exactly; switching it on is a real behaviour change. emptyViewport reproduces master's out-of-provider fallbacks exactly (250 / 0 / 36 / 78).

Validation

lint:all clean — 0 bailed out, 0 whole-props deps, tsc clean both projects.
jest --runInBand232 suites / 2257 tests (baseline 230 / 2236).

New composer-geometry.test.ts (18 tests) and composer-viewport-context.test.tsx with render-counting probes proving measuring the box does not re-render the list.

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.

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

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.

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