Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
38024f5
feat: enable ios profiler for physical device
stachbial Mar 23, 2026
60e5b2b
Merge branch 'main' of github.com:software-mansion-labs/radon-lite in…
stachbial Mar 23, 2026
9ae200c
Merge branch 'main' of github.com:software-mansion-labs/radon-lite in…
stachbial Mar 24, 2026
47eb874
Merge branch 'main' of github.com:software-mansion/argent into @stach…
stachbial Mar 26, 2026
224e0cd
feat: rename list-simulators to list-devices with optional physical d…
stachbial Mar 26, 2026
d712aba
chore: rm unnecessary await from js-runtime-debbugger
stachbial Mar 31, 2026
a74e6a7
Merge remote-tracking branch 'origin/main' into @stachbial/ios-profil…
stachbial Mar 31, 2026
9b698fa
feat: mv agent guidelines on sim vs physical devices to skill from to…
stachbial Mar 31, 2026
31c4f41
Merge branch 'main' of github.com:software-mansion/argent into @stach…
stachbial Apr 3, 2026
77cbf15
fix: physical device scan err handling
stachbial Apr 16, 2026
79ad5a6
Merge remote-tracking branch 'origin/main' into @stachbial/ios-profil…
stachbial Apr 16, 2026
f6af860
Merge remote-tracking branch 'origin/main' (tree already reconciled i…
stachbial Apr 16, 2026
8408426
chore: prettier formatting on merged files
stachbial Apr 17, 2026
61ade52
chore: bump argent-private submodule to 73c9491 (physical device rese…
pFornagiel Jun 9, 2026
c28bc12
Merge origin/main into @stachbial/ios-profiler-for-physical-device
pFornagiel Jun 9, 2026
a06d595
Merge origin/main into @stachbial/ios-profiler-for-physical-device
latekvo Jun 25, 2026
ebf161f
fix(ios-profiler): attach cause to thrown errors in ios-physical-device
latekvo Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/skills/skills/argent-device-interact/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ If you delegate simulator tasks to sub-agents, make sure they have MCP permissio

Use `list-devices` to get a target id. Results are tagged with `platform` (`ios`, `android`, or `chromium`); booted/ready devices come first. Pick the first entry that matches the platform you need — if none are ready, call `boot-device` with `udid` (iOS), `avdName` (Android), or `electronAppPath` (boots an Electron app as a `chromium` device). A Chromium browser already running with a CDP port shows up directly — no `boot-device` needed. See `argent-ios-simulator-setup` / `argent-android-emulator-setup` for full setup flow.

> **Physical devices** do not support interaction tools (taps, swipes, screenshots, describe). For physical device workflows, use profiling and debugging tools only — see `argent-native-profiler` skill.

**Load tool schemas before first use.** Gesture tools (`gesture-tap`, `gesture-swipe`, `gesture-pinch`, `gesture-rotate`, `gesture-custom`) may be deferred — their parameter schemas are not loaded until fetched. Always use ToolSearch to load the schemas of all gesture tools you plan to use **before** calling any of them. If you skip this step, parameters may be coerced to strings instead of numbers, causing validation errors.

## 2. Best Practices
Expand Down
15 changes: 11 additions & 4 deletions packages/skills/skills/argent-native-profiler/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,20 @@ After presenting findings, ask the user whether to investigate further, implemen

**Complete all steps in order — do not break mid-flow.**

### Step 0: Ensure the target app is running
### Step 0: Choose device and ensure the target app is running

The `native-profiler-start` tool **auto-detects** the running app on the device.
**Simulator vs physical device:** Call `list-devices` to find a target device.

- **Prefer simulators** for fast iteration, CI, and most development workflows.
- **Use a physical device** (`include_physical_devices: true`) when the user explicitly asks for device profiling, or when you need accurate real-world data: CPU/GPU timings, thermal throttling, real memory behavior, or hardware-dependent features (camera, GPS, NFC, push notifications).

> Physical devices do **not** support automated interaction (taps, swipes, screenshots, describe) — only profiling and debugging tools work. The user must navigate the device by hand.

The `native-profiler-start` tool **auto-detects** the running app on the simulator or device.
You do not need to derive `app_process` manually — just make sure the app is launched.

1. If the app is already running on the device, skip to Step 1 (do not pass `app_process`).
2. If the app is not running, use `launch-app` with the correct bundle ID first.
1. If the app is already running, skip to Step 1 (do not pass `app_process`).
2. If the app is not running on a simulator, use `launch-app` with the correct bundle ID first. On a physical device, ask the user to launch the app.
3. Only pass `app_process` explicitly if the tool reports multiple running user apps and you need to disambiguate.

> **Note**: If multiple build flavors are installed (dev, staging, prod), the tool will detect whichever one is currently running. If both are running, it will ask you to specify.
Expand Down
4 changes: 3 additions & 1 deletion packages/tool-server/src/blueprints/js-runtime-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@ export const jsRuntimeDebuggerBlueprint: ServiceBlueprint<JsRuntimeDebuggerApi,

await cdp.evaluate(DISABLE_LOGBOX_SCRIPT).catch(warnOnError("DISABLE_LOGBOX_SCRIPT"));

await sourceMaps.waitForPending();
// Let source maps load in the background – consumers that need them
// (e.g. debugger-set-breakpoint) already call waitForPending() themselves.
sourceMaps.waitForPending().catch(ignore);

const sourceResolver = createSourceResolver(port, metro.projectRoot);

Expand Down
66 changes: 53 additions & 13 deletions packages/tool-server/src/tools/devices/list-devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import type { ToolDefinition } from "@argent/registry";
import { listAndroidDevices, listAvds, consolePortFromAdbSerial } from "../../utils/adb";
import { listRunningVvdConsolePorts } from "../../utils/vega-process";
import { listIosSimulators, type IosSimulator } from "../../utils/ios-devices";
import { listPhysicalDevices, type PhysicalDevice } from "../../utils/ios-physical-device";
import { discoverChromiumDevices, type ChromiumDevice } from "../../utils/chromium-discovery";
import {
listVegaDevices,
filterVvdShadowsFromAndroid,
type VegaDevice,
} from "../../utils/vega-devices";
type IosDevice = IosSimulator & { platform: "ios" };

type IosSimulatorDevice = IosSimulator & { platform: "ios"; kind: "simulator" };
type IosPhysicalDevice = PhysicalDevice & { platform: "ios"; kind: "physical" };
type IosDevice = IosSimulatorDevice | IosPhysicalDevice;

type AndroidDevice = {
platform: "android";
Expand All @@ -29,9 +33,13 @@ type AndroidDevice = {
type ListDevicesResult = {
devices: Array<IosDevice | AndroidDevice | ChromiumDevice | VegaDevice>;
avds: Array<{ name: string }>;
// Only present when a physical-device scan was requested but devicectl failed
// (e.g. Xcode < 15, device locked/untrusted) — lets the caller surface the
// reason instead of silently reporting "no devices".
physicalDevicesError?: string;
};

function sortIos(a: IosDevice, b: IosDevice): number {
function sortIos(a: IosSimulatorDevice, b: IosSimulatorDevice): number {
const aBooted = a.state === "Booted" ? 0 : 1;
const bBooted = b.state === "Booted" ? 0 : 1;
if (aBooted !== bBooted) return aBooted - bBooted;
Expand All @@ -51,8 +59,12 @@ function sortAndroid(a: AndroidDevice, b: AndroidDevice): number {

// Float booted/ready devices to the top of the merged list regardless of
// platform — without this, all iOS entries are emitted before any Android.
// Connected physical devices are always ready.
function readinessRank(d: IosDevice | AndroidDevice | ChromiumDevice | VegaDevice): number {
if (d.platform === "ios") return d.state === "Booted" ? 0 : 1;
if (d.platform === "ios") {
if (d.kind === "physical") return 0;
return d.state === "Booted" ? 0 : 1;
}
if (d.platform === "android") return d.state === "device" ? 0 : 1;
if (d.platform === "vega") return d.state === "running" || d.state === "device" ? 0 : 1;
return 0; // Chromium entries are only listed when their CDP is responsive
Expand All @@ -78,31 +90,58 @@ async function resolveVvdShadowAdbSerials<T extends { serial: string }>(
return shadows;
}

const zodSchema = z.object({});
const zodSchema = z.object({
include_physical_devices: z
.boolean()
.optional()
.describe(
'Also scan for physical iOS devices connected via USB or Wi-Fi (tagged `kind: "physical"`). Defaults to false — the scan is slower (~5s) and requires Xcode 15+. Physical devices support profiling/debugging only, not automated interaction.'
),
});

export const listDevicesTool: ToolDefinition<Record<string, never>, ListDevicesResult> = {
type Params = z.infer<typeof zodSchema>;

export const listDevicesTool: ToolDefinition<Params, ListDevicesResult> = {
id: "list-devices",
description: `List iOS simulators, Android emulators, connected physical Android devices, running Chromium apps, and Vega (Fire TV) devices in one place.
Use at the start of a session to pick a target id ('udid' for iOS entries, 'serial' for Android/Vega entries, 'id' for Chromium) to pass to interaction tools, and to see which targets are already running.
Returns { devices, avds } where each device carries a 'platform' discriminator ('ios', 'android', 'chromium', or 'vega'); 'avds' lists Android AVDs bootable via boot-device. A Vega VVD is listed under 'devices' whether running or stopped (state 'running'/'stopped'); start a stopped one with boot-device using its 'vvdImage'.
Returns { devices, avds } where each device carries a 'platform' discriminator ('ios', 'android', 'chromium', or 'vega'); iOS entries also carry a 'kind' ('simulator' or 'physical'). 'avds' lists Android AVDs bootable via boot-device. A Vega VVD is listed under 'devices' whether running or stopped (state 'running'/'stopped'); start a stopped one with boot-device using its 'vvdImage'.
Android entries also carry a 'kind' ('emulator' for a local AVD, 'device' for a physical phone connected over USB / wireless adb) — physical phones are detected from \`adb devices\` (any serial that is not an \`emulator-*\` one) and are driven through the same interaction tools as emulators; they do not need boot-device (just connect the phone with USB debugging authorised).
Set include_physical_devices: true to also scan for connected physical iOS devices (slower, requires Xcode 15+); physical devices support profiling/debugging only, not automated interaction.
Chromium apps are discovered by probing CDP debugging ports (default 9222; extend via the ARGENT_CHROMIUM_PORTS=<comma-separated-ports> env var). They must already be running with --remote-debugging-port=<port> — use boot-device with electronAppPath to launch one.
Booted/ready devices are listed first. Platforms whose CLI is unavailable are silently omitted — an empty result usually means xcode-select, Android platform-tools, or the Vega SDK is not installed.`,
alwaysLoad: true,
searchHint:
"list devices simulators emulators avd serial udid ios android chromium vega app fire tv session start",
"list devices simulators emulators physical avd serial udid ios android chromium vega app fire tv session start",
zodSchema,
services: () => ({}),
async execute(_services, _params) {
const [ios, android, avds, chromium, vega] = await Promise.all([
async execute(_services, params) {
const includePhysical = params.include_physical_devices ?? false;
const [ios, android, avds, chromium, vega, physical] = await Promise.all([
listIosSimulators(),
listAndroidDevices().catch(() => []),
listAvds(),
discoverChromiumDevices().catch(() => []),
listVegaDevices().catch(() => []),
includePhysical ? listPhysicalDevices() : Promise.resolve(null),
]);
const iosTagged: IosDevice[] = ios.map((s) => ({ platform: "ios", ...s }));
iosTagged.sort(sortIos);

const iosSimTagged: IosSimulatorDevice[] = ios.map((s) => ({
platform: "ios",
kind: "simulator",
...s,
}));
iosSimTagged.sort(sortIos);

let physicalDevicesError: string | undefined;
const iosPhysTagged: IosPhysicalDevice[] = [];
if (physical) {
physicalDevicesError = physical.error;
for (const pd of physical.devices) {
iosPhysTagged.push({ platform: "ios", kind: "physical", ...pd });
}
}

const androidTagged: AndroidDevice[] = android.map((d) => ({
platform: "android",
serial: d.serial,
Expand All @@ -119,13 +158,14 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
androidDeduped.sort(sortAndroid);

const devices: Array<IosDevice | AndroidDevice | ChromiumDevice | VegaDevice> = [
...iosTagged,
...iosSimTagged,
...iosPhysTagged,
...androidDeduped,
...chromium,
...vega,
];
devices.sort((a, b) => readinessRank(a) - readinessRank(b));

return { devices, avds };
return physicalDevicesError ? { devices, avds, physicalDevicesError } : { devices, avds };
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ import { exportIosTraceData } from "../../../../utils/ios-profiler/export";
import type { ExportDiagnostics } from "../../../../utils/ios-profiler/export";
import { shutdownChild } from "../../../../utils/profiler-shared/lifecycle";
import { runIosProfilerPipeline } from "../../../../utils/ios-profiler/pipeline/index";
import { selectIosCaptureStrategy } from "../../../../utils/ios-profiler/capture-strategy";
import {
selectIosCaptureStrategy,
deviceStrategy,
} from "../../../../utils/ios-profiler/capture-strategy";
import type { NativeProfilerAnalyzeResult } from "../../../../utils/ios-profiler/types";
import { renderNativeProfilerReport } from "../../../../utils/ios-profiler/render";
import { formatTraceFreshness } from "../../../../utils/profiler-shared/freshness";
import { RECORDING_CAP_MS } from "../../../../utils/profiler-shared/types";
import { checkIsSimulator, detectRunningAppOnDevice } from "../../../../utils/ios-physical-device";

// Two candidates because __dirname differs by runtime: bundled it's argent/dist/
// (template in argent/assets/); in dev it's tool-server/dist/tools/profiler/
Expand Down Expand Up @@ -300,16 +304,31 @@ export async function startNativeProfilerIos(
}

const templatePath = params.template_path ?? resolveDefaultTemplatePath();
const detected = params.app_process
? resolveExplicitApp(params.device_id, params.app_process)
: detectRunningApp(params.device_id);

// Resolve the target app, branching by device type only when auto-detecting.
// Simulators expose a host PID via `simctl … launchctl list` (used for the
// PID-based attach and the all-processes fallback); physical devices go
// through `devicectl`, which yields only the executable name — their apps are
// not host processes, so there is no host PID to attach by.
let detected: DetectedApp;
let isPhysicalDevice = false;
if (params.app_process) {
detected = resolveExplicitApp(params.device_id, params.app_process);
} else if (await checkIsSimulator(params.device_id)) {
detected = detectRunningApp(params.device_id);
} else {
isPhysicalDevice = true;
detected = { executable: await detectRunningAppOnDevice(params.device_id), pid: null };
}
const appProcess = detected.executable;

// Pick the capture approach for this environment. On Xcode versions where
// `xctrace --device` works this is the original device/attach path; on the
// 26.4–27.0 regression (where --device deadlocks) it is the host-wide
// --all-processes fallback, filtered to the app PID. See capture-strategy.
const strategy = selectIosCaptureStrategy();
// Pick the capture approach for this environment. A physical device always
// uses the device/attach path: its app is not a host process, so the host-wide
// --all-processes fallback (a simulator-only workaround for the Xcode
// 26.4–27.0 `xctrace --device` deadlock) cannot isolate it. For simulators,
// pick per Xcode version — the original device/attach path where it works, or
// the all-processes fallback filtered to the app PID. See capture-strategy.
const strategy = isPhysicalDevice ? deviceStrategy : selectIosCaptureStrategy();
// The all-processes fallback records host-wide and isolates the app by PID, so
// it can only run when the target is actually running (PID known).
if (strategy.name === "all-processes" && detected.pid == null) {
Expand Down
Loading
Loading