Repository navigation
refactor(mobile): seam moves and the shared shell route guard for the source-control domain (OTA phase C, C4.1) - #21732
Conversation
… (OTA phase C, C4.1) The three openers in the PR sidebar called `Linking.openURL` directly: a check's "open on the web", a comment's permalink, and a link inside comment Markdown. Inside the shell's WebView react-native-web routes that to `window.open(url, '_blank', 'noopener')`, which both shells refuse and which resolves anyway, so the tap reports success and opens nothing. Both C4 routes reach the sidebar, so both would have shipped that. The census is the point rather than the three edits. It derives the two future route closures through `mobileWebAppRouteClosure` and holds every module in them to the seam, so a module entering either closure later is ruled without anyone adding it here. Red first it named all three by `path:line`: CommentMarkdown.tsx:2, PRChecksSection.tsx:2, PRCommentCard.tsx:2, on both routes. The walk it runs was the third copy of one function, so it moves into the seam's own module beside the predicate that module exists to share, and the files and tasks censuses now call it too. It reports `path:line` where the copies reported paths; `reachesReactNativeLinking` keeps its name and its meaning and is now derived from the line list, so there is one rule. An empty offender list is empty in either shape, which is why repointing the two landed censuses moves nothing they assert. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…and conflicts (OTA phase C, C4.1) The two copy actions both C4 routes reach called `expo-clipboard` directly: the conflict section's refresh commands and the review sheet's notes. On the web that module is `navigator.clipboard`, which needs a secure context — the iOS shell serves the page from a custom scheme and Android from https, so the path works on one platform and silently not on the other. `useClipboardWriter` is the seam C2.4 landed for exactly that. Red first, the census named both routes: `ExpoClipboard.web.js` in each closure, and `src/platform/clipboard.web.ts` in neither. Both call sites also stopped ignoring whether the pasteboard took the text. The conflict section already returned on a throw, so the seam's rejection reaches an arm it had. `copyNotes` had none and its only caller is `void controller.copyNotes()`, so a rejection would have been unhandled with "Review notes copied" left on screen; it now catches and reports through the screen's own error line. That is the one behaviour change here and the reason `clipboard` joins its dependency array. Its suite mocked `setStringAsync` as resolving `undefined`, which the seam reads as a pasteboard that refused, so every copy would have gone down the new refusal arm unseen. The mock now resolves `true` and two cases pin both arms; mutating the catch away kills the refusal one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…m (OTA phase C, C4.1) The hub takes its router once, in the openers hook, and passes it down to the runners and the panel — so one `useRouter()` is this domain's whole reach into routing, and it was expo-router's own. Inside the shell's page that posts no `navigate`, so the hub's push to review would stay in the document whatever its grants, and its push to a native route would paint Unmatched over the page. Inert today: no C4 route is registered yet. `use-mobile-source-control-runners.ts` is the second case and the reason the rule reads value imports rather than identifiers: it named expo-router only to write `ReturnType<typeof useRouter>`, a value import in a type position that keeps the module in the graph. `RouteHandoff` is the seam's own name for that type. The census is C3.1's, and its walk moves to `src/navigation` rather than being copied a second time; each domain keeps only its own evidence, the list of modules meant to hold a router. Red first it named both modules on the expo-router rule and reported no handoff caller at all. The C2.9 hop census is unchanged and cannot move: its targets come from the call sites, and the derivation over this tree returns the same ten targets and the same 26 unresolved sites before and after this commit, byte for byte. Its `HANDED_OFF` pin is over registered routes, of which this adds none. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…(OTA phase C, C4.1) `src/files/mobile-file-shell-route.ts` was never about files: it parses a route against `BridgeInitRouteSchema` and builds the key a shell screen remounts on. It moves to `src/mobile-web-shell/shell-screen-route.ts` as `shellScreenRoute` / `shellScreenRouteKey`, with its test. The move is pure — with the rename applied and comments stripped, the old file and the new one diff to nothing. Three routes had grown their own copy of the call and two had none. The copies go: `agent-history` and `tasks` now ask the shared predicate, which is the same schema and the same fallback they already had. `index.tsx` had no guard at all, so a `.` or `..` host id was handed over and came back as "Update Orca to open this workspace" painted over the native list behind the switch; it now stays native. That is the one behaviour change here, pinned red first and killed by mutation. `web.tsx` keeps handing that route over on purpose and is exempt by name: its fallback is a redirect to the route the user came from, so the host's own verdict is the better answer there, which `mobile-web-shell-route.test.tsx` already pins. No `key=` expression moved; the three switches still key differently (host id, pathname, pathname plus params) and making them agree is a behaviour change for another PR. The census walks the route tree rather than a list, so a switch added later is held to both rules without being added here. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…g one (OTA phase C, C4.1) The changed-code gate flagged four type assertions on the two cases added with the clipboard seam: they stubbed an `RpcClient` the way the file's older cases do, and the gate reads changed lines. Copying reaches no client at all, so they mount without one, which is both cast-free and a truer statement of what the path needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe change centralizes external-link detection and route-closure checks. Mobile shell switches now use Priority: ➖ Normal 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Description checkExplanation The description gives detailed scope, rationale, implementation changes, visible behavior changes, and testing results. It does not follow the required template and omits the mandatory linked issue, visual proof or an explicit N/A statement, checklist items, and several required sections. Resolution Use the repository template headings. Add a linked issue in the required
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 702e32a1-0d0a-4fc5-a84e-f028ac00e9ff
📒 Files selected for processing (26)
config/scripts/mobile-web-app-external-link-seam.mjsconfig/scripts/mobile-web-app-external-link-seam.test.mjsconfig/scripts/mobile-web-app-files-external-links.test.mjsconfig/scripts/mobile-web-app-source-control-external-links.test.mjsconfig/scripts/mobile-web-app-tasks-external-links.test.mjsmobile/app/h/[hostId]/agent-history/[worktreeId].tsxmobile/app/h/[hostId]/files/[worktreeId].tsxmobile/app/h/[hostId]/files/preview/[worktreeId].tsxmobile/app/h/[hostId]/index.tsxmobile/app/h/[hostId]/tasks.tsxmobile/src/components/pr-sidebar/CommentMarkdown.tsxmobile/src/components/pr-sidebar/PRChecksSection.tsxmobile/src/components/pr-sidebar/PRCommentCard.tsxmobile/src/components/pr-sidebar/PRConflictingFilesSection.tsxmobile/src/files/files-router-seam-census.test.tsmobile/src/files/mobile-file-path-route-encoding.test.tsmobile/src/mobile-web-shell/mobile-web-shell-host-list-route.test.tsxmobile/src/mobile-web-shell/shell-screen-route-census.test.tsmobile/src/mobile-web-shell/shell-screen-route.test.tsmobile/src/mobile-web-shell/shell-screen-route.tsmobile/src/navigation/router-seam-census.test-support.tsmobile/src/session/use-mobile-diff-review-send-actions.test.tsmobile/src/session/use-mobile-diff-review-send-actions.tsmobile/src/source-control/source-control-router-seam-census.test.tsmobile/src/source-control/use-mobile-source-control-openers.tsmobile/src/source-control/use-mobile-source-control-runners.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.
…ine (OTA phase C, C4.1 round 1) Round-1 folds, four wordings and one ordering. `externalLinkOffenders` said "every call site" and reports the line the name enters the module: a named import once, however many times the module calls `openURL`, because the import is what the rule is about and what has to go. Only a namespace import reports its uses, there being no single line to name. The docstring now says that. Its red list sorted the rendered strings, which puts `:10` before `:2`. It now sorts by path and then by line as a number. Pinned against a written fixture rather than the tree, because the case needs a module with sites either side of line ten and no module in a closure has to keep having one — the first fixture used lines 11 and 12, where both orders agree, and the mutation walked straight through it. `shell-screen-route.test.ts` still named the files screens in its describe after the guard stopped being theirs; it names what a switch does now. `router-seam-census.test-support.ts` excluded `.test-support.ts` from the walk, which the files census it was extracted from never did. Dropped, so both censuses walk the same set. Inert today: neither `src/files` nor `src/source-control` holds such a file, so it only decides the next one. The `web.tsx` exemption claimed a redirect "that looks like nothing happened". What was measured: adopting the guard there sends a `..` deep link through `Redirect href="/h/.."` to the host route, which this PR keeps native, so the developer lands on the host list with nothing said about why the page did not open. The route is `__DEV__`-only and the host's own failure screen is the better verdict. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- External-link seam: the three raw
Linking.openURLsites in the PR sidebar (CommentMarkdown,PRChecksSection,PRCommentCard) now open throughsrc/platform/external-link, which is the module theexternalLinkgrant is about. - Clipboard seam:
PRConflictingFilesSectionanduseMobileDiffReviewSendActionscopy throughuseClipboardWriter. The review-notes copy now surfaces a refused write instead of claiming "Review notes copied"; the conflicts copy keeps its silent no-op. - Router seam:
use-mobile-source-control-openers.tstakes its router fromuseRouteHandoff, anduse-mobile-source-control-runners.tsimports theRouteHandofftype, so neither reaches expo-router's runtime router from inside the shell page. - Guard move:
mobile-file-shell-route.tsbecomesmobile-web-shell/shell-screen-route.ts(shellScreenRoute/shellScreenRouteKey); the function bodies are identical apart from the import path and names, and every switch imports the shared guard. index.tsxgains the guard: a.or..host id now stays native rather than handing the page a route the bridge refuses.web.tsxis exempted by name, with the exemption asserted against the file tree so it cannot outlive it.- Censuses: a shared
externalLinkOffenderswalk replaces the three per-domain copies, a shared router-seam test-support module serves the files and source-control walks, and newshell-screen-route/ source-control censuses pin the guard and router rules.
I verified the changed mobile tests (7 files, 77 passed), the config script censuses with mobile deps present (4 files, 33 passed), pnpm --dir mobile run typecheck, and oxlint on the changed files — all clean. The move, the guard rejection of dot segments, and the clipboard refusal path all check out.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…face a refused copy (OTA phase C, C4.1 CodeRabbit) Three bots findings on #21732, all real. **Every alias, not the first.** `reactNativeLinkingSites` found namespace imports with `exec` and inspected only the first binding, so a module importing the namespace twice and calling `Linking.openURL` on the second reported no site at all. It reads every alias now and counts a line once however many meet on it. Red first with exactly that fixture. **The host id can be an array.** `app/h/[hostId]/index.tsx` read it bare, and Expo Router answers a repeated key with one: `String(['a','b'])` is `a,b`, `encodeURIComponent` makes that the single segment `a%2Cb`, and the segment rule accepts it — so the shell opened a page for a host nobody has. Through `firstParam`, as the other four switches do. Red first it handed over `/h/host-1%2Chost-2`, and the empty-array case found a second one: `[]` is truthy, so a bare read built `/h/` and handed that over too; `firstParam` answers `''` and the route stays native. That import pulls the source-control screen state, and with it the lucide barrel whose `LucideProvider` re-export is the gap the web build patches, so the suite mocks the barrel as the other suites do. It moves no page closure: the closure resolves `index.web.tsx`, which this does not touch, and the index route still measures 3426 modules, 289 local, 22 families. **A refused copy said nothing.** `PRConflictingFilesSection` caught the rejection and returned: no tick, no message, a tap indistinguishable from one that copied. The label now carries the third state, reusing the tasks page's own wording for it, and the component has its first test. Mutating the failure arm away reds it. Its prop narrows to `Pick<PRInfo, 'mergeable' | 'conflictSummary'>`, which is what it reads and what let the test drop a cast the gate flagged; every caller holds a full `PRInfo` and satisfies it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
…hase C, C4.1 round 2)
Round-2 additions. A separate commit because `d4b14d54e5` was already
made and this lane does not amend.
**The seam walk parses now.** Matching `X.Linking` in the text named it
inside a comment that talks about it and inside a string that quotes it,
and the named-import regex did the same for a commented-out import.
Checked against the previous implementation, all three fixtures were red
there: the comment case reported lines 2 and 3, the string case reported
the string's line beside the real call, and `// import { Linking } from
'react-native'` reported line 1. The walk builds a `SourceFile` and reads
import declarations and property accesses, so comments and strings are
gone by construction and the quote styles stop being a special case. Cost
measured on the three closure censuses: 3.3 s, unchanged.
**The route census reads the call, not the import.** A switch that keeps
the import while the call goes — deleted, or moved behind a branch that
never runs — looked exactly like one that asks. It now needs both, proved
by mutation: dropping `shellScreenRoute(` from `tasks.tsx` while leaving
its import names `tasks.tsx`. A fixture carries the same rule in
isolation, since every switch in the tree calls what it imports and the
case would otherwise be unfalsifiable against it.
**And recognises a switch by its import** of `MobileWebShellScreen` rather
than by `<MobileWebShellScreen` in the text, so an alias or a line break
the formatter chose cannot hide one and a comment cannot invent one.
The `app/h/[hostId]` root stays written out: deriving it from the manifest
is not a one-liner from here, the manifest being an `.mjs` this test reads
as text. What ties the two together instead is a new case asserting every
registered pathname starts with that prefix, so a page route outside it
fails rather than going unwalked.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 02d646ce-3c5c-4eea-9649-aed34c0519bc
📒 Files selected for processing (7)
config/scripts/mobile-web-app-external-link-seam.mjsconfig/scripts/mobile-web-app-external-link-seam.test.mjsmobile/app/h/[hostId]/index.tsxmobile/src/components/pr-sidebar/PRConflictingFilesSection.test.tsmobile/src/components/pr-sidebar/PRConflictingFilesSection.tsxmobile/src/mobile-web-shell/mobile-web-shell-host-list-route.test.tsxmobile/src/mobile-web-shell/shell-screen-route-census.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- mobile/src/mobile-web-shell/shell-screen-route-census.test.ts
- mobile/app/h/[hostId]/index.tsx
- mobile/src/mobile-web-shell/mobile-web-shell-host-list-route.test.tsx
- config/scripts/mobile-web-app-external-link-seam.test.mjs
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review.
| aliases.add(bindings.name.text) | ||
| continue | ||
| } | ||
| if (bindings.elements.some((element) => element.name.text === 'Linking')) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Check the imported name instead of the local binding name.
For import { Linking as NativeLinking } from 'react-native', element.name.text is NativeLinking. The census therefore misses the import and permits direct calls through NativeLinking.
Use propertyName when the import has an alias.
Proposed fix
- if (bindings.elements.some((element) => element.name.text === 'Linking')) {
+ if (
+ bindings.elements.some(
+ (element) => (element.propertyName ?? element.name).text === 'Linking'
+ )
+ ) {
sites.push(lineOf(statement))
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (bindings.elements.some((element) => element.name.text === 'Linking')) { | |
| if ( | |
| bindings.elements.some( | |
| (element) => (element.propertyName ?? element.name).text === 'Linking' | |
| ) | |
| ) { |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The commits since the prior pullfrog review harden the two new censuses to read code rather than text, and close the host-id and copy edge cases the CodeRabbit round raised.
- Rewrote the external-link census to parse each module's AST:
reactNativeLinkingSitesnow reads aLinkingnamed import and every react-native namespace alias from the TypeScript tree, so a comment, a string, or a commented-out import no longer produces a line, and a second namespace import is no longer invisible. - Made the guard census read the call, not just the import: a switch is recognised by importing
MobileWebShellScreenand must both import and callshellScreenRoute; a new case walks the page-route manifest and fails if any registered pathname sits outside/h/[hostId]. - Normalised the host-list switch's param:
index.tsxreadshostIdthroughfirstParam, so a repeated key opens the first host instead of the joineda%2Cbsegment, and an empty array stays native. - Surfaced a refused clipboard write in the conflicts section: the copy control carries
idle/copied/failed, so a rejected write shows "Failed to copy text" instead of a silent no-op; its first component test pins both arms.
Verified the changed mobile tests (8 files, 83 passed), the external-link censuses with mobile deps present (4 files, 40 passed), and oxlint on the changed files — all clean.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
… C4.1 CodeRabbit)
`import { Linking as NativeLinking } from 'react-native'` went through the
census untouched: the walk compared the specifier's local binding, which
is `NativeLinking`, while the imported name lives in `propertyName` when
a specifier renames it and only in `name` when it does not. Reproduced
before the fix — the aliased import with a call beside it reported no
site at all.
Reading `propertyName ?? name` closes it in both directions. A module
that renames `Linking` is named at its import line like any other, and a
module that imports `View as Linking` is no longer named for a local
binding that reaches nothing. The second was a false positive the old
comparison had by construction.
One more of the same class, found while checking and verified rather than
assumed: `import RN from 'react-native'` typechecks in this project (tsc
accepts it), and a default binding is the whole namespace exactly as
`* as RN` is, so `RN.Linking.openURL` through it was invisible too. The
default binding joins the alias set, which already reports uses rather
than the import.
Four fixtures. Three red on the previous walk: the renamed import, the
local-only `Linking`, and the default import. The fourth — an alias
imported that never reaches `Linking` — passed before and is here to hold
the other half of the rule, that importing react-native is not itself the
offence.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: af5894d2-e960-4d0c-9c59-8d9f4c3739f0
📒 Files selected for processing (2)
config/scripts/mobile-web-app-external-link-seam.mjsconfig/scripts/mobile-web-app-external-link-seam.test.mjs
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
The delta since the prior pullfrog review (172891d1bc) is one CodeRabbit fold plus a main merge, and both hold up.
- Read the imported name, not the local binding.
reactNativeLinkingSitesnow compareselement.propertyName ?? element.nameagainstLinking, soimport { Linking as NativeLinking }is named (andimport { View as Linking }is correctly left alone); readingnamealone let the renamed import through the census entirely. - Read a default import as the namespace it is.
clause.nameis added to the alias set, soimport RN from 'react-native'followed byRN.Linking…reports, matching the namespace-import arm. - Four new fixtures pin the renamed, local-name, default-import and unused-alias cases with exact
toEqual([...])assertions; the renamed and default cases both fail against the prior walk. - Merge of
main(b766f512ec) picks up theDiffViewer.tsxmax-lines fix with no conflicts and no product changes.
Verified the seam census directly (22 passed). The three closure censuses skip in this environment because mobile/node_modules/react-native-web is absent and run for real in CI's mobile_web_app job; I confirmed no module under mobile/ default-imports bare react-native, so the new default-import arm is inert against the current tree and is forward coverage only.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…(OTA phase C, C4.1 CodeRabbit)
Two ways a module reached `Linking` past the census, both reproduced
before the change.
Every file was parsed as TSX. In a `.ts` module `const id = <T>(value:
T) => value` is a generic arrow; as TSX it is an unclosed JSX element,
and the parser folds the rest of the file into the error node. A
`RN.Linking.openURL` after one reported nothing, and so did the same call
with its import above the arrow. The file name goes into the parse now
and TypeScript reads the kind off the extension; `externalLinkOffenders`
passes the real path, which it had all along.
`ExportDeclaration` was never inspected, so `export { Linking } from
'react-native'` put the name back in reach of anything importing that
module while the census saw an import list it was not on. All four shapes
are read — named, renamed, `export *` and `export * as` — and reported at
the export statement, which is the line to delete exactly as an import
is. A re-export of another name, or of `Linking` from somewhere that is
not react-native, stays unnamed.
Seven fixtures. Five red on the previous walk: the `.ts` generic arrow
and the four re-export shapes. The two that pass before and after hold
the other half, that re-exporting is not itself the offence.
The named-import and re-export clauses read `propertyName ?? name`
through one helper rather than two spellings of it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review (7ff4b1595f), a single CodeRabbit fold.
- Parsed each module by its own extension.
reactNativeLinkingSitesnow hands the file path tots.createSourceFileinstead of forcingScriptKind.TSX, so a.tsmodule with a generic arrow (const id = <T>(v: T) => v) is no longer mis-parsed as an unclosed JSX element that swallows the sites below it;externalLinkOffenderspasses each closure file through. - Reported re-exports.
export { Linking } from 'react-native'and its renamed,export *, andexport * as RNforms now name the export line, with negative fixtures forexport { View }and a specifier from another module. - Routed the predicate through the walker.
reachesReactNativeLinkingdelegates toreactNativeLinkingSites, so the boolean and the line list cannot drift into two rules.
The extension-based parse only changes behavior for .ts files, where the TS grammar is a superset of what TSX parsed, so the walk can find more sites and never fewer. The re-export arm is inert against the current tree (no mobile module re-exports react-native) and the four new positive fixtures plus two negatives exercise it directly; export { L as Linking } correctly stays clean since propertyName ?? name reads the imported name.
Verified the seam census directly: config/scripts/mobile-web-app-external-link-seam.test.mjs — 29 passed. The three closure censuses skip here because mobile/node_modules is absent and run in CI's mobile_web_app job.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Summary
Phase C, C4.1: the inert preparation for the source-control and review page routes (
design-ota-c4.md§4 and §7, ruling 4 inrulings-ota-c4.md). Eleven commits (five product, the round-1 fold, the CodeRabbit fold, the round-2 fold, a second CodeRabbit fold, a merge of main, a third CodeRabbit fold), no route registered, nothing a released phone or the desktop can observe.acb40930f3Linking.openURLsites in the PR sidebar (CommentMarkdown,PRChecksSection,PRCommentCard) open throughsrc/platform/external-link.ts, the seam theexternalLinkgrant is aboutce42ea2fb7expo-clipboardsites (PRConflictingFilesSection,use-mobile-diff-review-send-actions) copy throughsrc/platform/clipboard.ts; on web the raw module would have reachednavigator.clipboardand worked on Android only6669c0f9d4use-mobile-source-control-openers.tsanduse-mobile-source-control-runners.tstake their router fromuseRouteHandoff, as C3.1 did at its two sites; natively the plain router, inside a shell the C2.9 rule once a route is registeredd5f1eb4098src/files/mobile-file-shell-route.ts→src/mobile-web-shell/shell-screen-route.ts(shellScreenRoute,shellScreenRouteKey), a pure move (renames applied and comments stripped, the bodies diff to nothing), imported by every switch2639852dd8Censuses, red first
Under
config/scripts, over the closures of the two future routes derived throughmobileWebAppRouteClosure:mobile-web-app-source-control-external-links.test.mjs(every external URL through the seam; the seam present) named the three importing modules bypath:line(the import line, once per module, whatever the call count) before the fix; the clipboard census foundExpoClipboard.web.jsin the closure and the platform seam absent; the router census found both modules on the rawexpo-routerimport and no handoff caller. The offender walk is one module now,mobile-web-app-external-link-seam.mjs, serving the files and tasks censuses too (they reportpath:linewhere they reported paths; an empty list is the same assertion), and C3.1's router-seam walk lives insrc/navigation/router-seam-census.test-support.ts.The guard census in the mobile suite asserts every switch imports the shared guard:
agent-history,index.tsx,tasks.tsxwere not asking it, andagent-historyandtasks.tsxspelled the schema themselves.Three things that are more than a move
PRInfofields it reads). At the base a rejectingsetStringAsyncin the review send sheet escaped as a floating rejection and afalsereturn still said "Review notes copied"; the conflict section ticked "Copied" the same way. Through the seam a refusal throws, the send sheet reports it on the screen's error line and the conflict section does not tick. That is the seam's point, and it is a visible change.index.tsxgains a guard it never had. A.or..host id was handed to the shell, which painted "Update Orca to open this workspace" over the native list; it now stays native like every other switch. Pinned red first. The switch also reads the param throughfirstParamlike its four siblings (CodeRabbit): an array id no longer builds/h/a%2Cbfor the shell, it opens the first host as the siblings do, and an empty array (truthy, so the bare read built/h/) stays native.web.tsxis exempted by name. Adopting the guard there redsmobile-web-shell-route.test.tsx, which pins on purpose that a..host id reaches the bridge and comes back as the host's verdict, because that route's fallback is a redirect to where the user came from. The census asserts the exempted file exists.Noted, not changed: the switches still key remounts differently (tasks on
hostId, agent-history onroute.pathname, files on pathname plus params); only the last makes a same-path param change a remount, which is whatshellScreenRouteKey's docstring argues for. Making them agree is a behaviour change for its own PR.The C2.9 hop census cannot have moved: no route is registered, and the derivation at
adfcbaf685gives the same ten targets and the same unresolved sites at the base and at the head.Verified
2639852dd8: mobile tsc, ratchet, whole-tree oxlint, oxfmt (2955 files), root oxfmt and oxlint on the five changed root files, full mobile suite (722 files, 8204 passed, noErrorsline), config/scripts with mobile deps required (221 files, 2013 passed), changed-code quality and react-doctor against6a0200f416, merge-tree max-lines clean, machine-path grep clean. The lane usedgit stashonce (tagged, applied by sha, dropped) to observe the clipboard census red, against the lane rules; reported, not repeated.172891d1bc: the parsed walk read the local binding name, soimport { Linking as NativeLinking }was missed;82aba28dfareads the imported name and tracks the local alias (and a default binding,import RN from 'react-native', which typechecks here and carries the whole namespace), which also removed a false positive (import { View as Linking }was named for a binding that reaches nothing); four fixtures, three red on the previous walk.db077f5faa): approve, every claim reproduced; folded in172891d1bc: the external-link walk parses with the TypeScript API instead of matching text (aLinking.openURLin a comment, in a string, and a commented-out import were all named before; three fixtures), the guard census reads the call as well as the import (droppingshellScreenRoute(fromtasks.tsxwhile keeping the import names it) and recognises a switch by itsMobileWebShellScreenimport, and a case asserts every registered pathname sits underapp/h/[hostId]so a route outside it fails rather than going unwalked. Gates re-measured atdb077f5faa: config/scripts 221 files / 2015 passed, mobile 722 files / 8204 passed, noErrors.2639852dd8): approve; folded indb077f5faa: the walk's docstring says what it reports (the line a named import enters the module; a namespace import reports its uses) and its red list sorts by path then line as a number (the first fixture, lines 11 and 12, could not tell numeric from text order and let the mutation pass; it is lines 2 and 10 now, in a temp dir); the moved test's describe; the.test-support.tsexclusion the files walk never had; theweb.tsxexemption's wording carries what was measured (the guard there would send a..deep link throughRedirect href="/h/.."to a host list with nothing said about why; the route is__DEV__-only and the host's failure screen is the better verdict).7ff4b1595f, two Majors on the walk, both reproduced and folded in69da504f90: every file was parsed as TSX, so in a.tsmodule a generic arrow parsed as an unclosed tag swallowed everything after it (a namespace import above it was walked, the call below it was not, in either order); the file path now goes into the parse and TypeScript takes the kind from the extension. AndExportDeclarations were ignored, soexport { Linking } from 'react-native'(named, renamed,export *,export * as) let a route import the re-export; all four shapes are named at the export statement, with two negative fixtures (another name;Linkingfrom elsewhere) holding the other half. No real closure newly reds.b766f512ec(7ff4b1595f, no conflicts) to pick up theDiffViewer.tsxmax-lines fix that had every PR's static analysis red.verify-gates.shon2639852dd8,db077f5faaand172891d1bcagainst6a0200f416, and on7ff4b1595fand69da504f90againstb766f512ec; the dropped-call mutation reds the guard census on172891d1bc; the links census reds naming the three importing modules with them reverted; the guard's old and new bodies differ only in the import path and the two names.Part of the mobile OTA plan (Phase C). Reference PR #13386, never merged.