Official plugins
Automation
Drive a running app from another process, as end-to-end test runners and agents do: its windows, elements, input, menus and screenshots, and mygo-automation, its command line client for agents.
The automation plugin (plugins/automation) lets another process drive a
running app: an end-to-end test runner or an agent lists its windows,
reads what their native UI shows as elements, acts on them, sends
pointer, wheel, key and text input, chooses items of the application
menu, evaluates scripts in pages, captures windows as PNG images, and
waits for them to settle. It speaks one protocol on macOS, Linux and
Windows: newline-delimited JSON over a Unix domain socket.
Elements are the ones MyGo gives assistive technology, so a client sees
what a screen reader sees and acts as one does, with the IDs of the
engine and the Keys the app gave elements. ui.Tester
remains the way to test a view inside go test; this plugin is for the
built app, from outside.
Clients speak the protocol themselves, or run
mygo-automation, its command line
client for agents.
Turning it on
An app opts in by importing the package. Keep it out of the app's release with a build tag, in a file of its own:
//go:build e2e
package main
import _ "github.com/egoist/mygo/plugins/automation"go build -tags e2e -o app .
MYGO_AUTOMATION=/tmp/app.sock ./appIt listens only when MYGO_AUTOMATION names the path of the socket, and
does nothing otherwise. Builds without the inspector of native UI leave
it out, as they do the inspector: go build -tags mygo_noinspector and
the release builds of mygo build, unless -debug or
MYGO_INSPECTOR=1 keeps the inspector. Such a build that
imports it ignores MYGO_AUTOMATION, and says so on its standard error.
A socket left at the path by an app that ended without closing it is replaced; one that another app listens on is not, nor a file that is no socket. The app removes its socket when it quits.
The protocol
A client connects to the socket and writes requests, one JSON object per
line. The app answers each on a line of its own, in order, each once the
one before it is answered; clients that want requests to run side by
side, as idle with one window while another acts, open more
connections.
{"id": 1, "method": "tree", "params": {"window": 1}}id is any JSON value, which the answer repeats. An answer has a
result, or an error with a code to test and a message to read:
{"id": 1, "result": {"window": 1, "nodes": [...]}}
{"id": 2, "error": {"code": "no_window", "message": "no window 7"}}| Code | Meaning |
|---|---|
parse_error |
The line is not a JSON object (its id is then null). |
invalid_request |
No method, params the method does not take, or values it cannot use. |
unknown_method |
No such method. |
no_window |
No window has the ID, or the app has none. |
no_element |
No element of the window's last frame has the ID or the key. |
unsupported |
The window cannot do it, as reading the elements of a page. |
timeout |
idle gave up after its timeout. |
failed |
The app could not do it, as a script that threw. |
Every method but windows takes a window, the ID windows lists; left
out, it is the focused window, else the first. Coordinates and sizes are
in DIPs, relative to the window's content, as the elements' bounds are.
| Method | Params | Result |
|---|---|---|
windows |
{windows: [{id, title, kind, url?, bounds, contentBounds, focused, visible}]} |
|
tree |
window |
{window, focus?, nodes: [...], announcements?} |
act |
window, id or key, action, text?, from?, to? |
{} |
input |
window, events |
{} |
menu |
window, path |
{} |
capture |
window |
{png, width, height} |
eval |
window, script |
{value} |
idle |
window, timeout? |
{} |
windows
Lists the open windows. kind is content for a window of native UI and
page for one showing a web page, whose url it gives; bounds is the
window's frame on screen and contentBounds its content's, each {x, y, width, height}.
{"id": 1, "method": "windows"}
{"id": 1, "result": {"windows": [{"id": 1, "title": "Counter", "kind": "content", "bounds": {"x": 440, "y": 362, "width": 400, "height": 300}, "contentBounds": {"x": 440, "y": 387, "width": 400, "height": 275}, "focused": true, "visible": true}]}}tree
The elements of the window's last frame, in tree order, a parent before its children:
{"id": 2, "method": "tree", "params": {"window": 1}}
{"id": 2, "result": {"window": 1, "focus": "5878896818624359798", "nodes": [
{"id": "15956256791619451359", "role": "text", "label": "Count: 0", "bounds": {"x": 20, "y": 20, "width": 360, "height": 18}},
{"id": "5424175759328386547", "role": "button", "key": "add", "label": "Add", "bounds": {"x": 20, "y": 48, "width": 360, "height": 32}, "states": ["focusable"], "actions": ["press", "focus"]},
{"id": "5878896818624359798", "role": "textField", "key": "name", "label": "Name", "value": "Ada", "bounds": {"x": 20, "y": 90, "width": 360, "height": 32}, "states": ["focusable"], "actions": ["focus", "setValue"], "selection": [3, 3]}
]}}A node has its id, the parent's ID unless it is at the top, its
role, and those of key, label, value, placeholder,
description, bounds, states, actions, min, max, now and
step (of sliders, progress bars, meters and steppers), selection (of
text fields, in runes), level, posInSet, setSize, rowCount,
colCount, rowIndex and colIndex (the size of a table or a grid,
and the place of a row or a cell in it from 1, as the app gave them:
ARIA's aria-rowcount and the like), rowSpan and colSpan (how many
rows and columns a cell covers, when more than one: aria-rowspan and
aria-colspan), current
(what it is the current one of, as ARIA's aria-current: true,
page, step, location, date or time), autoComplete (what
a text field's or a combo box's suggestions complete, as ARIA's
aria-autocomplete: inline, list or both), roleDescription (as
aria-roledescription), hasPopup (the kind of popup it opens, as
aria-haspopup: menu, listbox, tree, grid or dialog),
controls (the IDs of the elements it controls, as aria-controls),
describedBy (the IDs of the elements describing it, as
aria-describedby) and errorMessage (those holding its error while it
is invalid, as aria-errormessage) that it has.
IDs are strings of digits: they are 64-bit, which JavaScript's numbers
would round. keyShortcuts is the keys that press or focus the element,
as ARIA's aria-keyshortcuts, written as the platform's menus write
them (⌘K on macOS, Ctrl+K elsewhere) and separated by spaces.
key is the element's Key, given with Element.Key or, for widgets
that take theirs before they are made, Context.Key: a string, a
boolean or an integer as itself, another value as Go prints it. Unlike
an ID, which a frame may change as the view changes, it is the app's
own name for the element, made to last; give the elements tests look
for keys of their own:
c.Key("name")
ui.TextInput(c, &name).Label("Name")
ui.Button(c, "Add").Key("add")Only the elements assistive technology reads are in the tree: a row or
a column that lays others out has no node, keyed or not. Roles are
group, text, button, link, checkBox, radio, switch,
slider, progress, textField, image, list, scroll, dialog,
popup, tooltip, popUpButton, tabList, tab, splitter,
status, table, row, cell, columnHeader, tree, treeItem,
listItem, menuButton, toolbar, radioGroup, toggleButton,
comboBox, disclosure, meter, stepper, colorWell,
alertDialog, menu, menuBar, menuItem, menuItemCheckBox,
menuItemRadio, heading, region, log, timer, alert,
separator, rowHeader, search, article, navigation, grid,
treeGrid, deletion, insertion, tabPanel, main, banner, contentInfo and
complementary. States are focusable, disabled,
checked, mixed, selected, expanded, multiline, password,
readOnly, selectable, offscreen, multiselectable, segment,
search, invalid, sortAscending, sortDescending, expandable,
vertical, busy, sortOther and horizontal.
A web view in native UI shows as a node marked webView, without the
elements of its page. A window showing a web page has no tree: tree
answers unsupported, and clients read the page with eval.
act
Performs an action on an element, as assistive technology does: the
element is found by its id, or by its key when exactly one element
has it.
{"id": 3, "method": "act", "params": {"window": 1, "key": "add", "action": "press"}}
{"id": 4, "method": "act", "params": {"window": 1, "key": "name", "action": "setValue", "text": "Grace"}}| Action | Does |
|---|---|
press |
Clicks a button, checks a check box, opens a menu button. |
focus |
Gives the element the keyboard focus. |
increment, decrement |
Steps a slider or a stepper. |
setValue |
Replaces a text field's text with text. |
insertText |
Types text at a text field's caret. |
selectText |
Selects runes from to to of a text field. |
scrollIntoView |
Scrolls the containers around the element to show it. |
expand, collapse |
Opens or closes an item of a tree. |
input
Sends events to the window's content through the path the system's take, in order:
{"id": 5, "method": "input", "params": {"window": 1, "events": [
{"type": "click", "x": 200, "y": 99},
{"type": "text", "text": "Grace"},
{"type": "key", "key": "CmdOrCtrl+A"}
]}}| Type | Fields |
|---|---|
pointerMove |
x, y, mods? |
pointerDown, pointerUp |
x, y, button?, clicks?, mods? |
click |
x, y, button?, clicks?, mods?: a move, then a press and a release for each click |
wheel |
x, y, dx, dy, precise?, mods?: scrolls by dx, dy DIPs, as a touchpad when precise |
keyDown, keyUp, key |
key: a key and its modifiers as menu accelerators write them, as Enter, Shift+Tab or CmdOrCtrl+Z; key presses and releases it |
text |
text: text typed, or committed by an input method |
button is left (the default), right or middle; clicks counts
the presses of a double or triple click; mods lists shift, ctrl,
alt, super and cmdOrCtrl. A key event types no text: send the text
with a text event, as backends send both for a key that types.
The content handles the events of a request one after the other, before
it draws its next frame, as it would events that came together. When an
event depends on what an earlier one showed, as typing into a field a
click focused, wait with idle in between. Input goes to windows of
native UI; pages answer unsupported, and clients drive them with
eval.
menu
Chooses an item of the application menu by the labels along its path, as
the menu bar does, and answers once the item's Click returned:
{"id": 6, "method": "menu", "params": {"path": ["File", "New Window"]}}On Linux and Windows it chooses from the window's menu bar, which shows
the application menu. The menus of the Dock and of tray icons, and open
context menus, are left out: the system shows a context menu in a loop
of its own, which the plugin cannot choose from on every platform, so
don't open one with a right click through input. Test them with
ui.Tester, whose RightClick and ChooseMenuItem
open and choose from the view's context menus.
capture
The window as a PNG image, in base64, width × height device pixels:
native UI rendered as it is, or a page's screenshot.
eval
Evaluates JavaScript in the page of a window showing one, as Page.Eval
does: an expression's value, with promises awaited, or what statements
return.
{"id": 7, "method": "eval", "params": {"window": 2, "script": "document.title"}}
{"id": 7, "result": {"value": "Todo"}}idle
Answers once the window settled, so that clients wait for the UI rather
than sleep: for native UI, once the content asks for no frame it has not
drawn, as Tester.FramePending tells; for a page, once it has loaded and
answered a script. It waits for nothing that Context.After or
Painter.After asked for later, as a tester's frames don't. Content that
never settles, as an indicator that keeps moving, never answers: give
timeout, in milliseconds, to have it fail with timeout instead.
{"id": 8, "method": "idle", "params": {"window": 1, "timeout": 5000}}Windows out of sight
A window none of which shows, as a hidden or minimized one, one that
other windows cover, or any while the screen is locked, holds its frames
until it shows, and its surface may draw none before then: macOS draws
none for a hidden window, nor for any window while the screen is locked.
There, requests build the frame the window waits for themselves: act
and input take effect at once, as a press does in the frame that
builds, and tree, capture and idle tell what the window shows now,
with idle waiting for nothing the window would draw once it shows.
A client
Any language that opens Unix domain sockets can be a client. A test in Go:
conn, err := net.Dial("unix", "/tmp/app.sock")
if err != nil {
t.Fatal(err)
}
r := bufio.NewReader(conn)
call := func(method string, params any) json.RawMessage {
line, _ := json.Marshal(map[string]any{"id": 1, "method": method, "params": params})
conn.Write(append(line, '\n'))
answer, _ := r.ReadBytes('\n')
var res struct {
Result json.RawMessage
Error *struct{ Code, Message string }
}
json.Unmarshal(answer, &res)
if res.Error != nil {
t.Fatalf("%s: %s", method, res.Error.Message)
}
return res.Result
}
call("act", map[string]any{"key": "add", "action": "press"})
call("idle", nil)
tree := call("tree", nil)and in a shell, with socat:
echo '{"id": 1, "method": "windows"}' | socat - UNIX-CONNECT:/tmp/app.sockThe command line: mygo-automation
An AI coding agent driving the app it develops wants to look at the UI
and act on it with short commands, and to read short answers: not to
write a socket client of its own, nor to read whole JSON trees and
20-digit element IDs into its context. mygo-automation is that client,
over the same socket:
mygo-automation open --socket /tmp/app.sock
mygo-automation snapshot -i
# @e2 [button] "Add" key=add
# @e3 [textField] "Name" key=name
mygo-automation press @e2 --settle
# - @e1 [text] "Count: 0"
# + @e1 [text] "Count: 1"
# = @e2 [button] "Add" key=add
mygo-automation fill @e3 "Ada" --settle
mygo-automation screenshot /tmp/app.png
mygo-automation closesnapshot prints a window's elements a line each: a short ref (@e1),
the role, label, value, key and states, leaving out what is empty;
-i keeps the controls, --json prints the protocol's answer. Actions
take a ref, key=<key> or id=<digits>, and cover the protocol's
methods: press, fill and the other actions, click, pointer,
wheel, type, key, menu, eval, wait, screenshot (to a file)
and windows. With --settle, an action waits for the window to settle,
with idle and a timeout, and prints what changed since the last
snapshot: + and - for elements that came and went, or changed, =
for the elements around them, with their refs.
A session (--session, or MYGO_AUTOMATION_SESSION) keeps its socket,
window and refs in a file of its own under the user's cache directory,
so agents side by side don't change each other's refs. A ref lasts while
its window shows its element, and is never given to another element: one
whose element went is refused as stale. close ends the session and
leaves the app running.
mygo-automation skills prints its guide, a SKILL.md built into the
command: the commands, sessions, selectors, refs, settling and diffs,
limits and errors of the version installed, and nothing else.
Installing
The command is in MyGo's module. In the app's module directory, go install builds the version of MyGo the app's go.mod requires, so that
the command, and its guide, match the app:
go install github.com/egoist/mygo/plugins/automation/cmd/mygo-automationElsewhere, name that version, which go list -m github.com/egoist/mygo
prints in the app's module:
go install github.com/egoist/mygo/plugins/automation/cmd/mygo-automation@vX.Y.Zmygo-automation version prints the version it was built from. Upgrading
the app's MyGo does not upgrade the installed command: install it again
along with the upgrade, and the guide comes with it. It needs only Go's
standard library, and runs on macOS, Linux and Windows, as the plugin.
A skill for agents
Since the guide comes from the command, an agent's configuration only
needs to say that it exists. Keep this stub as a skill: in
.claude/skills/mygo-automation/SKILL.md for Claude Code, in
.agents/skills/mygo-automation/SKILL.md for Codex and the agents that
read skills there:
---
name: mygo-automation
description: Use when reading a MyGo app's native UI elements and acting on them, sending pointer, wheel, key and text input, choosing items of the application menu, evaluating scripts in pages, or taking screenshots of a running MyGo app.
---
This file is a discovery stub, not the usage guide. Before running a mygo-automation command, load the actual workflow from the CLI:
```bash
mygo-automation skills # start here
```
Do not probe first with --help or --version. Only if the command is missing, install it from the app's module directory with `go install github.com/egoist/mygo/plugins/automation/cmd/mygo-automation`.Security
Whoever opens the socket drives the app as its user would, and runs
scripts in its pages: keep the plugin out of the builds you ship.
mygo-automation is a client of the socket like any other: it adds no
listener, and reaches only apps that listen. It is
opt-in twice: an app links it only by importing it, which a build tag
keeps to test builds, and listens only when MYGO_AUTOMATION is set.
Builds without the inspector leave it out whatever they import.
The socket is the user's alone: on macOS and Linux it is made with mode
0600, and on Windows, where a socket's file has no mode, it takes the
access control list of its directory, which is the user's alone in the
user's profile and temporary directory. Put it in a directory only the
user can write to, not one shared with others.
Platforms
Windows need Windows 10 1803 or later, which has Unix domain sockets.
Browsers (GOOS=js) have no sockets: the plugin compiles there, and
listens nowhere.