Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/argent-private
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 @@ -15,6 +15,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` or `android`); 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) or `avdName` (Android). 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 @@ -153,7 +153,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
69 changes: 56 additions & 13 deletions packages/tool-server/src/tools/devices/list-devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { z } from "zod";
import type { ToolDefinition } from "@argent/registry";
import { listAndroidDevices, listAvds } from "../../utils/adb";
import { listIosSimulators, type IosSimulator } from "../../utils/ios-devices";
import { listPhysicalDevices, type PhysicalDevice } from "../../utils/ios-physical-device";

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 @@ -18,9 +21,13 @@ type AndroidDevice = {
type ListDevicesResult = {
devices: Array<IosDevice | AndroidDevice>;
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 @@ -40,31 +47,63 @@ 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): 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;
}
return d.state === "device" ? 0 : 1;
}

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 and Android devices/emulators in one place.
Use at the start of a session to pick a target id ('udid' for iOS entries, 'serial' for Android) to pass to interaction tools, and to see which targets are already running.
Returns { devices, avds } where each device carries a 'platform' discriminator ('ios' or 'android'), and 'avds' lists Android AVDs that can be booted via boot-device.
Returns { devices, avds } where each device carries a 'platform' discriminator ('ios' or 'android'); iOS entries also carry 'kind' ('simulator' or 'physical'). 'avds' lists Android AVDs that can be booted via boot-device.
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.
Booted/ready devices are listed first. Platforms whose CLI is unavailable are silently omitted — an empty result usually means xcode-select or Android platform-tools is not installed.`,
alwaysLoad: true,
searchHint: "list devices simulators emulators avd serial udid ios android session start",
searchHint:
"list devices simulators emulators physical avd serial udid ios android session start",
zodSchema,
services: () => ({}),
async execute(_services, _params) {
const [ios, android, avds] = await Promise.all([
async execute(_services, params) {
const includePhysical = params.include_physical_devices ?? false;
const [ios, android, avds, physical] = await Promise.all([
listIosSimulators(),
listAndroidDevices().catch(() => []),
listAvds(),
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 @@ -76,9 +115,13 @@ Booted/ready devices are listed first. Platforms whose CLI is unavailable are si
}));
androidTagged.sort(sortAndroid);

const devices: Array<IosDevice | AndroidDevice> = [...iosTagged, ...androidTagged];
const devices: Array<IosDevice | AndroidDevice> = [
...iosSimTagged,
...iosPhysTagged,
...androidTagged,
];
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 @@ -10,6 +10,10 @@ import { resolveDevice } from "../../../utils/device-info";
import { getDebugDir } from "../../../utils/react-profiler/debug/dump";
import { listenForDarwinNotification, type NotifyHandle } from "../../../utils/ios-profiler/notify";
import { waitForXctraceReady } from "../../../utils/ios-profiler/startup";
import {
checkIsSimulator,
detectRunningAppOnDevice,
} from "../../../utils/ios-physical-device";

const DEFAULT_TEMPLATE_PATH = path.resolve(__dirname, "Argent.tracetemplate");
const STARTUP_TIMEOUT_MS = 10_000;
Expand All @@ -28,7 +32,7 @@ const zodSchema = z.object({
.string()
.optional()
.describe(
"The exact CFBundleExecutable of the app to profile. If omitted, auto-detects the currently running foreground app on the simulator. Only provide this if auto-detection picks the wrong app (e.g. multiple apps running)."
"The exact CFBundleExecutable of the app to profile. If omitted, auto-detects the currently running foreground app on the simulator or device. Only provide this if auto-detection picks the wrong app (e.g. multiple apps running)."
),
template_path: z
.string()
Expand Down Expand Up @@ -208,7 +212,16 @@ Fails if no app is running on the device, the platform is not supported yet, or
}

const templatePath = params.template_path ?? DEFAULT_TEMPLATE_PATH;
const appProcess = params.app_process ?? detectRunningApp(params.device_id);

// Auto-detect the running app when not explicitly provided. Simulators are
// queried via simctl; physical devices via devicectl (a different code path).
let appProcess = params.app_process;
if (!appProcess) {
const isSimulator = await checkIsSimulator(params.device_id);
appProcess = isSimulator
? detectRunningApp(params.device_id)
: await detectRunningAppOnDevice(params.device_id);
}

const debugDir = await getDebugDir();
const timestamp = new Date()
Expand Down
Loading
Loading