Files
orca/mobile/web-entry/web-overrides.json
T

198 lines
26 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/text-input-content-height.web.ts",
"reason": "react-native-web renders a multiline TextInput as a <textarea> that keeps the browser's default two rows whatever it holds, where a native multiline input grows between its min and max heights by itself. The web file measures the text in a hidden copy of the textarea and returns that height, so the queued-message edit field grows from one line to four; the native file returns nothing to measure. Resizing the live field to measure it was rejected: WebKit clamps the scroll position of the list around it, so the queue jumped while typing."
},
{
"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/MobileNativeChatComposerField.web.tsx",
"reason": "A picked skill leads the composer's draft and is drawn highlighted in place. Natively that is a styled span of the TextInput's own text, but react-native-web renders TextInput as a textarea, which cannot style part of its text and ignores Text children. This one draws a copy of the draft behind the field with the skill highlighted, clears the field's own text while keeping its caret, and moves the copy with the field's scroll."
},
{
"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 the question field at 15 and the composer at 16."
},
{
"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/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:45, 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."
}
]
}