feat(studio): multi-platform playground — Android + iOS + HarmonyOS + Computer#2371
feat(studio): multi-platform playground — Android + iOS + HarmonyOS + Computer#2371
Conversation
Prepare the contract layer for supporting iOS, HarmonyOS and Computer alongside the existing Android runtime. No runtime behaviour changes — the legacy channel names are now aliases that resolve to the same generic channel string. - `PlaygroundBootstrap` type replaces `AndroidPlaygroundBootstrap` (kept as deprecated alias). - `PlaygroundRuntimeService` interface replaces `AndroidPlaygroundRuntimeService` (kept as deprecated alias). - `IPC_CHANNELS.getPlaygroundBootstrap` / `restartPlayground` are the canonical names; `getAndroidPlaygroundBootstrap` / `restartAndroidPlayground` now point to the same values. - `StudioRuntimeApi` gains the generic methods with `@deprecated` tags on the old ones. - electron-contract.test.ts updated to verify alias identity.
…ony + Computer) Replace the single-platform Android runtime service with a unified multi-platform runtime that registers all four device platforms: Android — ADB + scrcpy preview iOS — WebDriverAgent (manual host:port, mjpeg preview) HarmonyOS — HDC device discovery, screenshot preview Computer — local display enumeration, screenshot preview Architecture: - `multi-platform-runtime.ts` builds a `RegisteredPlaygroundPlatform[]` array from the four platform packages, each resolved lazily via `require.resolve` — missing packages are marked unavailable instead of crashing. - Calls `prepareMultiPlatformPlayground(platforms)` from the generic `@midscene/playground` infra to get a unified `sessionManager` that routes to the selected platform. - Launches a SINGLE HTTP server via `launchPreparedPlaygroundPlatform`; the renderer talks to this one server. - The platform selector UI (cards) is built into the playground's session setup form. Renderer changes: - `StudioPlaygroundProvider` calls the generic `getPlaygroundBootstrap` / `restartPlayground` IPC instead of the Android-specific ones. - `usePlaygroundController` no longer passes `defaultDeviceType` — the multi-platform session manager owns platform selection. - MainContent + Playground panel text de-Androidified: "Playground starting..." / "Runtime Error" / "Retry runtime" instead of hardcoded "Android" everywhere. - Context type gains `restartPlayground` alongside the deprecated `restartAndroidPlayground` alias. Build: - `package.json` adds `@midscene/ios`, `@midscene/harmony`, `@midscene/computer`, `@midscene/computer-playground` as deps. - `rsbuild.config.ts` marks all four platform packages as externals for the main process bundle. - `preload/index.ts` exposes the new IPC methods + legacy aliases.
Deploying midscene with
|
| Latest commit: |
212e2fe
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://273570e5.midscene.pages.dev |
| Branch Preview URL: | https://feat-studio-multi-platform.midscene.pages.dev |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 648374008a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| prepare: async () => | ||
| harmonyPlaygroundPlatform.prepare({ staticDir: harmonyStaticDir }), |
There was a problem hiding this comment.
Avoid CLI-only Harmony prepare in Studio runtime
Registering Harmony with harmonyPlaygroundPlatform.prepare(...) wires Studio to the CLI-oriented @midscene/harmony flow, where prepare() calls selectDevice() and can invoke process.exit(1) when no HDC targets are found (packages/harmony/src/platform.ts). In the Electron main process this means selecting the Harmony card can block on a terminal prompt (multiple devices) or terminate the app (no devices) instead of returning a recoverable setup error, so Harmony support is effectively broken for normal GUI usage.
Useful? React with 👍 / 👎.
Previously only the Android section in the sidebar and overview had a
working onConnect / onClick handler. Other platform sections rendered
devices but clicking them was a no-op.
Generalize device click handling for the multi-platform session manager:
- Sidebar: replace the Android-specific `androidDevices` array with a
`buildDeviceItemsForPlatform(platformKey, devices)` factory that
produces onClick handlers for every platform section. Each handler
sets `platformId` + `{platformId}.deviceId` in the form and calls
`createSession` — matching the field prefixing convention from
`prepareMultiPlatformPlayground`.
- DeviceList overview: the `onConnect` handler now uses the `platform`
argument (was `_platform`) to set the same prefixed form fields.
- `selected` state is computed across all platforms (flattened device
id set) instead of Android-only.
The multi-platform server routes the session to the correct backend
based on `platformId`. Clicking an Android device works exactly as
before; clicking an iOS/Harmony/Computer device sets the right form
values and triggers a session create through the same unified path.
The multi-platform session manager requires a `platformId` in the form before it returns targets from `getSetupSchema`. Without it, the initial `refreshSessionSetup` poll gets a "Choose a platform" response with no targets, and the sidebar stays empty. Seed `platformId: 'android'` into the form via `useLayoutEffect` before the first poll fires. The controller's interval picks up the value on its next tick and returns Android targets immediately — matching the pre-multi-platform behaviour where devices appeared on boot. When the user clicks a different platform section in the sidebar, the onClick handler overrides `platformId` and triggers a fresh `refreshSessionSetup` with the new platform, swapping the device list.
The multi-platform session manager only returns targets for the currently selected platform. That left non-Android sidebar sections empty even when Harmony or Computer devices were connected. Add an independent device-discovery service that scans ALL platforms concurrently (ADB, HDC, display enumeration) and returns a flat DiscoveredDevice[] via a new IPC channel `studio:discover-devices`. Wiring: - Main: `device-discovery.ts` runs `scanAndroidDevices()`, `scanHarmonyDevices()`, `scanComputerDisplays()` via `Promise.allSettled` — each scan is isolated, failures on one platform don't block others. iOS is omitted (requires manual WDA). - Preload: exposes `discoverDevices()` on `StudioRuntimeApi`. - Renderer: `StudioPlaygroundProvider` polls `discoverDevices()` every 5 seconds. Results are bucketed by `platformId` into `DiscoveredDevicesByPlatform` and exposed on the context. - Sidebar: merges `discoveredDevices` into `deviceBuckets` so devices from ALL platforms appear simultaneously. Discovered devices that are already in the session-setup bucket (from the selected platform's `listTargets`) are deduplicated by id. Result: plug in an Android device AND an HDC device → both appear in their respective sidebar sections at the same time.
Two build errors: 1. `@midscene/android` was not in studio's package.json deps (only `@midscene/android-playground` was). TS couldn't resolve the import for `getConnectedDevicesWithDetails`. 2. Harmony's `getConnectedDevices()` returns `HarmonyDeviceInfo[]` (objects with `deviceId` field), not `string[]`. Updated the mapper to destructure `.deviceId`. Also add `@midscene/android` to the rsbuild externals list so the main bundle doesn't try to inline it.
When a non-Android device was connected, `buildGenericConnectedDeviceItem` used `runtimeInfo.title` (e.g. "Midscene HarmonyOS Playground") as the device label, since `metadata.sessionDisplayName` was unset and the function checked `.title` before `.metadata.deviceId`. Swap the priority: show the concrete `metadata.deviceId` (the serial number from `hdc list targets`) when available, fall back to `.title` only when neither `sessionDisplayName` nor `deviceId` is set. Also add Screen Recording permission note: dev-mode Electron binary at node_modules/electron/dist/Electron.app needs explicit macOS Screen Recording permission for the Computer platform to capture screenshots.
The computer platform stores its display id in `metadata.displayId` (not `metadata.deviceId`). `buildGenericConnectedDeviceItem` only checked `metadata.deviceId`, so after connecting to a computer display the connected entry got `id: "computer-connected"` while the discovery entry had `id: "0"` — no dedup match, both appeared in the sidebar. Check `metadata.displayId` as a fallback so the connected entry uses `id: "0"` and the discovery entry with the same id is suppressed.
The device label in the top bar was hardcoded to read from `resolveAndroidDeviceLabel`, which returned "No Android device selected" when connected to a non-Android platform (Computer, Harmony, iOS). Resolve the label from `runtimeInfo.metadata` first — check `sessionDisplayName`, then `deviceId`, then `displayId` — so any connected platform shows its actual device name. Fall back to the Android resolver for backward compat, and finally to a generic "No device selected" instead of the Android-specific string.
…ller Lets hosts seed the session-setup form on mount (e.g. to pre-select a default platform) so the first refreshSessionSetup poll lands on a real platform instead of the generic "Choose a platform" setup, without forcing the host into a post-mount setFieldsValue workaround.
- Delete legacy Android-only aliases: android-runtime.ts, AndroidPlaygroundBootstrap / AndroidPlaygroundRuntimeService types, legacy IPC_CHANNELS entries, and the restartAndroidPlayground StudioRuntimeApi / context field. - Drop unreachable unavailableReason branches in the runtime (workspace deps are always present) by driving registration from a table and letting resolveStaticDir fail fast. - Log per-platform scan failures in device-discovery via the shared getDebug logger instead of swallowing them; normalize the dynamic imports and fix DisplayInfo field usage (name/primary, not label/width). - Share StudioPlatformId + STUDIO_PLATFORM_IDS across main and renderer so bucketing can never silently drop a new platform. - Move bucketDiscoveredDevices and a new resolveConnectedDeviceLabel into selectors so the label logic is defined once and MainContent no longer re-reads runtime metadata by hand. - Seed the default platformId via usePlaygroundController's initialFormValues option, removing the post-mount setFieldsValue hack. - Gate device-discovery polling on the ready phase, flag sidebar placeholder rows with isPlaceholder so they can't get "selected", and surface an iOS setup hint when no iOS devices are discovered. - Tests: add bucket-discovered-devices.test.ts and resolveConnectedDeviceLabel cases; wire @main/@preload/@renderer/@shared aliases into vitest.
Problem: after unplugging a phone while the playground session was still "connected", the device kept showing in the sidebar. The controller stops polling session-setup while connected, so the stale target hung around, and the sidebar merge only appended new discovered devices — it never removed session items that had disappeared. Fix: - Add mergeSidebarDeviceBucketsWithDiscovery selector that treats the live discovery snapshot as source of truth for ADB/HDC/display platforms: items not in the current discovery bucket are dropped (catching unplug), items only in discovery are appended as idle rows. iOS and web pass through unchanged since they have no discovery source. - Expose refreshDiscoveredDevices on StudioPlaygroundContext and run it alongside refreshSessionSetup when the overview refresh button fires, so a user-initiated refresh hits both data sources. - Share the polling callback with the imperative refresh so we only have one discovery code path.
Summary
Evolve Studio from Android-only to a multi-platform playground that supports Android, iOS, HarmonyOS, and Computer with real device connections. Web is excluded per design.
Replaces the single-platform `createAndroidPlaygroundRuntimeService()` with a unified `createMultiPlatformRuntimeService()` that registers all four platform descriptors and launches ONE server via `prepareMultiPlatformPlayground()` from `@midscene/playground`. The renderer talks to this single server; the built-in platform selector card UI routes to the correct backend.
Platform support matrix
Changes
Main process
Renderer
Build
Known limitations
Test plan