diff --git a/config/knip.mobile.json b/config/knip.mobile.json
index ac35e9fc4be..fc5095b09b2 100644
--- a/config/knip.mobile.json
+++ b/config/knip.mobile.json
@@ -23,15 +23,6 @@
"packages/*/src/**/*.{js,jsx,ts,tsx}",
"*.{js,ts}"
],
- "ignoreFiles": [
- "src/accounts/use-open-mobile-accounts.ts",
- "src/components/mobile-pr-url.ts",
- "src/notifications/use-open-notification-route.ts",
- "src/session/mobile-native-chat-message-occurrences.ts",
- "src/session/use-mobile-native-chat-cancel-ask.ts",
- "src/session/use-open-mobile-session.ts",
- "src/tasks/use-open-mobile-tasks.ts"
- ],
"ignoreExportsUsedInFile": true,
"includeEntryExports": false
}
diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md
index e20c97bd7d5..f593fd853c4 100644
--- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md
@@ -21,11 +21,11 @@ performance, accessibility, or production-promotion gates.
| Candidate checkpoint | `e931b2db07` |
| `origin/main` and merge base at checkpoint | `4c984d4c1b` |
| Checkpoint comparison | 81 commits; 1,396 files; 140,153 additions; 8,772 deletions |
-| Post-audit integration checkpoint | `b14fe74214` |
-| Current `origin/main` checkpoint | `b2902cb61e` |
-| Post-audit comparison | 89 commits; 1,401 files; 134,626 additions; 8,771 deletions |
-| Latest verified package | `121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5` |
-| Latest package size | 51 assets; 9,151,993 raw bytes; 2,649,166 gzip bytes |
+| Post-audit integration checkpoint | `b8d04428c0` |
+| Current `origin/main` checkpoint | `e50cc309c3` |
+| Post-audit comparison | 92 commits; 1,401 files; 134,633 additions; 8,771 deletions |
+| Latest verified package | `015f15ca1bbbc28f25318283a2fff0ea616db868540a2e86bd3d5ce61c4a3f25` |
+| Latest package size | 52 assets; 9,310,227 raw bytes; 2,691,744 gzip bytes |
Commit and branch counts are historical evidence for that checkpoint; rerun
`git merge-base origin/main HEAD`, `git rev-list --count origin/main..HEAD`, and
@@ -45,7 +45,7 @@ Commit and branch counts are historical evidence for that checkpoint; rerun
| Bridge | Exact v2 contract, named operations, generated grants/schemas/bounds, opaque authority, mutation reauthorization, response correlation, lifecycle cleanup | `548d8d64aa`, `e526780848`, `b2068661f7`, `ee15298704`, `2094dde1f9` |
| Routes | Workspace, Accounts, Tasks, Session, Agent History, Files/Preview, Source Control, and Review use explicit hosted adapters | [Parity inventory](2026-07-22-mobile-hybrid-webview-parity-inventory.md) |
| Terminal | Real xterm/PTY byte path, bounded ACK/backpressure, links, selection/paste, reconnect/resnapshot, SSH recovery | Direct, SSH, and packaged SSH E2E commands |
-| Native capabilities | Gesture/permission/foreground-mediated clipboard, documents/photos, haptics, external URLs, speech/audio, settings, and diagnostics | iOS/Android capability journeys and focused tests |
+| Native capabilities | Gesture/permission/foreground-mediated clipboard, documents/photos, haptics, external URLs, speech/audio, alerts, Android Back, settings, and diagnostics | iOS/Android capability journeys and focused tests |
| Privacy | Credentials, endpoints, paths, durable host identity, page payloads, and full build IDs excluded from page authority, storage, logs, cache identity, and diagnostics | `8ee4fcdac1`, `d6b8b14c82`, exact-app privacy audits |
| Security review | 2026-08-21 independent OpenCode review covered bridge, shells, package store, runtime RPC, Relay, and SSH; high-severity findings fixed | Review record summarized below; not physical/store certification |
| Rollback | Automatic active/previous recovery, process-loss rollback, host-scoped manual recovery, and corrected Desktop/native incident procedure | [Rollback runbook](../mobile-hybrid-webview-rollback.md) |
@@ -68,6 +68,7 @@ Run from the repository root unless noted.
| iOS hosted journey | `pnpm --dir mobile test:e2e:hosted-webview` | Route/capability/reconnect simulator journey |
| iOS hostile-content journey | `pnpm --dir mobile test:e2e:hosted-webview:security` | Network/navigation/executable/storage isolation |
| Android route journey | `pnpm --dir mobile test:e2e:hosted-webview:android-routes` | Hosted Source Control/Review and route interactions |
+| Android real hardware Back | `pnpm --dir mobile test:e2e:hosted-webview:android-back` | Real `KEYCODE_BACK`: nested route, Session, workspace, native shell |
| Android hostile-content journey | `pnpm --dir mobile test:e2e:hosted-webview:android-security` | Executable/network/navigation/privacy isolation |
| Android locally signed Release inspection | `pnpm --dir mobile test:e2e:hosted-webview:android-release` | Local Release WebView behavior only; not Play signing |
| iOS process-loss rollback | `pnpm --dir mobile test:e2e:hosted-webview:ios-crash-loop` | Repeated WebView loss and generation recovery on Simulator |
@@ -83,8 +84,8 @@ and raw result location.
## Package and Topology Evidence
- Latest independent verification:
- `121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5`,
- 51 assets, 9,151,993 raw bytes, 2,649,166 gzip bytes.
+ `015f15ca1bbbc28f25318283a2fff0ea616db868540a2e86bd3d5ce61c4a3f25`,
+ 52 assets, 9,310,227 raw bytes, 2,691,744 gzip bytes.
- Packaged macOS arm64 Desktop through an isolated Docker SSH provider to an
actual iOS WKWebView used
`7c7c673deb74e158cdfb99b1ca536fd88cd3ab5dac4eb8db78c43ca12f6ce31d`.
@@ -115,6 +116,15 @@ native-versus-hosted results must replace the invalidated figures before parity
certification. Simulator evidence will still not close physical-device,
accessibility, input, or performance gates.
+The 2026-08-23 strict iPhone Simulator Source Control/Review run used package
+`015f15ca1bbbc28f25318283a2fff0ea616db868540a2e86bd3d5ce61c4a3f25` and an
+actual WKWebView. It covered host-list long press into Source Control, changed
+file Review, return to Source Control and workspace root, physical workspace
+selection into Session, and a second session-origin Source Control mount. The
+corrected native-versus-hosted captures passed at 1.172% and 1.189% changed
+pixels against a 3% budget, with 1.470 and 1.321 mean channel differences
+against a 4.0 budget. The second mount retained branch and pull-request state.
+
## Security and Lifecycle Evidence
- TypeScript, Swift, and Kotlin share exact path, hash, MIME/role, manifest,
@@ -124,13 +134,17 @@ accessibility, input, or performance gates.
traversal, symlinks, linked parents, unexpected file types, and out-of-root
reads before activation. Mirrored concurrency tests cover same-host stage,
activation, cleanup, commit/abort, and removal races.
-- All 224 recorded production grants rejected eight malformed request shapes
- plus an oversized request before native/host access: 2,016 cases. Exported
+- All 225 recorded production grants rejected eight malformed request shapes
+ plus an oversized request before native/host access: 2,025 cases. Exported
results and subscription events have invalid-payload admission coverage;
invalid events retire their subscriptions.
- Cross-build/session tests cover a 15-pair stale authority grid. Cancellation,
client replacement, disposal, replay, late subscription registration, and
delayed mutation results fail closed.
+- Branch comparisons retain one bounded shell-owned host snapshot across
+ single-use page continuations. Only the exact next page bypasses the initial
+ rate charge; forged and replayed continuations remain throttled, and large
+ comparisons no longer rerun Git for every 128-entry page.
- Exact iOS and Android emulator apps rendered hostile filenames, diffs,
terminal links, provider/task strings, errors, Markdown, HTML, SVG, Mermaid,
and image metadata inertly, with no execution marker or sentinel traffic.
@@ -141,6 +155,10 @@ accessibility, input, or performance gates.
- Automatic recovery covered corruption and repeated process loss. The iOS
Simulator candidate-to-final three-crash drill restored the verified previous
generation. Final physical/store-signed rollback drills remain open.
+- Android API 36 exercised three real hardware Back events from Agent History
+ through Session and workspace root to the reset app's native shell, followed
+ by a clean bridge-log audit. Dirty-state, mixed-version, and physical-device
+ Back coverage remain open.
- The 2026-08-21 independent OpenCode review found issues in live bridge/native/
package/runtime RPC/Relay/SSH boundaries. High-severity findings were repaired,
including bounded SSH/Relay methods, connection-owned file-watch teardown,
@@ -149,11 +167,19 @@ accessibility, input, or performance gates.
## Evidence Interpretation
-The latest recorded full mobile validation passed 599 mobile files and 3,568
-tests with two expected skips, plus mobile typecheck/lint/format and diff
-hygiene. Root typecheck/lint/code-quality, 56 reliability gates, localization,
-and max-lines also passed at that checkpoint. Rerun current CI before release;
-historical counts are not evergreen.
+The latest recorded full mobile validation passed 722 files and 4,708 tests,
+with three skips. The latest concurrent root run passed 6,193 files and 58,084
+tests, with 39 skipped files and 245 skipped tests; two deadline-sensitive tests
+failed under load and then passed 25/25 together with one worker. Root, mobile,
+and hosted-web typechecks and lints, changed-code quality, 90 reliability gates,
+localization, max-lines, and Electron ratchets also passed. Relay Markdown
+discovery passed its shared cross-platform process-launcher boundary tests.
+Rerun current CI before release; historical counts are not evergreen.
+
+The 2026-08-23 branch-comparison correction additionally passed 33 focused
+broker/history/grant tests, mobile and hosted-web typechecks, focused lint and
+formatting, diff hygiene, and the strict actual-WKWebView Source Control/Review
+journey above.
No entry in this document claims the following passed: physical phones or
tablets, production-store-signed apps, Windows/Linux/headless packaged Desktop,
diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md
index 8efc9bb57e5..89bdc8a6d39 100644
--- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md
+++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md
@@ -559,8 +559,8 @@ renderer-independent, while the authoritative RNW package still verifies as
build `f293890e…`.
Raw bridge envelopes, including initial shell setup, now pass an exact
unique-key, paired-surrogate, bounded JSON grammar before schema parsing. The
-generated broker corpus rejects 2,016 malformed or oversized requests across
-all 224 production grants before host or native access. Valid response
+generated broker corpus rejects 2,025 malformed or oversized requests across
+all 225 production grants before host or native access. Valid response
envelopes also cross 157 exported result schemas and all eight subscription
event schemas with invalid payloads; requests fail closed and invalid events
cancel their subscriptions. Pending terminal/native-chat setup cannot register
diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md
index 7cf0a05d45f..6219798c5d5 100644
--- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md
+++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md
@@ -158,8 +158,8 @@ pnpm test:e2e:hosted-mobile-webview:ssh:packaged
```
The final recorded independently verified package was
-`121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5`:
-51 assets, 9,151,993 raw bytes, and 2,649,166 gzip bytes. A packaged macOS arm64
+`5eda0f5c7f5265e9be2420d9178c03605dbe4c8e5ea9e669327d680f1e9c0eb3`:
+52 assets, 9,308,959 raw bytes, and 2,691,373 gzip bytes. A packaged macOS arm64
Desktop to Docker SSH to actual iOS WKWebView journey used package
`7c7c673deb74e158cdfb99b1ca536fd88cd3ab5dac4eb8db78c43ca12f6ce31d`
without checkout fallback.
@@ -179,6 +179,8 @@ performance gates.
- Shared workspace, Accounts, Tasks, Session, Files/Preview, Agent History,
Source Control, Review, terminal, browser, attachments, and native-chat
presentation through explicit native/web adapters.
+- Negotiated Android hardware Back and native Alert presentation so hosted
+ routes retain current shell behavior and mixed versions degrade safely.
- Bound files, source control, provider review, tasks, agent mutations,
terminal streams, browser controls, native chat, and continuations to opaque
current authority.
diff --git a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md
index 51a1c641a45..8c857df0590 100644
--- a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md
+++ b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md
@@ -23,8 +23,8 @@ composition have recorded evidence. The 2026-08-21 independent OpenCode review
was completed and its high-severity findings were fixed.
Latest recorded package:
-`121fe8682fc221fd7e6f2955fe1f246017d164db3122d71526bc3f66b19578c5`,
-51 assets, 9,151,993 raw bytes, 2,649,166 gzip bytes. This summary is not a
+`015f15ca1bbbc28f25318283a2fff0ea616db868540a2e86bd3d5ce61c4a3f25`,
+52 assets, 9,310,227 raw bytes, 2,691,744 gzip bytes. This summary is not a
production-readiness claim.
## Packaged Desktop and Signed App Matrix
@@ -43,9 +43,12 @@ production-readiness claim.
## Physical Devices and Layouts
+- [x] Re-run the corrected iPhone Simulator native-versus-hosted fixture for
+ Source Control/Review, including host-origin navigation and a second
+ session-origin Source Control mount.
- [ ] Re-run the corrected iPhone Simulator native-versus-hosted fixture for
- Workspace, Accounts, Tasks, Session, Agent History, Files/Preview, Source
- Control/Review, host editing, Floating Workspace, and recovery. Require
+ Workspace, Accounts, Tasks, Session, Agent History, Files/Preview, host
+ editing, Floating Workspace, and recovery. Require
CDP proof that no hosted private-origin target exists around each native
capture and replace the invalidated historical screenshot metrics.
- [ ] Test the oldest supported low-memory and current iPhone.
@@ -55,6 +58,13 @@ production-readiness claim.
memory pressure, attachment permission/denial/revocation/interruption,
notification/deep-link routing, rotation, and host/session replacement on
those devices.
+- [x] Validate Android hardware Back from nested Agent History through Session
+ and workspace root to the native shell on the current API 36 emulator,
+ using real `KEYCODE_BACK` input and a clean bridge-log audit.
+- [ ] Validate Android hardware Back at dirty drafts, timeout, reconnect, mixed
+ Desktop/mobile versions, and on a physical device.
+- [ ] Validate native Alert button ordering, dismissal, queueing, destructive
+ actions, and old-shell fallback on iOS and Android release candidates.
## Topology and Compatibility
diff --git a/docs/reference/plans/2026-08-23-mobile-hybrid-webview-simplification-audit.md b/docs/reference/plans/2026-08-23-mobile-hybrid-webview-simplification-audit.md
index ed099c522c7..62dedbb270a 100644
--- a/docs/reference/plans/2026-08-23-mobile-hybrid-webview-simplification-audit.md
+++ b/docs/reference/plans/2026-08-23-mobile-hybrid-webview-simplification-audit.md
@@ -4,8 +4,8 @@
- **Status:** Simplifications integrated; release validation remains
- **Candidate checkpoint:** `e931b2db07`
- **Base and `origin/main` checkpoint:** `4c984d4c1b`
-- **Post-audit integration checkpoint:** `b14fe74214`
-- **Current `origin/main` checkpoint:** `b2902cb61e`
+- **Post-audit integration checkpoint:** `b8d04428c0`
+- **Current `origin/main` checkpoint:** `e50cc309c3`
- **Decision record:** [migration record](2026-07-22-mobile-hybrid-webview-single-pr-migration.md)
- **Release gates:** [remaining-work tracker](2026-07-27-mobile-hybrid-webview-remaining-work.md)
@@ -30,9 +30,9 @@ files, 140,153 additions, and 8,772 deletions. Those counts describe
`4c984d4c1b...e931b2db07`; they are not current-main release metrics.
After the approved reductions, fixes, and current-main merge, the comparison is
-89 commits, 1,401 files, 134,626 additions, and 8,771 deletions relative to
-`b2902cb61e`. Documentation condensation accounts for most of the reduction;
-the explicit 224-operation bridge and shared RNW presentation remain the main
+92 commits, 1,401 files, 134,633 additions, and 8,771 deletions relative to
+`e50cc309c3`. Documentation condensation accounts for most of the reduction;
+the explicit 225-operation bridge and shared RNW presentation remain the main
implementation span.
The non-negotiable constraints were:
@@ -157,10 +157,11 @@ Tasks, Files, and Session before any broad conversion.
| Parity and cutover seams | `7815f3afb3` | Preserved native host editing, made workspace routes hybrid-only, removed the hosted picker duplicate, added test-only native baselines, and negotiated host gates |
| Subscription regression | `ee15298704` | Restored the shared bridge envelope used by subscriptions after the parity refactor exposed a removed-method call |
| Parity bounds | `2094dde1f9` | Made native CDP exclusion fail closed above its inspection limit and expanded the bounded session-capability response allowance |
+| Relay process boundary | `c3725a60fc` | Moved Relay Markdown discovery to the shared cross-platform process launcher and preserved the child-process import ratchet |
The declarative registry prototype covered Tasks, Files, and Session and proved
that metadata, grants, and typed calls can be generated without replacing the
-handwritten authorization executors. Converting all 224 operations in this PR
+handwritten authorization executors. Converting all 225 operations in this PR
would increase review and compatibility risk, so the registry remains a
non-blocking follow-up.
@@ -180,9 +181,13 @@ non-blocking follow-up.
presentation.
- [x] Prototype the declarative registry on Tasks, Files, and Session and defer
broad conversion to a follow-up.
-- [ ] Rerun package verification, typecheck/lint, bridge/cache/security suites,
- Direct/SSH hosted E2E, emulator parity, and diff hygiene after integrated
- simplifications.
+- [x] Rerun deterministic package verification, full mobile/root suites,
+ typecheck/lint, bridge/cache/security suites, and diff hygiene after
+ integrated simplifications.
+- [x] Rerun corrected Source Control/Review native-versus-hosted emulator parity,
+ including host-origin navigation and the second session-origin mount.
+- [ ] Complete corrected native-versus-hosted emulator parity for the remaining
+ route matrix after integrated simplifications.
- [ ] Keep the [release-gate tracker](2026-07-27-mobile-hybrid-webview-remaining-work.md)
current; do not promote simulator/local evidence into physical, store,
production cloud Relay, mixed-version, performance, or App Review proof.
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index 6996bd9809b..1813087c12f 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -249,6 +249,7 @@ export default function RootLayout() {
+
diff --git a/mobile/app/h/[hostId]/agent-history/[worktreeId].tsx b/mobile/app/h/[hostId]/agent-history/[worktreeId].tsx
index 50c62205cd7..89d062079f7 100644
--- a/mobile/app/h/[hostId]/agent-history/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/agent-history/[worktreeId].tsx
@@ -1,9 +1,9 @@
-import { useLocalSearchParams } from 'expo-router'
import { MobileAgentSessionHistoryPanel } from '../../../../src/agent-history/MobileAgentSessionHistoryPanel'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
import { firstParam } from '../../../../src/source-control/mobile-source-control-screen-state'
export default function MobileAgentSessionHistoryScreen() {
- const params = useLocalSearchParams<{
+ const params = useMobileWebRouteParams<{
hostId?: string | string[]
worktreeId?: string | string[]
name?: string | string[]
diff --git a/mobile/app/h/[hostId]/files/[worktreeId].tsx b/mobile/app/h/[hostId]/files/[worktreeId].tsx
index 6ee06c9248b..2f371391974 100644
--- a/mobile/app/h/[hostId]/files/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/files/[worktreeId].tsx
@@ -1,5 +1,5 @@
-import { useLocalSearchParams } from 'expo-router'
import { MobileFileExplorerPanel } from '../../../../src/files/MobileFileExplorerPanel'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
import type { HostFileExplorerOperations } from '../../../../src/files/host-file-explorer-operations'
import type { ConnectionState } from '../../../../src/transport/types'
@@ -12,7 +12,7 @@ export function MobileFileExplorerScreen({
connectionState?: ConnectionState
nativeHostBinding?: boolean
} = {}) {
- const { hostId, worktreeId, name } = useLocalSearchParams<{
+ const { hostId, worktreeId, name } = useMobileWebRouteParams<{
hostId: string
worktreeId: string
name?: string
diff --git a/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx b/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
index a67dffdab0b..018e01bcd1e 100644
--- a/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/files/preview/[worktreeId].tsx
@@ -1,5 +1,5 @@
-import { useLocalSearchParams } from 'expo-router'
import { MobileFilePreviewScreen } from '../../../../../src/files/MobileFilePreviewScreen'
+import { useMobileWebRouteParams } from '../../../../../src/mobile-web/use-mobile-web-route-params'
import type { HostFilePreviewOperations } from '../../../../../src/files/host-file-preview-operations'
import { normalizeMobileFilePreviewRouteParams } from '../../../../../src/files/mobile-file-preview-route'
import type { ConnectionState } from '../../../../../src/transport/types'
@@ -13,7 +13,7 @@ export function MobileFilePreviewRoute({
connectionState?: ConnectionState
nativeHostBinding?: boolean
} = {}) {
- const params = useLocalSearchParams<{
+ const params = useMobileWebRouteParams<{
hostId?: string | string[]
worktreeId?: string | string[]
relativePath?: string | string[]
diff --git a/mobile/app/h/[hostId]/history/[worktreeId].tsx b/mobile/app/h/[hostId]/history/[worktreeId].tsx
index 19198e4e5f1..7413203be1b 100644
--- a/mobile/app/h/[hostId]/history/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/history/[worktreeId].tsx
@@ -1,11 +1,12 @@
-import { Redirect, useLocalSearchParams } from 'expo-router'
+import { Redirect } from 'expo-router'
import { firstParam } from '../../../../src/source-control/mobile-source-control-screen-state'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
// History is now a segment of the Source Control hub. This route stays as a thin
// redirect so existing deep links (and any cached navigation) land on the hub with
// the History segment selected.
export default function HistoryRedirect() {
- const params = useLocalSearchParams<{
+ const params = useMobileWebRouteParams<{
hostId?: string | string[]
worktreeId?: string | string[]
name?: string | string[]
diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx
index 8de27b0de03..420df73cd18 100644
--- a/mobile/app/h/[hostId]/index.tsx
+++ b/mobile/app/h/[hostId]/index.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
import { View, Text, StyleSheet, SectionList, Pressable, Alert, RefreshControl } from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useFocusEffect, useLocalSearchParams } from 'expo-router'
+import { useFocusEffect } from 'expo-router'
import {
Search,
X,
@@ -21,6 +21,7 @@ import {
SquareTerminal
} from 'lucide-react-native'
import { useHostClient } from '../../../src/transport/client-context'
+import { useMobileWebRouteParams } from '../../../src/mobile-web/use-mobile-web-route-params'
import { useWorktreeResync } from '../../../src/transport/use-worktree-resync'
import { startHostWorktreeRefresh } from '../../../src/worktree/host-worktree-refresh'
import {
@@ -140,7 +141,7 @@ export function HostScreen({
hostState: hostStateProp,
shellOperations: shellOperationsProp
}: HostScreenProps = {}) {
- const params = useLocalSearchParams<{ hostId: string; action?: string; notice?: string }>()
+ const params = useMobileWebRouteParams<{ hostId: string; action?: string; notice?: string }>()
const hostId = hostIdProp ?? params.hostId
const action = actionProp ?? params.action
const [dismissedNotice, setDismissedNotice] = useState(null)
diff --git a/mobile/app/h/[hostId]/pr/[worktreeId].tsx b/mobile/app/h/[hostId]/pr/[worktreeId].tsx
index 682e4d3481c..8035ea4df7e 100644
--- a/mobile/app/h/[hostId]/pr/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/pr/[worktreeId].tsx
@@ -1,12 +1,13 @@
-import { Redirect, useLocalSearchParams } from 'expo-router'
+import { Redirect } from 'expo-router'
import { firstParam } from '../../../../src/source-control/mobile-source-control-screen-state'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
// The Pull Request view is now a segment of the Source Control hub. This route
// stays as a thin redirect so existing deep links land on the hub with the Pull
// Request segment selected. The wide-layout dock opens the same hub with
// initialTab="pr" (SessionDockColumn), not this route.
export default function PrRedirect() {
- const params = useLocalSearchParams<{
+ const params = useMobileWebRouteParams<{
hostId?: string | string[]
worktreeId?: string | string[]
name?: string | string[]
diff --git a/mobile/app/h/[hostId]/review/[worktreeId].tsx b/mobile/app/h/[hostId]/review/[worktreeId].tsx
index 853938cfa57..134dea261b3 100644
--- a/mobile/app/h/[hostId]/review/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/review/[worktreeId].tsx
@@ -1,6 +1,7 @@
import { useCallback, useMemo } from 'react'
-import { useLocalSearchParams, useRouter } from 'expo-router'
+import { useRouter } from 'expo-router'
import { MobileDiffReviewScreenView } from '../../../../src/components/MobileDiffReviewScreenView'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
import {
firstReviewParam,
normalizeReviewFilterParam
@@ -11,11 +12,13 @@ import type { HostDiffReviewBinding } from '../../../../src/session/host-diff-re
import { useHostDiffReviewBinding } from '../../../../src/session/use-host-diff-review-binding'
export function MobileDiffReviewRoute({
- binding
+ binding,
+ routeName
}: {
binding?: HostDiffReviewBinding
+ routeName?: string
} = {}) {
- const params = useLocalSearchParams<{
+ const params = useMobileWebRouteParams<{
hostId?: string | string[]
worktreeId?: string | string[]
name?: string | string[]
@@ -25,7 +28,7 @@ export function MobileDiffReviewRoute({
}>()
const hostId = firstReviewParam(params.hostId)
const worktreeId = firstReviewParam(params.worktreeId)
- const name = firstReviewParam(params.name)
+ const name = routeName ?? firstReviewParam(params.name)
const initialFilter = normalizeReviewFilterParam(firstReviewParam(params.scope))
const initialFile = firstReviewParam(params.file)
const initialArea = normalizeReviewAreaParam(firstReviewParam(params.area))
diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index 581817ec4d0..18036d84ef2 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -14,12 +14,11 @@ import {
Keyboard,
Platform,
ActivityIndicator,
- type KeyboardEvent,
type LayoutChangeEvent,
type ListRenderItem
} from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
-import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router'
+import { useFocusEffect, useRouter } from 'expo-router'
import {
AlertTriangle,
ArrowUp,
@@ -54,6 +53,7 @@ import {
type MobileTerminalLinkOpenMode
} from '../../../../src/storage/preferences'
import { useHostClient, useForceReconnect } from '../../../../src/transport/client-context'
+import { useMobileWebRouteParams } from '../../../../src/mobile-web/use-mobile-web-route-params'
import {
useLastConnectedAt,
useRelayRecoveryStatus,
@@ -92,6 +92,7 @@ import type {
} from '../../../../src/terminal/terminal-webview-contract'
import { computeActiveTerminalKeyboardLift } from '../../../../src/terminal/terminal-keyboard-avoidance-lift'
import { useTerminalViewportRefit } from '../../../../src/terminal/terminal-viewport-refit'
+import { useMobileKeyboardInset } from '../../../../src/hooks/use-mobile-keyboard-inset'
import {
getDefaultTerminalAccessoryBuiltInIds,
getVisibleTerminalAccessoryKeys
@@ -751,7 +752,7 @@ export function SessionScreen({
name: routeWorktreeName,
created,
warning: createdWarning
- } = useLocalSearchParams<{
+ } = useMobileWebRouteParams<{
hostId: string
worktreeId: string
name?: string
@@ -1013,7 +1014,7 @@ export function SessionScreen({
[visibleBuiltInIds]
)
// Why: Expo SDK 55 edge-to-edge doesn't resize the window on IME open, so track keyboard height ourselves and lift the input without resizing the desktop PTY.
- const [keyboardHeight, setKeyboardHeight] = useState(0)
+ const keyboardHeight = useMobileKeyboardInset().height
// Why: server-authoritative display mode per terminal, populated from subscribe responses.
const [terminalModes, setTerminalModes] = useState