Files
orca/mobile/web-entry/web-overrides.json
T
Jinwoo Hong 90c1c0ac50 feat(mobile): draw the host Accounts screen from the desktop's page (#30020)
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens

* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable

* test(mobile): read the nullable page route refusal in its policy test

* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch

The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): delete native screens the OTA shell no longer reaches

With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.

Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).

Claude-Session: https://claude.ai/code/session_6cea92f6

* test(mobile): drop the phone-preview comparison the deleted native sibling held

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind

The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.

Claude-Session: https://claude.ai/code/session_6cea92f6

* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps

MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.

Claude-Session: https://claude.ai/code/session_6cea92f6

* feat(mobile): draw the host Accounts screen from the desktop's page

Accounts was the last host screen the shell still drew natively, so its
changes needed a phone build. It is now a page route like Tasks: the
screen moves to src/accounts/MobileAccountsScreen.tsx, accounts.web.tsx
renders it on the page, and accounts.tsx mounts the shell for it.

What React Native Web needed: Alert.alert is a no-op there, so the
reset confirmation, its outcome and a failed account switch are
ConfirmModal drawers (ConfirmModal can now drop Cancel for a notice);
Back goes through the route handoff, which pops the shell's stack; and
the reset idempotency key is built from getRandomBytes, since
randomUUID needs a secure context the iOS custom scheme is not.

edit.tsx edits the phone's own pairing and never renders on the page,
so the route manifest leaves it out and the page's host stack does not
declare it. The script sweep is re-measured for the 14-route tree.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): answer a same-offer Codex reset retry with its recorded outcome; mobile mirrors the desktop

The desktop keeps no client journal for a reset credit: the host's ledger
owns the attempt. A remote client that lost a reply and retried the same
offer under a new key was refused with "already attempted" (or "unknown
outcome" while the provider call was unresolved), which is why the phone
carried its own durable journal to replay the old key.

The coordinator now answers a request for an offer an earlier key already
claimed as that attempt: the settled outcome, the running attempt, or a
resume of the pending one under its original key. A different offer for
the account is still refused while an outcome is unknown, so nothing can
spend twice. The reply shape is the one a same-key replay always had, so
installed clients with their journal read it unchanged.

With that, the Accounts page sends a fresh key per attempt like the
desktop and keeps nothing on the phone: the AsyncStorage journal, its
client singleflight and the retry-record warnings are deleted, and the
journal-resume recording goes with them.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(accounts): resume an account's unresolved Codex reset when the retry names a newer offer

Review follow-ups:
- Host: after an unknown provider outcome the usage refresh moves the
  offer, so a retry names a newer one. The coordinator now answers it as
  the account's unresolved attempt (resumed under its own key and scope),
  as the desktop's consumeCurrent does, instead of refusing forever. One
  ledger lookup finds own key, offer claimant, then unresolved attempt.
- Mobile: a reply is matched on target, account and account revision,
  not the offer, so the resumed attempt's scope is accepted.
- The key is minted inside requestCodexResetCredit from randomV4Uuid, the
  session helper moved to src/platform/random-v4-uuid.ts.
- RefreshControl goes; the header refresh stays. ConfirmModal's
  onConfirm is optional for an OK-only notice.
- Stale journal comments and the mobile docs line are corrected.
- The edit.tsx page exclusion is dropped from this PR; the script sweep
  is re-measured for 15 routes.
- Root oxlint: escape the apostrophe; zod v4 strictObject.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* fix(mobile): secure UUID fallback from expo-crypto; pin reset retry routing

- randomV4Uuid never falls back to Math.random: without crypto.randomUUID
  it builds the v4 UUID from ExpoCrypto.getRandomBytes, as the old reset
  key did. Tests get Node's secure bytes for expo-crypto from the setup
  file, since the native module has no Node build.
- Host tests: a new offer after a settled attempt spends again rather
  than replaying, and a retry naming a newer offer joins a resumed
  attempt in flight (one provider call, original key).

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* chore: restore main's bytes for files the merge's pre-commit format touched

The merge commit's lint-staged run reformatted 27 files this PR does not
change. Committed without the hook so they match origin/main exactly.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* test(mobile): stand in for expo-crypto with a Node alias, not a setup-file mock

Anti-slop forbids module mocking outside test files. The vitest config
now aliases expo-crypto to a faithful Node implementation of the names
shared code reaches; a test file's own mock still wins.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb

* docs(mobile): match the bridged-parity comments to the measured 795-golden tally

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-17 03:11:24 -07:00

190 lines
25 KiB
JSON

{
"$comment": "Every .web.* sibling the Route A web build resolves ahead of its native file. One entry per documented React Native Web gap; config/scripts/mobile-web-app-web-overrides.test.mjs fails on an unlisted one, a listed file that is gone, or one with no native sibling.",
"overrides": [
{
"file": "src/transport/client-context.web.tsx",
"reason": "The page has no websocket transport and no pairing keychain. This is the single transport substitution point: it serves the BridgeRpcClient the entry built, for every hostId, because the bridge protocol names no host. The entry mounts nothing until init lands, so this provider never holds a client that cannot answer."
},
{
"file": "packages/expo-two-way-audio/src/ExpoTwoWayAudioModule.web.ts",
"reason": "Vendored with the module, not added for Route A. Nothing on the page reaches it any more: dictation's capture went behind src/platform/dictation-capture.web.ts, which asks the shell for the microphone, so the only importer of @orca/expo-two-way-audio is the native half of that seam. The file stays because it ships with the vendored module, and it answers denied microphone permission and no playback, which is what a browser outside the shell can honestly say."
},
{
"file": "src/navigation/use-back-claim.web.ts",
"reason": "There is no hardware Back key inside a WebView: react-native-web answers BackHandler.addEventListener with a console warning and an inert subscription, so every sheet that claimed the key on the page armed nothing and one press left the whole session route. The native file registers the key; this one claims it from the shell over the bridge, and the shell hands one press back for the newest claim to spend."
},
{
"file": "src/navigation/route-handoff.web.ts",
"reason": "The page is one document standing in for one screen, so a route it does not render has to be handed to the app that does. The native file is the router; this one posts a navigate notify for a target outside the page's routes and pushes locally for one inside them."
},
{
"file": "src/transport/host-store.web.ts",
"reason": "expo-secure-store resolves to {} on web, so the real store answers loadHosts() with an empty array and every screen that looks this host up paints \"Host not found\" over the host the shell just opened. This one serves the profile init carried, without the credential the bridge already holds."
},
{
"file": "src/transport/host-removal-lifecycle.web.ts",
"reason": "Removing a host is pairing work and the page holds neither the host list nor the credential, so the real lifecycle's removeHost resolved without doing anything and the screen reported success for a host that was still paired. This one refuses with PageHostRemovalUnavailableError, whose message names the app's host list. It is also the fence that keeps push-registration.ts out of the page bundle: the native file's import of it was that subsystem's only path into a page route, and the page has neither the device token nor the gateway client push needs."
},
{
"file": "src/transport/host-device-token-store.web.ts",
"reason": "expo-secure-store resolves to {} on web, and the bridge carries the RPC, so the page holds no device token."
},
{
"file": "src/platform/host-os.web.ts",
"reason": "Platform.OS is web inside the shell's page, but the keyboard the page lifts over is the phone's, reported in that OS's own terms: iOS counts the home indicator. Keyboard and inset arithmetic branches on the host OS, so this answers the phone's OS from the WebView's user agent, and the page computes what the native screen computes."
},
{
"file": "app/_layout.web.tsx",
"reason": "The page's route tree starts at app/h, so without a root layout expo-router mounts DefaultNavigator, an all-edges SafeAreaView that would pad every screen a second time. The native root owns pairing and push and cannot reach the page. This one adds no padding and feeds the insets the shell sent in init to react-native-safe-area-context, whose web provider reads env(safe-area-inset-*) as 0 in both WebViews."
},
{
"file": "app/h/[hostId]/index.web.tsx",
"reason": "The shell renders this page for this route, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose requireNativeViewManager call runs at import and throws in a browser."
},
{
"file": "src/platform/text-input-font-size.web.ts",
"reason": "iOS zooms the page on focus of any text input under 16px and does not zoom back out, which leaves the document at a scale other than 1 for a whole typing session. keyboard-occlusion.web.ts reads a scale other than 1 as 'not a keyboard' on purpose, because geometry cannot separate a zoom from a keyboard, so the commit bar and the note composer would never lift on the platform they were written for. The web file raises the app's 14px body size to that floor; the native file is the body size, so a phone renders exactly what it rendered before. maximum-scale=1 on the viewport meta was the other fix and was rejected: Android WebView honours it and it would take deliberate pinch zoom away from low-vision users."
},
{
"file": "src/platform/haptics.web.ts",
"reason": "expo-haptics has a web build that fakes an iOS haptic by appending a hidden <label><input type=\"checkbox\" switch> to document.head, clicking it and removing it, once per call. C1.9 traced a long press that never fired on the worktree list to that stray click, and the file explorer calls triggerSelection on every row tap. So the page asks the shell for the device's own haptic instead, over the native.haptics.trigger notify behind the haptics grant: 70 to 77 bytes per frame, no reply, and haptics.ts's own Platform.OS split on the other side. A notify rather than a verb because nothing is owed back and a reply would spend a slot in the same in-flight window a forwarded request does, at 90 call sites (rulings-ota-c7.md ruling 30). The five exports are the five kinds the frame admits; mobile-web-app-haptics-seam.test.mjs is the fence."
},
{
"file": "app/h/[hostId]/files/[worktreeId].web.tsx",
"reason": "The shell renders this page for the file explorer, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose requireNativeViewManager call runs at import and throws in a browser — reproduced in the render check, which painted that console error instead of the route."
},
{
"file": "app/h/[hostId]/files/preview/[worktreeId].web.tsx",
"reason": "The shell renders this page for the file preview, for the same reason as the explorer beside it: no nested shell, and a native file that reaches requireNativeViewManager at import. Normalizing the route params stays here, because the page reads them back out of its own URL exactly as the native screen reads them out of the router."
},
{
"file": "app/h/[hostId]/agent-history/[worktreeId].web.tsx",
"reason": "The shell renders this page for this route, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose requireNativeViewManager call runs at import and throws in a browser."
},
{
"file": "src/platform/external-link.web.ts",
"reason": "The page runs inside the shell's WebView, where react-native-web's Linking.openURL calls window.open(url, '_blank', 'noopener') and resolves whether or not anything opened; only a tel: URL assigns window.location, and none of the three allowed schemes is one. Both shells refuse window.open outright: iOS sets javaScriptCanOpenWindowsAutomatically = false and returns nil from WKUIDelegate's createWebViewWith, and Android sets javaScriptCanOpenWindowsAutomatically = false, setSupportMultipleWindows(false) and returns false from onCreateWindow. So the native path reports success into a tap that did nothing. This one posts the externalLink notify instead, after the same scheme check the frame enforces, and names its refusal rather than throwing inside a tap handler."
},
{
"file": "src/platform/keyboard-occlusion.web.ts",
"reason": "react-native-web's Keyboard is a stub: addListener returns a subscription that never fires and isVisible() is always false, so a screen waiting for keyboardDidShow inside the page waits forever and the software keyboard covers whatever sits at the bottom of the document — the source-control commit bar, and the review note composer whose KeyboardAvoidingView is driven by those same events. The browser publishes the geometry a different way: the layout viewport keeps its size and visualViewport shrinks, so the occluded strip is innerHeight minus the visual viewport's height and offsetTop, tracked on its resize and scroll. A document with no visualViewport answers 0 rather than guessing."
},
{
"file": "src/platform/clipboard.web.ts",
"reason": "expo-clipboard resolves to navigator.clipboard on the web, which needs a secure context; the iOS shell serves the page from the custom scheme orca-mobile-web://<session>/ while Android serves https, so that path would work on one platform and silently not on the other. This one asks the shell through the native.clipboard.write and native.clipboard.read verbs, where the pasteboard is the device's, and rejects when the route was not granted one so the caller's own catch puts that on screen. An image is the same pasteboard reached a different way: native.clipboard.read admits only text, and a 24 MiB base64 image cannot cross an 8 MiB reply cap, so readImage runs native.media.pick with source 'clipboard', reads the staged bytes back a chunk at a time and gives the handle back. contents answers per grant without probing, because the shell serves no 'is there text' verb and reading to find out would raise iOS's paste-consent prompt on every mount."
},
{
"file": "app/h/[hostId]/tasks.web.tsx",
"reason": "The shell renders this page for this route, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose module calls requireNativeViewManager at import and throws in a browser, and one throwing route module takes the whole bundle down because the manifest imports them all."
},
{
"file": "app/h/[hostId]/accounts.web.tsx",
"reason": "The shell renders this page for the accounts screen, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose module calls requireNativeViewManager at import and throws in a browser, and one throwing route module takes the whole bundle down because the manifest imports them all."
},
{
"file": "app/h/[hostId]/source-control/[worktreeId].web.tsx",
"reason": "The shell renders this page for the source-control hub, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose module calls requireNativeViewManager at import and throws in a browser, and the route manifest imports every route, so one throwing module takes the whole bundle down. The tab, name and origin params are parsed here exactly as the native sibling parses them, because the page reads them back out of its own URL the way the native screen reads them out of the router."
},
{
"file": "app/h/[hostId]/review/[worktreeId].web.tsx",
"reason": "The shell renders this page for diff review, for the same reason as the source-control hub beside it: no nested shell, and a native file that reaches requireNativeViewManager at import. Shorter than its siblings because the route body is MobileDiffReviewRouteScreen, which reads its own params — the native file had to extract that anyway so the review controller does not subscribe behind the page."
},
{
"file": "app/h/[hostId]/session/[worktreeId].web.tsx",
"reason": "The shell renders this page for the session screen, so the page has no shell to mount inside itself. Its native file reaches OrcaMobileWebShellView, whose module calls requireNativeViewManager at import and throws in a browser, and the route manifest imports every route, so one throwing module takes the whole bundle down. Shorter than its siblings because the route body is MobileSessionRouteScreen, which the switch also needs as an element: useMobileSessionController opens the terminal, chat and tab subscriptions, and at the switch's top level it would open them behind the page too."
},
{
"file": "src/browser/browser-frame-layer-paint.web.ts",
"reason": "setNativeProps does not exist on React Native Web: an Image or View ref is the DOM node itself, so both native writes throw rather than paint and the pane never shows a frame. This file writes the same two things through the element — the frame as a background-image on the child RN Web sizes, the double buffer as one opacity write per layer — so the pane still never re-renders while it streams. It also answers whenBrowserFrameDisplayable, which is a no-op natively: a background write fires no load event, so the flip to the pending layer is armed from an image decode instead of the Image's onLoad."
},
{
"file": "src/browser/browser-frame-data-uri.web.ts",
"reason": "The bridge hands the page its frame as base64 and the data URI wants base64, so the native file encodes the bytes back into the string they arrived as. The page resolves buffer to the JS shim rather than Node's native encoder, which makes that the largest per-frame cost the page pays. This file reads the base64 the bridge kept and falls back to the native encoding for a frame that carried none."
},
{
"file": "src/browser/use-browser-binary-screencast-grant.web.ts",
"reason": "Natively the socket carries the binary screencast frame and this app is both halves of that path, so there is nothing to negotiate. In the page the frames come through a shell that may predate the encoder, and subscribing with wantsBinary against one leaves the pane on a stream no frame can arrive on. This file asks the shell through the grants init carried, per C6 ruling 5."
},
{
"file": "src/browser/browser-screencast-request.web.ts",
"reason": "Not an RN Web API gap but a transport one that exists only in the page: a screencast frame crosses the bridge as one message under BRIDGE_MAX_MESSAGE_BYTES, and a phone's mobile view at the native device scale factor produces a worst-case JPEG larger than that. This file budgets the mobile view's area against the cap, the envelope it measures rather than names, and one worst-case bytes-per-pixel constant. Web view mode is untouched, because a letterboxed desktop viewport is not an area the page can predict."
},
{
"file": "src/session/terminal-snapshot-byte-budget.web.ts",
"reason": "Not an RN Web API gap but a transport one that exists only in the page. A terminal's first frame is its scrollback snapshot, which the desktop trims to 512 KiB of raw terminal text while the bridge measures the serialized event against BRIDGE_MAX_MESSAGE_BYTES. An ANSI snapshot is mostly ESC bytes and JSON spends six on each, so a colour-dense 80-column screen comes back at 669,268 bytes against a 655,360-byte cap and the stream ends with overflow before a live byte is painted. This file derives the budget the page sends in terminal.subscribe from the cap less the event envelope; the native sibling sends nothing, because a socket frame has no ceiling above it and a budget there would shrink a phone's scrollback for a limit that does not exist."
},
{
"file": "src/session/mobile-native-chat-input-styles.web.ts",
"reason": "The chat's composer and its question field render one point above the app's body size, which is 15 and under the floor below which iOS zooms the page on focus. keyboard-occlusion.web.ts reads a scale other than 1 as 'no keyboard' and answers 0, and on this screen that lift is the terminal's only feedback that its hidden input has focus, so one focus of the composer would cost the rest of the session. This file puts both fields on TEXT_INPUT_FONT_SIZE; the native sibling keeps 15."
},
{
"file": "src/browser/browser-address-field-styles.web.ts",
"reason": "The address bar renders at the theme's 12px meta size, and in a browser an input under 16px makes iOS zoom the page on focus and never zoom back. keyboard-occlusion.web.ts reads that scale as 'no keyboard' and answers 0, so one focus would stop the pane lifting for the rest of the typing session. This file puts the input and its overlaid label on TEXT_INPUT_FONT_SIZE with a line box to match; the native sibling keeps 12px, which is what a phone has always rendered and where no page can zoom."
},
{
"file": "src/terminal/TerminalWebView.web.tsx",
"reason": "react-native-webview has no web build that renders anything: on the page it paints the line \"React Native WebView does not support this platform\" where the terminal was, which is a red line and no terminal rather than a crash. This file mounts the same document the WebView loads — xterm imported from @xterm/xterm with the unicode11 and webgl addons, and the document's own modules imported in the order the generator emits them — behind the identical TerminalWebViewProps and TerminalWebViewHandle, so nothing above the contract can tell the two apart."
},
{
"file": "src/terminal/terminal-webview-html.web.ts",
"reason": "The native file composes the whole WebView document, which splices in the 612 KiB minified xterm engine string. On the page that string is unusable — the shell's CSP is script-src 'self' with frame-src 'none', so there is no nested document to load it into — and it would be the largest single module in the session route's closure. The web file answers the caret options, the markup and the stylesheet, which is everything the page mounts, and nothing else; mobile-web-terminal-engine-closure.test.mjs is the fence."
},
{
"file": "src/platform/dictation-capture.web.ts",
"reason": "Dictation's capture seam. The native file holds the microphone through @orca/expo-two-way-audio, which a browser does not have; this one asks the shell for it over native.audio.start|read|stop, draining the shell's ring on a timer and raising each reply as the events the engine emits directly, and taking the utterance's tail off the stop's own reply. The screen an open microphone holds awake is not in this seam at all: the device module that opens the mic takes it and gives it back on both hosts. The flow above the seam is one file on both hosts."
},
{
"file": "src/platform/media-picker.web.ts",
"reason": "expo-image-picker and expo-document-picker are native modules whose import runs a codegen lookup that throws in a browser, and the route manifest imports every route, so one of them in a page closure takes the whole bundle down rather than one picker. This one asks the shell through native.media.pick, reads the bytes back a chunk at a time over native.media.read because a picked image reaches 18 MiB raw against an 8 MiB reply ceiling, and releases every handle it was handed, including the ones its caller never took. The pasteboard is not here: clipboard.web.ts owns it on both platforms and reaches the same verbs for an image."
},
{
"file": "src/components/AuthFailedBannerActions.web.tsx",
"reason": "The auth-failed banner offers Retry, Re-pair and Remove, and on the page only Re-pair works. /pair-scan sits outside the page's route root of app/h (config/scripts/mobile-web-app-route-manifest.mjs:6), so route-handoff.web.ts:207 hands the push to the shell, which opens the native scan screen and returns to the same page document on Back. Retry and Remove are inert here: forceReconnect is null (client-context.web.tsx:57, read through host-client-hooks.ts:88) and removal refuses (page-host-removal-refusal.ts). The banner is reachable on the page because the shell forwards the native client's state (mobile-web-shell/bridge-host.ts:301) and auth-failed is in the wire enum (bridge/bridge-envelope.ts:44). So this one renders Re-pair as native does and one line naming the app for reconnect and removal."
},
{
"file": "src/components/use-html-preview-link-grant.web.ts",
"reason": "Natively the HTML preview is its own WebView and onShouldStartLoadWithRequest hands every request to openExternalLink, so there is nothing to negotiate and the native sibling answers yes. In the page a tap on a link inside the sealed frame becomes a top-frame navigation only the shell around the document can cancel and open, and a shell built before that event drops it in silence. This file asks the shell through the grants init carried (C8.1, ruling 37), and without the externalNavigation grant the preview renders the artifact with its links as text rather than offering a tap that does nothing."
},
{
"file": "src/mobile-web-shell/catch-all-page-route.web.tsx",
"reason": "The catch-all's native body mounts the shell, and the page is what that shell displays; nesting one here would open a second document inside the first, and the shell module calls requireNativeViewManager at import, which throws in a browser."
},
{
"file": "src/mobile-web-shell/page-owns-host-area.web.ts",
"reason": "Natively there is no `init` and the shell layout decides who draws the host sidebar. Inside the page the only answer is the `ownsHostArea` init fact, never the page's own width, because a detail-route page on a wide iPad sits beside the shell's sidebar."
},
{
"file": "src/terminal/terminal-settings-scroll-lock.web.ts",
"reason": "setNativeProps does not exist on React Native Web, where an Animated.ScrollView ref is the DOM node, so the drag's scroll lock threw instead of landing; this one sets overflowY inline and clears it again, which RN Web's own class then takes back over."
},
{
"file": "src/terminal/terminal-live-input-text-write.web.ts",
"reason": "setNativeProps does not exist on React Native Web, where a TextInput ref is the DOM node, so the native write threw out of the session route's mount effect and faulted the page instead of clearing the field; this one sets value on the <input> or <textarea> RN Web rendered."
},
{
"file": "src/terminal/terminal-text-field-submit-binding.web.ts",
"reason": "react-native-web's keydown handler withholds onSubmitEditing whenever the Enter keydown reports a composition — nativeEvent.isComposing, or the Android keyCode 229 that stands for it — which is a soft keyboard's normal state mid-word, so Enter on the page neither sent the line nor cleared the field while native Android's editor action fired as it always does. Both terminal fields reach their send through onSubmitEditing alone and both are bound here: the live input's carriage return, and the buffered command field's draft. The native file binds nothing, because that editor action needs no help; this one listens for beforeinput and acts on insertLineBreak and insertParagraph, the browser's own end-of-line signals. react-native-web calls preventDefault on every keydown it does submit on, so those events exist only in the cases it dropped and never produce a second carriage return; an IME still choosing a candidate reports insertCompositionText and is left alone."
},
{
"file": "src/transport/host-app-version-store.web.ts",
"reason": "The page keeps no record of the host's status.get app version because nothing in it reads one — the reader is the native troubleshoot screen's — and a key the page only writes is not page state, so it is not admitted through page-storage-keys.ts and every mount's write was refused and logged instead."
},
{
"file": "src/notifications/push-token.web.ts",
"reason": "The page holds no device push token and cannot register one, and the import is the defect rather than the calls: expo-notifications runs DevicePushTokenAutoRegistration.fx at import, which reads the persisted registration behind a typeof localStorage === 'undefined' guard that the shell's DOM-storage-off WebView walks through with null, raising \"Cannot read properties of null (reading 'getItem')\" on every page load."
},
{
"file": "src/notifications/desktop-notification-channel.web.ts",
"reason": "The page creates no Android notification channel and the native file already returns early off Android, so what this sibling changes is the import of expo-notifications, whose import-time read breaks the document for the reason push-token.web.ts beside it gives."
},
{
"file": "src/navigation/host-stack.web.tsx",
"reason": "expo-router's Stack renders @react-navigation/native-stack's web view on the page, which flips each screen's display and ignores the animation option, so pushing the session from the worktree list and Back were instant cuts where the native stack slides. This one keeps a popped screen mounted until it has slid out."
},
{
"file": "src/app-update/use-wall-app-update.web.ts",
"reason": "The update checker runs in the app, not the page. The native file hands the compatibility wall the release that check found; this one offers none, so the wall inside the page keeps its store or releases link and the page bundle never carries the checker."
}
]
}