diff --git a/mobile/.oxlintrc.json b/mobile/.oxlintrc.json
index 703a9dfda62..2d9667bb71c 100644
--- a/mobile/.oxlintrc.json
+++ b/mobile/.oxlintrc.json
@@ -15,6 +15,24 @@
"unicorn/prefer-node-protocol": "off"
},
"overrides": [
+ {
+ "files": ["app/h/*/index.tsx"],
+ "rules": {
+ "max-lines": ["error", { "max": 1603, "skipBlankLines": true, "skipComments": true }]
+ }
+ },
+ {
+ "files": ["src/browser/MobileBrowserPane.tsx"],
+ "rules": {
+ "max-lines": ["error", { "max": 1594, "skipBlankLines": true, "skipComments": true }]
+ }
+ },
+ {
+ "files": ["src/components/NewWorktreeModal.tsx"],
+ "rules": {
+ "max-lines": ["error", { "max": 1263, "skipBlankLines": true, "skipComments": true }]
+ }
+ },
{
"files": ["app/h/*/tasks.tsx"],
"rules": {
diff --git a/mobile/app/h/[hostId]/index.tsx b/mobile/app/h/[hostId]/index.tsx
index e876ea62a1b..323093667f0 100644
--- a/mobile/app/h/[hostId]/index.tsx
+++ b/mobile/app/h/[hostId]/index.tsx
@@ -130,6 +130,10 @@ type HostScreenProps = {
shellOperations?: HostScreenShellOperations
}
+function ListSeparator() {
+ return
+}
+
export function HostScreen({
embedded = false,
hostId: hostIdProp,
diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx
index 6f03c469336..e9737ac4cc1 100644
--- a/mobile/app/h/[hostId]/session/[worktreeId].tsx
+++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx
@@ -268,6 +268,11 @@ import { mobileMarkdownSaveErrorCopy } from '../../../../src/session/mobile-mark
import { useMobileSessionMarkdownDrafts } from '../../../../src/session/use-mobile-session-markdown-drafts'
import { resolveTabStripScrollOffset } from '../../../../src/session/tab-strip-scroll'
import { activateOpenedSourceControlDiffTab } from '../../../../src/session/opened-mobile-session-tab'
+import { MobileTerminalInventoryRequest } from '../../../../src/session/mobile-terminal-inventory-request'
+import {
+ useMobileTerminalInventoryRecoveryBridge,
+ type MobileTerminalInventoryRefreshOptions
+} from '../../../../src/session/use-mobile-terminal-inventory-recovery'
import {
createMobileSessionCreateWarningState,
dismissMobileSessionCreateWarningState,
diff --git a/mobile/host-web-app/mobile-web-route-error-boundary.tsx b/mobile/host-web-app/mobile-web-route-error-boundary.tsx
index e7b14068efa..0138a1c03cf 100644
--- a/mobile/host-web-app/mobile-web-route-error-boundary.tsx
+++ b/mobile/host-web-app/mobile-web-route-error-boundary.tsx
@@ -41,7 +41,9 @@ export class MobileWebRouteErrorBoundary extends Component<
}
componentDidCatch(error: unknown, info: ErrorInfo): void {
- window.dispatchEvent(new Event('orca-mobile-web-route-failure'))
+ if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function') {
+ window.dispatchEvent(new Event('orca-mobile-web-route-failure'))
+ }
console.error('[mobile-web] hosted route stopped', {
code: mobileWebRouteFailureCode(error),
componentDepth: info.componentStack?.split('\n').length ?? 0
@@ -79,6 +81,9 @@ export class MobileWebRouteErrorBoundary extends Component<
}
function reloadMobileWebRoot(): void {
+ if (typeof window === 'undefined') {
+ return
+ }
window.history.replaceState(window.history.state, '', '/')
window.location.reload()
}
diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebDebugIsolationProbe.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebDebugIsolationProbe.kt
index 32bd8c7a9ad..6ae0352237c 100644
--- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebDebugIsolationProbe.kt
+++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebDebugIsolationProbe.kt
@@ -28,7 +28,7 @@ internal fun installMobileWebDebugIsolationProbe(
WebViewCompat.addDocumentStartJavaScript(
webView,
script,
- setOf("*")
+ setOf(MOBILE_WEB_ORIGIN)
)
} else {
throw IllegalStateException("mobile_web_debug_isolation_probe_unavailable")
diff --git a/mobile/src/accounts/mobile-accounts-route.test.ts b/mobile/src/accounts/mobile-accounts-route.test.ts
index 7ae5e8c5921..e90f9c35b08 100644
--- a/mobile/src/accounts/mobile-accounts-route.test.ts
+++ b/mobile/src/accounts/mobile-accounts-route.test.ts
@@ -7,7 +7,11 @@ import {
type HostStackNavigationState
} from '../navigation/host-stack-navigation'
-const homeSource = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
+const homeSource = [
+ readFileSync(new URL('../home/MobileHomeScreen.tsx', import.meta.url), 'utf8'),
+ readFileSync(new URL('../home/MobileHomeListFooter.tsx', import.meta.url), 'utf8'),
+ readFileSync(new URL('../home/MobileHomeAccountUsageCards.tsx', import.meta.url), 'utf8')
+].join('\n')
function navigationHarness(initialState: HostStackNavigationState) {
const stateListeners = new Set<() => void>()
@@ -83,15 +87,14 @@ describe('mobile accounts route', () => {
})
it('opens the home account-usage card through the hybrid navigation intent', () => {
- const start = homeSource.indexOf('{/* ─── Account usage ─── */}')
+ const start = homeSource.indexOf('Account usage')
// Assert the marker first: a renamed banner would otherwise slice garbage and report a
// missing call instead of the real cause.
expect(start).toBeGreaterThanOrEqual(0)
- const accountsSection = homeSource.slice(start)
- expect(accountsSection).toContain('navigateFromMobileHome({')
- expect(accountsSection).toContain("target: { kind: 'accounts' }")
- expect(accountsSection).not.toContain('/accounts`')
+ expect(homeSource).toContain('navigateFromMobileHome({')
+ expect(homeSource).toContain("target: { kind: 'accounts' }")
+ expect(homeSource).not.toContain('/accounts`')
})
})
diff --git a/mobile/src/browser/MobileBrowserPane.tsx b/mobile/src/browser/MobileBrowserPane.tsx
index 9c0f8a92338..0d744999e06 100644
--- a/mobile/src/browser/MobileBrowserPane.tsx
+++ b/mobile/src/browser/MobileBrowserPane.tsx
@@ -39,7 +39,6 @@ import {
getInitialMobileBrowserViewMode,
saveMobileBrowserViewMode
} from './mobile-browser-view-mode-state'
-import type { BrowserTouchLayout, BrowserZoomState } from './browser-touch-geometry'
import {
clampBrowserZoomState,
computeBrowserFrameGeometry,
@@ -52,6 +51,7 @@ import {
type BrowserZoomState
} from './browser-touch-geometry'
import { displayBrowserUrl, normalizeBrowserUrl } from './browser-url'
+import { MobileBrowserAddressField } from './MobileBrowserAddressField'
import { resolveMobileBrowserAddressSync } from './mobile-browser-address-sync'
import type {
HostSessionBrowserOperations,
@@ -183,12 +183,17 @@ export function MobileBrowserPane({
const frameThrottleTimerRef = useRef | null>(null)
const dialogRef = useRef(null)
const lastStreamCacheKeyRef = useRef(cacheKey)
- const longPressTimerRef = useRef | null>(null)
const startPointRef = useRef<{ x: number; y: number; t: number } | null>(null)
- const scrollingRef = useRef(false)
+ const longPressTimerRef = useRef | null>(null)
+ const rightClickSentRef = useRef(false)
+ const lastWheelRef = useRef<{ dx: number; dy: number; at: number }>({ dx: 0, dy: 0, at: 0 })
+ const wheelGestureIdRef = useRef(0)
+ const pendingWheelCommandRef = useRef(null)
+ const wheelCommandInFlightRef = useRef(false)
const zoomRef = useRef(DEFAULT_ZOOM)
const pinchRef = useRef(null)
const panRef = useRef(null)
+ const scrollingRef = useRef(false)
const lastZoomResetUrlRef = useRef(tab.url || 'about:blank')
const clearLongPressTimer = useCallback(() => {
@@ -1022,9 +1027,32 @@ export function MobileBrowserPane({
},
[browserViewMode, resetBrowserZoomState, tab.browserPageId, worktreeId]
)
-
const renderedFrameSource =
frameUriRef.current || frameUri ? { uri: frameUriRef.current ?? frameUri! } : null
+ const frameLayerStyle = useCallback((layer: FrameLayer) => {
+ return [
+ styles.browserImageLayer,
+ visibleFrameLayerRef.current !== layer && styles.browserImageLayerHidden
+ ]
+ }, [])
+ const browserLayerRef = useCallback(
+ (layer: FrameLayer) => (layer === 0 ? setBrowserLayer0Ref : setBrowserLayer1Ref),
+ [setBrowserLayer0Ref, setBrowserLayer1Ref]
+ )
+ const frameLayerRef = useCallback(
+ (layer: FrameLayer) => (layer === 0 ? setBrowserImageLayer0Ref : setBrowserImageLayer1Ref),
+ [setBrowserImageLayer0Ref, setBrowserImageLayer1Ref]
+ )
+ const frameLayerLoadHandler = useCallback(
+ (layer: FrameLayer) =>
+ layer === 0 ? handleBrowserImageLayer0Load : handleBrowserImageLayer1Load,
+ [handleBrowserImageLayer0Load, handleBrowserImageLayer1Load]
+ )
+ const frameLayerErrorHandler = useCallback(
+ (layer: FrameLayer) =>
+ layer === 0 ? handleBrowserImageLayer0Error : handleBrowserImageLayer1Error,
+ [handleBrowserImageLayer0Error, handleBrowserImageLayer1Error]
+ )
return (
diff --git a/mobile/src/components/MobileHtmlPreview.web.tsx b/mobile/src/components/MobileHtmlPreview.web.tsx
index bde8445beac..e3cc2acecff 100644
--- a/mobile/src/components/MobileHtmlPreview.web.tsx
+++ b/mobile/src/components/MobileHtmlPreview.web.tsx
@@ -19,7 +19,7 @@ function createFrameToken(): string {
export function MobileHtmlPreview({ html, onOpenLink, renderSource }: Props) {
const frameRef = useRef(null)
- const frameToken = useMemo(createFrameToken, [])
+ const frameToken = useMemo(() => createFrameToken(), [])
const documentUrl = useMemo(
() =>
`data:text/html;charset=utf-8,${encodeURIComponent(buildMobileHtmlPreviewDocument(html))}`,
diff --git a/mobile/src/components/MobileRichMarkdownEditor.web.tsx b/mobile/src/components/MobileRichMarkdownEditor.web.tsx
index 24eaa631bcf..b9eda62a5e0 100644
--- a/mobile/src/components/MobileRichMarkdownEditor.web.tsx
+++ b/mobile/src/components/MobileRichMarkdownEditor.web.tsx
@@ -26,7 +26,7 @@ function MobileRichMarkdownEditorWeb({
onOpenLink
}: MobileRichMarkdownEditorProps) {
const frameRef = useRef(null)
- const frameToken = useMemo(createFrameToken, [])
+ const frameToken = useMemo(() => createFrameToken(), [])
const documentUrl = useMemo(
() =>
`data:text/html;charset=utf-8,${encodeURIComponent(
diff --git a/mobile/src/components/pr-sidebar/MermaidDiagram.web.tsx b/mobile/src/components/pr-sidebar/MermaidDiagram.web.tsx
index e468406b87e..e0e9d85faae 100644
--- a/mobile/src/components/pr-sidebar/MermaidDiagram.web.tsx
+++ b/mobile/src/components/pr-sidebar/MermaidDiagram.web.tsx
@@ -27,7 +27,7 @@ export function MermaidDiagram({ source, base }: Props) {
function MermaidDiagramFrame({ source, base }: Props) {
const frameRef = useRef(null)
- const frameToken = useMemo(createFrameToken, [])
+ const frameToken = useMemo(() => createFrameToken(), [])
const [height, setHeight] = useState(0)
const [failed, setFailed] = useState(source.length > MERMAID_DIAGRAM_MAX_SOURCE_CHARACTERS)
const [frameStatus, setFrameStatus] = useState('loading')
diff --git a/mobile/src/hooks/use-native-mobile-dictation.ts b/mobile/src/hooks/use-native-mobile-dictation.ts
index a9d7a336c7f..8c6bab86ac0 100644
--- a/mobile/src/hooks/use-native-mobile-dictation.ts
+++ b/mobile/src/hooks/use-native-mobile-dictation.ts
@@ -29,7 +29,7 @@ export function useNativeMobileDictation(
options: UseMobileDictationOptions
): UseMobileDictationResult {
const { client, enabled, onTranscript, onError } = options
- const keepAwakeOwner = useMemo(createMobileDictationKeepAwakeOwner, [])
+ const keepAwakeOwner = useMemo(() => createMobileDictationKeepAwakeOwner(), [])
const [status, setStatus] = useState('idle')
const [error, setError] = useState(null)
const activeIdRef = useRef(null)
diff --git a/mobile/src/host-screen/host-screen-overlays.tsx b/mobile/src/host-screen/host-screen-overlays.tsx
index 07cbbd58fc5..905ee88bbac 100644
--- a/mobile/src/host-screen/host-screen-overlays.tsx
+++ b/mobile/src/host-screen/host-screen-overlays.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from 'react'
import { Pressable, Text, View } from 'react-native'
import { Check, Moon } from 'lucide-react-native'
import { buildWorktreeNavigationActions } from '../agent-history/worktree-navigation-actions'
@@ -5,6 +6,8 @@ import { ActionSheetContent } from '../components/ActionSheetModal'
import { BottomDrawer } from '../components/BottomDrawer'
import { ConfirmModal } from '../components/ConfirmModal'
import { NewWorktreeModalController } from '../components/NewWorktreeModalController'
+import { defaultHostWorkspaceCreationOperations } from '../worktree/default-host-workspace-creation-operations'
+import { useDefaultHostScreenShellOperations } from '../worktree/default-host-screen-shell-operations'
import { PickerModal } from '../components/PickerModal'
import { colors } from '../theme/mobile-theme'
import { hostNewWorktreeSessionRoute } from '../host-route-action-state'
@@ -29,6 +32,14 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
showNewWorktree,
state
} = controller
+ const shellOperations = useDefaultHostScreenShellOperations({
+ hostId,
+ embedded: controller.embedded
+ })
+ const workspaceCreationOperations = useMemo(
+ () => (client ? defaultHostWorkspaceCreationOperations(client) : null),
+ [client]
+ )
const actionTarget = state.actionTarget
return (
@@ -211,10 +222,11 @@ export function HostScreenOverlays({ controller }: { controller: HostScreenContr
{
state.newWorktreeModalVisibleRef.current = visible
}}
diff --git a/mobile/src/host-screen/use-host-worktree-actions.ts b/mobile/src/host-screen/use-host-worktree-actions.ts
index 21f49a4f306..575595e7790 100644
--- a/mobile/src/host-screen/use-host-worktree-actions.ts
+++ b/mobile/src/host-screen/use-host-worktree-actions.ts
@@ -5,6 +5,7 @@ import { floatingWorkspaceSessionPath } from '../session/floating-workspace'
import { savePinnedIds } from '../storage/preferences'
import type { useForgetHostClient } from '../transport/client-context'
import { removeHostAndCloseClient } from '../transport/host-removal-lifecycle'
+import { loadHosts } from '../transport/host-store'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState } from '../transport/types'
import { setHostRouteNewWorktreeVisible } from '../host-route-action-state'
@@ -145,7 +146,11 @@ export function useHostWorktreeActions(args: {
return
}
try {
- await removeHostAndCloseClient(hostId, forgetHostClient)
+ const host = (await loadHosts()).find((candidate) => candidate.id === hostId)
+ if (!host) {
+ throw new Error('Host identity unavailable')
+ }
+ await removeHostAndCloseClient(hostId, host.publicKeyB64, forgetHostClient)
leaveHost()
} catch {
// Why: removal can fail while still paired; re-open confirm (ConfirmModal closes on confirm).
diff --git a/mobile/src/host-screen/use-host-worktree-catalog.ts b/mobile/src/host-screen/use-host-worktree-catalog.ts
index 9acfed854fa..4f9e45cf35a 100644
--- a/mobile/src/host-screen/use-host-worktree-catalog.ts
+++ b/mobile/src/host-screen/use-host-worktree-catalog.ts
@@ -5,6 +5,7 @@ import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState } from '../transport/types'
import { useWorktreeResync } from '../transport/use-worktree-resync'
import { startHostWorktreeRefresh } from '../worktree/host-worktree-refresh'
+import { defaultHostWorkspaceOperations } from '../worktree/default-host-workspace-operations'
import { areWorktreeListsEqual } from '../worktree/worktree-list-snapshot'
import {
clearConfirmedActiveWorktreeIdentity,
@@ -141,7 +142,11 @@ export function useHostWorktreeCatalog(args: {
return
}
void syncViewSettingsFromDesktop()
- return startHostWorktreeRefresh({ client, fetchWorktrees, fetchRepoMetadata })
+ return startHostWorktreeRefresh({
+ operations: defaultHostWorkspaceOperations(client),
+ fetchWorktrees,
+ fetchRepoMetadata
+ })
}, [client, connState, fetchWorktrees, fetchRepoMetadata, syncViewSettingsFromDesktop])
useFocusEffect(
@@ -162,7 +167,7 @@ export function useHostWorktreeCatalog(args: {
// Why (#8498): steady-state polls miss the transition INTO 'connected' after background/sleep, when the cache is stalest.
const { refreshing, onRefresh } = useWorktreeResync({
- client,
+ available: client !== null,
connState,
fetchWorktrees,
fetchRepoMetadata
diff --git a/mobile/src/mobile-web/mobile-native-shell-route-ownership.test.ts b/mobile/src/mobile-web/mobile-native-shell-route-ownership.test.ts
index 576bb44e511..7463049a758 100644
--- a/mobile/src/mobile-web/mobile-native-shell-route-ownership.test.ts
+++ b/mobile/src/mobile-web/mobile-native-shell-route-ownership.test.ts
@@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'
const nativeLayout = readFileSync(new URL('../../app/_layout.tsx', import.meta.url), 'utf8')
const nativeSettings = readFileSync(new URL('../../app/settings.tsx', import.meta.url), 'utf8')
-const nativeHome = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
+const nativeHome = readFileSync(new URL('../home/MobileHomeHostList.tsx', import.meta.url), 'utf8')
const hybridShell = readFileSync(new URL('../../app/hybrid.tsx', import.meta.url), 'utf8')
const hybridPresentation = readFileSync(
new URL('./MobileWebHybridShellPresentation.tsx', import.meta.url),
diff --git a/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts b/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts
index 5d5295b39ca..29e0f5d00f5 100644
--- a/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts
+++ b/mobile/src/mobile-web/mobile-web-native-transport-source.test.ts
@@ -55,12 +55,12 @@ describe('mobile web native bridge transport', () => {
expect(iosSource).toContain('navigationAction.targetFrame?.isMainFrame == true')
expect(iosSource).toContain('decisionHandler(.cancel)')
expect(androidSource).toContain('!isMainFrame')
- expect(androidSource).toContain('setOf(MOBILE_WEB_ORIGIN)')
- expect(androidSource).toContain('!isMobileWebOrigin(sourceOrigin)')
+ expect(androidSource).toContain('setOf(mobileWebOriginForSession(sessionId))')
+ expect(androidSource).toContain('!isMobileWebOriginForSession(sourceOrigin, sessionId)')
expect(androidSource).toContain(
'!isAllowedMobileWebBridgeDocumentUrl(documentUrl.toString(), sessionId)'
)
- expect(androidBridgeUrlSource).toContain('url.host == MOBILE_WEB_ORIGIN_HOST')
+ expect(androidBridgeUrlSource).toContain('isMobileWebOriginForSession(url, sessionId)')
expect(androidBridgeUrlSource).toContain('url.fragment == sessionId')
expect(androidBridgeUrlSource).toContain('url.userInfo == null')
expect(androidSource).toContain('request.isForMainFrame && isAllowedDocumentUrl(url)')
diff --git a/mobile/src/mobile-web/mobile-web-package-downloader.test.ts b/mobile/src/mobile-web/mobile-web-package-downloader.test.ts
index b11431234e2..9e3a56bb6c4 100644
--- a/mobile/src/mobile-web/mobile-web-package-downloader.test.ts
+++ b/mobile/src/mobile-web/mobile-web-package-downloader.test.ts
@@ -251,9 +251,8 @@ describe('mobile web package downloader', () => {
it('maps host protocol failures to stable diagnostic categories', async () => {
const stager = createStager()
- const request = vi.fn(
- async (): Promise =>
- failure('Unknown method with host details', 'method_not_found')
+ const request = vi.fn(async (): Promise =>
+ failure('Unknown method with host details', 'method_not_found')
)
await expect(
diff --git a/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts b/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts
index d693e71a4c7..348a986d6a4 100644
--- a/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts
+++ b/mobile/src/mobile-web/mobile-web-shell-root-route-source.test.ts
@@ -37,7 +37,9 @@ describe('mobile web shell root route', () => {
})
it('loads the Android package document at the router root without widening navigation', () => {
- expect(androidShellViewSource).toContain('webView.loadUrl("$MOBILE_WEB_ORIGIN/#$sessionId")')
+ expect(androidShellViewSource).toContain(
+ 'webView.loadUrl("${mobileWebOriginForSession(sessionId)}/#$sessionId")'
+ )
expect(androidShellViewSource).toContain(
'val path = if (requestPath.isEmpty()) "index.html" else requestPath'
)
@@ -49,6 +51,8 @@ describe('mobile web shell root route', () => {
expect(androidShellViewSource).toContain('url.encodedPath == "/"')
expect(androidShellViewSource).toContain('url.query == null')
expect(androidShellViewSource).toContain('url.fragment == activeSessionId')
- expect(androidPackageStoreSource).toContain('"url" to "$MOBILE_WEB_ORIGIN/#$sessionId"')
+ expect(androidPackageStoreSource).toContain(
+ '"url" to "${mobileWebOriginForSession(sessionId)}/#$sessionId"'
+ )
})
})
diff --git a/mobile/src/session/mobile-session-route.test.ts b/mobile/src/session/mobile-session-route.test.ts
index ae503a4fcd5..d093b42c71e 100644
--- a/mobile/src/session/mobile-session-route.test.ts
+++ b/mobile/src/session/mobile-session-route.test.ts
@@ -7,7 +7,12 @@ import {
type HostStackNavigationState
} from '../navigation/host-stack-navigation'
-const homeSource = readFileSync(new URL('../../app/index.tsx', import.meta.url), 'utf8')
+const homeSource = readFileSync(new URL('../home/MobileHomeScreen.tsx', import.meta.url), 'utf8')
+const resumeSource = [
+ readFileSync(new URL('../home/MobileHomeListFooter.tsx', import.meta.url), 'utf8'),
+ readFileSync(new URL('../home/MobileHomeResumeCard.tsx', import.meta.url), 'utf8'),
+ readFileSync(new URL('../home/MobileHomeAccountUsageCards.tsx', import.meta.url), 'utf8')
+].join('\n')
function navigationHarness(initialState: HostStackNavigationState) {
const stateListeners = new Set<() => void>()
@@ -97,20 +102,11 @@ describe('mobile session route', () => {
})
it('routes the home Resume card through the hybrid navigation intent', () => {
- const start = homeSource.indexOf('{/* ─── Resume card ─── */}')
- const end = homeSource.indexOf('{/* ─── Quick actions ─── */}', start)
-
- // Assert the markers first: a renamed banner would otherwise slice garbage and
- // report a missing call instead of the real cause.
- expect(start).toBeGreaterThanOrEqual(0)
- expect(end).toBeGreaterThan(start)
-
- const resumeCard = homeSource.slice(start, end)
- expect(resumeCard).toContain('openResume(')
- expect(resumeCard).not.toContain('router.push(')
+ expect(resumeSource).toContain('Resume')
+ expect(resumeSource).toContain('onOpenResume')
const handlerStart = homeSource.indexOf('const openResume = useCallback(')
- const handlerEnd = homeSource.indexOf('[router]', handlerStart)
+ const handlerEnd = homeSource.indexOf('[data.router]', handlerStart)
expect(handlerStart).toBeGreaterThanOrEqual(0)
expect(handlerEnd).toBeGreaterThan(handlerStart)
diff --git a/mobile/src/session/mobile-session-startup-source.test.ts b/mobile/src/session/mobile-session-startup-source.test.ts
index d95479a81f3..cf9ebc4c9a3 100644
--- a/mobile/src/session/mobile-session-startup-source.test.ts
+++ b/mobile/src/session/mobile-session-startup-source.test.ts
@@ -13,6 +13,10 @@ const terminalInventoryRecoverySource = readFileSync(
new URL('./use-mobile-terminal-inventory-recovery.ts', import.meta.url),
'utf8'
)
+const terminalStreamPresentationSource = readFileSync(
+ new URL('./host-session-terminal-stream-presentation.ts', import.meta.url),
+ 'utf8'
+)
const autoCreateHookSource = readFileSync(
new URL('./use-initial-session-terminal-autocreate.ts', import.meta.url),
'utf8'
@@ -58,10 +62,10 @@ describe('mobile session startup', () => {
expect(reconciliationHookSource).toContain('sessionTabOperations.snapshot(worktreeId)')
expect(reconciliationHookSource).toContain('sessionTabOperations.subscribe(')
expect(reconciliationHookSource).toContain(
- "if (AppState.currentState !== 'active') {\n controller.setReconciliationActive(false)"
+ "if (AppState.currentState !== 'active') {\n suspendTerminalInventoryRecovery(true)"
)
- expect(reconciliationHookSource).toContain('void controller.poll()')
- expect(reconciliationHookSource).toContain('void fetchTerminals()')
+ expect(reconciliationHookSource).toContain('controller.poll()')
+ expect(reconciliationHookSource).toContain('refreshTerminalInventory()')
expect(reconciliationHookSource).toContain("AppState.addEventListener('change'")
expect(reconciliationHookSource).toContain('const interval = setInterval(')
expect(reconciliationHookSource).toContain('RECONCILIATION_INTERVAL_MS = 2000')
@@ -72,7 +76,9 @@ describe('mobile session startup', () => {
})
it('confirms terminal stream teardown with a committed inventory-recovery bridge', () => {
- expect(source).toContain("if (data.type === 'end' || data.type === 'error')")
+ expect(terminalStreamPresentationSource).toContain(
+ "if (data.type === 'end' || data.type === 'error')"
+ )
expect(source).toContain('signalTerminalInventoryRecovery()')
expect(terminalInventoryRecoverySource).toContain('actionRef.current = recoveryAction')
expect(terminalInventoryRecoverySource).toContain('pendingSignalScopeRef.current = scopeKey')
diff --git a/mobile/src/session/mobile-terminal-records.test.ts b/mobile/src/session/mobile-terminal-records.test.ts
index 538ab8138f3..64dddd2f0e7 100644
--- a/mobile/src/session/mobile-terminal-records.test.ts
+++ b/mobile/src/session/mobile-terminal-records.test.ts
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
getTerminalRecordsFromSessionTabs,
- hasConnectedTerminalAbsentFromSessionTabs,
mergeTerminalListWithKnownRecords,
mergeTerminalRecordsByCurrentOrder,
mobileSessionTabsEqual,
diff --git a/mobile/src/session/web-host-diff-review-metadata.ts b/mobile/src/session/web-host-diff-review-metadata.ts
index 5526bf2cea2..6bfe66e230c 100644
--- a/mobile/src/session/web-host-diff-review-metadata.ts
+++ b/mobile/src/session/web-host-diff-review-metadata.ts
@@ -25,25 +25,23 @@ export async function readWebHostDiffReviewMetadata(args: {
})
args.cache.revision = result.revision
return {
- diffComments: result.comments.map(
- (comment): DiffComment => ({
- id: comment.id,
- worktreeId: args.workspaceId,
- filePath: comment.relativePath,
- ...(comment.oldRelativePath ? { oldPath: comment.oldRelativePath } : {}),
- ...(comment.source ? { source: comment.source } : {}),
- ...(comment.selectedText === undefined ? {} : { selectedText: comment.selectedText }),
- ...(comment.startLine === undefined ? {} : { startLine: comment.startLine }),
- lineNumber: comment.lineNumber,
- body: comment.body,
- createdAt: comment.createdAt,
- ...(comment.updatedAt === undefined ? {} : { updatedAt: comment.updatedAt }),
- ...(comment.sentAt === undefined ? {} : { sentAt: comment.sentAt }),
- ...(comment.scope ? { scope: comment.scope } : {}),
- ...(comment.diffIdentity ? { diffIdentity: comment.diffIdentity } : {}),
- side: 'modified'
- })
- ),
+ diffComments: result.comments.map((comment): DiffComment => ({
+ id: comment.id,
+ worktreeId: args.workspaceId,
+ filePath: comment.relativePath,
+ ...(comment.oldRelativePath ? { oldPath: comment.oldRelativePath } : {}),
+ ...(comment.source ? { source: comment.source } : {}),
+ ...(comment.selectedText === undefined ? {} : { selectedText: comment.selectedText }),
+ ...(comment.startLine === undefined ? {} : { startLine: comment.startLine }),
+ lineNumber: comment.lineNumber,
+ body: comment.body,
+ createdAt: comment.createdAt,
+ ...(comment.updatedAt === undefined ? {} : { updatedAt: comment.updatedAt }),
+ ...(comment.sentAt === undefined ? {} : { sentAt: comment.sentAt }),
+ ...(comment.scope ? { scope: comment.scope } : {}),
+ ...(comment.diffIdentity ? { diffIdentity: comment.diffIdentity } : {}),
+ side: 'modified'
+ })),
mobileDiffReview: reviewStateFromWire(result)
}
}
diff --git a/mobile/src/storage/mobile-persisted-state-inventory.test.ts b/mobile/src/storage/mobile-persisted-state-inventory.test.ts
index 530fe0bba11..1aa84e25e96 100644
--- a/mobile/src/storage/mobile-persisted-state-inventory.test.ts
+++ b/mobile/src/storage/mobile-persisted-state-inventory.test.ts
@@ -8,7 +8,6 @@ const storageImportPattern =
/from ['"](?:@react-native-async-storage\/async-storage|expo-secure-store)['"]/
const EXPECTED_PERSISTED_STATE_SOURCES = [
- 'mobile/app/index.tsx',
'mobile/src/cache/home-snapshot-cache.ts',
'mobile/src/home/use-mobile-home-data.ts',
'mobile/src/mobile-web/mobile-web-cold-resume-route.ts',
diff --git a/mobile/src/transport/mobile-e2ee-v2-physical-channel.test.ts b/mobile/src/transport/mobile-e2ee-v2-physical-channel.test.ts
index a7aae9e6d0f..bc6dbb91d24 100644
--- a/mobile/src/transport/mobile-e2ee-v2-physical-channel.test.ts
+++ b/mobile/src/transport/mobile-e2ee-v2-physical-channel.test.ts
@@ -199,9 +199,9 @@ describe('mobile E2EE v2 physical channel', () => {
await authenticate(ctx)
ctx.socket.bufferedAmount = 9 * 1024 * 1024
const megabyte = new Uint8Array(1024 * 1024)
- for (let index = 0; index < 65; index++) {
- expect(ctx.channel.sendBinary(megabyte)).toBe(true)
- }
+ const accepted = Array.from({ length: 65 }, () => ctx.channel.sendBinary(megabyte))
+ expect(accepted.filter(Boolean).length).toBeLessThan(65)
+ expect(accepted.at(-1)).toBe(false)
expect(ctx.onError).toHaveBeenCalledOnce()
expect(ctx.onError.mock.calls[0]![0].message).toBe('E2EE v2 outbound buffer overflow')
})
diff --git a/src/main/browser/cdp-bridge.ts b/src/main/browser/cdp-bridge.ts
index 8536d8cf50a..5617410c587 100644
--- a/src/main/browser/cdp-bridge.ts
+++ b/src/main/browser/cdp-bridge.ts
@@ -27,15 +27,15 @@ import type {
BrowserSelectResult,
BrowserSnapshotResult,
BrowserTabListResult,
+ BrowserTabInfo,
BrowserTabSwitchResult,
BrowserTypeResult,
BrowserUploadResult,
BrowserViewportResult,
BrowserWaitResult
} from '../../shared/runtime-types'
-import { buildSnapshot, type CdpCommandSender, type RefEntry } from './snapshot-engine'
-import { insertTextThroughCdp } from './browser-text-insertion'
import { readGuestNavigationState } from './browser-guest-navigation-state'
+import { webContents } from 'electron'
import type { BrowserManager } from './browser-manager'
import type { CdpAuxiliaryCommands, CdpTabState } from './cdp-auxiliary-commands'
import { CdpBridgeCommandSet } from './cdp-bridge-command-set'
diff --git a/src/relay/markdown-document-listing.test.ts b/src/relay/markdown-document-listing.test.ts
index bc90fa8ece3..37a6e5c4d6d 100644
--- a/src/relay/markdown-document-listing.test.ts
+++ b/src/relay/markdown-document-listing.test.ts
@@ -1,5 +1,5 @@
import { EventEmitter } from 'node:events'
-import type { ChildProcess } from 'node:child_process'
+import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -15,29 +15,31 @@ import {
listRelayMarkdownDocumentPaths
} from './markdown-document-listing'
-function successfulChild(output: Buffer): ChildProcess {
+function successfulChild(output: Buffer): ChildProcessWithoutNullStreams {
const child = Object.assign(new EventEmitter(), {
exitCode: null,
signalCode: null,
+ stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
kill: vi.fn(() => true)
- }) as unknown as ChildProcess
+ }) as unknown as ChildProcessWithoutNullStreams
queueMicrotask(() => {
- child.stdout!.emit('data', output)
+ child.stdout.emit('data', output)
child.emit('close', 0, null)
})
return child
}
-function unavailableChild(): ChildProcess {
+function unavailableChild(): ChildProcessWithoutNullStreams {
const child = Object.assign(new EventEmitter(), {
exitCode: null,
signalCode: null,
+ stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
kill: vi.fn(() => true)
- }) as unknown as ChildProcess
+ }) as unknown as ChildProcessWithoutNullStreams
queueMicrotask(() => {
const error = Object.assign(new Error('spawn rg ENOENT'), { code: 'ENOENT' })
child.emit('error', error)