feat(mobile): take the session domain's three routers through the handoff seam (OTA phase C, C7.2)

Inside the page a screen is one document standing in for one screen, and
`useRouteHandoff` is the only thing that knows which targets the page keeps and
which it hands back to the app. The three holders here are the workspace-missing
bounce, the file-tap preview push, and the pane-tap param consume.

The domain's census is narrower than the two landed ones because it has to be:
eight of its hooks take `useFocusEffect` and two take `useLocalSearchParams`,
neither of which can navigate, so the rule is a closed list of names rather than
a ban on any value import — which also catches expo-router's module-singleton
`router`, a spelling a `useRouter` rule would have read as clean.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-20 06:04:11 -04:00
parent 3b6f56a689
commit b097598c2a
3 changed files with 8 additions and 6 deletions
@@ -1,5 +1,5 @@
import { useCallback, useLayoutEffect, useRef, type MutableRefObject } from 'react'
import { useRouter } from 'expo-router'
import { useRouteHandoff } from '../navigation/route-handoff'
import { triggerSelection } from '../platform/haptics'
import { openMobileFileTap, type FileTapSessionTab } from './mobile-file-tap-open'
import { openMobileNativeChatFileTap } from './mobile-native-chat-open-file'
@@ -58,7 +58,7 @@ export function useMobileFileTapHandlers<T extends FileTapSessionTab>(
worktreeId,
worktreeName
} = options
const router = useRouter()
const router = useRouteHandoff()
const routerRef = useRef(router)
const optionsRef = useRef(options)
const activationSeqRef = useRef(0)
@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useLocalSearchParams } from 'expo-router'
import { useRouteHandoff } from '../navigation/route-handoff'
import { HOST_DOCK_MIN_WIDTH } from '../storage/preferences'
import { useHostClient, useForceReconnect } from '../transport/client-context'
import {
@@ -33,7 +34,7 @@ export function useMobileSessionFoundation() {
const isFolderWorkspaceRoute = worktreeId.startsWith('folder:') // Synthetic ids have no repo scope.
// Why: the floating sentinel has no repo/worktree, so repo-backed surfaces hide.
const isFloatingWorkspaceRoute = isFloatingWorkspaceWorktreeId(worktreeId)
const router = useRouter()
const router = useRouteHandoff()
const insets = useSafeAreaInsets()
// Why: shared client per host owned by RpcClientProvider (docs/mobile-shared-client-per-host.md).
const { client, clientId, state: connState } = useHostClient(hostId)
@@ -1,5 +1,6 @@
import { useEffect } from 'react'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useLocalSearchParams } from 'expo-router'
import { useRouteHandoff } from '../navigation/route-handoff'
import { parsePaneKey } from '../../../src/shared/stable-pane-id'
import type { MobileSessionTab } from './mobile-session-route-types'
@@ -25,7 +26,7 @@ export function useNotificationPaneNavigation({
switchSessionTab: (tab: MobileSessionTab) => void
}) {
const { paneKey } = useLocalSearchParams<{ paneKey?: string }>()
const router = useRouter()
const router = useRouteHandoff()
useEffect(() => {
if (!terminalsLoaded || typeof paneKey !== 'string' || !paneKey) {
return