Repository navigation
Description
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 (
groupDevicesinDevicePanel.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
resolveDeviceModelIdreturns 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_listalso returnsnewDeviceOptionsper host, and a newdevice_createtool (hostId,platform,runtime,deviceType, optionalname) returns adeviceIdfordevice_open, ordevice_openacceptsmodel+runtimewhen there's nodeviceId, 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
- Server,
DeviceHubOperations.ts:- Add
newDeviceOptions(GET/api/new-device-options, schema-decoded likeHubDeviceList) andcreate(POST/api/devices/createwith{ platform, name, runtime, deviceType },BOOT_TIMEOUT, the sameDeviceBootErrormapping asboot). - After an iOS create, call
attachIosStream(udid)likebootdoes. The hub boots the simulator but doesn't attach serve-sim.
- Add
- Server,
DeviceHost/DeviceService.ts:- Make the two operations optional on a host (Simlock and EAS leave them out).
- Add
DeviceService.newDeviceOptions(hostId)andDeviceService.create(input). After creating, refresh the host's list, then run the existing open path for the thread sobootingDevicesand sessions behave as they do for Start.
- Contracts,
packages/contracts/src/device.ts:- Add
DeviceNewOptions(runtimes → models,supported) andDeviceCreateInput(threadId,hostId,platform,runtime,deviceType,name) plus the RPC methods. - Optionally add a structured
runtimetoDeviceSummaryso the UI can group without parsing theversionstring.
- Add
- Web,
DevicePanel.tsx:- Sub-group iOS rows by
versioningroupDevices. - Add the New simulator dialog.
WizardPopup,Dialog,Selectand the combobox primitives already exist in~/components/ui. - Wire the dialog through
deviceEnvironmentatoms likeopen/list.
- Sub-group iOS rows by
- Web,
DeviceSetup.tsx: offer New simulator from the "no iOS Simulator is available" state. - Scopes: creating a simulator changes the host, so gate it like
open. If it should be stricter, gate it on the settings-write scope. - Tests:
- Extend
DeviceService.test.ts/DeviceMultiHost.test.tswith a hub stub for the two endpoints. - Add a
DevicePanel.browser.test.tsxcase for the dialog: pick a version → models filtered → create → device opens in the thread.
- Extend
Steps to see it
- On macOS with Xcode, run
xcrun simctl list devices availableandxcrun simctl list runtimes. - In T3 Code, open a thread → Device panel.
- 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.
- Compare with Xcode → Window → Devices and Simulators → "+", which offers a device type and an OS version.
Related
- Device setup hides iOS simulators that were never booted, and says no simulator is installed #16083: hub hides never-booted simulators (open). Fix in PR fix(server): discover iOS simulators before their first boot #24027 (open). That fixes discovery; this issue is about choosing and creating.
- Device panel does not list watchOS simulators #27675: watchOS simulators not listed (closed). Same "list only what the hub reports" area.
- PR fix(web): pick the 3D device body from the simulator's hardware type #26884 (closed without merging): picking the 3D device body from the simulator's hardware type. It's the same model-awareness area.
- feat(devices): add simulator and emulator support #10677: original device support. feat(server): EAS Simulator device hosts #21844: EAS Simulator hosts.
Version
- Desktop: T3 Code (Beta) 0.1.6 on macOS (Darwin 27.0, arm64),
expo-device-hub0.19.0-canary-20261023-1357114,agent-device0.21.31, Xcode with the iOS 26.3 runtime. - Also verified against
main37d7615a(pinsexpo-device-hub0.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