mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 00:02:37 +00:00
fix(mobile): reuse the current workspace on notification taps
This commit is contained in:
@@ -7,7 +7,7 @@ export type HostStackNavigationState = Readonly<{
|
||||
export type HostStackNavigationRoute = Readonly<{
|
||||
key?: string
|
||||
name: string
|
||||
params?: Readonly<{ hostId?: unknown }>
|
||||
params?: Readonly<{ hostId?: unknown; worktreeId?: unknown }>
|
||||
state?: HostStackNavigationState
|
||||
}>
|
||||
|
||||
@@ -27,7 +27,16 @@ export type HostStackReplaceAction = Readonly<{
|
||||
|
||||
export type HostStackRootNavigation = {
|
||||
addListener: (event: 'state', listener: () => void) => () => void
|
||||
dispatch: (action: HostStackReplaceAction) => void
|
||||
dispatch: (
|
||||
action:
|
||||
| HostStackReplaceAction
|
||||
| Readonly<{
|
||||
type: 'SET_PARAMS'
|
||||
target: string
|
||||
source: string
|
||||
payload: { params: Readonly<Record<string, string>> }
|
||||
}>
|
||||
) => void
|
||||
// Why: the root layout's navigator has no committed state until it hydrates, and a
|
||||
// notification tap can arm the transition before that first commit.
|
||||
getState: () => HostStackNavigationState | undefined
|
||||
@@ -124,6 +133,25 @@ export function navigateToHostStackRoute(
|
||||
hostId: string,
|
||||
target: HostStackRouteTarget
|
||||
): HostStackNavigationController {
|
||||
const state = navigation.getState()
|
||||
const hostState = state && focusedHostRoute(state)?.state
|
||||
const focused = hostState?.routes[hostState.index]
|
||||
if (
|
||||
hostState?.key &&
|
||||
focused?.key &&
|
||||
focused.name === target.name &&
|
||||
hostParamMatches(focused.params?.hostId, hostId) &&
|
||||
hostParamMatches(focused.params?.worktreeId, target.params.worktreeId)
|
||||
) {
|
||||
navigation.dispatch({
|
||||
type: 'SET_PARAMS',
|
||||
target: hostState.key,
|
||||
source: focused.key,
|
||||
payload: { params: target.params }
|
||||
})
|
||||
return { cancel: () => {}, isActive: () => false, retarget: () => {} }
|
||||
}
|
||||
|
||||
let active = true
|
||||
let hostRouteSeen = false
|
||||
let selectedTarget = target
|
||||
|
||||
@@ -40,7 +40,7 @@ export function NotificationDeliverySection({ value, disabled, onChange }: Props
|
||||
{row(
|
||||
'suppressWhileViewing',
|
||||
'Suppress while focused',
|
||||
'Skip alerts for the workspace open on this phone.'
|
||||
'Skip alerts for this workspace only while Orca is open.'
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.footer}>
|
||||
|
||||
@@ -115,3 +115,78 @@ describe('notification route coordination', () => {
|
||||
expect(notificationEffect).not.toContain('router.push(')
|
||||
})
|
||||
})
|
||||
|
||||
it('reuses the current workspace screen and targets the notification pane without pushing', () => {
|
||||
const target = getNotificationNavigationTarget({
|
||||
hostId: 'host',
|
||||
worktreeId: 'folder::/workspace',
|
||||
paneKey: 'agent-tab:leaf'
|
||||
})!
|
||||
const harness = navigationHarness(
|
||||
rootLayoutScopedState({
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'index' },
|
||||
{
|
||||
name: 'h',
|
||||
state: {
|
||||
key: 'host-stack',
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
key: 'existing-workspace',
|
||||
name: target.sessionTarget!.name,
|
||||
params: { hostId: 'host', worktreeId: 'folder::/workspace' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
const router = { push: vi.fn(), replace: vi.fn() }
|
||||
const controller = navigateToHostStackRoute(
|
||||
harness.navigation,
|
||||
router,
|
||||
target.hostId,
|
||||
target.sessionTarget!
|
||||
)
|
||||
expect(router.push).not.toHaveBeenCalled()
|
||||
expect(router.replace).not.toHaveBeenCalled()
|
||||
expect(harness.navigation.dispatch).toHaveBeenCalledExactlyOnceWith({
|
||||
type: 'SET_PARAMS',
|
||||
target: 'host-stack',
|
||||
source: 'existing-workspace',
|
||||
payload: { params: target.sessionTarget!.params }
|
||||
})
|
||||
expect(controller.isActive()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not reuse a different workspace on the same host', () => {
|
||||
const target = getNotificationNavigationTarget({ hostId: 'host', worktreeId: 'workspace-b' })!
|
||||
const harness = navigationHarness(
|
||||
rootLayoutScopedState({
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
name: 'h',
|
||||
state: {
|
||||
key: 'host-stack',
|
||||
index: 0,
|
||||
routes: [
|
||||
{
|
||||
key: 'workspace-a',
|
||||
name: target.sessionTarget!.name,
|
||||
params: { hostId: 'host', worktreeId: 'workspace-a' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
const router = { push: vi.fn(), replace: vi.fn() }
|
||||
navigateToHostStackRoute(harness.navigation, router, target.hostId, target.sessionTarget!)
|
||||
expect(router.push).toHaveBeenCalledOnce()
|
||||
expect(harness.navigation.dispatch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -272,3 +272,28 @@ it('preflight does not consume the final presentation claim and observes later d
|
||||
await expect(canPresentForegroundPush(payload)).resolves.toBe(false)
|
||||
await expect(shouldSuppressForegroundPush(apnsData(payload))).resolves.toBe(true)
|
||||
})
|
||||
|
||||
it('allows the viewed workspace after backgrounding during eligibility reads', async () => {
|
||||
const payload = {
|
||||
hostFingerprint,
|
||||
worktreeId: 'workspace',
|
||||
notificationId: 'background-transition',
|
||||
notificationEpoch: 'epoch',
|
||||
notificationSeq: 1
|
||||
}
|
||||
setNotificationViewingWorkspace({ hostId: 'host-1', worktreeId: 'workspace' })
|
||||
AppState.currentState = 'active'
|
||||
await expect(canPresentForegroundPush(payload)).resolves.toBe(false)
|
||||
let resolveHosts!: (value: HostCatalogEntry[]) => void
|
||||
vi.mocked(loadHostCatalog).mockReturnValueOnce(
|
||||
new Promise((resolve) => {
|
||||
resolveHosts = resolve
|
||||
})
|
||||
)
|
||||
const eligibility = canPresentForegroundPush(payload)
|
||||
await vi.waitFor(() => expect(resolveHosts).toBeDefined())
|
||||
AppState.currentState = 'background'
|
||||
resolveHosts(hosts)
|
||||
await expect(eligibility).resolves.toBe(true)
|
||||
await expect(shouldSuppressForegroundPush(apnsData(payload))).resolves.toBe(false)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user