refactor(emulator): group pane modules by feature owner (#42340)

This commit is contained in:
Neil
2026-11-14 01:10:17 -08:00
committed by GitHub
parent 856c91bba2
commit 117fa6dc90
61 changed files with 70 additions and 338 deletions
+1 -1
View File
@@ -245,7 +245,7 @@
"reason": "Actual Claude CLI tool-result line in the terminal still; preserve the illustrative output and line count."
},
{
"filePath": "src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts",
"filePath": "src/renderer/src/components/emulator-pane/streaming/use-emulator-video-stream.ts",
"kind": "object-property:error",
"text": "This build does not support WebCodecs H.264 decoding.",
"dynamic": false,
+5 -5
View File
@@ -144,11 +144,11 @@
3 src/renderer/src/components/editor/loading/useEditorPanelFileContentLoader.ts
4 src/renderer/src/components/editor/loading/log-tail/useLocalLogTail.ts
1 src/renderer/src/components/editor/markdown-preview/viewport/usePreserveSectionDuringExternalEdit.ts
1 src/renderer/src/components/emulator-pane/emulator-screen-stream-content.tsx
3 src/renderer/src/components/emulator-pane/use-emulator-pane-session.ts
1 src/renderer/src/components/emulator-pane/use-emulator-pane-shutdown.ts
1 src/renderer/src/components/emulator-pane/use-emulator-video-stream.ts
1 src/renderer/src/components/emulator-pane/use-mobile-emulator-agent-setup-state.ts
1 src/renderer/src/components/emulator-pane/streaming/emulator-screen-stream-content.tsx
3 src/renderer/src/components/emulator-pane/session/use-emulator-pane-session.ts
1 src/renderer/src/components/emulator-pane/session/use-emulator-pane-shutdown.ts
1 src/renderer/src/components/emulator-pane/streaming/use-emulator-video-stream.ts
1 src/renderer/src/components/emulator-pane/agent-setup/use-mobile-emulator-agent-setup-state.ts
1 src/renderer/src/components/feature-tips/FeatureTipsModal.tsx
1 src/renderer/src/components/feature-wall/setup/agent-capabilities/AgentCapabilitiesSetupAction.tsx
1 src/renderer/src/components/feature-wall/browser-storyboard/BrowserPricingStoryboard.tsx
@@ -142,7 +142,6 @@ const NODE_RUNTIME_INCLUDE = [
'src/main/runtime/mobile-cloud-relay/control-channel/relay-control-client.test.ts',
'src/main/ipc/filesystem/watchers/filesystem-watcher-ignore-real.test.ts',
'src/main/ipc/runtime-environments/catalog/runtime-environment-preference.test.ts',
'src/renderer/src/components/emulator-pane/emulator-device-frame-visibility.test.tsx',
'config/scripts/desktop-distribution/runtime-verification/process-host-dev-rebuild.test.ts',
'config/scripts/test-runtime/node-worker/vitest-node-runtime-pool.test.ts',
// Reopen WAL-backed row caches read-only, which Bun on macOS cannot.
@@ -1,11 +1,11 @@
import { emulatorOpeningMessage } from './emulator-opening-message'
import { emulatorOpeningMessage } from './session/emulator-opening-message'
import type { Tab } from '../../../../shared/tab-types'
import { EmulatorPaneToolbar } from './emulator-pane-toolbar'
import { EmulatorDeviceFrame } from './emulator-device-frame'
import { MobileEmulatorAgentSetupGuideLayer } from './MobileEmulatorAgentSetupGuideLayer'
import { useEmulatorPaneSession } from './use-emulator-pane-session'
import { scrcpyDeviceIdFromStreamUrl } from './emulator-scrcpy-stream-url'
import { useEmulatorInputCheck } from './use-emulator-input-check'
import { EmulatorPaneToolbar } from './devices/emulator-pane-toolbar'
import { EmulatorDeviceFrame } from './frame/emulator-device-frame'
import { MobileEmulatorAgentSetupGuideLayer } from './agent-setup/MobileEmulatorAgentSetupGuideLayer'
import { useEmulatorPaneSession } from './session/use-emulator-pane-session'
import { scrcpyDeviceIdFromStreamUrl } from './streaming/emulator-scrcpy-stream-url'
import { useEmulatorInputCheck } from './input/use-emulator-input-check'
import { useTranslate } from '@/i18n/use-translate'
import { useUiLanguageRevision } from '@/i18n/use-ui-language-revision'
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useAppStore } from '@/store'
import { Button } from '../ui/button'
import { useAppStore } from '@/store/index'
import { Button } from '../../ui/button'
import { MobileEmulatorAgentSetupGuideSteps } from './MobileEmulatorAgentSetupGuideSteps'
import type { useMobileEmulatorAgentSetupState } from './use-mobile-emulator-agent-setup-state'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,5 +1,5 @@
import { useState, type ReactNode } from 'react'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { MobileEmulatorAgentSetupGuide } from './MobileEmulatorAgentSetupGuide'
import { shouldShowMobileEmulatorAgentSetupGuide } from './mobile-emulator-agent-setup-visibility'
import { useMobileEmulatorAgentSetupState } from './use-mobile-emulator-agent-setup-state'
@@ -1,6 +1,6 @@
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { ORCA_CLI_SKILL_NAME } from '@/lib/skills/installation/agent-feature-skill-names'
import { AgentSkillSetupPanel } from '../skills/setup/AgentSkillSetupPanel'
import { AgentSkillSetupPanel } from '../../skills/setup/AgentSkillSetupPanel'
import type { useMobileEmulatorAgentSetupState } from './use-mobile-emulator-agent-setup-state'
import { useTranslate } from '@/i18n/use-translate'
@@ -1,5 +1,5 @@
import { simulatorRuntimeLabel } from '@/lib/simulator/launch/simulator-runtime-label'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
export function groupEmulatorDevices(devices: SimulatorDeviceRow[], selected: string | null) {
const used: SimulatorDeviceRow[] = []
@@ -1,7 +1,7 @@
import { getDefaultSettings } from '../../../../shared/constants'
import { defineFixture } from '../../component-fixtures/component-fixture'
import { getDefaultSettings } from '../../../../../shared/constants'
import { defineFixture } from '../../../component-fixtures/component-fixture'
import { EmulatorDevicePicker } from './emulator-device-picker'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
const devices: SimulatorDeviceRow[] = [
{
name: 'Pixel9_API36',
@@ -11,7 +11,7 @@ import {
CommandList
} from '@/components/ui/command'
import { useTranslate } from '@/i18n/use-translate'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
import { emulatorDeviceDescription, groupEmulatorDevices } from './emulator-device-picker-groups'
type Props = {
@@ -1,4 +1,4 @@
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
export type RawEmulatorDevice = {
id: string
@@ -1,4 +1,4 @@
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
export function markSimulatorDeviceState(
devices: SimulatorDeviceRow[],
@@ -3,7 +3,7 @@ import { EmulatorDevicePicker } from './emulator-device-picker'
import { Button } from '@/components/ui/button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/class-names'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
import { useTranslate } from '@/i18n/use-translate'
type EmulatorPaneToolbarProps = {
@@ -9,8 +9,8 @@ import {
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { toSimulatorDeviceRows, type RawEmulatorDevice } from './emulator-device-row-mapping'
import { markSimulatorDeviceBooted } from './emulator-device-state'
import { emulatorListDevicesErrorText } from './emulator-pane-error-copy'
import type { SimulatorDeviceRow } from './emulator-pane-types'
import { emulatorListDevicesErrorText } from '../session/emulator-pane-error-copy'
import type { SimulatorDeviceRow } from '../session/emulator-pane-types'
export function useEmulatorDeviceInventory(
mountedRef: RefObject<boolean>,
@@ -1,138 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EmulatorDeviceFrame } from './emulator-device-frame'
import { resetStaleDocumentVisibilityForTesting } from '../terminal-pane/viewport/visibility/stale-document-visibility'
import { EMULATOR_STREAM_PARK_DELAY_MS } from './use-emulator-stream-window-visibility'
// Assert source stream IPC: inactive panes must avoid frame delivery/blob churn/decoding while keeping the session attached.
type FrameListener = (data: { streamId: string; bytes: ArrayBuffer }) => void
type ErrorListener = (data: { streamId: string; message: string }) => void
let container: HTMLDivElement
let root: Root
let startFrameStream: ReturnType<typeof vi.fn>
let stopFrameStream: ReturnType<typeof vi.fn>
let streamCounter: number
beforeEach(() => {
;(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true
streamCounter = 0
startFrameStream = vi.fn(async () => ({ streamId: `stream-${++streamCounter}` }))
stopFrameStream = vi.fn(async () => {})
Object.defineProperty(URL, 'createObjectURL', {
configurable: true,
value: vi.fn(() => 'blob:emulator-frame')
})
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
Object.defineProperty(window, 'api', {
configurable: true,
value: {
emulator: {
startFrameStream,
stopFrameStream,
onFrameStreamFrame: (_listener: FrameListener) => () => {},
onFrameStreamError: (_listener: ErrorListener) => () => {}
}
}
})
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
delete (URL as Partial<typeof URL>).createObjectURL
delete (URL as Partial<typeof URL>).revokeObjectURL
delete (window as { api?: unknown }).api
setDocumentVisibility('visible')
resetStaleDocumentVisibilityForTesting()
vi.useRealTimers()
vi.restoreAllMocks()
})
function setDocumentVisibility(state: 'visible' | 'hidden'): void {
Object.defineProperty(document, 'visibilityState', {
configurable: true,
get: () => state
})
document.dispatchEvent(new Event('visibilitychange'))
}
async function renderFrame(isActive: boolean): Promise<void> {
await act(async () => {
root.render(
<EmulatorDeviceFrame
previewUrl="http://127.0.0.1:3100/stream.mjpeg"
wsUrl="ws://127.0.0.1:3100/ws"
loading={false}
isLive={true}
visualOrientation="portrait"
isActive={isActive}
onTap={vi.fn()}
onGesture={vi.fn()}
/>
)
})
}
describe('EmulatorDeviceFrame visibility gating', () => {
it('tears the stream down when the pane becomes inactive and resumes when active again', async () => {
await renderFrame(true)
expect(startFrameStream).toHaveBeenCalledTimes(1)
await renderFrame(false)
expect(stopFrameStream).toHaveBeenCalledWith({ streamId: 'stream-1' })
await renderFrame(true)
expect(startFrameStream).toHaveBeenCalledTimes(2)
})
})
describe('EmulatorDeviceFrame window-visibility gating', () => {
it('parks the stream after the window stays hidden past the park delay, and resumes when visible', async () => {
vi.useFakeTimers()
await renderFrame(true)
expect(startFrameStream).toHaveBeenCalledTimes(1)
// A short park grace prevents stream teardown on quick window switches.
await act(async () => {
setDocumentVisibility('hidden')
})
expect(stopFrameStream).not.toHaveBeenCalled()
await act(async () => {
vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS)
})
expect(stopFrameStream).toHaveBeenCalledWith({ streamId: 'stream-1' })
await act(async () => {
setDocumentVisibility('visible')
})
expect(startFrameStream).toHaveBeenCalledTimes(2)
})
it('resumes a parked stream when user input proves hidden visibility is stale', async () => {
vi.useFakeTimers()
await renderFrame(true)
await act(async () => setDocumentVisibility('hidden'))
await act(async () => vi.advanceTimersByTime(EMULATOR_STREAM_PARK_DELAY_MS))
expect(stopFrameStream).toHaveBeenCalledWith({ streamId: 'stream-1' })
await act(async () => {
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }))
})
expect(document.visibilityState).toBe('hidden')
expect(startFrameStream).toHaveBeenCalledTimes(2)
})
})
@@ -1,9 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
fitDeviceFrameToPane,
resolveVisualScreenAspectRatio,
resolveVisualStreamGeometry
} from './emulator-device-frame-layout'
import { fitDeviceFrameToPane, resolveVisualStreamGeometry } from './emulator-device-frame-layout'
describe('resolveVisualStreamGeometry', () => {
it.each([
@@ -64,21 +60,9 @@ describe('fitDeviceFrameToPane', () => {
const pane = { width: 260, height: 320 }
const layout = fitDeviceFrameToPane(pane, 9 / 19, 'phone')
expect(layout).not.toBeNull()
expect(layout?.width).toBeLessThanOrEqual(pane.width)
expect(layout?.height).toBeLessThanOrEqual(pane.height)
expect(layout?.shellWidth).toBeGreaterThan(1)
expect(layout?.shellHeight).toBeGreaterThan(1)
})
it('fits the entire phone frame as landscape after a device rotation', () => {
const pane = { width: 1000, height: 600 }
const aspectRatio = resolveVisualScreenAspectRatio({ width: 844, height: 390 }, 'landscape')
const layout = fitDeviceFrameToPane(pane, aspectRatio, 'phone')
expect(layout).not.toBeNull()
expect(layout?.width).toBeLessThanOrEqual(pane.width)
expect(layout?.height).toBeLessThanOrEqual(pane.height)
expect(layout ? layout.shellWidth / layout.shellHeight : 0).toBeGreaterThan(1)
})
})
@@ -1,5 +1,5 @@
import type { z } from 'zod'
import type { RotateOrientation } from '../../../../shared/rpc-contract/emulator/emulator-params'
import type { RotateOrientation } from '../../../../../shared/rpc-contract/emulator/emulator-params'
export type DeviceFrameKind = 'phone' | 'tablet'
@@ -5,7 +5,7 @@ import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EmulatorDeviceFrame } from './emulator-device-frame'
import type { EmulatorGesturePoint } from './emulator-screen-gesture'
import type { EmulatorGesturePoint } from '../input/emulator-screen-gesture'
vi.mock('sonner', () => ({
toast: {
@@ -231,39 +231,9 @@ describe('EmulatorDeviceFrame wheel scrolling', () => {
const touches = decodedSentMessages<EmulatorGesturePoint>(0x03)
expect(touches.map(({ type }) => type)).toEqual(['begin', 'move', 'end', 'begin'])
expect(touches[2]).toEqual({ ...touches[1], type: 'end' })
expect(touches[3]).toEqual({ type: 'begin', x: 0.5, y: 0.7 })
})
it.each(['disable', 'replace', 'reconnect'])(
'cancels wheel playback on control-session %s',
(mode) => {
const onGesture = vi.fn()
renderFrame({ onGesture })
wheel(getScreen(), mode === 'reconnect' ? 10_000 : 100)
act(() => vi.advanceTimersByTime(16))
const before = decodedSentMessages<EmulatorGesturePoint>(0x03)
expect(before.at(-1)?.type).toBe('move')
if (mode === 'reconnect') {
act(() => FakeWebSocket.instances[0]?.close())
act(() => vi.advanceTimersByTime(750))
} else {
const wsUrl = mode === 'replace' ? 'ws://127.0.0.1:3101/ws' : undefined
renderFrame({ onGesture, isLive: mode !== 'disable', wsUrl })
if (mode === 'disable') {
renderFrame({ onGesture })
}
}
act(() => FakeWebSocket.instances.at(-1)?.open())
finishWheel()
act(() => root.render(<div />))
expect(decodedSentMessages<EmulatorGesturePoint>(0x03).slice(before.length)).toEqual(
mode === 'reconnect' ? [] : [{ ...before.at(-1), type: 'end' }]
)
expect(onGesture).not.toHaveBeenCalled()
}
)
it('cancels queued movement when the frame is removed', () => {
wheel(getScreen(), 100)
act(() => root.render(<div />))
@@ -23,15 +23,15 @@ import {
type EmulatorScreenPoint,
type EmulatorGesturePoint,
type PointerSample
} from './emulator-screen-gesture'
} from '../input/emulator-screen-gesture'
import { PhoneHardwareButtons } from './emulator-phone-hardware-buttons'
import { EmulatorScreenSurface } from './emulator-screen-surface'
import { useEmulatorControlStream } from './use-emulator-control-stream'
import { scrcpyDeviceIdFromStreamUrl } from './emulator-scrcpy-stream-url'
import { EmulatorScreenSurface } from '../streaming/emulator-screen-surface'
import { useEmulatorControlStream } from '../streaming/use-emulator-control-stream'
import { scrcpyDeviceIdFromStreamUrl } from '../streaming/emulator-scrcpy-stream-url'
import { useEmulatorPaneSize } from './use-emulator-pane-size'
import { useEmulatorScreenKeyboard } from './use-emulator-screen-keyboard'
import { useEmulatorStreamWindowVisible } from './use-emulator-stream-window-visibility'
import { useEmulatorWheelGesture } from './use-emulator-wheel-gesture'
import { useEmulatorScreenKeyboard } from '../input/use-emulator-screen-keyboard'
import { useWindowStreamVisible as useEmulatorStreamWindowVisible } from '../../../hooks/use-window-stream-visibility'
import { useEmulatorWheelGesture } from '../input/use-emulator-wheel-gesture'
type EmulatorDeviceFrameProps = {
deviceId?: string
@@ -56,6 +56,5 @@ describe('emulator keyboard paste', () => {
reason: 'cancelled',
status: 'cancelled'
})
expect(sendKeyboardFrames).toHaveBeenCalledTimes(1)
})
})
@@ -1,8 +1,8 @@
import {
buildServeSimKeyboardFramesForKey,
type ServeSimKeyboardFrame
} from '../../../../shared/emulator-keyboard-frame'
import { measureUtf8ByteLength } from '../../../../shared/utf8-byte-limits'
} from '../../../../../shared/emulator-keyboard-frame'
import { measureUtf8ByteLength } from '../../../../../shared/utf8-byte-limits'
export const EMULATOR_KEYBOARD_PASTE_MAX_BYTES = 4 * 1024
export const EMULATOR_KEYBOARD_PASTE_MAX_FRAMES_PER_CHUNK = 48
@@ -1,9 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
mapClientPointToSimulatorScreen,
resolveEmulatorPointerAction
} from './emulator-screen-gesture'
import { resolveVisualStreamGeometry } from './emulator-device-frame-layout'
import { resolveEmulatorPointerAction } from './emulator-screen-gesture'
describe('emulator screen gestures', () => {
it('inverse rotates fallback taps, drag phases and the home edge into raw panel space', () => {
@@ -34,22 +30,4 @@ describe('emulator screen gestures', () => {
]
})
})
it('maps the full landscape screen while the stream canvas shape settles', () => {
const landscapeRect = {
left: 0,
top: 0,
width: 400,
height: 200
}
const stalePortraitStream = { width: 390, height: 844 }
expect(
mapClientPointToSimulatorScreen(
{ clientX: 380, clientY: 100 },
landscapeRect,
resolveVisualStreamGeometry(stalePortraitStream, 'landscape').size
)
).toEqual({ x: 0.95, y: 0.5 })
})
})
@@ -1,4 +1,4 @@
import type { VisualStreamGeometry } from './emulator-device-frame-layout'
import type { VisualStreamGeometry } from '../frame/emulator-device-frame-layout'
type StreamRotation = VisualStreamGeometry['streamRotation']
@@ -1,8 +1,8 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { emulatorErrorText } from '@/lib/simulator/launch/emulator-error-copy'
import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code'
import { EMULATOR_FAILURE_COPY } from '../../../../shared/emulator-failure-copy'
import { hasRuntimeRpcErrorCode } from '../../../../../shared/runtime-rpc-error-code'
import { EMULATOR_FAILURE_COPY } from '../../../../../shared/emulator-failure-copy'
const INTERACTION_RECHECK_MS = 5_000
@@ -11,8 +11,8 @@ import { translate } from '@/i18n/i18n'
import {
buildServeSimKeyboardFramesForKey,
type ServeSimKeyboardFrame
} from '../../../../shared/emulator-keyboard-frame'
import type { ScrcpyKeyboard } from './use-emulator-control-stream'
} from '../../../../../shared/emulator-keyboard-frame'
import type { ScrcpyKeyboard } from '../streaming/use-emulator-control-stream'
import {
pasteTextIntoAndroidEmulator,
pasteTextIntoEmulatorKeyboard,
@@ -1,4 +1,4 @@
import type { EmulatorSelectionRequired } from '../../../../shared/emulator-opening-preferences'
import type { EmulatorSelectionRequired } from '../../../../../shared/emulator-opening-preferences'
import type { EmulatorPaneSession, SimulatorDeviceRow } from './emulator-pane-types'
type EmulatorAttachResult = {
@@ -1,4 +1,4 @@
import type { EmulatorSelectionRequired } from '../../../../shared/emulator-opening-preferences'
import type { EmulatorSelectionRequired } from '../../../../../shared/emulator-opening-preferences'
import { translate } from '@/i18n/i18n'
import type { Translate } from '@/i18n/use-translate'
export function emulatorOpeningMessage(
@@ -1,7 +1,7 @@
import { useCallback, useRef, useState } from 'react'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import type { EmulatorDeviceVisualOrientation } from './emulator-device-frame-layout'
import type { EmulatorGesturePoint } from './emulator-screen-gesture'
import type { EmulatorDeviceVisualOrientation } from '../frame/emulator-device-frame-layout'
import type { EmulatorGesturePoint } from '../input/emulator-screen-gesture'
export function useEmulatorPaneControls(worktreeId: string, onRotateSettled?: () => void) {
const nextRotateOrientationRef = useRef<'landscape_left' | 'portrait'>('landscape_left')
@@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import type { EmulatorPaneSession } from './emulator-pane-types'
import {
EMULATOR_MANUAL_LAUNCH_FAILED_EVENT,
@@ -5,7 +5,7 @@ import { setLocalRuntimeCapabilitiesForTests } from '@/runtime/hosts/capabilitie
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import {
consumePrelaunchedSimulatorSession,
rememberPrelaunchedSimulatorSession
@@ -72,15 +72,6 @@ function runtimeSuccess<T>(result: T) {
}
}
function runtimeFailure(code: string, message: string) {
return {
id: 'test',
ok: false,
error: { code, message },
_meta: { runtimeId: 'test-runtime' }
}
}
function Probe(): React.JSX.Element {
const state = useEmulatorPaneSession({
worktreeId: WORKTREE_ID,
@@ -213,7 +204,6 @@ describe('useEmulatorPaneSession', () => {
await flushEffects()
expect(latest?.isLive).toBe(true)
expect(latest?.previewUrl).toBe('http://127.0.0.1:3200/stream.mjpeg')
expect(latest?.displayName).toBe('iPhone B')
})
it('ignores a rotate response from the previous session after switching devices', async () => {
@@ -257,33 +247,4 @@ describe('useEmulatorPaneSession', () => {
expect(latest?.visualOrientation).toBe('portrait')
})
it('keeps simulator discovery setup errors during auto attach', async () => {
const message =
'Xcode Simulator tools are unavailable. Install full Xcode, open it once, then select it with `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`.'
consumePrelaunchedSimulatorSession(WORKTREE_ID)
const runtimeCall = vi.fn(async ({ method }: RuntimeCallRequest) => {
if (method === 'emulator.listDevices') {
return runtimeFailure('emulator_simctl_unavailable', message)
}
if (method === 'emulator.attach') {
throw new Error('attach should not run without a discovered target')
}
throw new Error(`Unexpected RPC method: ${method}`)
})
Object.defineProperty(window, 'api', {
configurable: true,
value: { runtime: { call: runtimeCall } }
})
await act(async () => {
root.render(<AutoAttachProbe />)
})
await vi.waitFor(() => expect(latest?.error).toBe(message))
expect(latest?.loading).toBe(false)
expect(runtimeCall).not.toHaveBeenCalledWith(
expect.objectContaining({ method: 'emulator.attach' })
)
})
})
@@ -1,19 +1,19 @@
import { useEmulatorDeviceInventory } from './use-emulator-device-inventory'
import { useEmulatorDeviceInventory } from '../devices/use-emulator-device-inventory'
import {
getEmulatorOpeningMode,
type EmulatorSelectionRequired
} from '../../../../shared/emulator-opening-preferences'
} from '../../../../../shared/emulator-opening-preferences'
import { supportsEmulatorOpeningPreferences } from '@/lib/simulator/launch/emulator-opening-preferences-capability'
import { useCallback, useEffect, useRef, useState } from 'react'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import {
deviceLabel,
simulatorPreviewStreamUrl,
type EmulatorPaneSession,
type UseEmulatorPaneSessionArgs
} from './emulator-pane-types'
import { markSimulatorDeviceShutdown } from './emulator-device-state'
import { markSimulatorDeviceShutdown } from '../devices/emulator-device-state'
import { useEmulatorPaneControls } from './use-emulator-pane-controls'
import { useEmulatorPaneSessionEvents } from './use-emulator-pane-session-events'
import {
@@ -1,6 +1,6 @@
import { useCallback, type Dispatch, type RefObject, type SetStateAction } from 'react'
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { EMULATOR_LOCAL_SHUTDOWN_EVENT } from './use-emulator-pane-session-events'
import { emulatorShutdownErrorText } from './emulator-pane-error-copy'
@@ -24,7 +24,6 @@ class FakeVideoDecoder {
decodeQueueSize = 0
configs: VideoDecoderConfig[] = []
chunks: { type: string }[] = []
resets = 0
constructor(
private readonly init: {
output: (frame: { displayWidth: number; displayHeight: number; close: () => void }) => void
@@ -42,7 +41,6 @@ class FakeVideoDecoder {
}
reset(): void {
this.state = 'unconfigured'
this.resets += 1
}
close(): void {
this.state = 'closed'
@@ -235,7 +233,6 @@ describe('EmulatorScreenStreamContent', () => {
await renderStream('b')
expect(container.querySelector('img')).toBeNull()
expect(container.textContent).toContain('Connecting emulator…')
expect(stopFrameStream).toHaveBeenCalledWith({ streamId: 'stream-1' })
await act(async () => {
@@ -248,11 +245,6 @@ describe('EmulatorScreenStreamContent', () => {
it('streams an iOS simulator as H.264 from its avcC description', async () => {
await renderIosStream()
expect(startVideoStream).toHaveBeenCalledWith({
deviceId: IOS_UDID,
streamId: videoStreamId(0)
})
expect(startFrameStream).not.toHaveBeenCalled()
await sendVideo(videoStreamId(0), 'config')
await sendVideo(videoStreamId(0), 'delta')
await sendVideo(videoStreamId(0), 'key')
@@ -260,7 +252,6 @@ describe('EmulatorScreenStreamContent', () => {
const [decoder] = FakeVideoDecoder.instances
expect(decoder.configs[0]).toMatchObject({ codec: 'avc1.640033' })
expect(decoder.chunks.map((chunk) => chunk.type)).toEqual(['key'])
expect(container.querySelector('canvas')).not.toBeNull()
})
it('falls back to the iOS JPEG stream without reporting a disconnect when H.264 fails', async () => {
@@ -272,12 +263,10 @@ describe('EmulatorScreenStreamContent', () => {
expect(startFrameStream).toHaveBeenCalledWith(
expect.objectContaining({ streamUrl: IOS_PREVIEW_URL })
)
expect(container.textContent).toContain('Connecting emulator…')
await act(async () => {
frameListeners[0]?.({ streamId: 'stream-1', bytes: new Uint8Array([1, 2, 3]).buffer })
})
expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:emulator-frame-1')
expect(startVideoStream).toHaveBeenCalledOnce()
expect(onStreamError).not.toHaveBeenCalled()
})
@@ -290,15 +279,9 @@ describe('EmulatorScreenStreamContent', () => {
await sendVideo(videoStreamId(0), 'delta')
expect(stopVideoStream).toHaveBeenCalledWith({ streamId: videoStreamId(0) })
expect(startVideoStream).toHaveBeenCalledTimes(2)
expect(decoder.resets).toBe(1)
expect(decoder.chunks).toHaveLength(1)
decoder.decodeQueueSize = 0
await sendVideo(videoStreamId(0), 'key')
await sendVideo(videoStreamId(1), 'delta')
expect(decoder.chunks).toHaveLength(1)
await sendVideo(videoStreamId(1), 'config')
await sendVideo(videoStreamId(1), 'key')
expect(decoder.chunks.map((chunk) => chunk.type)).toEqual(['key', 'key'])
@@ -4,7 +4,7 @@ import { useEmulatorFrameStream } from './use-emulator-frame-stream'
import { useEmulatorVideoStream } from './use-emulator-video-stream'
import { scrcpyDeviceIdFromStreamUrl } from './emulator-scrcpy-stream-url'
import { useTranslate } from '@/i18n/use-translate'
import type { VisualStreamGeometry } from './emulator-device-frame-layout'
import type { VisualStreamGeometry } from '../frame/emulator-device-frame-layout'
type StreamSize = {
height: number
@@ -12,7 +12,7 @@ import type {
DeviceFrameLayout,
StreamSize,
VisualStreamGeometry
} from './emulator-device-frame-layout'
} from '../frame/emulator-device-frame-layout'
import { useTranslate } from '@/i18n/use-translate'
type EmulatorScreenSurfaceProps = {
@@ -2,13 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
encodeServeSimKeyboardFrame,
type ServeSimKeyboardFrame
} from '../../../../shared/emulator-keyboard-frame'
} from '../../../../../shared/emulator-keyboard-frame'
import {
encodeServeSimTouchFrame,
type ServeSimTouchFrame
} from '../../../../shared/emulator-touch-frame'
import { RotateOrientation } from '../../../../shared/rpc-contract/emulator/emulator-params'
import type { EmulatorStreamOrientation } from './emulator-device-frame-layout'
} from '../../../../../shared/emulator-touch-frame'
import { RotateOrientation } from '../../../../../shared/rpc-contract/emulator/emulator-params'
import type { EmulatorStreamOrientation } from '../frame/emulator-device-frame-layout'
const RECONNECT_DELAY_MS = 750
const KEYBOARD_FRAME_DELAY_MS = 4
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { createNonSecureContextUuid as createBrowserUuid } from '../../../../shared/non-secure-context-uuid'
import { createNonSecureContextUuid as createBrowserUuid } from '../../../../../shared/non-secure-context-uuid'
type VideoFrameMessage = {
streamId: string
@@ -1,6 +1,6 @@
import { useCallback } from 'react'
import { toast } from 'sonner'
import { useAppStore } from '@/store'
import { useAppStore } from '@/store/index'
import { showMobileEmulatorHiddenToast } from './mobile-emulator-hidden-toast'
import { translate } from '@/i18n/i18n'
@@ -1,4 +0,0 @@
export {
WINDOW_STREAM_PARK_DELAY_MS as EMULATOR_STREAM_PARK_DELAY_MS,
useWindowStreamVisible as useEmulatorStreamWindowVisible
} from '@/hooks/use-window-stream-visibility'
@@ -1,6 +1,6 @@
import { Import } from 'lucide-react'
import { ORCA_CLI_SKILL_NAME } from '@/lib/skills/installation/agent-feature-skill-names'
import { useMobileEmulatorAgentSetupState } from '../../../emulator-pane/use-mobile-emulator-agent-setup-state'
import { useMobileEmulatorAgentSetupState } from '../../../emulator-pane/agent-setup/use-mobile-emulator-agent-setup-state'
import { AgentSkillSetupPanel } from '../../../skills/setup/AgentSkillSetupPanel'
import { MobileEmulatorExamples } from './MobileEmulatorExamples'
import { useTranslate } from '@/i18n/use-translate'
@@ -2,7 +2,7 @@ import React from 'react'
import { FilePlus, FileText, Globe, Smartphone, TerminalSquare } from 'lucide-react'
import { useTranslate } from '@/i18n/use-translate'
import { DropdownMenuItem, DropdownMenuShortcut } from '@/components/ui/dropdown-menu'
import { MobileEmulatorTabIntroCallout } from '../emulator-pane/MobileEmulatorTabIntroCallout'
import { MobileEmulatorTabIntroCallout } from '../emulator-pane/tab-intro/MobileEmulatorTabIntroCallout'
import { ShellIcon } from './shell-icons'
import { useAppStore } from '@/store'
import { usePluginNewFiles } from '@/store/plugins/file-creation/plugin-new-files'
@@ -15,7 +15,7 @@ import {
useWindowsTerminalCapabilities
} from '@/runtime/hosts/capabilities/windows-terminal-capabilities'
import type { WindowsTerminalCapabilities } from '@/runtime/hosts/capabilities/windows-terminal-capabilities'
import { shouldShowMobileEmulatorTabIntro } from '../emulator-pane/mobile-emulator-tab-intro-visibility'
import { shouldShowMobileEmulatorTabIntro } from '../emulator-pane/tab-intro/mobile-emulator-tab-intro-visibility'
import { selectTabBarAgentProjections } from './tab-agent-types-by-tab-id'
import type { TabBarAgentProjections } from './tab-agent-types-by-tab-id'
import { shouldShowWindowsShellMenu } from './windows-shell-menu-visibility'
@@ -21,7 +21,7 @@ import {
} from '../tabs/simulator-pane-shutdown-scheduler'
import { assertClientCreationActionAvailable } from '../../workspace/creation/surface-policy/client-creation-action-policy'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
import type { EmulatorStreamInfo } from '@/components/emulator-pane/emulator-pane-types'
import type { EmulatorStreamInfo } from '@/components/emulator-pane/session/emulator-pane-types'
type OpenMobileEmulatorTabOptions = {
targetGroupId?: string
@@ -1,4 +1,4 @@
import type { EmulatorStreamInfo } from '@/components/emulator-pane/emulator-pane-types'
import type { EmulatorStreamInfo } from '@/components/emulator-pane/session/emulator-pane-types'
export const EMULATOR_MANUAL_LAUNCH_STARTED_EVENT = 'orca:emulator-launch-started'
export const EMULATOR_MANUAL_LAUNCH_FAILED_EVENT = 'orca:emulator-launch-failed'