Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ The native side should own anything that depends on macOS frameworks, `xcrun sim
Defines REST routes for simulator control, health, metrics, and chrome assets.
- `server/src/transport/webrtc.rs`
Exposes the H.264 WebRTC offer/answer endpoint for browser live video.
- `server/src/webkit.rs`
Discovers simulator WebKit Remote Inspector targets and bridges WebInspectorUI
WebSocket traffic to the simulator `webinspectord` binary-plist socket.
- `server/src/simulators/registry.rs`
Tracks Rust-side simulator session state and lazy native attachment by UDID.
- `cli/XCWSimctl.*`
Expand Down Expand Up @@ -71,6 +74,7 @@ Private simulator behavior is implemented locally in:

The current repo uses the private boot path, private display bridge, and private accessibility translation bridge directly. The browser streams frames from that bridge, injects touch and keyboard events through the same native session layer, inspects accessibility through `AccessibilityPlatformTranslation`, and renders device chrome from `cli/XCWChromeRenderer.*`.
Physical chrome button support uses DeviceKit `chrome.json` input geometry for browser hit targets. Volume, action, and mute buttons dispatch through `IndigoHIDMessageForHIDArbitrary` with consumer/telephony HID usage pairs from the device chrome metadata; home, lock, and app-switcher remain on the existing SimulatorKit button paths.
WebKit inspection uses the simulator `webinspectord` Unix socket named `com.apple.webinspectord_sim.socket` and WebKit's binary-plist Remote Inspector selectors. It lists only WebKit content that the runtime exposes as inspectable. For app-owned `WKWebView` on iOS 16.4 and newer, the app must set `isInspectable = true`.

## Build and Run

Expand Down
22 changes: 22 additions & 0 deletions client/src/api/simulators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { apiRequest } from "./client";
import type {
AccessibilitySourcePreference,
AccessibilityTreeResponse,
ChromeDevToolsTargetDiscovery,
ChromeProfile,
InspectorRequestResponse,
SimulatorLogsResponse,
SimulatorMetadata,
SimulatorsResponse,
WebKitTargetDiscovery,
} from "./types";

export async function listSimulators(
Expand Down Expand Up @@ -74,6 +76,26 @@ export async function fetchSimulatorLogs(
);
}

export async function fetchWebKitTargets(
udid: string,
options: RequestInit = {},
): Promise<WebKitTargetDiscovery> {
return apiRequest<WebKitTargetDiscovery>(
`/api/simulators/${encodeURIComponent(udid)}/webkit/targets`,
options,
);
}

export async function fetchChromeDevToolsTargets(
udid: string,
options: RequestInit = {},
): Promise<ChromeDevToolsTargetDiscovery> {
return apiRequest<ChromeDevToolsTargetDiscovery>(
`/api/simulators/${encodeURIComponent(udid)}/devtools/targets`,
options,
);
}

export async function sendInspectorRequest<T = unknown>(
udid: string,
method: string,
Expand Down
46 changes: 46 additions & 0 deletions client/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,52 @@ export interface SimulatorsResponse {
simulators: SimulatorMetadata[];
}

export interface WebKitTarget {
id: string;
appId: string;
appName?: string | null;
pageId: number;
title?: string | null;
url?: string | null;
kind: "safari-page" | "app-web-content" | "web-content-proxy" | string;
inspectorUrl: string;
webSocketUrl: string;
}

export interface WebKitTargetDiscovery {
udid: string;
socketPath?: string | null;
targets: WebKitTarget[];
warnings: string[];
}

export interface ChromeDevToolsTarget {
id: string;
appName?: string | null;
bundleIdentifier?: string | null;
description: string;
devtoolsFrontendUrl: string;
processIdentifier: number;
source:
| "react-native"
| "react-native-metro"
| "chrome-inspector"
| "nativescript"
| "swiftui"
| "in-app-inspector"
| string;
title: string;
type: string;
url: string;
webSocketDebuggerUrl: string;
}

export interface ChromeDevToolsTargetDiscovery {
udid: string;
targets: ChromeDevToolsTarget[];
warnings: string[];
}

export interface HealthResponse {
ok: boolean;
videoCodec?: string;
Expand Down
43 changes: 28 additions & 15 deletions client/src/app/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
openSimulatorUrl,
pressHome,
pressSimulatorButton,
rotateLeft,
rotateRight,
simulatorControlSocketUrl,
shutdownSimulator,
Expand All @@ -41,6 +40,7 @@ import type {
TouchPhase,
} from "../api/types";
import { AccessibilityInspector } from "../features/accessibility/AccessibilityInspector";
import { DevToolsPanel } from "../features/devtools/DevToolsPanel";
import { isEditableTarget } from "../features/input/keycodes";
import { useKeyboardInput } from "../features/input/useKeyboardInput";
import { usePointerInput } from "../features/input/usePointerInput";
Expand Down Expand Up @@ -86,9 +86,11 @@ import {
import { useElementSize } from "../shared/hooks/useElementSize";
import {
ACCESSIBILITY_SOURCE_STORAGE_KEY,
CHROME_DEVTOOLS_VISIBLE_STORAGE_KEY,
clearLegacyVolatileUiState,
DEFAULT_VIEWPORT_STATE,
DEBUG_VISIBLE_STORAGE_KEY,
DEVTOOLS_VISIBLE_STORAGE_KEY,
HIERARCHY_VISIBLE_STORAGE_KEY,
nextAccessibilitySourcePreference,
readPersistedUiState,
Expand All @@ -97,6 +99,7 @@ import {
sanitizeAccessibilitySources,
TOUCH_OVERLAY_VISIBLE_STORAGE_KEY,
viewportStateForUDID,
WEBKIT_INSPECTOR_VISIBLE_STORAGE_KEY,
writePersistedUiState,
writeStoredFlag,
} from "./uiState";
Expand Down Expand Up @@ -349,6 +352,12 @@ export function AppShell({
const [hierarchyVisible, setHierarchyVisible] = useState(() =>
readStoredFlag(HIERARCHY_VISIBLE_STORAGE_KEY),
);
const [devToolsVisible, setDevToolsVisible] = useState(
() =>
readStoredFlag(DEVTOOLS_VISIBLE_STORAGE_KEY) ||
readStoredFlag(CHROME_DEVTOOLS_VISIBLE_STORAGE_KEY) ||
readStoredFlag(WEBKIT_INSPECTOR_VISIBLE_STORAGE_KEY),
);
const [selectedUDID, setSelectedUDID] = useState(initialSelectedUDID ?? "");
const [search, setSearch] = useState("");
const [openURLValue, setOpenURLValue] = useState(
Expand Down Expand Up @@ -752,10 +761,18 @@ export function AppShell({
writeStoredFlag(HIERARCHY_VISIBLE_STORAGE_KEY, hierarchyVisible);
}, [hierarchyVisible]);

useEffect(() => {
writeStoredFlag(DEVTOOLS_VISIBLE_STORAGE_KEY, devToolsVisible);
}, [devToolsVisible]);

useEffect(() => {
writeStoredFlag(TOUCH_OVERLAY_VISIBLE_STORAGE_KEY, touchOverlayVisible);
}, [touchOverlayVisible]);

const toggleDevTools = useCallback(() => {
setDevToolsVisible((current) => !current);
}, []);

useEffect(() => {
window.localStorage.setItem(
ACCESSIBILITY_SOURCE_STORAGE_KEY,
Expand Down Expand Up @@ -1815,20 +1832,6 @@ export function AppShell({
);
}
}}
onRotateLeft={() => {
if (!selectedSimulator) {
return;
}
beginZoomAnimation();
if (sendControl(selectedSimulator.udid, { type: "rotateLeft" })) {
setRotationQuarterTurns((current) => (current + 3) % 4);
return;
}
void runAction(async () => {
await rotateLeft(selectedSimulator.udid);
setRotationQuarterTurns((current) => (current + 3) % 4);
}, false);
}}
onOpenBundlePrompt={promptForBundleID}
onOpenUrlPrompt={promptForURL}
onRotateRight={() => {
Expand Down Expand Up @@ -1873,6 +1876,7 @@ export function AppShell({
}
}}
onToggleDebug={() => setDebugVisible((current) => !current)}
onToggleDevTools={toggleDevTools}
onToggleHierarchy={() => {
setHierarchyVisible((current) => !current);
if (hierarchyVisible) {
Expand Down Expand Up @@ -1902,6 +1906,7 @@ export function AppShell({
selectedSimulator?.isBooted && !selectedSimulatorTransitionKind,
)}
touchOverlayVisible={touchOverlayVisible}
devToolsVisible={devToolsVisible}
/>
<SimulatorViewport
accessibilityHoveredId={accessibilityHoveredId}
Expand Down Expand Up @@ -2002,6 +2007,14 @@ export function AppShell({
touchIndicators={touchIndicators}
touchOverlayVisible={touchOverlayVisible}
viewMode={viewMode}
devtoolsPanel={
devToolsVisible ? (
<DevToolsPanel
onClose={() => setDevToolsVisible(false)}
selectedSimulator={selectedSimulator}
/>
) : null
}
zoomDockRef={handleZoomDockRef}
zoomAnimating={zoomAnimating}
/>
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/uiState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface PersistedUiState {
export const UI_STATE_STORAGE_KEY = "xcw-ui-state";
export const DEBUG_VISIBLE_STORAGE_KEY = "xcw-debug-visible";
export const HIERARCHY_VISIBLE_STORAGE_KEY = "xcw-hierarchy-visible";
export const DEVTOOLS_VISIBLE_STORAGE_KEY = "xcw-devtools-visible";
export const WEBKIT_INSPECTOR_VISIBLE_STORAGE_KEY =
"xcw-webkit-inspector-visible";
export const CHROME_DEVTOOLS_VISIBLE_STORAGE_KEY =
"xcw-chrome-devtools-visible";
export const ACCESSIBILITY_SOURCE_STORAGE_KEY = "xcw-hierarchy-source";
export const TOUCH_OVERLAY_VISIBLE_STORAGE_KEY = "xcw-touch-overlay-visible";

Expand Down
Loading
Loading