fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(mobile): keep the composer from covering the last message on thread open - #4999

Open
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race
Open

fix(mobile): keep the composer from covering the last message on thread open#4999
carTloyal123 wants to merge 1 commit into
pingdotgg:mainfrom
carTloyal123:fix/mobile-composer-inset-race

Conversation

@carTloyal123

@carTloyal123carTloyal123 commented Jul 30, 2026

Copy link
Copy Markdown

What changed

Opening a remote thread could leave the last message pinned underneath the chat input bar, with no way to scroll down to it.

Why it happens

The feed's bottom inset is derived from an asynchronous measurement of the floating composer (onComposerLayoutcontentInsetEndAdjustment), not from layout flow — the composer is an absolutely-positioned overlay, so the list has to reserve space for it explicitly.

Meanwhile ThreadFeed remounts its list synchronously the moment messages arrive (the emptyfilledlistMountKey) and primes the fresh instance from whatever that shared value currently holds.

The composer's real height is data-dependent too. The "Loading messages..." / "Syncing messages..." pill and the pending approval / user-input cards appear on that same transition, animated over 220ms. So the composer's true height lands a frame or more after the list has already finished its initial end-positioning against a stale, too-small inset — and nothing re-scrolls afterwards.

On top of the timing race, the status pill was rendered outside layout flow (absolute bottom-full inside the composer), so the overlay's onLayout measurement never included it at all — the reserved inset was structurally wrong whenever the pill was visible, not just transiently.

The fix

Three parts — the measurement, the correction, and the estimate each needed work:

  • Make the measurement truthful. The status pill now renders in normal layout flow inside the measured composer overlay (next to the pending approval/user-input cards, which already lived there), instead of absolutely positioned inside ThreadComposer. The overlay's onLayout height therefore genuinely includes everything the feed must reserve space for, and "the real measurement wins" is actually correct. This also collapses the duplicated composerConnectionStatus computation (screen-side estimate vs composer-side render) into one call, and drops the composer props that only existed to feed the pill.

  • Correct once the real height is known. The composer's measured height is mirrored into plain React state (the reanimated shared value can't trigger effects) and threaded to ThreadFeed, which distinguishes the initial prime for a fresh list from a later correction. When the height changes post-mount it re-reports the inset and re-pins to the end if the user is still near it.

  • Over-reserve during the pre-measurement frames. The seed estimate adds conservative chrome for the status pill and pending cards whenever they're knowable synchronously from props, so the brief window before the first onLayout errs tall rather than short. These constants are now only first-frame seeds (the standard estimated-size pattern) — correctness no longer depends on them, since the real measurement includes the pill and cards and always overrides.

Also tightens the bottom > 0 guard, which previously let a zero-inset mount count as primed and skip the corrective path entirely.

Notes

No visual change when things go right — the pill sits a few px higher (it now clears the composer's top padding), otherwise this only affects the failure case. Addresses the high-severity finding from Cursor Bugbot and Macroscope that the absolutely-positioned pill was excluded from the measured height.

Verification

  • tsc --noEmit clean
  • vp lint clean on changed files
  • Full mobile suite green (580 tests)

Branched off current main; the patch applies cleanly on top of #4882.

Related issues

No open issue tracks this exact mobile bug, so nothing is auto-closed. Related but not closed by this PR: #4619 — the web ChatView has the same architecture (composer overlay measurement feeding LegendList's contentInsetEndAdjustment) and shows the mirror-image transient symptom (over-inset blank space at the end of long threads, self-correcting later). Its "stale composer measurement" investigation lead matches the race fixed here; the web side likely wants the same late-measurement correction.


Note

Medium Risk
Touches chat list inset priming and auto scroll-to-end behavior on inset updates; wrong guards could surprise users who scrolled up, but changes are scoped to near-bottom, no-drag cases.

Overview
Fixes mobile thread open where the last message could sit under the floating composer because the feed’s bottom inset was seeded from estimates and async onLayout while ThreadFeed remounts when messages arrive.

ThreadDetailScreen adds COMPOSER_PENDING_CARD_MIN_CHROME to the overlap estimate when pending approval or user-input cards are active, tracks collapsed composer overlay height from layout (skipping updates while the composer is keyboard-expanded), and passes composerMeasuredHeight into the feed.

ThreadFeed reprimes LegendList’s bottom inset on mount and when that measured height changes: initial mount for a listMountKey only reports the inset; later updates scroll to end if the user is still near the bottom and hasn’t dragged since mount. onScrollBeginDrag sets that drag guard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Fix thread composer covering the last message on thread open

  • Computes a composerExtraChrome value in ThreadDetailScreen.tsx that adds COMPOSER_PENDING_CARD_MIN_CHROME (160px) per visible pending card (approval or user input) to the bottom overlap/inset.
  • Captures the composer overlay's measured height when not expanded and passes it to ThreadFeed.tsx so the feed knows how much space the composer occupies.
  • Auto-scrolls to the end of the feed when the content inset changes (e.g. composer height changes), provided the user was near the end and has not manually dragged since mount.
  • Tracks composer expansion via a ref in addition to state so layout measurement is suppressed during keyboard transitions.

Macroscope summarized 0a62ad9.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 30, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@coderabbitai

coderabbitaiBot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ab55ef52-7d02-4885-9cee-05160f580e42

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@macroscopeapp

macroscopeappBot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This mobile UI fix has two unresolved review comments identifying potential bugs: the scrollToEnd logic may not trigger when the composer is expanded, and it may conflict with anchor-based scrolling after message sends. These substantive issues warrant human review before merging.

You can customize Macroscope's approvability policy. Learn more.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 34f0ff2 to e4f2237CompareJuly 30, 2026 19:12
@skjiisa

skjiisa commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

I haven't looked at the proposed fix myself, but this is an issue I have also noticed that bugs me. The bottom safe-area seems inconsistent depending on things like keyboard and sheet states.

macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 00:49

Dismissing prior approval to re-evaluate 73772b5

@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
macroscopeapp[bot]
macroscopeappBot previously approved these changes Jul 31, 2026
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 73772b5 to 67c9bb9CompareJuly 31, 2026 01:44
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 67c9bb9 to a20141cCompareJuly 31, 2026 02:28
@macroscopeapp
macroscopeappBot dismissed their stale reviewJuly 31, 2026 02:28

Dismissing prior approval to re-evaluate a20141c

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from a20141c to 1522d46CompareJuly 31, 2026 03:22
Comment threadapps/mobile/src/features/threads/ThreadDetailScreen.tsx Outdated
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 1522d46 to 32f6a09CompareJuly 31, 2026 04:45
@github-actionsgithub-actionsBot added size:M 30-99 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 31, 2026
Comment threadapps/mobile/src/features/threads/ThreadFeed.tsx
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 32f6a09 to 0aa65aaCompareJuly 31, 2026 04:57
@@ -218,6 +234,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
Math.max(0, estimatedOverlayHeight - nativeInsetOvercount),
-nativeInsetOvercount,
);
const handleComposerOverlayLayout = useCallback(

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.

🟠 Highthreads/ThreadDetailScreen.tsx:237

When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — ThreadFeed's corrective scrollToEnd effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. handleComposerOverlayLayout skips updating composerMeasuredHeight whenever composerExpandedRef.current is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's props.composerMeasuredHeight dependency. Consider updating composerMeasuredHeight on every layout, or using the overlay's measured height from onComposerLayout as the effect dependency instead of the gated value.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/mobile/src/features/threads/ThreadDetailScreen.tsx around line 237:
When the composer is focused (expanded) and the feed list remounts — e.g. after a pending-approval or pending-user-input card appears or the empty→filled transition fires — `ThreadFeed`'s corrective `scrollToEnd` effect never runs. The list stays positioned with a stale bottom inset and rests one composer-height short of the end. `handleComposerOverlayLayout` skips updating `composerMeasuredHeight` whenever `composerExpandedRef.current` is true, so the new overlay measurement (which the pending cards make larger) does not trigger the effect's `props.composerMeasuredHeight` dependency. Consider updating `composerMeasuredHeight` on every layout, or using the overlay's measured height from `onComposerLayout` as the effect dependency instead of the gated value.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Pushing back on this one — both premises do not hold, and the suggested fix (mirror on every layout) is the exact code that caused a reproduced-on-device bug. (1) The gate only skips the React-state mirror; onComposerLayout still runs on every layout, so contentInsetEndAdjustment is always current and a remounting list primes from the live measurement in the listMountKey layout effect — there is no stale-inset remount. (2) While the composer is expanded the keyboard is up, and the keyboard integration reacts to that same shared value (useExtraContentPadding), inset- and scroll-compensating overlay growth natively. Mirroring during that window made the corrective effect reportContentInset the resting overlay height while the keyboard inset was active, which pinned the feed under the composer on a real device — that regression is why the gate exists. Any residue heals on collapse: the collapse onLayout mirrors the final height and the correction runs with the keyboard closed.

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.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 0aa65aa to 3540214CompareJuly 31, 2026 05:04
@carTloyal123

Copy link
Copy Markdown
Author

Been using this for a few hours and generally is better. There are still some other weird behaviors with threads rendering in the right place with respect to the composer box and keyboard but this is a decent start.

…ad open
Opening a remote thread could leave the last message pinned underneath the
floating composer with no way to scroll to it. The feed's bottom inset comes
from an async onLayout measurement of the composer overlay, but ThreadFeed
remounts its list synchronously when messages arrive and primes it from
whatever the shared inset value holds — while the overlay's true height (the
pending approval / user-input cards animate in on that same transition) lands
a frame or more later, and nothing re-scrolled afterwards.
Fix, in two parts:
- Correct once the real height is known: mirror the overlay's measured height
into plain React state (the reanimated shared value can't trigger effects)
and thread it to ThreadFeed, which distinguishes the initial prime for a
fresh list from a later correction — re-reporting the inset and re-pinning
to the end if the user is still near it. Also tightens the bottom > 0 guard
so a zero-inset mount no longer counts as primed.
- Over-reserve during the pre-measurement frames: when a pending card is
knowable synchronously from props, seed the estimate with its conservative
chrome so the window before the first onLayout errs tall rather than short.
The cards render in normal flow inside the measured overlay, so the real
measurement includes them and always overrides the seed.
The "Loading/Syncing messages..." status pill is deliberately NOT part of the
inset — in the estimate or the measurement. It stays an absolutely-positioned
overlay: sync starts and stops on every fetch, and counting the pill would
bounce the feed up and down each time it toggles. It may briefly cover the
last message while visible instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@carTloyal123
carTloyal123force-pushed the fix/mobile-composer-inset-race branch from 3540214 to 0a62ad9CompareJuly 31, 2026 05:11

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

}, [listMountKey, props.contentInsetEndAdjustment, props.listRef]);
props.listRef.current?.reportContentInset({ bottom });
if (!isInitialMountForKey && nearListEnd.value && !userDraggedSinceMountRef.current) {
props.listRef.current?.scrollToEnd({ animated: true });

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.

Inset correction ignores send anchor

Medium Severity

After a send, ThreadDetailScreen keeps anchorMessageId set and drives anchoredEndSpace so the list pins the sent message, but the new post-mount inset effect still calls scrollToEnd when composerMeasuredHeight changes. That scroll targets the raw list end, not the anchor, so composer remeasurement (keyboard collapse, pending cards, etc.) can override the anchor scroll while the user is still near the bottom.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 0a62ad9. Configure here.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M30-99 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@carTloyal123@skjiisa