Skip to content

Fix mobile legend anchor under automatic iOS insets#3684

Merged
juliusmarminge merged 11 commits into
mainfrom
t3code/fix-mobile-legend-anchor
Jul 6, 2026
Merged

Fix mobile legend anchor under automatic iOS insets#3684
juliusmarminge merged 11 commits into
mainfrom
t3code/fix-mobile-legend-anchor

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Adjust the thread feed anchor offset on iOS when automatic content insets are enabled.
  • Fold the header height into the anchor math so newly sent messages do not anchor beneath the header.
  • Keep maintainScrollAtEnd from snapping to the wrong position when UIKit-managed insets are in play.

Testing

  • Not run (PR summary only; no local verification requested).
  • Existing change is limited to apps/mobile/src/features/threads/ThreadFeed.tsx anchor calculations.

Note

High Risk
Touches core chat scrolling via vendored patches and changes OTA runtime matching (fingerprint), so regressions could break thread UX or mis-deliver updates; production CI/submit config also affects store releases.

Overview
Fixes iOS thread chat layout when the list uses automatic safe-area insets under a transparent header: composer overlay height is adjusted for double-counted bottom inset, ThreadFeed feeds LegendList new inset/anchor props (contentInsetStartAdjustment, contentInsetEndStaticAdjustment, adjustedInsetCompensation, remount inset re-report), and maintainScrollAtEnd is animated again. Large patches to @legendapp/list and react-native-keyboard-controller implement the underlying scroll/clamp/end-pin behavior.

Adds Reanimated enter/layout transitions on the composer, feed rows, work log, and pending cards; Settings shows version, variant, and embedded vs OTA bundle info.

Release pipeline: new manual GitHub workflow for Linux EAS production build+submit or OTA; default runtimeVersion policy → fingerprint (was appVersion); EAS profiles enable corepack; production submit ASC app id and Android internal track updated.

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

Note

Fix mobile legend anchor position under automatic iOS safe-area insets

  • Fixes the keyboard/composer inset calculation in ThreadDetailScreen to subtract the native safe-area bottom inset on iOS when usesAutomaticContentInsets is active, preventing the legend anchor from being offset incorrectly.
  • Updates ThreadFeed to derive anchorTopInset from HeaderHeightContext when automatic content insets are active, fixing header-material visibility reporting and anchored end-space calculations.
  • Adds enter/exit animations (fade and layout transitions) to the composer, feed items, and work-log rows for a smoother UX.
  • Adds a GitHub Actions workflow for manual EAS production builds and OTA updates, and updates eas.json with Corepack support and corrected App Store Connect submission IDs.
  • Behavioral Change: runtimeVersion policy now defaults to fingerprint instead of appVersion, so OTA updates require matching native fingerprints.

Macroscope summarized ab86724.

@coderabbitai

coderabbitai Bot commented Jul 3, 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

Run ID: 5e2db130-ee98-4955-8683-2708012e9c85

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/fix-mobile-legend-anchor

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

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Jul 3, 2026
Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

6 blocking correctness issues found. This PR includes ~900-line patches to third-party scroll libraries, a new production CI workflow with an unresolved shell injection vulnerability, and multiple identified bugs affecting scroll positioning and animation timing. The scope and unresolved security concern require human review.

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

@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:S 10-29 changed lines (additions + deletions). labels Jul 4, 2026

@cursor cursor Bot 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.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Header blur uses wrong inset
    • handleScroll now measures the scroll threshold with anchorTopInset instead of the zeroed topContentInset, so header material visibility toggles at the true rest position under iOS automatic content insets while non-automatic behavior is unchanged.

Create PR

Or push these changes by commenting:

@cursor push 18fb3f23d9
Preview (18fb3f23d9)
diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx
--- a/apps/mobile/src/features/threads/ThreadFeed.tsx
+++ b/apps/mobile/src/features/threads/ThreadFeed.tsx
@@ -1282,11 +1282,15 @@
     },
     [props.onHeaderMaterialVisibilityChange],
   );
+  // anchorTopInset, not topContentInset: with automatic insets the list rests
+  // at contentOffset.y = -headerHeight (the inset only exists in UIKit's
+  // adjustedContentInset), so the header inset must be added back for the
+  // material to toggle relative to the true rest position.
   const handleScroll = useCallback(
     (event: NativeSyntheticEvent<NativeScrollEvent>) => {
-      reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + topContentInset > 6);
+      reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + anchorTopInset > 6);
     },
-    [reportHeaderMaterialVisibility, topContentInset],
+    [reportHeaderMaterialVisibility, anchorTopInset],
   );
   const handleViewportLayout = useCallback((event: LayoutChangeEvent) => {
     const nextWidth = Math.round(event.nativeEvent.layout.width);

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx
Comment thread patches/@legendapp__list@3.2.0.patch
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Jul 4, 2026
- Fold header inset into the anchor offset when iOS automatic content insets are enabled
- Prevent new messages from anchoring under the header and snapping past earlier messages

@cursor cursor Bot 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.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Work pill height estimate stale
    • Increased WORKING_INDICATOR_HEIGHT from 44 to 52 so the overlay height estimate includes the pill wrapper's new pt-2 (8px) padding, keeping the pre-measurement inset math consistent with the measured layout.

Create PR

Or push these changes by commenting:

@cursor push 8b598399e0
Preview (8b598399e0)
diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx
--- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx
+++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx
@@ -174,7 +174,10 @@
   }, [threadId, feed]);
 }
 
-const WORKING_INDICATOR_HEIGHT = 44;
+// Estimated height of WorkingDurationPill including its pt-2/pb-2 wrapper
+// padding; must track the pill layout so the pre-measurement overlay height
+// estimate matches what onComposerLayout later reports.
+const WORKING_INDICATOR_HEIGHT = 52;
 
 const WorkingDurationPill = memo(function WorkingDurationPill(props: {
   readonly startedAt: string;

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/threads/ThreadDetailScreen.tsx
- Patch LegendList and keyboard inset handling for transparent headers
- Animate composer, feed, and work-log transitions more smoothly
- Add tests and dependency patches for the mobile scroll fixes
Comment thread patches/@legendapp__list@3.2.0.patch
// composer-height short of the end. Layout effect: it must land before the
// list's first positioning tick or the one-shot initial scroll misses it.
const listMountKey = `${props.threadId}:${props.feed.length === 0 ? "empty" : "filled"}`;
useLayoutEffect(() => {

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.

🟡 Medium threads/ThreadFeed.tsx:1330

The new useLayoutEffect keyed on listMountKey reads props.contentInsetEndAdjustment.value and reports it to the remounted list, but that shared value still holds the previous thread's composer-overlay height when the thread changes (because useKeyboardChatComposerInset deduplicates by height and hasn't re-measured the fresh overlay yet). initialScrollAtEnd is one-shot during attach, so the first scroll-to-end on a thread switch computes with a stale bottom inset and rests too high or too low by the overlay-height delta. Consider resetting contentInsetEndAdjustment.value to 0 on thread change before the list remounts, or guarding the effect so it only reports the inset once the composer overlay has re-measured for the current thread.

Also found in 1 other location(s)

apps/mobile/src/features/threads/ThreadDetailScreen.tsx:195

Adding pt-2 to WorkingDurationPill makes the overlay 8 px taller, but the parent still budgets a fixed WORKING_INDICATOR_HEIGHT = 44 when activeWorkStartedAt is set. That leaves the thread feed's estimated bottom inset/anchor space too small whenever this pill is shown, so the last message or empty-state content can end up partially underneath the floating overlay until later layout correction.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/ThreadFeed.tsx around line 1330:

The new `useLayoutEffect` keyed on `listMountKey` reads `props.contentInsetEndAdjustment.value` and reports it to the remounted list, but that shared value still holds the previous thread's composer-overlay height when the thread changes (because `useKeyboardChatComposerInset` deduplicates by height and hasn't re-measured the fresh overlay yet). `initialScrollAtEnd` is one-shot during attach, so the first scroll-to-end on a thread switch computes with a stale bottom inset and rests too high or too low by the overlay-height delta. Consider resetting `contentInsetEndAdjustment.value` to 0 on thread change before the list remounts, or guarding the effect so it only reports the inset once the composer overlay has re-measured for the current thread.

Also found in 1 other location(s):
- apps/mobile/src/features/threads/ThreadDetailScreen.tsx:195 -- Adding `pt-2` to `WorkingDurationPill` makes the overlay 8 px taller, but the parent still budgets a fixed `WORKING_INDICATOR_HEIGHT = 44` when `activeWorkStartedAt` is set. That leaves the thread feed's estimated bottom inset/anchor space too small whenever this pill is shown, so the last message or empty-state content can end up partially underneath the floating overlay until later layout correction.

- Pass safe-area inset compensation through the mobile thread list
- Adjust keyboard controller and legend list math so end anchors stay aligned
- Fix overlay spacing and drag handling to prevent end-position drift
@juliusmarminge
juliusmarminge force-pushed the t3code/fix-mobile-legend-anchor branch from 8bcfdef to 60867bd Compare July 4, 2026 21:46
Comment thread apps/mobile/src/features/threads/ThreadFeed.tsx
Comment thread apps/mobile/src/features/threads/ThreadDetailScreen.tsx
onMomentumScrollEnd(event);
}
},
+ onScrollBeginDrag: (event) => {

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.

🟡 Medium patches/@legendapp__list@3.2.0.patch:481

The onScrollBeginDrag handler is forwarded through the fns object, but fns is created with an empty dependency array []. After mount, updates to the onScrollBeginDrag prop are never captured — drags always invoke the callback captured on first render, so any parent state that the drag callback depends on goes stale. Consider adding the prop (or a ref holding it) to the dependency array so the handler stays current.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @patches/@legendapp__list@3.2.0.patch around line 481:

The `onScrollBeginDrag` handler is forwarded through the `fns` object, but `fns` is created with an empty dependency array `[]`. After mount, updates to the `onScrollBeginDrag` prop are never captured — drags always invoke the callback captured on first render, so any parent state that the drag callback depends on goes stale. Consider adding the prop (or a ref holding it) to the dependency array so the handler stays current.

Comment on lines +74 to +75
+ blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0;
(_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size);

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.

🟡 Medium patches/@legendapp__list@3.2.0.patch:74

onSizeChanged receives the original unadjusted size, but the spacer actually applied to blankSpace.value is size - adjustedInsetCompensation. Any caller tracking the real anchored spacer via onSizeChanged will be off by the compensation amount, causing overlays or end-of-list calculations to drift by the safe-area inset. Pass the same adjusted value to onSizeChanged that is written to blankSpace.value.

Suggested change
+ blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0;
(_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size);
blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0;
const adjustedSize = blankSpace.value;
(_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, adjustedSize);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @patches/@legendapp__list@3.2.0.patch around lines 74-75:

`onSizeChanged` receives the original unadjusted `size`, but the spacer actually applied to `blankSpace.value` is `size - adjustedInsetCompensation`. Any caller tracking the real anchored spacer via `onSizeChanged` will be off by the compensation amount, causing overlays or end-of-list calculations to drift by the safe-area inset. Pass the same adjusted value to `onSizeChanged` that is written to `blankSpace.value`.

<ControlPillMenu
actions={optionsMenuActions}
onPressAction={({ nativeEvent }) => handleOptionsMenuAction(nativeEvent.event)}
<Animated.View entering={FadeIn.duration(160)} exiting={FadeOut.duration(120)}>

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.

🟡 Medium threads/ThreadComposer.tsx:843

The exiting={FadeOut} wrappers on the expanded-only sections (toolbar row, queue count, attachment strip) keep those elements mounted and occupying space until the exit animation finishes (~120–180 ms). ThreadDetailScreen measures the overlay height via onComposerLayout and uses it for the feed's bottom inset and anchor-scroll math, so when the composer collapses after send the measured height still reflects the expanded layout. This causes the feed to compute its resting position against the wrong inset and anchor the newly sent message too high relative to the collapsed composer. Consider removing the exiting animations on these sections (or using a layout-only transition) so the height measurement updates immediately when isExpanded flips to false.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/src/features/threads/ThreadComposer.tsx around line 843:

The `exiting={FadeOut}` wrappers on the expanded-only sections (toolbar row, queue count, attachment strip) keep those elements mounted and occupying space until the exit animation finishes (~120–180 ms). `ThreadDetailScreen` measures the overlay height via `onComposerLayout` and uses it for the feed's bottom inset and anchor-scroll math, so when the composer collapses after send the measured height still reflects the expanded layout. This causes the feed to compute its resting position against the wrong inset and anchor the newly sent message too high relative to the collapsed composer. Consider removing the `exiting` animations on these sections (or using a layout-only transition) so the height measurement updates immediately when `isExpanded` flips to false.

Gate LegendList's readyToRender on a new insetEndRevealHold flag owned by
the inset-end settle watchdog: content stays hidden while the initial
end-scroll chases late inset/size reports, upgrades adaptiveRender to
normal while still hidden (the light->normal re-render wave was causing
~100ms of visible drift), then reveals only after the offset holds stable
for 3 consecutive frames (40-frame cap, released early on user drag).
First visible frame is the final resting position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
+ const maintainAnchoredEndSpace = state.props.anchoredEndSpace;
+ const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex;
+ if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) {
+ return false;

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.

🟡 Medium patches/@legendapp__list@3.2.0.patch:277

When contentSize < state.scrollLength and maintainInsetStartAdjustment > 0, doMaintainScrollAtEnd sets state.scroll = -maintainInsetStartAdjustment and returns true without calling scrollTo on the native scroller. So when a list shrinks from overflowing to underflow while maintainScrollAtEnd is active, the internal scroll state says the feed is at -headerInset but the native scroll view stays at its old offset, leaving the viewport and end-anchor out of sync. Consider dispatching a scrollTo({ animated, y: -maintainInsetStartAdjustment }) (or falling through to the normal scrollTo path) so the native scroller matches state.scroll.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @patches/@legendapp__list@3.2.0.patch around line 277:

When `contentSize < state.scrollLength` and `maintainInsetStartAdjustment > 0`, `doMaintainScrollAtEnd` sets `state.scroll = -maintainInsetStartAdjustment` and returns `true` without calling `scrollTo` on the native scroller. So when a list shrinks from overflowing to underflow while `maintainScrollAtEnd` is active, the internal scroll state says the feed is at `-headerInset` but the native scroll view stays at its old offset, leaving the viewport and end-anchor out of sync. Consider dispatching a `scrollTo({ animated, y: -maintainInsetStartAdjustment })` (or falling through to the normal `scrollTo` path) so the native scroller matches `state.scroll`.

- Point production iOS submit config at the new ASC app ID
- Keeps EAS submission aligned with the current app record
- Update `apps/mobile/eas.json` so production Android builds target the internal track
- Leave iOS release config unchanged
Comment thread apps/mobile/eas.json
"ascAppId": "6787819824"
},
"android": {
"track": "internal"

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.

🟠 High mobile/eas.json:52

The submit.production.android.track is set to "internal", so eas submit --profile production -p android uploads the release to Google Play's internal testing track instead of the production track. Because production is the only Android submit profile, production Android releases never reach end users — they go only to internal testers. If internal testing is intended as a staging step before promotion, consider documenting that workflow; otherwise set track to "production" (or remove it so the default production track is used).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/mobile/eas.json around line 52:

The `submit.production.android.track` is set to `"internal"`, so `eas submit --profile production -p android` uploads the release to Google Play's internal testing track instead of the production track. Because `production` is the only Android submit profile, production Android releases never reach end users — they go only to internal testers. If internal testing is intended as a staging step before promotion, consider documenting that workflow; otherwise set `track` to `"production"` (or remove it so the default production track is used).

juliusmarminge and others added 2 commits July 5, 2026 21:18
Address two PR review findings on the automatic-inset work:
- ThreadFeed: the header-material scroll check used topContentInset (0
  under automatic insets) instead of anchorTopInset, so the header blur
  toggled a full header height too late. Use anchorTopInset to match the
  true rest position.
- ThreadDetailScreen: WORKING_INDICATOR_HEIGHT (44) no longer matched the
  pill's rendered height after the pt-2 padding was added; bump to 52 so
  the pre-measurement overlay estimate seeds an accurate bottom inset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- app.config.ts: default runtimeVersion policy to fingerprint (was
  appVersion) for every environment. appVersion made all 0.1.0 builds
  share a runtime version, so an OTA could land on a binary missing the
  native changes it needs and crash. Fingerprint ties the runtime version
  to the native project (deps, config plugins, patches/) so updates only
  reach compatible binaries. Previews already ran fingerprint.
- Settings: replace the hardcoded "Alpha" version label with the real
  app version + variant, plus which JS bundle is running (embedded vs
  OTA <id>) so "am I on the right build?" is answerable at a glance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot 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.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Wrong variant fallback in settings
    • Replaced the "development" fallback and unchecked string cast with a typeof guard that defaults to "production", matching resolveAppVariant() in app.config.ts and preventing a potential capitalize crash on non-string values.

Create PR

Or push these changes by commenting:

@cursor push b5d3470297
Preview (b5d3470297)
diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
--- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
+++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx
@@ -425,7 +425,10 @@
   const icon = useThemeColor("--color-icon");
 
   const version = Constants.expoConfig?.version ?? "0.0.0";
-  const variant = (Constants.expoConfig?.extra?.appVariant as string | undefined) ?? "development";
+  // app.config.ts resolves unknown/missing APP_VARIANT to "production", so a
+  // missing value here must not be mislabeled as a development build.
+  const rawVariant = Constants.expoConfig?.extra?.appVariant;
+  const variant = typeof rawVariant === "string" ? rawVariant : "production";
   const variantLabel = variant === "production" ? "" : capitalize(variant);
   const versionLabel = variantLabel ? `${version} · ${variantLabel}` : version;
   // Which JS is actually running: the bundle shipped in the binary, or an OTA

You can send follow-ups to the cloud agent here.

Comment thread apps/mobile/src/features/settings/SettingsRouteScreen.tsx
juliusmarminge and others added 3 commits July 5, 2026 22:13
Match resolveAppVariant in app.config.ts, which treats a missing variant
as production. The version row previously fell back to "development",
which could mislabel a production build (Cursor Bugbot finding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
EAS Build was using its image-default pnpm (9.x, which names virtual-store
dirs as <pkg>@<ver>_<full-hash>) instead of the pinned pnpm@10.24.0 (which
uses the readable-prefix scheme). That made EAS's fingerprint diverge from
the locally-computed one, so `eas build` under the fingerprint runtime
policy errored with a runtime-version mismatch. corepack:true makes EAS
honor the packageManager pin so both sides run pnpm 10.24 and fingerprints
match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Production builds and OTA updates must run from Linux CI, not a laptop:
under the fingerprint runtime policy the fingerprint is computed at both
`eas build` time and on the EAS builder, and a macOS-computed fingerprint
never matches EAS's Linux one (platform-specific deps + pnpm version), so
local `eas build` errors. This workflow_dispatch job runs on Linux, uses
corepack-pinned pnpm 10.24, and does either a production build (auto-submit
to TestFlight) or an OTA update on the production channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot 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 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: OTA message shell injection
    • The user-controlled message input is now passed via a step-level OTA_MESSAGE env var and referenced as "$OTA_MESSAGE" at runtime instead of being interpolated into the shell script.
  • ✅ Fixed: Missing fingerprint env override
    • Added MOBILE_VERSION_POLICY: fingerprint to the production job's env block, matching the preview workflow so remote EAS environment values cannot override the intended runtime-version policy.

Create PR

Or push these changes by commenting:

@cursor push c592a9e02e
Preview (c592a9e02e)
diff --git a/.github/workflows/mobile-eas-production.yml b/.github/workflows/mobile-eas-production.yml
--- a/.github/workflows/mobile-eas-production.yml
+++ b/.github/workflows/mobile-eas-production.yml
@@ -39,6 +39,7 @@
     env:
       APP_VARIANT: production
       NODE_OPTIONS: --max-old-space-size=8192
+      MOBILE_VERSION_POLICY: fingerprint
     steps:
       - id: expo-token
         name: Check for EXPO_TOKEN
@@ -101,10 +102,11 @@
         working-directory: apps/mobile
         env:
           EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
+          OTA_MESSAGE: ${{ inputs.message || format('Production OTA ({0})', github.sha) }}
         run: |
           eas update \
             --channel production \
             --environment production \
             --platform ${{ inputs.platform }} \
-            --message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \
+            --message "$OTA_MESSAGE" \
             --non-interactive

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ab86724. Configure here.

--channel production \
--environment production \
--platform ${{ inputs.platform }} \
--message "${{ inputs.message || format('Production OTA ({0})', github.sha) }}" \

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.

OTA message shell injection

High Severity

The Publish OTA update step is vulnerable to shell injection. The workflow_dispatch message input is directly interpolated into the eas update --message argument, allowing metacharacters to execute arbitrary commands on the runner. This could expose EXPO_TOKEN and impact production.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab86724. Configure here.

contents: read
env:
APP_VARIANT: production
NODE_OPTIONS: --max-old-space-size=8192

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.

Missing fingerprint env override

Low Severity

The production job sets APP_VARIANT but not MOBILE_VERSION_POLICY, while the preview workflow pins MOBILE_VERSION_POLICY to fingerprint. After eas env:pull production, a MOBILE_VERSION_POLICY value from the remote environment can override the app.config.ts default and change runtime versioning for builds and OTAs triggered here.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ab86724. Configure here.

@juliusmarminge
juliusmarminge merged commit 32e7844 into main Jul 6, 2026
17 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/fix-mobile-legend-anchor branch July 6, 2026 05:50
eyeveil added a commit to eyeveil/t3code that referenced this pull request Jul 10, 2026
Upstream pingdotgg#3684 made maintainScrollAtEnd animated, but on native an in-flight
animated scrollToEnd is not reconciled with maintainVisibleContentPosition:
each streamed token has MVCP restore the anchor (up) then the ~500ms animated
end-scroll fire (down), looping before it settles. Keep the end pin instant
during streaming (resolves in the same frame as the MVCP restore) and animate
only for a short window after a new send anchor so the just-sent message still
glides into place.
tarik02 added a commit to tarik02/t3code that referenced this pull request Jul 13, 2026
* Add middle-click close for right panel tabs (pingdotgg#3161)

Co-authored-by: Julius Marminge <jmarminge@gmail.com>

* fix: warm WSL before preflight in WSL-only backend mode (pingdotgg#3588)

* Add Claude Sonnet 5 as the default Claude model (pingdotgg#3620)

* Restore the ultrathink frame border effect (pingdotgg#3625)

* fix(dev): Fix electron dev launch and add test (pingdotgg#3662)

* Add adaptive split-view layout for iPad/mobile workspace (pingdotgg#3514)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* fix(mobile): compile patched native pods from source on EAS (pingdotgg#3667)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Make the thread composer read as elevated liquid glass (pingdotgg#3668)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Upgrade Vite Plus and enable bundled dev opt-in (pingdotgg#3679)

* Surface pending tasks in mobile home and draft flow (pingdotgg#3670)

* fix(mobile): combined test branch — scroll, back-swipe, thread lists, computer switching (pingdotgg#3687)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Add repo-root favicon.svg so t3 code shows its own icon (pingdotgg#3683)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Load thread snapshots over HTTP before live sync (pingdotgg#3719)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Fix mobile legend anchor under automatic iOS insets (pingdotgg#3684)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Improve live activity routing and diagnostics (pingdotgg#3685)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* Prevent Add Project sheet from collapsing on relayout (pingdotgg#3759)

* Use variant-specific splash icons in mobile app (pingdotgg#3762)

* Fix Expo widget asset wiring order (pingdotgg#3763)

* Extend Done display to 15 minutes and show up to 5 Live Activity banner rows (pingdotgg#3761)

* Clear VCS presentation state on finish (pingdotgg#3764)

* Lead with the outcome when no agents are active in the Live Activity (pingdotgg#3768)

* Add T3 Connect onboarding for mobile and web (pingdotgg#3765)

* Revert "Add T3 Connect onboarding for mobile and web" (pingdotgg#3776)

* Expose Clerk Google sign-in env vars to Expo (pingdotgg#3772)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Set up Cursor Cloud dev environment (web + Android toolchain) (pingdotgg#3755)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>

* Revert "Revert "Add T3 Connect onboarding for mobile and web"" (pingdotgg#3777)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* Use rounded depth logo for production splash screen (pingdotgg#3780)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* fix(release): stage pnpm 11 allowBuilds for desktop installs (pingdotgg#3781)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>

* Upgrade Clerk toolchain to latest versions (pingdotgg#3785)

* fix(release): bump electron-builder so pnpm 11 deduped deps land in the asar (pingdotgg#3790)

* Fix desktop native optional dependency packaging (pingdotgg#3816)

* [codex] Upgrade Clerk stack (pingdotgg#3821)

Co-authored-by: codex <codex@users.noreply.github.com>

* [codex] Preserve worktree metadata during branch sync (pingdotgg#3822)

Co-authored-by: codex <codex@users.noreply.github.com>

* feat(client): persist offline environment data and mobile preferences (pingdotgg#3795)

Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: codex <codex@users.noreply.github.com>

* [codex] Label max and ultra reasoning (pingdotgg#3824)

Co-authored-by: codex <codex@users.noreply.github.com>

* fix(mobile): embed fonts and render project favicons reliably (pingdotgg#3823)

Co-authored-by: codex <codex@users.noreply.github.com>

* Show compact PR number badges in mobile thread rows (pingdotgg#3827)

Co-authored-by: codex <codex@users.noreply.github.com>

* Expose mobile PR indicator labels to accessibility (pingdotgg#3828)

Co-authored-by: codex <codex@users.noreply.github.com>

* Fix truncated chat error alert layout (pingdotgg#3899)

* fix(marketing): show platform-appropriate commit shortcut on the website (pingdotgg#3644)

* [codex] Add Android mobile support (pingdotgg#3579)

Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>

---------

Co-authored-by: Hugo Blom <6117705+huxcrux@users.noreply.github.com>
Co-authored-by: Julius Marminge <jmarminge@gmail.com>
Co-authored-by: Utkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Theo Browne <me@t3.gg>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Theo Browne <t3dotgg@users.noreply.github.com>
Co-authored-by: Rowan <rowan@cardow.co>
Co-authored-by: Patricio Gómez Meneses <107218376+Prgm-code@users.noreply.github.com>
Co-authored-by: Jake Leventhal <jakeleventhal@me.com>
Co-authored-by: Vedank Purohit <VedankPurohit2@gmail.com>
Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant