Skip to content

feat(studio): multi-platform playground — Android + iOS + HarmonyOS + Computer#2371

Open
quanru wants to merge 11 commits intomainfrom
feat/studio-multi-platform
Open

feat(studio): multi-platform playground — Android + iOS + HarmonyOS + Computer#2371
quanru wants to merge 11 commits intomainfrom
feat/studio-multi-platform

Conversation

@quanru
Copy link
Copy Markdown
Collaborator

@quanru quanru commented Apr 17, 2026

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

Platform Device Discovery Preview Session Manager
Android ADB (`adb devices`) Scrcpy live stream Yes (existing)
iOS Manual WDA host:port MJPEG via WDA Yes (existing)
HarmonyOS HDC (`hdc list targets`) Screenshot polling agentFactory (existing)
Computer Display enumeration Screenshot polling Yes (existing)

Changes

Main process

  • `multi-platform-runtime.ts` (new): builds `RegisteredPlaygroundPlatform[]`, calls `prepareMultiPlatformPlayground`, launches a single server with CORS
  • `main/index.ts`: swapped to multi-platform runtime; IPC handlers use generic channel names
  • `preload/index.ts`: exposes `getPlaygroundBootstrap` / `restartPlayground` + legacy aliases
  • `electron-contract.ts`: `PlaygroundBootstrap` type (generic); old `AndroidPlaygroundBootstrap` is a deprecated alias; IPC channels aliased

Renderer

  • `StudioPlaygroundProvider.tsx`: calls `getPlaygroundBootstrap()` / `restartPlayground()`; drops `defaultDeviceType`
  • `MainContent/index.tsx` + `Playground/index.tsx`: de-Androidified text ("Playground starting..." instead of "Android playground starting...")
  • `types.ts`: context gains `restartPlayground` + deprecated `restartAndroidPlayground` alias

Build

  • `package.json`: adds `@midscene/ios`, `@midscene/harmony`, `@midscene/computer`, `@midscene/computer-playground`
  • `rsbuild.config.ts`: all four platform packages externalized for main bundle

Known limitations

  • iOS: no auto-discovery; user must manually enter WDA host:port in setup form. Sidebar shows "No devices" until connected.
  • Computer: `getWindowController` passes `null` — agent works but won't auto-minimize Studio during tasks.
  • Single active session: switching platforms destroys the previous session (by design in `prepareMultiPlatformPlayground`).
  • Sidebar click handlers: currently only Android section has real connect logic; iOS/Harmony/Computer devices appear via setup form. A follow-up commit will wire all sidebar sections.

Test plan

  • `pnpm run lint`
  • `npx nx test studio` (52 tests)
  • `pnpm --filter studio dev` — verify Studio launches; platform selector cards appear in setup form; Android device connects as before
  • Connect iOS device (WDA running at known host:port); select iOS platform → enter host:port → verify mjpeg preview
  • Connect HarmonyOS device (HDC) → verify device appears in targets → screenshot preview
  • Select Computer platform → verify display listing → screenshot preview

quanru added 2 commits April 17, 2026 14:22
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.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 17, 2026

Deploying midscene with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1d0b8fd
Status: ✅  Deploy successful!
Preview URL: https://58e67c24.midscene.pages.dev
Branch Preview URL: https://feat-studio-multi-platform.midscene.pages.dev

View logs

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +100 to +101
prepare: async () =>
harmonyPlaygroundPlatform.prepare({ staticDir: harmonyStaticDir }),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

quanru added 9 commits April 17, 2026 17:39
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant