Files
orca/mobile/src/tasks/mobile-task-navigation.ts
T
Kaynan Sampaio de Camargoandkaynan 7948e46db8 fix(mobile): open the Resume workspace through a mounted host stack (#12001)
* fix(mobile): open the Resume workspace through a mounted host stack

Tapping Resume on Home landed on a blank host screen instead of the
session. A cold push straight into the nested /h/[hostId] navigator
resolves to the host index route without the dynamic id, so
HostProtocolGate mounts with hostId undefined and never connects.

Home already worked around this for the host editor (#11635) and Tasks
(#11853) by mounting /h/[hostId] first and replacing it once the stack
is committed. Extract that mechanism into host-stack-navigation so
Resume uses the same transition instead of a direct push.

The previous Resume fix (#11876) swapped the manual href for a typed
dynamic href, but expo-router's encodeParam already applies
encodeURIComponent to dynamic segments, so it resolved to the same URL
the manual string produced and left the cold-navigator path unchanged.

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

* fix(mobile): harden the host-stack transition after bot review

- match a host route committed as the encoded segment it was pushed as,
  so an id containing `/`, `#`, or `%` still triggers the REPLACE
- share one pending transition across the Home entry points; per-hook
  refs let a Tasks tap and a Resume tap arm two pushes that could not
  cancel each other
- assert the source markers before slicing in the Resume wiring test

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

* test(mobile): lock the host-stack transition state machine

Assert the replace waits for the host mount (zero dispatches before it,
exactly one after), and cover cancel/retarget — the paths the shared
pending transition relies on.

* test(mobile): model listener removal in the navigation harness

A no-op unsubscribe let setState keep calling a canceled listener, so the
teardown assertions only exercised the active guard. Dropping the
unsubscribe call from dispose now fails the suite.

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
2026-08-04 16:06:37 -07:00

65 lines
1.8 KiB
TypeScript

import {
coordinateHostStackNavigation,
hostStackHostRoute,
navigateToHostStackRoute,
type HostStackHostRoute,
type HostStackNavigationController,
type HostStackNavigationState,
type HostStackRootNavigation,
type HostStackRouteTarget,
type HostStackRouter,
type PendingHostStackNavigation
} from '../navigation/host-stack-navigation'
import type { TaskProvider } from './mobile-task-providers'
export type MobileTasksHostRoute = HostStackHostRoute
export type MobileTasksNavigationState = HostStackNavigationState
export type MobileTasksRootNavigation = HostStackRootNavigation
export type MobileTasksRouter = HostStackRouter
export type MobileTasksNavigationController = HostStackNavigationController
export type PendingMobileTasksNavigation = PendingHostStackNavigation
export function mobileTasksHostRoute(hostId: string): MobileTasksHostRoute {
return hostStackHostRoute(hostId)
}
export function mobileTasksRouteTarget(
hostId: string,
provider?: TaskProvider
): HostStackRouteTarget {
return {
name: '[hostId]/tasks',
params: provider ? { hostId, taskSource: provider } : { hostId }
}
}
export function navigateToMobileTasks(
navigation: MobileTasksRootNavigation,
router: MobileTasksRouter,
hostId: string,
provider?: TaskProvider
): MobileTasksNavigationController {
return navigateToHostStackRoute(
navigation,
router,
hostId,
mobileTasksRouteTarget(hostId, provider)
)
}
export function coordinateMobileTasksNavigation(
current: PendingMobileTasksNavigation | null,
navigation: MobileTasksRootNavigation,
router: MobileTasksRouter,
hostId: string,
provider?: TaskProvider
): PendingMobileTasksNavigation {
return coordinateHostStackNavigation(
current,
navigation,
router,
hostId,
mobileTasksRouteTarget(hostId, provider)
)
}