# App remote control Use this reference when inspecting or driving a Makepad app. Ownership, focus, and capture policy live in [AGENTS.md](../../AGENTS.md). The implementation is [platform/src/remote.rs](../../platform/src/remote.rs). Fetch `GET /` from your running instance for its current route list; it can contain newer diagnostics than this guide. ## Launch and discovery Build the package in release mode, then launch the resulting standalone binary with `--remote`. For a hidden verification run from the repo root: ```sh cargo build --release -p makepad-example-splash MAKEPAD_HIDE_WINDOWS=1 ./target/release/makepad-example-splash --remote ``` Capture the launch output using your process tool. The startup line contains everything needed to address and clean up that instance: ```text [makepad-remote] listening on 127.0.0.1:53412 pid=9931 app=makepad-example-splash grabs=/tmp/makepad-remote/makepad-example-splash-9931 ``` The port is ephemeral. `--remote=PORT` pins it; `MAKEPAD_REMOTE=1` or `MAKEPAD_REMOTE=PORT` also enables the service. Prefer the explicit launch flag for agent inspection. On macOS `--focus` brings the app to the front as its window opens; a binary launched from a terminal otherwise stays behind the launcher. Do not discover a port by taking over another running instance. Retain your own launch's PID and endpoint. On code changes, close that instance, rebuild, and relaunch before inspecting again. ## Common routes The core routes use GET. JSON replies are one line; help/tree dumps are text and raw grabs are PNG. Errors carry an `err` field. | Route | Purpose | |---|---| | `/`, `/help` | Current protocol help | | `/s?w=ID` | App/PID and window geometry; omit `w` to list windows | | `/g?w=ID&scale=0.5`, `/gseq?n=8&every_ms=50&scale=0.5` | Capture the next presented frame, or N frames on request cadence (1–64, ≥8 ms, ≤60 s span); Metal/raw readbacks downsample before worker PNG encoding; return absolute `png` path(s) and per-frame `capture_ms`/`encode_ms` (`gseq.frames`); input `wait=1` remains a next-frame barrier | | `/g?raw=1` | Return PNG bytes instead of a path | | `/gq?scale=0.5` | Grab windows, then quit; returns `png` paths and `quit:1` | | `/snap?q=TEXT&w=ID&all=1` | Widget ids/types/text and window-local rectangles | | `/d`, `/dump` | Indented widget tree | | `/m?k=click&x=X&y=Y` | Mouse input; kinds also include move, down, up, scroll | | `/click?x=X&y=Y` | Click alias | | `/k?k=press&c=KeyA` | Key input; down/up are also supported | | `/t?t=TEXT`, `/k?t=TEXT` | Text/IME input | | `/drop?path=ABSOLUTE_PATH&x=X&y=Y` | One file through native drag, drop, and drag-end events | | `/log?n=50&since=N` | App log tail; `n` in the reply is the latest sequence | | `/close?w=ID` | Close one window normally | | `/quit` | Graceful shutdown without a final grab | `/snap` entries include `window_id` and `enabled`, plus `selected` when the widget exposes a selection, alongside the compact `i`/`ty`/`r`/`w` fields. Query parameters are optional unless needed for the operation. Input routes accept `w=ID` to target a window and `wait=1` to answer after the next frame. With no window specified, routes generally use the first window. Mouse buttons: `b=0` left, `b=1` right, `b=2` middle. Scroll takes `dx` and `dy`. Add `hw=1` when testing the hardware pointer-lock/pin transform. Keyboard modifiers are `shift=1`, `ctrl=1`, `alt=1`, and `cmd=1`. POST with a flat JSON body is supported. The input parser also accepts long names such as `window`, `kind`, `button`, `text`, and `code`. Use `curl --get --data-urlencode` for arbitrary text in query strings. File drops require an absolute path (at most 4096 bytes) and finite, nonnegative `x`/`y` inside the window. Optional parameters are `w`/`window` and `wait=1`; duplicate or unknown parameters are rejected. The remote thread sends only the path; the app owns validation and loading. The reply reports `drop_handled` and `drag_response`, which confirm event handling, not that an asynchronous file import has completed. For example: ```sh curl --get --data-urlencode 'path=/absolute/path/reference car.png' \ --data-urlencode 'x=300' --data-urlencode 'y=400' --data-urlencode 'wait=1' \ 'http://127.0.0.1:53412/drop' ``` ## Drive an owned instance After replacing the example port with the one your app printed: ```sh curl --fail --silent --show-error 'http://127.0.0.1:53412/' curl --fail --silent --show-error 'http://127.0.0.1:53412/snap?q=press_demo' ``` Read the returned `r: [x, y, width, height]`, calculate its center, and feed that point to `/click?...&wait=1`. Do not reuse coordinates from another window or an earlier layout. ```sh curl --fail --silent --show-error 'http://127.0.0.1:53412/log?n=20' curl --fail --silent --show-error 'http://127.0.0.1:53412/gq?scale=0.5' ``` Read a returned PNG with the local image viewer when visual inspection is needed. Confirm the owned process exits after cleanup. Use `/quit` if a backend cannot grab; do not replace a failed grab with an OS screenshot. ## GPU runs, hidden windows, and the simulated-GPU backend - The proof rig for anything visual is the native GPU backend (Metal on macOS) in an owned `--remote` instance. `MAKEPAD_HIDE_WINDOWS=1` keeps its windows off the user's screen; grabs and input still work because `/g` forces a present on a hidden window. Measured on macOS (2026-09-10): the first hidden `/g` answers in ~100 ms, later ones in ~2 s each, and `/gseq` delivers at that ~2 s spacing regardless of `every_ms`; on an app that is not redrawing, `/gseq` can time out ("grab timeout"). Rest/settle timing proofs need a window that presents on its own (the user's, or a visible unfocused one). - `MAKEPAD=gpusim` builds the simulated-GPU backend (`cfg(gpusim)`, `platform/src/os/gpusim/`): a CPU raster that writes frames to files with no window, no Metal shader compile and no presentation. It is for logic and data-structure tests only. Never use it to prove a picture or to chase a rendering bug, and never build it into the shared `target/` (`CARGO_TARGET_DIR=target-gpusim`). - Known remote hazards: a hidden-window click is occasionally lost (`Event::MouseDown` never arrives) — relaunch before debugging the widget; tick-sampled keys need `/k?k=down` … ≥150 ms … `/k?k=up`, a `press` lands between ticks; in the code map every `/g` drops keyboard focus, click the map before the next key batch; `MAKEPAD_HIDE_WINDOWS` is implemented only on macOS. Test instances of apps with audio or a shared home run with `SANDBOX_MUTE=1` and their own `SANDBOX_HOME` / `--state-dir`. ## Coordinate and lifecycle details ### Sharing an instance with a person Read `/activity` before driving an app. `/s` includes the same `activity` object. It reports `user_active`, the monotonic `user_seq`, `idle_ms`, the two-second `quiet_ms`, whether input is `held`, and the last input's kind and window. It never exposes typed text, key values or pointer coordinates. Only native input advances the counter; HTTP and Studio injections are marked remote, including hardware-path mouse injection and file drops. Focus, layout and paint notifications do not count as interaction. Carry `if_user_seq=N` from the beginning of an automation sequence on every mutating request. A request without it is gated by the quiet period alone; with it, the request is also refused once the person has interacted since N. Input, window changes, closing/quitting, tweaker mutations, AI overlay mutations and shader patches return **HTTP 409** if the person is active or the counter differs. This check happens on the UI thread immediately before dispatch. Queued mutations that outlive their request deadline expire. Read-only status, snapshots, logs and grabs remain available. User input does not revoke authorization to test or restart an app in the active development workflow (user instruction, 2026-09-22). If input interrupts a sequence, discard its interrupted evidence, inspect the reply's `applied` flag and current state, then read a fresh counter and continue without requesting a handoff. Wait for the protocol's quiet period when it refuses an input request; do not replay an already-applied toggle or edit blindly. Graceful close and replacement launches remain authorized. `/gq` also rechecks its counter after its grabs; if it refuses to quit, inspect the state and retry with a fresh counter. These permissions apply to the current workflow's app, not unrelated user instances. Every HTTP response, including raw PNGs, carries `X-Makepad-User-Seq-Start` and `X-Makepad-User-Seq`. A changed counter means the person interacted during the request; compare the ending counter with the sequence's original counter before attributing a capture to your test. A `wait=1` command interrupted before its frame acknowledgment returns 409 with `applied:true`; its action already ran, so retrying it could duplicate an effect. A command refused before dispatch returns `applied:false`. - Rectangles are layout points, window-local, with Y increasing downward. Window `sz` is logical size; `px` is physical pixels. No DPI arithmetic is needed to turn a widget rectangle into a click. - Window ids are stable slots. A human-closed window reports `window N closed by user`; its closure is not a crash. Launch a replacement when needed for the active workflow, unless the user has asked to stop. - Remote input is injected through the app event loop and does not need OS focus. Remote windows have a `[remote]` title suffix by default; `--remote-title-tag=NAME` customizes it. - Grabs read the app's own drawable. Files are stored under the startup grab directory, with a bounded retained history per window. - Backend support varies. Inspect the current implementation and response rather than assuming every platform supports readback. - The Studio websocket protocol remains an internal implementation detail; agent inspection uses the standalone HTTP surface. The existing [remote smoke script](../../tools/remote_smoke.sh) exercises the protocol across example apps. Inspect its current launch/setup behavior before using it for a task. For design feedback and styling, see [Tweaker](tweaker.md).