10 KiB
App remote control
Use this reference when inspecting or driving a Makepad app. Ownership, focus, and capture policy live in AGENTS.md.
The implementation is 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:
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:
[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:
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:
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.
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
--remoteinstance.MAKEPAD_HIDE_WINDOWS=1keeps its windows off the user's screen; grabs and input still work because/gforces a present on a hidden window. Measured on macOS (2026-09-10): the first hidden/ganswers in ~100 ms, later ones in ~2 s each, and/gseqdelivers at that ~2 s spacing regardless ofevery_ms; on an app that is not redrawing,/gseqcan 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=gpusimbuilds 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 sharedtarget/(CARGO_TARGET_DIR=target-gpusim).- Known remote hazards: a hidden-window click is occasionally lost
(
Event::MouseDownnever arrives) — relaunch before debugging the widget; tick-sampled keys need/k?k=down… ≥150 ms …/k?k=up, apresslands between ticks; in the code map every/gdrops keyboard focus, click the map before the next key batch;MAKEPAD_HIDE_WINDOWSis implemented only on macOS. Test instances of apps with audio or a shared home run withSANDBOX_MUTE=1and their ownSANDBOX_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
szis logical size;pxis 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=NAMEcustomizes 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 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.