Skip to content

[Feature]: Device panel can't choose the iOS Simulator model or iOS version (create one from installed runtimes; the bundled device hub already supports it) #28567

Closed

Description

@MdSadiqMd

Summary

The Device panel has no way to choose which iPhone or iPad model to run, or which iOS version to run it on. It only lists simulators that already exist on the host and were booted at least once (by Xcode or simctl). To test on a model you have never used, or on an older iOS runtime, you must leave T3 Code, open Xcode → Devices and Simulators (or run xcrun simctl create), boot the new simulator once, and then come back and click Refresh devices.

The device hub that T3 Code already ships has this feature: expo-device-hub exposes GET /api/new-device-options (installed runtimes × the models each one supports) and POST /api/devices/create (create and boot). T3 Code never calls either one.

This request asks for a model + iOS version picker in the Device panel, backed by those two hub endpoints.

Checked on main at 37d7615a (2026-11-01, pins expo-device-hub 0.19.1) and on the v0.1.6 desktop app (expo-device-hub 0.19.0-canary-20261023-1357114). The device UI and server code are the same on both; the only difference is the pinned hub version.

What you see today (real example, one Mac)

Installed: one runtime (iOS 26.3), 11 simulators that Xcode created by default.

Source What it reports
xcrun simctl list devices available 11 simulators: iPhone 17 Pro, 17 Pro Max, Air, 17, 16e, iPad Pro 13" (M5), iPad Pro 11" (M5), iPad mini (A17 Pro), iPad (A16), iPad Air 13" (M3), iPad Air 11" (M3)
xcrun simctl list runtimes --json → supportedDeviceTypes for iOS 26.3 62 models you could create (30 iPhone, 32 iPad)
T3 Code Device panel / device_list 3: iPhone 17 Pro (Running), iPhone 17 Pro Max, iPhone Air, all "iOS 26.3"

The 3 that show up are exactly the 3 whose device.plist has a lastBootedAt. The other 8 have never been booted, so the hub's /api/devices drops them (.filter((device) => device.lastUsedAt !== undefined) in listIosSimulators; still present in 0.19.1). That part is already tracked in #16083, with an open fix in #24027. It is listed here only because it makes the missing picker worse: the iPad Pro 13" (M5), which T3 Code even ships a 3D frame for (apps/web/src/components/device/models/ipad-pro-13-m5.glb), can't be opened from T3 Code on a fresh setup.

Even after #24027 lands, the panel still can't do any of these:

  • Test on a model that has no simulator yet. Example: the iPhone SE (3rd generation) or iPhone 16 Pro, to check a small-screen or Dynamic Island layout.
  • Test on an older iOS version. Example: you install the iOS 18.5 runtime to reproduce a user's bug. Xcode creates simulators for the new runtime, but there's no way to say "iPhone 16 on iOS 18.5" from T3.
  • Tell same-name simulators apart. "iPhone 17 Pro" on two runtimes appears as two identical rows that differ only in the grey subtitle. The rows are sorted by booted state and then name (groupDevices in DevicePanel.tsx), so the runtimes are interleaved.

Where this is decided in the code

Layer File What it does
UI list apps/web/src/components/device/DevicePanel.tsx (groupDevices, the DiscoveryList map, "Refresh devices") One section per platform. Each row shows name and host · version · Running/Stopped, with Start/Open. No create action, no grouping by version.
UI setup apps/web/src/components/device/DeviceSetup.tsx platformSetupStatus With no listed iOS devices it says "Install a runtime in Xcode Settings → Components" (see #16083).
Contract packages/contracts/src/device.ts DeviceSummary (version is a free string), DeviceToolOpenInput (deviceId / platform only) No runtime or device-type fields, and no create input.
Local/SSH host apps/server/src/device/DeviceHubOperations.ts Calls only /api/devices, /api/devices/boot, /api/devices/shutdown, the serve-sim grid start/shutdown, and screenshot. Never calls /api/new-device-options or /api/devices/create.
Simlock host apps/server/src/device/SimlockDeviceHost.ts:299-311 The catalog already has modelRuntimes (every runtime per model), but T3 lists one entry per model and always reports runtimes.at(-1), the newest runtime. Older runtimes can't be chosen.
EAS host apps/server/src/device/EasDeviceHost.ts:74 "EAS picks the simulator model": one "iPhone (EAS Simulator)" entry, version "Cloud".
Agent tools apps/server/src/mcp/toolkits/device/tools.ts device_list / device_open can only open existing devices, so an agent asked to "check this on iOS 18" can't do it.

What the hub already provides (expo-device-hub 0.19.1, dist/server/index.mjs):

// GET /api/new-device-options → listNewDeviceOptions()
// iOS: available runtimes (newest first), each with the models it supports
{ ios: { runtimes: [{ value: "com.apple.CoreSimulator.SimRuntime.iOS-26-3", label: "iOS 26.3",
    models: [{ value: "com.apple.CoreSimulator.SimDeviceType.iPhone-17-Pro", label: "iPhone 17 Pro",
               supported: true, deviceFrame: ... }, ...] }] },
  android: { runtimes: [/* system images × compatible AVD profiles */] }, errors: [] }

// POST /api/devices/create  { platform, name, runtime, deviceType }
// iOS: xcrun simctl create <name> <deviceType> <runtime>, then boots it; returns { ok, id: udid }

So most of the hard work (parsing runtimes, matching models to runtimes, simctl create, Android image/profile matching) already exists in a dependency T3 pins and installs on every device host.

Proposal (product view)

Principle: keep the panel a list of real devices. Creating a new simulator is an action in that list. It isn't a settings page, and it isn't a step in the setup wizard.

1. A "New simulator" action in the Device panel's iOS section (main entry point)

At the end of the iOS Simulators list, next to "Refresh devices", add a New simulator… button. It opens a small popover/dialog with:

New iOS simulator                      [This machine ▾]   ← only if more than one host supports iOS
┌──────────────────────────────────────────────────────┐
│ iOS version   [ iOS 26.3 (newest)             ▾ ]     │  ← runtimes from /api/new-device-options
│ Model         [ iPhone 17 Pro                 ▾ ]     │  ← searchable, grouped: iPhone / iPad
│ Name          [ iPhone 17 Pro (iOS 26.3)        ]     │  ← prefilled, editable
└──────────────────────────────────────────────────────┘
                              [Cancel]  [Create and start]
  • Version first, then model. A runtime limits which models are valid (supportedDeviceTypes), so picking the version first avoids showing invalid combinations. Default to the newest runtime and the newest Pro iPhone.
  • Model list: group it into iPhone and iPad, newest first, and make it searchable (there are 62 entries on one runtime). Optionally show a small "3D frame" hint for models where resolveDeviceModelId returns a frame.
  • Name: prefill it with the runtime included (iPhone 16 (iOS 18.5)) so the new simulator is easy to tell apart later in Xcode and in this list.
  • "Create and start": create, boot, and open it in the current thread, the same way a row's Start button does (selectDevice → openDevice). Reuse the existing "Starting … This can take a minute." status and error notice.
  • When a runtime is missing: if the version you want isn't installed, the dropdown should say so: "Install more iOS versions in Xcode → Settings → Components." Downloading runtimes from T3 (xcodebuild -downloadPlatform iOS) is a large, slow, admin-ish action and is out of scope.

2. Make the existing list readable by version

  • In the iOS section, sub-group rows by runtime ("iOS 26.3", "iOS 18.5") when the host has more than one. With one runtime, keep today's flat list (no extra chrome).
  • Optionally add a compact version filter chip above the list if it gets long. The grouping alone is probably enough.

3. Fix the empty state to offer the action

When iOS is available but no simulator is listed (the #16083 case, or a user who deleted them all), show "No iOS simulators yet. [New simulator…]" instead of only sending people to Xcode.

4. Agents (follow-up, same plumbing)

Agents need this too: "reproduce on iOS 18" and "check the iPad layout" are common requests. Two options:

  • device_list also returns newDeviceOptions per host, and a new device_create tool (hostId, platform, runtime, deviceType, optional name) returns a deviceId for device_open, or
  • device_open accepts model + runtime when there's no deviceId, and reuses an existing match before creating one.

The first keeps creation explicit and auditable (it changes the user's Xcode simulator list), so it's probably better.

5. Other host kinds

Host Proposal
Local / SSH Full feature (both run expo-device-hub).
Simlock No creation. Show one row per model × runtime from modelRuntimes (or a version select on the row) instead of always using runtimes.at(-1).
EAS Hide "New simulator" (EAS chooses the model). Revisit if EAS exposes a device type or runtime option.
Android Same dialog later, backed by the same endpoint ("Android version" = system image, "Model" = AVD profile). Not required for the first version.

Explicitly not proposed

  • No new setting. Nothing here needs a toggle.
  • Deleting simulators from T3: the hub has POST /api/devices/remove, but deleting is destructive and can be a follow-up.
  • Downloading iOS runtimes from T3.

Implementation pointers

  1. Server, DeviceHubOperations.ts:
    • Add newDeviceOptions (GET /api/new-device-options, schema-decoded like HubDeviceList) and create (POST /api/devices/create with { platform, name, runtime, deviceType }, BOOT_TIMEOUT, the same DeviceBootError mapping as boot).
    • After an iOS create, call attachIosStream(udid) like boot does. The hub boots the simulator but doesn't attach serve-sim.
  2. Server, DeviceHost / DeviceService.ts:
    • Make the two operations optional on a host (Simlock and EAS leave them out).
    • Add DeviceService.newDeviceOptions(hostId) and DeviceService.create(input). After creating, refresh the host's list, then run the existing open path for the thread so bootingDevices and sessions behave as they do for Start.
  3. Contracts, packages/contracts/src/device.ts:
    • Add DeviceNewOptions (runtimes → models, supported) and DeviceCreateInput (threadId, hostId, platform, runtime, deviceType, name) plus the RPC methods.
    • Optionally add a structured runtime to DeviceSummary so the UI can group without parsing the version string.
  4. Web, DevicePanel.tsx:
    • Sub-group iOS rows by version in groupDevices.
    • Add the New simulator dialog. WizardPopup, Dialog, Select and the combobox primitives already exist in ~/components/ui.
    • Wire the dialog through deviceEnvironment atoms like open/list.
  5. Web, DeviceSetup.tsx: offer New simulator from the "no iOS Simulator is available" state.
  6. Scopes: creating a simulator changes the host, so gate it like open. If it should be stricter, gate it on the settings-write scope.
  7. Tests:
    • Extend DeviceService.test.ts / DeviceMultiHost.test.ts with a hub stub for the two endpoints.
    • Add a DevicePanel.browser.test.tsx case for the dialog: pick a version → models filtered → create → device opens in the thread.

Steps to see it

  1. On macOS with Xcode, run xcrun simctl list devices available and xcrun simctl list runtimes.
  2. In T3 Code, open a thread → Device panel.
  3. The list shows only simulators that were booted before. No control lets you pick a model or an iOS version, and nothing lets you create one.
  4. Compare with Xcode → Window → Devices and Simulators → "+", which offers a device type and an OS version.

Related

Version

  • Desktop: T3 Code (Beta) 0.1.6 on macOS (Darwin 27.0, arm64), expo-device-hub 0.19.0-canary-20261023-1357114, agent-device 0.21.31, Xcode with the iOS 26.3 runtime.
  • Also verified against main 37d7615a (pins expo-device-hub 0.19.1). Its npm build still has the same list filter and the same create/options endpoints.

Contributions welcome

This is a self-contained feature with clear seams: two hub calls on the server, one contract, and one dialog in DevicePanel.tsx. If you'd like to take it, comment here so work isn't duplicated. A small first PR could ship just the local/SSH "New simulator" dialog; version grouping, agent tools, and Simlock runtimes can follow separately. Please confirm the scope and the agent-tool shape with a maintainer before building part 4.

Activity

  1. locked and limited conversation to collaborators on Nov 1, 2026
  2. converted this issue into a discussion #28570 on Nov 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions