mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 16:02:15 +00:00
fix(mobile): reconcile latest main APIs and checks
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -130,6 +130,10 @@ type HostScreenProps = {
|
||||
shellOperations?: HostScreenShellOperations
|
||||
}
|
||||
|
||||
function ListSeparator() {
|
||||
return <View style={styles.separator} />
|
||||
}
|
||||
|
||||
export function HostScreen({
|
||||
embedded = false,
|
||||
hostId: hostIdProp,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
@@ -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`')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<ReturnType<typeof setTimeout> | null>(null)
|
||||
const dialogRef = useRef<BrowserDialogState | null>(null)
|
||||
const lastStreamCacheKeyRef = useRef<string | null>(cacheKey)
|
||||
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const startPointRef = useRef<{ x: number; y: number; t: number } | null>(null)
|
||||
const scrollingRef = useRef(false)
|
||||
const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | 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<PendingWheelCommand | null>(null)
|
||||
const wheelCommandInFlightRef = useRef(false)
|
||||
const zoomRef = useRef<BrowserZoomState>(DEFAULT_ZOOM)
|
||||
const pinchRef = useRef<PinchGesture | null>(null)
|
||||
const panRef = useRef<PanGesture | null>(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 (
|
||||
<View ref={setRootViewRef} style={styles.root}>
|
||||
|
||||
@@ -19,7 +19,7 @@ function createFrameToken(): string {
|
||||
|
||||
export function MobileHtmlPreview({ html, onOpenLink, renderSource }: Props) {
|
||||
const frameRef = useRef<HTMLIFrameElement | null>(null)
|
||||
const frameToken = useMemo(createFrameToken, [])
|
||||
const frameToken = useMemo(() => createFrameToken(), [])
|
||||
const documentUrl = useMemo(
|
||||
() =>
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(buildMobileHtmlPreviewDocument(html))}`,
|
||||
|
||||
@@ -26,7 +26,7 @@ function MobileRichMarkdownEditorWeb({
|
||||
onOpenLink
|
||||
}: MobileRichMarkdownEditorProps) {
|
||||
const frameRef = useRef<HTMLIFrameElement | null>(null)
|
||||
const frameToken = useMemo(createFrameToken, [])
|
||||
const frameToken = useMemo(() => createFrameToken(), [])
|
||||
const documentUrl = useMemo(
|
||||
() =>
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(
|
||||
|
||||
@@ -27,7 +27,7 @@ export function MermaidDiagram({ source, base }: Props) {
|
||||
|
||||
function MermaidDiagramFrame({ source, base }: Props) {
|
||||
const frameRef = useRef<HTMLIFrameElement | null>(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')
|
||||
|
||||
@@ -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<DictationStatus>('idle')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const activeIdRef = useRef<string | null>(null)
|
||||
|
||||
@@ -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
|
||||
<NewWorktreeModalController
|
||||
ref={state.newWorktreeModalRef}
|
||||
routeVisible={showNewWorktree}
|
||||
client={client}
|
||||
operations={workspaceCreationOperations}
|
||||
hostId={hostId}
|
||||
existingWorktreePaths={existingWorktreePaths}
|
||||
existingWorktrees={state.worktrees}
|
||||
openExternalUrl={shellOperations.openExternalUrl}
|
||||
onVisibleChange={(visible) => {
|
||||
state.newWorktreeModalVisibleRef.current = visible
|
||||
}}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -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<RpcResponse> =>
|
||||
failure('Unknown method with host details', 'method_not_found')
|
||||
const request = vi.fn(async (): Promise<RpcResponse> =>
|
||||
failure('Unknown method with host details', 'method_not_found')
|
||||
)
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -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"'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getTerminalRecordsFromSessionTabs,
|
||||
hasConnectedTerminalAbsentFromSessionTabs,
|
||||
mergeTerminalListWithKnownRecords,
|
||||
mergeTerminalRecordsByCurrentOrder,
|
||||
mobileSessionTabsEqual,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user