Agent API
WebSocket API reference for programmatic control of lattices
Lattices 0.15.1 and later serve the agent API over authenticated WebSocket on
~/.lattices/run/lattices.sock. The same app hosts voice input on
~/.lattices/run/voice.sock (see Voice).
~120 RPC methods and real-time events. Prefer desktop.snapshot when you
need to know what is in front of the user, and lats map when you want
that same state drawn as ASCII.
Quick start
- Launch the server (it starts with the menu bar app):
lats app- Check that it's running:
lats daemon status- Call a method from Node.js:
import { daemonCall } from '@lattices/lattices'
const windows = await daemonCall('windows.list')
console.log(windows) // [{ wid, app, title, frame, ... }, ...]From another language, use a WebSocket client that supports Unix sockets and the authentication headers below.
Private connection
Install the Lattices app and CLI at version 0.15.2 together. Older clients that connect directly to TCP must migrate to the private transport.
- The daemon listens on a private Unix socket,
~/.lattices/run/lattices.sock, and writes a capability token to~/.lattices/run/lattices.token. The directory is mode0700; both files are mode0600. - Directory and token permissions restrict local access to the same user.
The macOS server also checks the peer's user ID. The WebSocket upgrade must
use
Host: localhost, carry the token inx-lattices-token, and send noOriginheader. latsanddaemonCall()find the socket and token for you. Code above keeps working. A plainwebsocat ws://127.0.0.1:9399does not: no TCP port is open by default. The macOS app can enable loopback TCP withLATTICES_ALLOW_LOOPBACK_TCP=1; that port still requires the token. The Linux host has no TCP compatibility listener.- The shared client sends local tokens over private IPC by default. An explicit
TCP target, including a loopback address that could be an SSH tunnel, receives
no local token unless loopback compatibility is explicitly enabled.
The CLI and Pi client require
LATTICES_ALLOW_LOOPBACK_TCP=1; programmaticdaemonCallTo()callers can deliberately supplytokenFilefor a loopback endpoint. Non-loopback targets never receive local tokens. - Remote clients reach a host only through the opt-in tailnet gateway, after a person on that host approves the pairing. See Remote hosts.
See Private IPC and the gateway for every product's socket.
Wire protocol
lattices uses a JSON-RPC-style protocol over authenticated WebSocket on the private socket.
Request
{
"id": "unique-string",
"method": "windows.list",
"params": { "wid": 1234 }
}| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Caller-chosen ID, echoed in response |
method |
string | yes | Method name (see below) |
params |
object | no | Method-specific parameters |
Response
{
"id": "unique-string",
"result": [ ... ],
"error": null
}| Field | Type | Description |
|---|---|---|
id |
string | Echoed from request |
result |
any | null | Method return value (null on error) |
error |
string | null | Error message (null on success) |
Errors
| Error | Meaning |
|---|---|
| Unknown method | The method string is not recognized |
| Missing parameter | A required param was not provided |
| Not found | The referenced resource doesn't exist |
Method names and aliases
Methods are named <domain>.<verb>, with plural domains: windows.place,
sessions.launch, layers.switch (LAT-012).
The pre-LAT-012 names still work as aliases, and api.schema lists them under
aliases (old name → new name):
| Old | New |
|---|---|
window.focus/move/place/present/resolve |
windows.focus/move/place/present/resolve |
window.pick.start |
windows.pick |
window.tile |
windows.place (position becomes placement) |
layer.activate, layer.switch |
layers.activate, layers.switch |
space.optimize |
spaces.optimize |
session.* |
sessions.* |
group.launch/kill |
groups.launch/kill |
tabStacks.list/add/select/layout |
tabs.list/add/select/layout |
tabStacks.create, tabStacks.delete |
tabs.stack, tabs.unstack |
lattices.search |
search.query |
focus.status/enter/exit/toggle (Focus Mode) |
solo.status/enter/exit/toggle |
tmux.sessions, tmux.inventory |
tmux.list, tmux.list({ includeOrphans: true }) |
ocr.recent |
ocr.history without wid |
intents.execute |
intents.run |
actions.history, actions.undo |
history.list, history.undo |
Action receipts keep their own type values (window.place, window.move);
those name the action, not the RPC method. The Node.js daemonCall() client
and LatticesKit retry a new name under its old one when an older daemon
answers Unknown method.
Connection lifecycle
- The server starts when the menu bar app launches and stops when it quits.
- Every connection requires the local token at the WebSocket handshake; see Private connection.
- The Node.js
daemonCall()client opens a fresh connection per call and closes it when the response arrives. For event subscriptions, hold the connection open (see Reactive event pattern). - If the server restarts (e.g. after
lats app restart), existing connections are dropped. Clients should reconnect and treat it as stateless. There is no session resumption.
Remote hosts
In 0.15.1, neither the macOS app nor lattices-host opens a remote listener.
Remote access uses the separate gateway, which the host user starts explicitly:
# On the host
lats gateway start --tailnet
# On the client: request access, then wait for approval on the host
lats --host HOST pair --read-only
# A viewer also needs screen contents (screenshots, streams, OCR):
lats --host HOST pair --read-only --screen
# Or request exactly these methods instead of the read-only scope:
lats --host HOST pair --grant lattices:windows.list --grant speech:speech.enqueue
# On the host: check the fingerprint and grants, then approve
lats gateway clients
lats gateway approve CLIENT_IDThe gateway binds only the host's Tailscale address, on port 9399 by default.
Core method names do not change. Methods of other products go through
gateway.call with { "product": "speech", "method": "speech.enqueue", "params": { "text": "Build finished" } }.
Each grant names one exact product:method, and only reviewed methods can be
granted. Re-pairing with the same client key adds approved grants to the existing
set. To reduce access, revoke the client and pair again. --grant cannot be
combined with --read-only, --drive, --screen or --shell.
Core scopes are read, screen (screenshots, streams, OCR, terminal
contents), act, drive (desktop input) and shell (typing text and
launching commands). --read-only requests read, plain pair requests
read, screen and act, and --drive requests all five. Stored grants are
exact methods, so clients paired before screen and shell were split out keep
what they had. Clients paired directly with a host before this
change must pair again with the gateway.
The native Machines/Hosts view reuses this CLI gateway pairing and signs its requests. Pairing a companion device alone does not authorize remote host status or screens. The Pi extension and Swift SDK do not add gateway signatures to explicit remote endpoints; a host or URL override alone is not a paired gateway client.
Remote core event subscriptions are limited to windows.changed,
spaces.changed, layers.changed and host.healthChanged. A client needs
lattices:events.subscribe and the corresponding read grant (windows.list,
spaces.list, layers.list or daemon.status). Unsubscribing requires
lattices:events.unsubscribe. Capture stream lifecycle and activity events go
directly to the connection that owns the stream; see capture.stream.* below.
Other event streams stay local.
Node.js client
lattices ships a zero-dependency WebSocket client that works with Node.js 18+. It handles connection, framing, and request/response matching internally.
daemonCall(method, params?, timeoutMs?)
Send an RPC call and await the response.
import { daemonCall } from '@lattices/lattices'
// Read-only
const status = await daemonCall('daemon.status')
const windows = await daemonCall('windows.list')
const win = await daemonCall('windows.get', { wid: 1234 })
// Mutations
await daemonCall('sessions.launch', { path: '/Users/you/dev/myapp' })
await daemonCall('windows.place', {
session: 'myapp-a1b2c3',
placement: { kind: 'tile', value: 'left' }
})
// Custom timeout (default: 3000ms)
await daemonCall('projects.scan', null, 10000)Returns the result field from the response.
Throws if the server returns an error, the connection fails, or the timeout is reached.
isDaemonRunning()
Check if the server is reachable.
import { isDaemonRunning } from '@lattices/lattices'
if (await isDaemonRunning()) {
console.log('daemon is up')
}Returns true if daemon.status responds within 1 second.
TypeScript SDK facade
The CLI is a human/debug surface. Product code and agents should prefer the typed SDK facade, which validates params with dependency-free schemas and calls the same daemon methods directly.
import { cua } from '@lattices/lattices/cua'
await cua.magicCursor({
app: 'Scout',
xRatio: 0.52,
yRatio: 0.91,
text: 'What are the most important docs in this project, and what would an agent say after reading them?',
treatment: 'execute',
trail: 'comet',
motion: 'rush',
trajectory: 'overshoot',
glow: 'halo',
idle: 'wiggle',
edge: 'ripple',
})
await cua.click({
app: 'Scout',
xRatio: 0.74,
yRatio: 0.95,
transport: 'ax',
axLabel: 'Send',
noFocus: true,
treatment: 'execute',
})@lattices/lattices/cua exposes this typed CUA module. Existing imports from
@arach/lattices/cua and @lattices/cli/cua remain supported.
Error handling
daemonCall throws on errors — always wrap calls in try/catch:
import { daemonCall } from '@lattices/lattices'
try {
await daemonCall('sessions.launch', { path: '/nonexistent' })
} catch (err) {
// err.message is one of:
// "Not found" — resource doesn't exist
// "Missing parameter: ..." — required param missing
// "Unknown method: ..." — bad method name
// "Daemon request timed out" — no response within timeout
// ECONNREFUSED — daemon not running
console.error('Daemon error:', err.message)
}Runs And Capture
Runs are local executions that produce trace events and artifacts. Capture
methods write into the Lattices run store under
~/Library/Application Support/Lattices/Runs/.
| Method | Type | Description |
|---|---|---|
runs.create |
write | Create a run record and artifact directory |
runs.list |
read | List recent runs |
runs.get |
read | Inspect one run, including artifacts and trace events |
runs.artifacts |
read | List artifacts for one run |
capture.screenshotWindow |
write | Capture a window screenshot as a run artifact |
capture.screenshotDisplay |
write | Capture one complete display as a run artifact and optional clipboard image |
capture.still |
read | One inline JPEG of a display, window or region for remote viewers; nothing is saved |
capture.stream.start |
read | Stream a display, window or region as H.264 binary frames on this connection |
capture.stream.ack |
read | Acknowledge frames up to seq; the host keeps at most 4 unacknowledged |
capture.stream.keyframe |
read | Make the stream's next frame a keyframe |
capture.stream.stop |
read | Stop a stream |
capture.screenshotRegion |
write | Capture a screen region as a run artifact |
capture.zoomArtifact |
write | Crop and zoom an image artifact into a linked artifact |
vision.analyzeWindow |
write | Capture a window and run local OCR-based visual analysis |
vision.analyzeArtifact |
write | Analyze an existing image artifact or path with local OCR |
computer.prepare |
write | Resolve and optionally capture a terminal target without mutating it |
computer.windowState |
write | Inspect a target window's AX tree and optionally write screenshot/run artifacts |
computer.elementAction |
write | Stage or execute an AX action against a snapshot element id |
computer.typeElement |
write | Stage or execute AXValue text insertion against a snapshot element id |
computer.setValue |
write | Alias for replacing a snapshot element's AXValue |
computer.pressKey |
write | Stage or execute one key press against an explicit target window |
computer.hotkey |
write | Stage or execute a keyboard shortcut against an explicit target window |
computer.focusWindow |
write | Resolve, capture, focus, and verify a target window |
computer.showCursor |
write | Show a visible cursor appearance and record it as a run |
computer.launchApp |
write | Launch or focus a normal macOS app and record the run |
computer.typeWindowText |
write | Type or paste into a normal app window, optionally after a click |
computer.click |
write | Stage or execute a window-relative click target; prefers no-focus AXPress in auto/ax transport |
computer.doubleClick |
write | Stage or execute a pointer double-click target |
computer.rightClick |
write | Stage or execute a right-click/context-click target |
computer.scroll |
write | Stage or execute scroll wheel input |
computer.drag |
write | Stage or execute a pointer drag between two points |
computer.verify |
write | Verify OCR, AX, or artifact-change expectations |
computer.demoScout |
write | Scout warm-up run for memo/demo recording |
computer.typeText |
write | Insert text into a safe terminal using the least intrusive transport |
computer.demoTerminal |
write | Compatibility wrapper for a bounded terminal text action |
browser.getText |
read | Read visible browser text through Accessibility |
browser.queryDom |
read | Query DOM summaries with explicit browser automation consent |
browser.executeJavascript |
write | Stage or execute browser JavaScript with explicit consent |
capture.screenshotWindow
Capture a window as a PNG artifact. If no target is provided, Lattices captures the frontmost non-Lattices window.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target CGWindowID |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title filter for app, or run title |
runId |
string | no | Existing run to append to |
source |
string | no | Calling surface label |
filename |
string | no | Optional artifact filename |
await daemonCall('capture.screenshotWindow', { source: 'agent' })
await daemonCall('capture.screenshotWindow', {
session: 'frontend-a1b2c3',
filename: 'before-layout.png'
})capture.still
Return one compressed frame inline. Other machines pull these one at a time to show this Mac in their remote viewer. Nothing is written to disk or the clipboard. Pass a window or a region, or neither for a display.
| Field | Type | Required | Description |
|---|---|---|---|
displayIndex |
int | no | Display to capture; defaults to the display under the pointer |
wid |
int | no | Window to capture instead of a display |
x, y, width, height |
double | no | Region to capture, in layout points |
maxWidth |
int | no | Largest pixel width to return; defaults to 1600 |
quality |
int | no | JPEG quality from 20 to 95; defaults to 70 |
The result has format, width, height, base64 data, and frame: the
captured rect in the same coordinates that computer.* input takes.
capture.stream.*
Push H.264 of a display, window or region as binary WebSocket messages on the
connection that asked, while a viewer is on screen. capture.still stays for
thumbnails, idle views and hosts without streaming. host.describe lists
capture.stream only when the host can stream now.
capture.stream.start takes the same target fields as capture.still, plus:
| Field | Type | Required | Description |
|---|---|---|---|
maxWidth |
int | no | Largest coded width, 320 to 3840; defaults to 1920 |
fps |
int | no | Frame rate cap, 5 to 60; defaults to 30 |
codec |
string | no | h264, the only codec required |
The result is {streamId, codec, width, height, frame:{x,y,w,h}, display}.
display is {index, name} for a display target and null otherwise. Hosts
that report capture activity also include activity, as described below.
capture.stream.ack {streamId, seq}acknowledges every frame up toseq. Send one per frame received. Hosts limit the number of frames in flight. With frames outstanding and no ack for 3 s, the host ends the stream. An ack for an oldseqchanges nothing and still answers{ok:true}, so viewers use it to check that a quiet stream is alive. Hosts that report capture activity include the currentactivityin successful ACK responses.capture.stream.keyframe {streamId}makes the next frame an IDR.capture.stream.stop {streamId}stops the stream without an event.- Streams belong to their connection. Only that connection can ack, request a keyframe or stop them, and they all stop when it closes.
- When the host ends a stream itself, it sends
{event:"capture.stream.ended", data:{streamId, reason}}to the owning connection, whatever it subscribed to. Reasons areack-timeout,window-closed,encoder-failed: …and, on a Mac,capture-stopped: ….
Host activity and variable frame delivery
Current macOS source builds suppress unchanged pictures before H.264 encoding.
The host compares the visible pixels of complete capture frames and accepts
ScreenCaptureKit's explicit unchanged-frame status as idle evidence. After at
least one second without a picture change, with unchanged-frame evidence, the
host reports idle. A pixel or capture-geometry change reports active
immediately and resumes frame delivery, subject to the ACK limit.
The capture rate remains at the requested fps ceiling so the host can observe
new changes promptly. The encoded video rate can fall to zero while the picture
is unchanged. The viewer holds the last picture. A requested recovery keyframe
can still send the same picture without changing the activity state.
Supporting hosts include this optional object in start and successful ACK responses:
{"activity":{"state":"idle","source":"host","activeFPS":30,"idleFPS":0,"revision":2}}state is active, idle or unknown. unknown means the host does not have
current evidence of picture activity, including before the first frame or when
capture is blank or suspended. activeFPS is the configured frame rate ceiling;
idleFPS is the intended unchanged-picture rate, currently zero. Neither field
is a measurement of received frames.
revision is a nonnegative integer, starting at zero for each stream and
increasing on each state change. Viewers ignore activity reports with an older
revision so a delayed ACK cannot overwrite a newer activity event.
When the state changes, the host sends
{event:"capture.stream.activity", data:{streamId, activity}} only to the
connection that owns the stream. The gateway forwards this event for streams
owned by that viewer. No event subscription is required.
Older hosts and the current Linux host omit activity. Viewers must not infer
idle from missing frames, low received FPS or an omitted activity object.
Those hosts do not promise the macOS unchanged-picture suppression behavior.
Binary frames
Each binary message is one access unit, big-endian:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | "LVF1" |
| 4 | u32 | streamId |
| 8 | u32 | seq, per stream, from 1 |
| 12 | u8 | flags: bit 0 keyframe, bit 1 parameter sets follow |
| 13 | u8 | codec: 1 H.264, 2 HEVC |
| 14 | u16 | reserved, 0 |
| 16 | u64 | pts, microseconds on the host clock |
| 24 | u32 | width of the coded picture |
| 28 | u32 | height |
| 32 | 4 × f32 | frame x, y, w, h: the host-space rect the picture shows |
| 48 | if bit 1: u16 count, then u16 length + bytes for each parameter set (SPS, PPS; VPS first for HEVC) | |
| the access unit: NAL units, each with a u32 length prefix (AVCC, not Annex B) |
Every keyframe carries its parameter sets. There are no B-frames. The codec
lives in apps/mac/Sources/Core/Hosts/RemoteVideoFrame.swift and
packages/host-linux/src/stream.ts.
capture.screenshotDisplay
Capture one complete composited display as a PNG artifact without focusing a window or changing visible overlays. The image includes the menu bar, pointer by default, and Lattices-owned surfaces already visible on that display, which makes it suitable for snapshots of transient UI such as Hyper+G. When no display is supplied, Lattices captures the display under the pointer.
| Field | Type | Required | Description |
|---|---|---|---|
displayIndex |
int | no | Zero-based target from spaces.list; defaults to the display under the pointer |
display |
int | no | Alias for displayIndex |
cursor |
bool | no | Include the pointer; defaults to true |
clipboard |
bool | no | Copy PNG image data to the system clipboard; defaults to true for raw RPC calls |
title |
string | no | Optional run title |
runId |
string | no | Existing run to append to |
source |
string | no | Calling surface label |
filename |
string | no | Optional artifact filename |
await daemonCall('capture.screenshotDisplay', {
displayIndex: 0,
clipboard: true,
filename: 'hyper-g-layout.png',
source: 'agent'
})The CLI uses the positional index as display, always writes the run artifact,
and copies the PNG only when --clipboard is present. --delay is a CLI-side
wait measured in seconds; it is not an RPC parameter.
lats capture display 0 --clipboard --delay 3 --filename hyper-g-layout.pngcapture.screenshotRegion
Capture an explicit screen rectangle as a PNG artifact. If no rectangle is
provided, the endpoint resolves wid/session/app/frontmost target and
captures that window frame.
| Field | Type | Required | Description |
|---|---|---|---|
x, y |
double | no | Region origin in screen coordinates |
width, height |
double | no | Region size |
w, h |
double | no | Size aliases |
wid, session, app, title |
target | no | Target window fallback |
runId, source, filename |
mixed | no | Standard run/artifact fields |
await daemonCall('capture.screenshotRegion', {
x: 120,
y: 160,
width: 640,
height: 420,
filename: 'composer-region.png'
})capture.zoomArtifact
Crop and zoom an existing image artifact into a new PNG artifact on the same run. Coordinates are image pixels; ratio fields are relative to the source image.
| Field | Type | Required | Description |
|---|---|---|---|
runId |
string | no | Run containing the artifact |
artifactId |
string | no | Source artifact id |
path |
string | no | Source image path fallback |
x, y, width, height |
double | no | Crop rectangle in image pixels |
xRatio, yRatio, widthRatio, heightRatio |
double | no | Crop rectangle as image ratios |
scale |
double | no | Zoom scale, default 2, max 8 |
filename, source |
string | no | Output artifact metadata |
await daemonCall('capture.zoomArtifact', {
runId: 'run_...',
artifactId: 'art_...',
xRatio: 0.35,
yRatio: 0.7,
widthRatio: 0.3,
heightRatio: 0.2,
scale: 3
})vision.analyzeWindow / vision.analyzeArtifact
Run explicit local OCR-based visual analysis. These endpoints do not call a
cloud vision model; they use the same on-device Vision OCR path as Lattices OCR
and return fullText, text blocks, a short answer, and optional verified
when contains or notContains is supplied.
| Field | Type | Required | Description |
|---|---|---|---|
instruction |
string | yes | Question or analysis instruction |
wid, session, app, title |
target | no | Window target for vision.analyzeWindow |
runId, artifactId, path |
mixed | no | Artifact target for vision.analyzeArtifact |
contains, notContains |
string | no | Optional text expectation |
source |
string | no | Calling surface label |
await daemonCall('vision.analyzeWindow', {
app: 'Safari',
instruction: 'Read the visible page heading',
contains: 'Documentation'
})CLI:
lats capture window
lats capture display 0 --clipboard --delay 3
lats runs
lats runs run_20260617-120000_a1b2c3
lats terminals
lats terminals --refresh
lats computer prepare --text "# hello" --treatment stage
lats call computer.windowState '{"app":"Finder","maxDepth":4}'
lats call computer.pressKey '{"app":"Finder","key":"escape","treatment":"stage"}'
lats call computer.hotkey '{"app":"Xcode","shortcut":"command+b","treatment":"stage"}'
lats computer focus-window --wid 7258 --treatment present
lats computer cursor --style marker --shape chevron --angle-deg -8 --label typing
lats computer launch-app Scout
lats computer scout --treatment present
lats computer scout "Draft memo text" --execute
lats computer type-window --app Scout --text "Draft memo text" --x-ratio .5 --y-ratio .86 --execute
lats computer click --app Scout --x-ratio .5 --y-ratio .86 --execute
lats cua click --app Scout --x-ratio .74 --y-ratio .95 --transport ax --ax-label Send --execute
lats call computer.scroll '{"app":"Safari","direction":"down","amount":420,"treatment":"stage"}'
lats call computer.drag '{"app":"Finder","fromXRatio":0.2,"fromYRatio":0.2,"toXRatio":0.7,"toYRatio":0.7,"treatment":"stage"}'
lats call computer.doubleClick '{"app":"Finder","xRatio":0.5,"yRatio":0.5,"treatment":"stage"}'
lats call computer.rightClick '{"app":"Finder","xRatio":0.5,"yRatio":0.5,"treatment":"stage"}'
lats call computer.verify '{"app":"Safari","mode":"ocr","contains":"Docs"}'
lats call capture.screenshotRegion '{"app":"Safari","filename":"safari-region.png"}'
lats call vision.analyzeWindow '{"app":"Safari","instruction":"Read visible text"}'
lats call browser.getText '{"app":"Safari"}'
lats call browser.queryDom '{"app":"Safari","selector":"h1","allowAutomation":true}'
lats computer type-text --text "# hello from lattices"
lats computer demo-terminal --dry-runComputer Action Treatments
Computer-use endpoints accept a treatment field that controls how intrusive
the action may be:
| Treatment | Behavior |
|---|---|
observe |
Resolve target and record a run, without focus or input |
stage |
Resolve target and stage intent/artifacts, without focus or input |
present |
Focus or present the target, without input |
execute |
Perform the action after safety checks |
The legacy dryRun: true flag maps to stage.
Background-Safety Receipts
Computer-use action responses include a backgroundSafety receipt plus
top-level compatibility fields:
{
"backgroundSafety": {
"route": "ax.press",
"lane": "same_session",
"session": "parent",
"background_safe": true,
"backgroundSafe": true,
"cursor_moved": false,
"foreground_changed": false
}
}Agents should treat background_safe: true as the signal that the call stayed
out of the user's foreground session. AX, tmux, iTerm session, overlay, staged,
and observe routes can be background-safe. Pointer input, app activation,
window focus, global keyboard input, and foreground typing report
background_safe: false.
computer.prepare
Resolve and score terminal candidates for a future computer-use action. This is the least intrusive endpoint: by default it observes the target and captures an artifact, but it does not focus or type.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Specific terminal window id |
tty |
string | no | Specific terminal TTY |
app |
string | no | Preferred terminal app, such as iTerm2 |
text |
string | no | Text to stage in the run trace |
treatment |
string | no | observe, stage, present, or execute |
capture |
bool | no | Capture target screenshot artifact. Defaults to true |
source |
string | no | Calling surface label |
await daemonCall('computer.prepare', {
text: '# review before typing',
treatment: 'stage'
})computer.windowState
Inspect a target window's Accessibility tree and return snapshot-local element
ids (e1, e2, ...), a flat elements list, and a compact treeMarkdown
view. mode: "ax" avoids Screen Recording. Use mode: "both" or
capture: true when you also want a screenshot artifact linked to a run. The
endpoint is classified as a write because those capture modes create run
artifacts.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target window id |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app target |
mode |
string | no | ax (default), both, or screenshot |
capture |
bool | no | Capture a screenshot artifact. Defaults true for both/screenshot, false for ax |
maxDepth |
int | no | Maximum AX tree depth. Defaults to 8, max 14 |
maxElements |
int | no | Maximum elements returned. Defaults to 250, max 1000 |
timeoutMs |
int | no | Traversal timeout. Defaults to 1200, max 5000 |
source |
string | no | Calling surface label when capture creates a run |
await daemonCall('computer.windowState', {
app: 'Finder',
maxDepth: 4,
maxElements: 120
})
await daemonCall('computer.windowState', {
wid: 7258,
mode: 'both',
source: 'agent'
})computer.elementAction
Stage or execute an Accessibility action against an element returned by a recent
computer.windowState call. Snapshot element ids are intentionally local to the
daemon process and expire from the in-memory cache after a short window.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
snapshotId |
string | yes | Snapshot id returned by computer.windowState |
elementId |
string | yes | Snapshot-local element id, such as e4 |
action |
string | no | press (default), showMenu, or focus |
treatment |
string | no | stage, present, or execute. Defaults to stage |
backgroundSafe / noFocus |
bool | no | With execute, perform AX actions without focusing the app when the requested action can run in the background |
dryRun |
bool | no | Stage without performing the action |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
const state = await daemonCall('computer.windowState', {
app: 'Calculator',
maxDepth: 6
})
await daemonCall('computer.elementAction', {
snapshotId: state.snapshotId,
elementId: 'e7',
action: 'press',
treatment: 'execute',
backgroundSafe: true
})computer.typeElement / computer.setValue
Stage or execute text/value insertion against an element returned by a recent
computer.windowState call. computer.typeElement accepts text;
computer.setValue accepts value and otherwise uses the same behavior. In
stage, Lattices records the intended value without changing the app.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
snapshotId |
string | yes | Snapshot id returned by computer.windowState |
elementId |
string | yes | Snapshot-local element id, such as e4 |
text |
string | yes for typeElement |
Text to set or append |
value |
string | yes for setValue |
Value to set or append |
append |
bool | no | Append to current AXValue instead of replacing it |
typeIntervalMs |
double | no | Per-character interval for typewriter-style AXValue updates |
treatment |
string | no | stage, present, or execute. Defaults to stage |
backgroundSafe / noFocus |
bool | no | With execute, set AXValue without focusing the app or AX element |
dryRun |
bool | no | Stage without setting AXValue |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
const state = await daemonCall('computer.windowState', {
app: 'Notes',
maxDepth: 8
})
await daemonCall('computer.typeElement', {
snapshotId: state.snapshotId,
elementId: 'e12',
text: 'Draft note',
treatment: 'execute',
backgroundSafe: true
})computer.pressKey / computer.hotkey
Stage or execute keyboard input. computer.pressKey sends one key, such as
escape, enter, tab, an arrow key, or a single character. computer.hotkey
sends a shortcut from either shortcut: "command+shift+p" or key plus
modifiers. Both default to stage; execute requires an explicit wid,
app, or session target unless allowGlobal: true is passed.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target window id |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app target |
key |
string | yes for pressKey |
Key name or single character |
shortcut |
string | yes for hotkey without key |
Shortcut shorthand, such as command+shift+p |
modifiers |
array/string | no | command, option, control, shift |
count |
int | no | Repeat count. Defaults to 1, max 20 |
delayMs |
double | no | Delay between repeats. Defaults to 80 |
allowGlobal |
bool | no | Permit execute without target by posting to the focused system target |
treatment |
string | no | stage, present, or execute |
dryRun |
bool | no | Stage without posting keyboard input |
capture |
bool | no | Capture before/after artifacts when targeting a window |
source |
string | no | Calling surface label |
await daemonCall('computer.pressKey', {
app: 'Finder',
key: 'escape',
treatment: 'stage'
})
await daemonCall('computer.hotkey', {
app: 'Xcode',
shortcut: 'command+b',
treatment: 'execute'
})computer.focusWindow
Resolve a target window, optionally capture it, focus it, and verify the focused
window id. Use treatment: 'observe' or stage to plan without presenting.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target window id |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app |
treatment |
string | no | observe, stage, present, or execute |
dryRun |
bool | no | Stage without focusing |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
await daemonCall('computer.focusWindow', {
app: 'iTerm2',
treatment: 'present'
})computer.showCursor
Resolve the current cursor location and show a visible cursor appearance. This
is the cursor equivalent of a typing action: it records a run, cursor target,
appearance parameters, and trace events. Use observe or stage to plan
without showing anything.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
x |
double | no | Screen x coordinate. Defaults to current cursor |
y |
double | no | Screen y coordinate. Defaults to current cursor |
treatment |
string | no | observe, stage, present, or execute |
style |
string | no | spotlight, pulse, or marker |
appearance |
string | no | Alias for style |
shape |
string | no | Marker shape: chevron, facet, shard, wedge, prism, or notch |
angleDeg |
double | no | Marker rotation in degrees. Positive rotates visually clockwise; default is -8 for marker |
size |
string | no | Marker size: small, regular, or large. Defaults to Settings |
color |
string | no | blue, green, amber, pink, red, or white |
durationMs |
int | no | Appearance duration in milliseconds |
label |
string | no | Optional marker label |
dryRun |
bool | no | Stage without showing |
source |
string | no | Calling surface label |
await daemonCall('computer.showCursor', {
style: 'marker',
shape: 'chevron',
angleDeg: -8,
size: 'regular',
color: 'white',
treatment: 'present'
})computer.launchApp
Launch or focus a normal macOS app and record the result as a run. Use
dryRun: true or treatment: 'stage' to plan without launching.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
app |
string | yes | App name, such as Scout, Slack, or Notes |
bundleId |
string | no | Bundle identifier fallback for precise launch |
path |
string | no | Explicit .app bundle path |
title |
string | no | Optional title substring for app window selection |
treatment |
string | no | observe, stage, present, or execute |
dryRun |
bool | no | Stage without launching |
capture |
bool | no | Capture the launched app window. Defaults to true outside dry-run |
source |
string | no | Calling surface label |
await daemonCall('computer.launchApp', {
app: 'Scout',
treatment: 'present'
})computer.typeWindowText
Focus a normal app window and insert text. If click coordinates are provided,
Lattices clicks that target before typing. Coordinates can be absolute screen
points (x, y) or ratios inside the target window (xRatio, yRatio).
For window ratios, 0,0 is the top-left of the window and 1,1 is the
bottom-right.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target window id |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app target |
text |
string | yes | Text to insert |
enter |
bool | no | Press Enter after typing. Defaults to false |
send |
bool | no | Alias for enter in chat-style demos |
x, y |
double | no | Absolute click point before typing |
xRatio, yRatio |
double | no | Window-relative click point before typing |
treatment |
string | no | observe, stage, present, or execute |
dryRun |
bool | no | Stage without typing |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
await daemonCall('computer.typeWindowText', {
app: 'Scout',
text: 'Draft memo text',
xRatio: 0.5,
yRatio: 0.86,
treatment: 'execute'
})computer.click / computer.doubleClick / computer.rightClick
Stage or execute a click target. stage records the target without clicking.
In execute, transport: "auto" prefers AXPress on the resolved accessibility
button/control before falling back to a pointer click. Use transport: "ax" or
noFocus: true when the action must not focus the app or move the hardware
pointer. computer.doubleClick forces two pointer clicks; computer.rightClick
forces the right button. When a window target is provided, ratios are relative to
that window.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Target window id |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app target |
x, y |
double | no | Absolute click point |
xRatio, yRatio |
double | no | Window-relative click point |
button |
string | no | left or right; defaults to left |
count |
int | no | Pointer click count. Defaults to 1, max 8 |
delayMs |
double | no | Delay between repeated pointer clicks |
transport |
string | no | auto, ax, or pointer; defaults to auto |
axLabel |
string | no | Optional AX text/title hint, such as Send |
noFocus |
bool | no | Require no-focus AX execution; disable pointer fallback |
treatment |
string | no | stage, present, or execute |
dryRun |
bool | no | Stage without clicking |
capture |
bool | no | Capture before/after artifacts when targeting a window |
source |
string | no | Calling surface label |
await daemonCall('computer.click', {
app: 'Scout',
xRatio: 0.5,
yRatio: 0.86,
treatment: 'execute'
})
await daemonCall('computer.click', {
app: 'Scout',
xRatio: 0.74,
yRatio: 0.95,
transport: 'ax',
axLabel: 'Send',
noFocus: true,
treatment: 'execute'
})computer.scroll / computer.drag
Stage or execute richer pointer input while keeping the same run/capture model.
computer.scroll posts scroll wheel events at an absolute or window-relative
point. computer.drag interpolates a pointer drag from a start point to an end
point. Both default to stage.
Scroll params:
| Field | Type | Required | Description |
|---|---|---|---|
wid, session, app, title |
target | no | Target window/session/app |
x, y |
double | no | Absolute pointer point before scrolling |
xRatio, yRatio |
double | no | Window-relative pointer point |
direction |
string | no | down (default), up, left, or right |
amount |
double | no | Scroll amount when using direction; default 420 |
deltaX, deltaY |
double | no | Explicit scroll wheel deltas |
count |
int | no | Number of scroll events; default 1, max 30 |
delayMs |
double | no | Delay between repeated events |
treatment, dryRun, capture, source |
mixed | no | Standard computer action fields |
Drag params:
| Field | Type | Required | Description |
|---|---|---|---|
wid, session, app, title |
target | no | Target window/session/app |
fromX, fromY |
double | no | Absolute drag start point |
toX, toY |
double | no | Absolute drag end point |
fromXRatio, fromYRatio |
double | no | Window-relative drag start point |
toXRatio, toYRatio |
double | no | Window-relative drag end point |
x, y, xRatio, yRatio |
double | no | Aliases for the drag end point |
button |
string | no | left or right; defaults to left |
durationMs |
double | no | Drag duration; default 360 |
steps |
int | no | Interpolated drag event count; default 18 |
treatment, dryRun, capture, source |
mixed | no | Standard computer action fields |
await daemonCall('computer.scroll', {
app: 'Safari',
direction: 'down',
amount: 420,
treatment: 'execute'
})
await daemonCall('computer.drag', {
app: 'Finder',
fromXRatio: 0.2,
fromYRatio: 0.2,
toXRatio: 0.7,
toYRatio: 0.7,
treatment: 'stage'
})computer.verify
Verify a post-action outcome using OCR, AX snapshot text/value, or artifact hash
comparison. OCR verification captures/analyzes the target when needed. AX
verification can use a snapshotId/elementId from computer.windowState.
| Field | Type | Required | Description |
|---|---|---|---|
mode |
string | no | ocr (default), ax, or artifactChanged |
wid, session, app, title |
target | no | Window target |
snapshotId, elementId |
string | no | AX snapshot/element target |
runId, artifactId, path |
mixed | no | Image artifact target for OCR verification |
contains, expected |
string | no | Text that should be present |
notContains |
string | no | Text that should be absent |
beforeArtifactId, afterArtifactId |
string | no | Artifact ids for artifactChanged |
beforePath, afterPath |
string | no | Path fallback for artifactChanged |
source |
string | no | Calling surface label |
await daemonCall('computer.verify', {
app: 'Safari',
mode: 'ocr',
contains: 'Lattices'
})
const state = await daemonCall('computer.windowState', { app: 'Notes' })
await daemonCall('computer.verify', {
mode: 'ax',
snapshotId: state.snapshotId,
elementId: 'e4',
contains: 'Draft'
})browser.getText / browser.queryDom / browser.executeJavascript
Browser primitives are intentionally gated. browser.getText reads visible text
through Accessibility and does not enable JavaScript automation. DOM querying
and arbitrary JavaScript use AppleScript browser automation and require
allowAutomation: true; JavaScript mutation also requires
treatment: "execute".
Supported browser app names: Safari, Google Chrome, Chrome, Brave Browser, Microsoft Edge, and Arc.
| Field | Type | Required | Description |
|---|---|---|---|
wid, app, title |
target | no | Browser target |
selector |
string | yes for queryDom |
CSS selector |
limit |
int | no | Max DOM nodes, default 20, max 200 |
script |
string | yes for executeJavascript |
JavaScript source |
allowAutomation |
bool | yes for DOM/execute | Explicit browser automation consent |
treatment |
string | no | stage (default) or execute for JavaScript |
source |
string | no | Calling surface label |
await daemonCall('browser.getText', { app: 'Safari' })
await daemonCall('browser.queryDom', {
app: 'Safari',
selector: 'h1, main a',
allowAutomation: true
})
await daemonCall('browser.executeJavascript', {
app: 'Safari',
script: 'document.title',
treatment: 'execute',
allowAutomation: true
})computer.demoScout
Warm up a Scout memo/demo recording run. In present mode it launches or
focuses Scout and records a run without typing. In execute mode it can click
the likely composer area, type a draft, and optionally press Enter when
enter or send is true. Dry-run/stage mode does not capture by default, so it
works before Screen Recording permission is granted.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
app |
string | no | Scout app name override. Defaults to Scout |
title |
string | no | Optional title substring for the Scout window |
text |
string | no | Draft text to type in execute mode |
enter |
bool | no | Press Enter after typing. Defaults to false |
send |
bool | no | Alias for enter |
click |
bool | no | Click the likely composer area before typing |
xRatio, yRatio |
double | no | Composer click point; defaults to 0.5, 0.86 |
treatment |
string | no | observe, stage, present, or execute |
dryRun |
bool | no | Stage without launching or typing |
capture |
bool | no | Capture before/after artifacts. Defaults to true outside dry-run |
source |
string | no | Calling surface label |
await daemonCall('computer.demoScout', { dryRun: true })
await daemonCall('computer.demoScout', {
treatment: 'present',
capture: false
})
await daemonCall('computer.demoScout', {
text: 'Draft memo text',
treatment: 'execute',
send: false
})computer.typeText
Resolve a terminal target and insert text after safety checks. Lattices prefers
the least intrusive available transport: tmux pane input when a tmux pane is
known, then target-pid key events/pasteboard insertion when window focus is
required. Enter is never pressed unless enter: true is provided.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Specific terminal window id |
tty |
string | no | Specific terminal TTY |
app |
string | no | Preferred terminal app, such as iTerm2 |
text |
string | yes | Text to insert |
enter |
bool | no | Press Enter after typing. Defaults to false |
treatment |
string | no | observe, stage, present, or execute |
transport |
string | no | auto, tmux, or pasteboard. Defaults to auto |
dryRun |
bool | no | Stage without typing |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
await daemonCall('computer.typeText', {
text: '# hello from lattices',
transport: 'auto',
enter: false
})computer.demoTerminal
Compatibility endpoint for the original terminal demo. It follows the same
treatment, safety, capture, and transport rules as computer.typeText, but
provides a default text payload when text is omitted.
Run a bounded computer-use sequence against a terminal window:
- synthesize and score terminal candidates
- choose a safe shell-like terminal unless
widorttyis supplied - capture a
beforescreenshot artifact - focus the terminal window
- insert text without pressing Enter by default
- capture an
afterscreenshot artifact
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
uint32 | no | Specific terminal window id |
tty |
string | no | Specific terminal TTY |
app |
string | no | Preferred terminal app, such as iTerm2 |
text |
string | no | Text to insert |
enter |
bool | no | Press Enter after typing. Defaults to false |
treatment |
string | no | observe, stage, present, or execute |
transport |
string | no | auto, tmux, or pasteboard. Defaults to auto |
dryRun |
bool | no | Plan and capture without typing |
capture |
bool | no | Capture before/after artifacts. Defaults to true |
source |
string | no | Calling surface label |
await daemonCall('computer.demoTerminal', { dryRun: true })
await daemonCall('computer.demoTerminal', {
app: 'iTerm2',
text: '# hello from lattices',
enter: false
})Overlay UI
The macOS app exposes a shared desktop overlay canvas for lightweight
agent-visible UI. Use overlay.publish for transient passive visuals,
and overlay.actor.* for persistent, movable actor surfaces.
Persistent actors are useful for representing agents or processes on the
desktop. Each actor has a stable id, can be moved independently through the
API, dragged by the user, hidden/restored with Hyper+B, and closed with
right-click. Click event callbacks and action surfaces are planned follow-on
capabilities.
| Method | Type | Description |
|---|---|---|
overlay.publish |
write | Publish a transient toast, label, highlight, or pet layer |
overlay.clear |
write | Clear one overlay layer by id, or clear an owner namespace |
overlay.actor.publish |
write | Create or update a persistent generative overlay actor |
overlay.actor.moveTo |
write | Move an actor with app-owned easing |
overlay.actor.hud |
write | Attach, update, or clear a hover web HUD for an actor |
overlay.actor.visibility |
write | Show, hide, toggle, or inspect the sticky actor layer |
overlay.publish
Publish a transient layer on the screen overlay canvas.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | yes | toast, label, highlight, or pet |
id |
string | no | Stable layer id; generated if omitted |
text |
string | no | Toast/label text or pet message fallback |
detail |
string | no | Secondary toast/label text |
message |
string | no | Pet message |
petId |
string | no | Bundled pet id from apps/mac/Resources/Pets |
state |
string | no | Pet animation state |
placement |
string | no | top, bottom, center, cursor, or point |
x, y |
double | no | Screen-local point for point placement |
w, h |
double | no | Highlight size |
ttlMs |
int | no | Time to live in milliseconds |
dismissible |
bool | no | Whether click-away dismissal removes the layer; defaults true |
Example:
await daemonCall('overlay.publish', {
kind: 'highlight',
x: 160,
y: 120,
w: 480,
h: 260,
text: 'Needs review',
style: 'warning',
ttlMs: 3000
})overlay.actor.publish
Create or update a generative overlay actor. Actors default to persistent:
omit ttlMs or pass 0, and dismissible defaults to false.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | no | Stable actor id; generated if omitted |
renderer |
string | no | sprite is currently supported |
asset |
string | no | Bundled sprite asset id, such as scout-ranger |
state |
string | no | Actor state or animation name |
name |
string | no | Actor display name |
message |
string | no | Attached message text |
targetApp |
string | no | App name to activate when the actor is clicked |
targetBundleId |
string | no | Bundle identifier to activate when the actor is clicked |
targetAppPath |
string | no | .app bundle path to open when the actor is clicked |
scale |
double | no | Actor scale multiplier |
labelHidden |
bool | no | Hide the actor label/message |
closeOnActivate |
bool | no | Remove the actor after activating its target app |
hudUrl |
string | no | URL to render in a transparent hover HUD web view |
hudHTML |
string | no | Inline HTML to render in a transparent hover HUD web view |
hudWidth |
double | no | Hover HUD width |
hudHeight |
double | no | Hover HUD height |
hudReadAccess |
string | no | Local folder a file-backed HUD may read |
placement |
string | no | top, bottom, center, cursor, or point |
x, y |
double | no | Screen-local point for point placement |
ttlMs |
int | no | Time to live; 0 means persistent |
dismissible |
bool | no | Whether click-away dismissal removes the actor |
Bundled sprite assets:
| Asset | Notes |
|---|---|
assistant-spark |
Animated states include idle, run_right, run_left, waving, jumping, failed, waiting, running, and review |
scout-ranger |
Bundled sprite asset with default frame fallback |
Example:
await daemonCall('overlay.actor.publish', {
id: 'agent-scout',
renderer: 'sprite',
asset: 'scout-ranger',
state: 'waiting',
name: 'Scout',
message: 'Waiting for feedback',
placement: 'point',
x: 640,
y: 320,
ttlMs: 0
})overlay.actor.moveTo
Move an actor with app-owned animation. The app interpolates position and switches directional sprite states while moving.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Actor id |
x, y |
double | yes | Target screen-local point |
durationMs |
int | no | Animation duration |
easing |
string | no | linear, easeInOut, or spring |
Example:
await daemonCall('overlay.actor.moveTo', {
id: 'agent-scout',
x: 820,
y: 280,
durationMs: 800,
easing: 'spring'
})overlay.actor.hud
Attach a transparent, blurred hover HUD to an actor. The HUD is backed by a
native WKWebView, so apps can point it at a local static HTML dashboard or,
in development, a local URL.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Actor id |
hudUrl |
string | no | URL or file path to render |
hudHTML |
string | no | Inline HTML to render instead of a URL |
hudTitle |
string | no | HUD title metadata |
hudWidth |
double | no | HUD width |
hudHeight |
double | no | HUD height |
hudReadAccess |
string | no | Local folder a file-backed HUD may read |
clear |
bool | no | Remove the actor HUD |
Example:
await daemonCall('overlay.actor.hud', {
id: 'switch-talkie',
hudUrl: '/Users/you/dev/talkie/.lattices/hud/index.html',
hudReadAccess: '/Users/you/Library/Application Support/Talkie/HUD',
hudWidth: 380,
hudHeight: 260
})overlay.actor.visibility
Show, hide, toggle, or inspect the persistent actor layer without destroying the actors. This is the daemon equivalent of the app's Hyper+B shortcut.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | no | show, hide, toggle, or status |
visible |
bool | no | Set layer visibility directly |
hidden |
bool | no | Set layer hidden state directly |
feedback |
bool | no | Show a short desktop feedback toast |
Example:
await daemonCall('overlay.actor.visibility', { action: 'toggle' })Static HUD Manifests
Apps and projects can expose a local hover dashboard without running a web server by publishing a static bundle at:
.lattices/hud/
manifest.json
index.html
assets/Minimal manifest:
{
"version": 1,
"id": "talkie",
"name": "Talkie",
"bundleId": "com.usabletalkie.Talkie",
"icon": "./assets/icon.png",
"entry": "./index.html",
"readAccess": "~/Library/Application Support/Talkie/HUD",
"surface": { "width": 380, "height": 260 },
"actor": {
"labelHidden": true,
"click": "activateApp"
},
"sources": [
{
"path": "~/Library/Application Support/Talkie/HUD/activity.jsonl",
"format": "jsonl",
"schema": "talkie.activity.v1",
"presentation": "timeline"
}
]
}The CLI resolves this manifest into overlay.actor.publish with a file-backed
HUD URL. The macOS app loads entry through WKWebView.loadFileURL, allowing
read access to the HUD folder by default, or to the manifest's readAccess
folder when one is declared.
sources is descriptive metadata for app-owned state, logs, or event streams.
Lattices does not append to those logs. The app writes them in its normal
runtime location, and the custom HUD renderer decides how to present them.
Useful commands:
lats hud register .lattices/hud/manifest.json --publish
lats hud publish talkie
lats hud sync
lats hud discover ~/dev --registerFor packaged apps, keep the renderer files in the app bundle and point mutable
sources at an app-owned folder such as ~/Library/Application Support/....
System
| Method | Type | Description |
|---|---|---|
deck.manifest |
read | Shared companion deck manifest |
deck.snapshot |
read | Current companion deck runtime snapshot |
deck.perform |
write | Perform a companion deck action |
solo.status |
read | Check whether Focus Mode is active |
stage.status |
read | Check whether Stage is active |
stage.enter |
write | Enter Stage for the windows on this display |
stage.exit |
write | Exit Stage and restore the windows |
stage.toggle |
write | Toggle Stage |
stage.cycle |
write | Move Stage to another window in the set |
solo.enter |
write | Spotlight the frontmost window |
solo.exit |
write | Exit Focus Mode and restore the window |
solo.toggle |
write | Toggle Focus Mode for the frontmost window |
daemon.status |
read | Health check and stats |
api.schema |
read | Full API schema for self-discovery |
diagnostics.list |
read | Recent diagnostic entries |
deck.manifest
Return the shared DeckKit manifest exposed by the macOS app. This is
the contract a future Lattices companion can consume to discover pages,
capabilities, and security mode.
Params: none
deck.snapshot
Return the current DeckKit runtime snapshot for the macOS host.
Params: none
Returns: a DeckRuntimeSnapshot object containing:
voicedesktopswitcherhistoryquestions
deck.perform
Perform a DeckKit action against the running macOS host and return a
DeckActionResult.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
pageID |
string | no | Deck page ID |
actionID |
string | yes | Deck action identifier |
payload |
object | no | Deck action payload |
Example:
{
"id": "1",
"method": "deck.perform",
"params": {
"pageID": "layout",
"actionID": "layout.optimize",
"payload": {
"strategy": "balanced",
"region": "right"
}
}
}solo.status, solo.enter, solo.exit, solo.toggle
Control the same reversible Focus Mode exposed through the Hyper+Z chord
and command palette. solo.enter targets the currently frontmost window;
solo.exit restores the exact frame captured on entry.
lats call solo.enter
lats call solo.status
lats call solo.exitEach method returns the resulting active state. Mutating methods also return
ok; solo.enter returns ok: false when there is no eligible frontmost
window.
stage.status, stage.enter, stage.exit, stage.toggle, stage.cycle
Stage is on or off. The windows on the display you are using decide the grid:
one window takes the stage, and more windows fill the grid around it. A window
that appears while Stage is on joins the grid. The window you are in grows to
40% of the display width and comes forward. The others stay in their cells,
slightly smaller. Clicking another window in the set, or stage.cycle, moves
the stage. Escape and stage.exit put every window back where it was when it
joined. Hyper+X toggles it. Control-Option-Tab moves through the set while it
is on.
lats call stage.enter
lats call stage.cycle '{"by":-1}'
lats call stage.exitstage.enter turns Stage on whatever the window count is. stage.cycle takes by (default 1).
daemon.status
Health check and basic stats.
Params: none
Returns:
{
"uptime": 3600.5,
"clientCount": 2,
"version": "1.0.0",
"windowCount": 12,
"tmuxSessionCount": 3,
"frontmostWid": 1234,
"permissions": {
"accessibility": true,
"screenRecording": true
}
}api.schema
Return the full API schema including version, models, and method definitions. Useful for agent self-discovery.
Params: none
CLI shortcut:
lats call api.schemadiagnostics.list
Return recent diagnostic log entries from the daemon.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
limit |
number | no | Max entries to return (default 50) |
Mouse & Input
| Method | Type | Description |
|---|---|---|
mouse.find |
read | Show a sonar pulse at the current cursor |
mouse.summon |
write | Move the cursor to a point or screen center |
mouse.shortcuts.get |
read | Return the live mouse shortcut config |
mouse.shortcuts.reload |
write | Reload ~/.lattices/mouse-shortcuts.json without restarting |
mouse.shortcuts.set |
write | Replace the full mouse shortcut config and activate it |
mouse.shortcuts.upsert |
write | Create or replace one mouse shortcut rule and activate it |
mouse.shortcuts.remove |
write | Remove one mouse shortcut rule and activate the new config |
mouse.shortcuts.restoreDefaults |
write | Restore default mouse shortcuts |
Mouse shortcut rules are data. Prefer shortcut.send for hotkeys an agent can
define or change directly; do not add a named action unless the behavior cannot
be expressed as data.
Create or replace a gesture that sends Hyper+D:
await daemonCall('mouse.shortcuts.upsert', {
rule: {
id: 'middle-up-voice',
enabled: true,
device: 'any',
trigger: { button: 'middle', kind: 'drag', direction: 'up' },
action: {
type: 'shortcut.send',
shortcut: {
key: 'd',
keyCode: 2,
modifiers: ['control', 'option', 'shift', 'command']
}
}
}
})If an agent edits ~/.lattices/mouse-shortcuts.json itself, refresh the running
app explicitly:
await daemonCall('mouse.shortcuts.reload')All write methods persist the config, checkpoint the previous version in
~/.lattices/mouse-shortcuts.history/, and update the active event-tap snapshot
immediately. No app restart is required.
Supported action types:
| Type | Purpose |
|---|---|
shortcut.send |
Send a data-defined key or keyCode with modifiers |
app.activate |
Activate an app by name |
space.previous |
Switch to the previous macOS Space |
space.next |
Switch to the next macOS Space |
screenmap.toggle |
Open the Screen Map overview |
dictation.start |
Legacy alias that presses the configured Voice Command hotkey |
Windows & Spaces
| Method | Type | Description |
|---|---|---|
desktop.snapshot |
read | One-call current desktop: frontmost, layer, displays, windows, sessions, permissions |
windows.list |
read | All visible windows |
windows.get |
read | Single window by ID |
windows.preview |
read | Cached PNG preview for a window |
windows.search |
read | Search windows by query |
spaces.list |
read | macOS display spaces |
windows.pick |
read | Ask the user to choose one visible window in a read-only Hyperspace survey |
windows.place |
write | Place a window or session using a typed placement spec |
windows.focus |
write | Focus a window / switch Spaces |
windows.move |
write | Move a window to another display, placement slot, or Space |
spaces.optimize |
write | Optimize a set of windows using an explicit scope and strategy |
layout.distribute |
write | Compatibility wrapper for visible-window balancing |
desktop.snapshot
One call for “what is in front of the user?” Structured state — not a drawing.
lats map is the ASCII render of this same current-space view (it now
reads desktop.snapshot when the app is new enough).
Params:
| Field | Type | Required | Description |
|---|---|---|---|
includeOffscreen |
boolean | no | Include off-screen windows. Default false. |
Returns:
{
"frontmost": { "wid": 1234, "app": "iTerm2", "title": "…", "pid": 1 },
"activeLayer": { "id": "web", "label": "Web", "index": 0, "slot": 1, "active": true, "layout": null, "entries": [] },
"displays": [ { "displayIndex": 0, "name": "Built-in", "frame": {}, "visibleFrame": {}, "currentSpaceId": 1, "spaces": [] } ],
"currentSpaceId": 1,
"windows": [
{
"wid": 1234,
"app": "iTerm2",
"title": "…",
"pid": 5678,
"frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
"spaceIds": [1],
"isOnScreen": true,
"zIndex": 0,
"focused": true,
"latticesSession": "lattices-a1b2c3"
}
],
"sessions": [ { "name": "lattices-a1b2c3", "attached": true, "windowCount": 1 } ],
"permissions": { "accessibility": true, "screenRecording": true }
}Window frames are top-left global, same as windows.list. Does not recapture
windows or run OCR.
lats call desktop.snapshot
lats mapwindows.list
List all visible windows tracked by the desktop model.
Params: none
Returns: array of window objects:
[
{
"wid": 1234,
"app": "Terminal",
"pid": 5678,
"title": "[lattices:myapp-a1b2c3] zsh",
"frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
"spaceIds": [1],
"isOnScreen": true,
"latticesSession": "myapp-a1b2c3"
}
]The latticesSession field is present only on windows that belong to
a lattices session (matched via the [lattices:name] title tag).
windows.get
Get a single window by its CGWindowID.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | yes | CGWindowID |
Returns: a single window object (same shape as windows.list items).
Errors: Not found if the window ID doesn't exist.
windows.search
Search windows by text query across title, app name, session tags, and OCR content. Returns results with matchSource indicating how the match was found, and ocrSnippet for OCR matches.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
query |
string | yes | Search query (matches title, app, session, OCR text) |
ocr |
boolean | no | Include OCR text in search (default true) |
limit |
number | no | Max results (default 50) |
Returns: array of window objects with additional search fields:
[
{
"wid": 265,
"app": "iTerm2",
"title": "✳ Claude Code",
"matchSource": "ocr",
"ocrSnippet": "…~/dev/vox StatusBarIconFolder…",
"frame": { "x": 688, "y": 3, "w": 1720, "h": 720 },
"isOnScreen": true
}
]matchSource values: "title", "app", "session", "ocr".
CLI usage:
# Basic search (uses windows.search)
lats search vox
# Deep search — adds terminal tab/process inspection for ranking
lats search vox --deep
# Same as --deep (all search sources)
# A TTY matched only at app level (see terminals.list) adds 1 point to each
# window of its terminal app, with the source "terminal-app". `lats place`
# does not act on a result that has only that source.
lats search vox --all
# Pipeable output
lats search vox --wid
lats search vox --json
# Search + focus + tile in one step
lats place vox rightwindows.preview
Return a cached PNG preview for a tracked window. When the cache is cold,
load starts an asynchronous warmup and the caller can poll until ok is true.
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | yes | CGWindowID from windows.list |
load |
boolean | no | Warm the cache when missing; defaults to true |
The response includes cached and loading. A ready preview also includes
mimeType: "image/png", base64 image data, and pixel dimensions.
spaces.list
List macOS display spaces (virtual desktops).
Display frame and visibleFrame use the same top-left global coordinate
system as window frames returned by windows.list.
See Workspace Map for the complete coordinate contract,
terminal/JSON projection, and visible-surface lifecycle guidance.
Params: none
Returns: array of display objects:
[
{
"displayIndex": 0,
"displayId": "main",
"name": "Built-in Liquid Retina XDR Display",
"frame": { "x": 0, "y": 0, "w": 1728, "h": 1117 },
"visibleFrame": { "x": 0, "y": 38, "w": 1728, "h": 1079 },
"currentSpaceId": 1,
"spaces": [
{ "id": 1, "index": 1, "name": "Desktop 1", "display": 0, "isCurrent": true },
{ "id": 2, "index": 2, "name": "Desktop 2", "display": 0, "isCurrent": false }
]
}
]windows.place
Canonical window placement mutation. Use this when an agent needs a single, typed placement contract across voice, CLI, and daemon clients.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | no | Target window ID |
session |
string | no | Target lattices session |
app |
string | no | Target app name |
title |
string | no | Optional title substring for app matching |
display |
number | no | Target display index |
placement |
string | object | yes | Placement shorthand or typed object |
dryRun |
bool | no | Plan and verify inputs without moving |
Target resolution priority is wid → session → app/title → frontmost window.
Placement strings: left, right, top, bottom, top-left, top-right,
bottom-left, bottom-right, left-third, center-third, right-third,
top-third, middle-third, bottom-third, left-quarter, right-quarter,
top-quarter, bottom-quarter, maximize, center, grid:CxR:C,R, or
compact CxR:C,R. The canonical grid: form is 0-indexed; the compact form is
1-indexed for command entry.
Typed placement examples:
{ "kind": "tile", "value": "top-right" }
{ "kind": "grid", "columns": 3, "rows": 2, "column": 2, "row": 0 }
{ "kind": "fractions", "x": 0.5, "y": 0, "w": 0.5, "h": 1 }Returns: execution receipt including resolved target, placement, and trace.
windows.pick
Open Hyperspace in a read-only selection mode and wait for the user to choose one visible window. Letter hints, Tab/Return, and clicking a survey tile select; Escape cancels. Placement, layer editing, and normal motion commands are disabled for the duration of the pick.
| Field | Type | Required | Description |
|---|---|---|---|
client |
string | no | Calling client label |
requestId |
string | no | Caller correlation id |
prompt |
string | no | Prompt shown in the selection HUD |
mode |
string | no | single-window (default and currently supported mode) |
currentWid |
number | no | Visible window to preselect |
timeoutMs |
number | no | Wait time, clamped to 5–300 seconds; default 120 seconds |
Returns status: "selected" with a window, or cancelled, timeout, or
unavailable without one.
windows.focus
Focus a window — bring it to front and switch Spaces if needed.
Params (one of):
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | no | CGWindowID (any window) |
session |
string | no | Session name (lats windows) |
Provide either wid or session. If wid is given, it takes priority.
windows.move
Move a specific window to a display and desktop in one verified operation. Without a placement, the window keeps its normalized position and size on the target display. A Space ID identifies one desktop on one display; Desktop 2 on a physical monitor differs from Desktop 2 on a virtual display.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | no | Target CGWindowID (preferred) |
session |
string | no | Session name (legacy target) |
display |
number | no | Target display index from spaces.list |
spaceId |
number | no | Absolute target Space ID from spaces.list |
placement |
string | object | no | Placement slot on the target display |
position |
string | no | Alias for placement |
dryRun |
bool | no | Validate and plan without moving the window or changing the view |
Provide wid or session, plus at least one of display, spaceId, or
placement. There is no frontmost-window fallback.
spaceIdselects its owning display whendisplayis omitted. If both are supplied, the Space must belong to that display. A mismatch is an error.displayalone selects that display's current desktop. Display indexes resolve through stable display UUIDs.placementcan accompany either destination. Without a placement, a cross-display move maps the source frame onto the target visible frame and clamps it to fit. A same-display desktop move preserves the frame.- Unknown displays, Spaces, and placements are errors. A window must belong to one ordinary desktop; full-screen and multi-Space windows are rejected when their relocation cannot be verified.
For an inactive source desktop, Lattices first carries the window to the source display's current desktop. It then moves the window to the target display and carries it to the requested desktop. Mission Control can appear during these steps. Keep the pointer free until the operation finishes.
Returns: an execution receipt with status
(ok/planned/blocked/failed), before/target/after frames in mutations,
sourceDisplay, display, spaceId, fromSpaceIds, and afterSpaceIds.
verified requires verifiedFrame, verifiedDisplay, and verifiedSpace
to succeed. An AX call or a desktop view switch alone cannot report success.
A placement receipt retains action.type: "window.place" and
compatibilityMethod: "window.move".
On partial failure, Lattices attempts to restore the original display,
desktop, and frame. The receipt reports failureReason, rollback, and the
observed final state. Successful moves support actions.undo, including the
original desktop. Allow up to 60 seconds for verification and recovery.
// Move to the current desktop on display 0, preserving relative geometry.
await daemonCall('window.move', { wid: 4182, display: 0 }, 60000)
// Choose the exact physical display and desktop in one operation.
await daemonCall('window.move', { wid: 4182, display: 0, spaceId: 3 }, 60000)
// Preview a move and placement without changing the window or desktop view.
await daemonCall('window.move', {
wid: 4182, display: 0, spaceId: 3, placement: 'left', dryRun: true
})spaces.optimize
Canonical space-balancing mutation. Use this when the goal is to make the current workspace coherent rather than placing one specific window.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
scope |
string | no | visible, active-app, app, or selection |
strategy |
string | no | balanced or mosaic |
app |
string | no | App name for app scope |
title |
string | no | Optional title substring for app matching |
windowIds |
number[] | no | Explicit window IDs for selection scope |
If windowIds is provided, scope is inferred as selection.
If app is provided and scope is omitted, scope is inferred as app.
Returns: execution receipt including resolved scope, strategy, affected window IDs, and trace.
layout.distribute
Compatibility wrapper for spaces.optimize with scope=visible and
strategy=balanced.
Params: none
Sessions
| Method | Type | Description |
|---|---|---|
tmux.list |
read | Lattices tmux sessions, or every session including orphans |
sessions.launch |
write | Launch a project session |
sessions.kill |
write | Kill a session |
sessions.detach |
write | Detach clients from a session |
sessions.sync |
write | Reconcile session to config |
sessions.restart |
write | Restart a pane's process |
All session methods require tmux to be installed.
tmux.list
List tmux sessions that belong to lattices.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
includeOrphans |
boolean | no | Return every tmux session, including ones lattices does not track |
Returns: array of session objects:
[
{
"name": "myapp-a1b2c3",
"windowCount": 1,
"attached": true,
"panes": [
{
"id": "%0",
"windowIndex": 0,
"windowName": "main",
"title": "claude",
"currentCommand": "claude",
"pid": 9876,
"isActive": true
}
]
}
]With includeOrphans: true, returns every tmux session instead, including
orphans (sessions not tracked by lattices):
{
"all": [ ... ],
"orphans": [ ... ]
}Both arrays contain session objects (same shape as tmux.list).
sessions.launch
Launch a new tmux session for a project. If a session already exists,
it will be reattached. The project must be in the scanned project list —
call projects.list to check, or projects.scan to refresh.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Absolute path to project directory |
Returns: a receipt, not a queued ok:
{
"ok": true,
"session": "myapp-a1b2c3",
"alreadyRunning": false,
"wid": 1234,
"verified": true
}ok / verified are false if no tagged window and no tmux session appeared
within ~4s. Errors: Not found if the path isn't in the scanned project
list.
sessions.kill
Kill a tmux session by name.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Session name |
Returns: { "ok": true, "session": "…", "verified": true } when the
session is gone. ok / verified are false if it is still listed.
sessions.detach
Detach all clients from a session (keeps it running).
Params:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Session name |
sessions.sync
Reconcile a running session to match its declared .lattices.json config.
Recreates missing panes, re-applies layout, restores labels, re-runs
commands in idle panes.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Absolute path to project directory |
Errors: Not found if the path isn't in the project list.
sessions.restart
Restart a specific pane's process within a session.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
path |
string | yes | Absolute path to project directory |
pane |
string | no | Pane name to restart (defaults to first pane) |
Projects & Layers
| Method | Type | Description |
|---|---|---|
projects.list |
read | Discovered projects |
projects.scan |
write | Re-scan project directory |
layers.list |
read | Workspace layers, active index, and what switches put away |
layers.members |
read | Each layer's entries and the windows they hold, on any desktop |
layers.exit |
write | Exit layer focus and restore parked windows |
layers.reveal |
write | Compatibility alias for layers.exit |
layers.create |
write | Save windows as a new layer |
layers.assign |
write | Add windows to a layer |
layers.unassign |
write | Take windows out of a layer |
layers.rename |
write | Rename a layer |
layers.delete |
write | Delete a layer |
layers.activate |
write | Switch to a workspace layer; focus, tile or launch |
layers.switch |
write | Same as layers.activate |
groups.launch |
write | Launch a tab group |
groups.kill |
write | Kill a tab group |
tabs.list |
read | List ephemeral cross-app tab stacks |
tabs.stack |
write | Create a stack from selected or explicit windows |
tabs.add |
write | Add windows to a live stack |
tabs.select |
write | Focus a member tab |
tabs.layout |
write | Toggle or set tabs/grid layout |
tabs.unstack |
write | Ungroup without closing windows |
projects.list
List all discovered projects.
Params: none
Returns: array of project objects:
[
{
"path": "/Users/you/dev/myapp",
"name": "myapp",
"sessionName": "myapp-a1b2c3",
"isRunning": true,
"hasConfig": true,
"paneCount": 2,
"paneNames": ["claude", "server"],
"devCommand": "pnpm dev",
"packageManager": "pnpm"
}
]devCommand and packageManager are present only when detected.
projects.scan
Trigger a re-scan of the project directory. Useful after cloning a new
repo or adding a .lattices.json config.
Params: none
layers.list
List configured workspace layers and whether layer focus is active.
Params: none
Returns:
{
"layers": [
{ "id": "web", "label": "Web", "index": 0, "projectCount": 2 },
{ "id": "mobile", "label": "Mobile", "index": 1, "projectCount": 2 }
],
"active": 0,
"focusActive": true,
"rememberedIndex": 0,
"stage": { "parked": [], "hidden": [] }
}active is null after exiting layers. rememberedIndex retains the last
selected layer index; it does not mean that layer focus is active. stage
lists windows parked and apps hidden by Lattices. Returns an empty layers
array if no workspace config is loaded.
layers.exit
Exit layer focus and restore windows that Lattices parked or hid. Saved layer definitions and window desktop assignments remain intact. Apps the user hid stay hidden. Select a layer explicitly to enter layer focus again.
Params: none
Returns:
{
"ok": true,
"focusActive": false,
"active": null,
"unparked": 3,
"stillParked": 0,
"rescued": 0,
"unhidden": ["Slack"],
"stillHidden": [],
"recoveryPending": false,
"persistenceWarning": null,
"stage": { "parked": [], "hidden": [] }
}unparked counts restored windows. rescued counts untracked windows
recovered from the parking corner. stillParked and stillHidden report
restoration that remains pending; ok confirms exit, not complete recovery.
recoveryPending remains true while window restoration or saving the recovery
state is pending. persistenceWarning is a string when saving fails, or null
when saving succeeds. Restoration can finish before the state is saved, so
recoveryPending can be true when stillParked is zero and stillHidden is
empty. Lattices retains the recovery state and retries saving it.
A window on an inactive desktop can remain parked until that desktop is
visible. Lattices retains its saved position and retries restoration when
you visit the desktop. The exit action does not switch desktops or gather
windows onto the current desktop. Check recoveryPending and stage for
outstanding recovery.
layers.reveal
Compatibility alias for layers.exit, with the same result. The former
Show All action also exits layer focus. Lattices centres untracked windows
found in the parking corner on the main screen and counts them in rescued.
layers.members
Each layer's entries and the windows each one holds, on every desktop,
from the same resolution a layer switch uses. A window belongs to one
layer. presence says where it is: showing (on a desktop a display is
showing), elsewhere (another desktop, or full screen), parked (in the
corner a switch parks windows in) or hidden (its app is hidden).
missing says why an entry holds nothing: No window when its app runs,
Not open when it doesn't. Moves nothing.
Params:
| Name | Type | Required | Description |
|---|---|---|---|
layer |
string | no | Layer id or label (default: every layer) |
index |
int | no | Layer index |
Returns:
{
"active": 0,
"layers": [
{
"id": "web", "label": "Web", "index": 0, "slot": 1, "active": true, "layout": "auto",
"entries": [
{
"index": 0, "name": "Ghostty", "rule": "App contains Ghostty · Name: web", "pattern": "web",
"missing": null,
"windows": [
{ "wid": 4812, "app": "Ghostty", "title": "mini: web · server", "presence": "parked", "where": "Parked" }
]
},
{ "index": 1, "name": "Figma", "rule": "App contains Figma", "pattern": null, "missing": "Not open", "windows": [] }
]
}
]
}desktop.snapshot reports the active layer in the same shape as
activeLayer.
Editing layers
layers.create, layers.assign, layers.unassign, layers.rename and
layers.delete write the layers in ~/.lattices/workspace.json, the
same ones ⌘⌥ switches. The first write each launch copies the file to
workspace.json.bak. A window is saved as an entry of its app and its
title; while the window lives it stays in the layer when its title changes.
Each takes the layer as layer (id or label) or index; assign and
unassign default to the active layer, and take wid or windowIds.
| Method | Params | Returns |
|---|---|---|
layers.create |
name; windowIds, or windows: [{wid, tile?}], or visible (the default: every window on screen) |
{index, id, label, count} |
layers.assign |
wid / windowIds |
{index, added} |
layers.unassign |
wid / windowIds |
{index, removed, held}: held lists windows an entry matching other windows too (a bare app, a project) still holds |
layers.rename |
name |
ok |
layers.delete |
ok |
await daemonCall('layers.create', { name: 'review', windows: [{ wid: 1234, tile: 'left' }, { wid: 5678 }] })
await daemonCall('layers.assign', { layer: 'review', wid: 4321 })
await daemonCall('layers.unassign', { layer: 'review', wid: 4321 })layers.activate
Switch to a workspace layer. With no mode it switches as ⌘⌥ does.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
index |
number | no | Layer index (0-based) |
name |
string | no | Layer ID or label |
mode |
string | no | focus (default), tile, or launch |
Provide either index or name.
Modes:
focus— put away what the layer doesn't use, raise its windows and apply itslayout: the ⌘⌥ switchtile— focus, then move entries with atileto their place (retileis accepted too)launch— start the projects that aren't running, then tile
Returns: execution receipt including resolved layer, mode, and trace.
layers.switch
The same as layers.activate, focus unless a mode is given. It
posts a layer.switched event.
groups.launch
Launch a tab group session.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Group ID |
Errors: Not found if the group ID doesn't match any configured group.
groups.kill
Kill a tab group session.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Group ID |
Live tab stacks
Live stacks operate on windows that are already open and last for the current
Lattices run. If windowIds is omitted, create/add uses the latest multi-window
selection from Hyperspace.
lats call tabs.stack '{"name":"Research","placement":"top-left"}'
lats call tabs.layout '{"mode":"grid"}'
lats call tabs.select '{"index":1}'
lats call tabs.layout '{"mode":"tabs"}'
lats call tabs.unstack '{}'tabs.stack needs at least two live windows. You may instead pass
{"windowIds":[123,456]}. The optional id on add/select/layout/delete defaults
to the active group. Layout mode is tabs, grid, or toggle; selecting uses a
zero-based index or windowId.
Processes & Terminals
| Method | Type | Description |
|---|---|---|
processes.list |
read | Running developer processes |
processes.tree |
read | Process tree from a PID |
terminals.list |
read | Terminal instances with processes |
terminals.search |
read | Search terminals by criteria |
processes.list
List running processes relevant to development (editors, servers, build tools).
Params:
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | no | Filter by command name substring |
Returns: array of process objects:
[
{
"pid": 1234,
"ppid": 567,
"command": "node",
"args": "server.js",
"cwd": "/Users/you/dev/myapp",
"tty": "/dev/ttys003",
"tmuxSession": "myapp-a1b2c3",
"tmuxPaneId": "%0"
}
]processes.tree
Get the process tree rooted at a given PID.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
pid |
number | yes | Root PID |
Returns: array of process objects (same shape as processes.list).
terminals.capture
Exact tmux pane text. Not OCR, not a screenshot.
Provide one of session, paneId, or tty. If a session has more than one
pane, also pass pane or paneId.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
session |
string | one of session / paneId / tty | tmux or lattices session name |
pane |
string | no | Pane title or %id |
paneId |
string | no | tmux pane id (%12) |
tty |
string | no | /dev/ttys003 or ttys003 |
lines |
number | no | Last N lines. Default 80, max 500 |
escape |
boolean | no | Keep ANSI. Default false (tmux capture-pane -p) |
Returns:
{
"ok": true,
"session": "lattices-a1b2c3",
"paneId": "%3",
"paneName": "claude",
"tty": "/dev/ttys012",
"text": "…last N lines…",
"lineCount": 80
}Ambiguous targets return an error listing candidates. Missing tmux returns a clear error. Never falls back to pixels.
lats call terminals.capture '{"session":"lattices-c36f74","lines":40}'terminals.list
List all discovered terminal instances with their processes, tabs, and tmux associations.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
refresh |
boolean | no | Explicitly refresh terminal-tab metadata through terminal app scripting |
Returns: array of terminal instance objects:
[
{
"tty": "/dev/ttys003",
"app": "Terminal",
"windowIndex": 0,
"tabIndex": 0,
"isActiveTab": true,
"tabTitle": "myapp",
"processes": [ ... ],
"shellPid": 1234,
"cwd": "/Users/you/dev/myapp",
"tmuxSession": "myapp-a1b2c3",
"tmuxPaneId": "%0",
"hasClaude": true,
"displayName": "Terminal — myapp",
"windowId": 4211,
"windowAttribution": "terminal"
}
]Terminal coverage. With refresh: true, Lattices reads tab data from these
terminal apps, and only from apps that are already running:
| Terminal | Tab data | Window match | What the terminal needs |
|---|---|---|---|
| iTerm2 | AppleScript | Window ID | Automation access |
| Terminal.app | AppleScript | Window ID | Automation access |
| Ghostty 1.3+ | AppleScript (tab title, terminal ID, cwd) | Window title, when one window has it | Automation access that you already granted. Lattices does not ask for it. |
| kitty | kitty @ ls |
Window ID | allow_remote_control and listen_on in kitty.conf |
| Warp, Alacritty, others | None | Process tree only | Nothing |
Ghostty reports no TTY, so Lattices joins each Ghostty terminal to a TTY by
its working directory. It skips a terminal when two shells share that
directory. When tab data is not available, Lattices finds the terminal
process that owns the TTY. It picks that process's window when the process
has one titled window, or when exactly one window title contains the shell's
cwd. Otherwise the TTY has no windowId. It has windowAttribution: "app",
and windowCandidates lists the windows of that process, frontmost first.
windowAttribution values: tag (the window title has the [lattices:…]
tag), terminal (the terminal reported the window ID), terminal-title,
position, cwd-title, single-window, app.
terminals.search
Search terminal instances by various criteria.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | no | Filter by command name substring |
cwd |
string | no | Filter by working directory substring |
app |
string | no | Filter by terminal app name |
session |
string | no | Filter by tmux session name |
hasClaude |
boolean | no | Filter to only Claude-running TTYs |
Returns: filtered array of terminal instance objects (same shape as terminals.list).
OCR
| Method | Type | Description |
|---|---|---|
ocr.snapshot |
read | Current OCR results for all visible windows |
ocr.search |
read | Full-text search across OCR history |
ocr.history |
read | OCR timeline for one window, or all windows |
ocr.scan |
write | Trigger an immediate OCR scan |
See Screen OCR for configuration, scan schedules, and storage details.
ocr.snapshot
Get the current in-memory OCR results for all visible windows.
Params: none
Returns: array of OCR result objects:
[
{
"wid": 1234,
"app": "Terminal",
"title": "zsh",
"frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
"fullText": "~/dev/myapp $ npm run dev\nready on port 3000",
"blocks": [
{
"text": "~/dev/myapp $ npm run dev",
"confidence": 0.95,
"x": 0.02, "y": 0.05, "w": 0.6, "h": 0.04
}
],
"timestamp": 1709568000.0
}
]ocr.search
Full-text search across OCR history using SQLite FTS5.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
query |
string | yes | FTS5 search query |
app |
string | no | Filter by application name |
limit |
number | no | Max results (default 50) |
live |
boolean | no | Search live snapshot instead of history (default false) |
FTS5 query examples: error, "build failed", error OR warning, npm AND dev, react*
ocr.history
Get the OCR timeline for a specific window, or across every window when
wid is omitted, ordered by most recent first.
Params:
| Field | Type | Required | Description |
|---|---|---|---|
wid |
number | no | CGWindowID; omit for every window |
limit |
number | no | Max results (default 50) |
ocr.scan
Trigger an immediate OCR scan of all visible windows, bypassing the
periodic timer. Results available via ocr.snapshot once complete;
an ocr.scanComplete event is broadcast to all clients.
Params: none
Events
Events are pushed to all connected WebSocket clients when state changes.
They have no id field — listen for messages with an event field.
| Event | Trigger |
|---|---|
windows.changed |
Desktop window list changes |
tmux.changed |
Sessions created, killed, or modified |
layer.switched |
Active workspace layer changes |
ocr.scanComplete |
OCR scan cycle finishes |
processes.changed |
Developer processes start or stop |
A connection receives every event until it filters them. events.subscribe
takes { "events": ["windows.changed"] } (or ["*"] for all again);
events.unsubscribe removes names, or every event when called without a
list. Both act on the connection they arrive on and return its current
filter. Linux hosts (LAT-013) push windows.changed and spaces.changed.
windows.changed
{ "event": "windows.changed", "data": { "windowCount": 12, "added": [1234], "removed": [5678] } }tmux.changed
{ "event": "tmux.changed", "data": { "sessionCount": 3, "sessions": ["myapp-a1b2c3"] } }layer.switched
{ "event": "layer.switched", "data": { "index": 1, "name": "mobile" } }ocr.scanComplete
{ "event": "ocr.scanComplete", "data": { "windowCount": 12, "totalBlocks": 342 } }processes.changed
{ "event": "processes.changed", "data": { "interestingCount": 5, "pids": [1234, 5678] } }Agent integration
CLAUDE.md snippet
Add this to your project's CLAUDE.md so any AI agent working in the
project knows how to control the workspace:
## Workspace Control
This project uses lattices for workspace management. The daemon API
uses authenticated WebSocket over ~/.lattices/run/lattices.sock.
Use a CLI or daemonCall client that matches the app version for discovery and authentication.
### Search (find windows)
- Search by content: `daemonCall('windows.search', { query: 'myproject' })`
Returns windows with `matchSource` ("title", "app", "session", "ocr") and `ocrSnippet`
- Search terminals: `daemonCall('terminals.search', {})` — tabs, cwds, processes
- CLI: `lats search myproject`, `lats search myproject --deep`, or `lats search myproject --all` (same as `--deep`)
### Actions
- Focus a window: `daemonCall('windows.focus', { wid: 1234 })`
- Place a window: `daemonCall('windows.place', { session: 'name', placement: 'left' })`
- Launch a project: `daemonCall('sessions.launch', { path: '/absolute/path' })`
- Switch layers: `daemonCall('layers.activate', { name: 'web' })`; add `mode: 'launch'` to start what isn't running
- Optimize the workspace: `daemonCall('spaces.optimize', { scope: 'visible', strategy: 'balanced' })`
- CLI: `lats place myproject left` (search + focus + tile in one step)
### Import
\```js
import { daemonCall } from '@lattices/lattices'
\```Multi-agent orchestration
An orchestrator agent can set up the full workspace for sub-agents:
import { daemonCall } from '@lattices/lattices'
// Discover what's available
const projects = await daemonCall('projects.list')
// Launch the projects we need
await daemonCall('sessions.launch', { path: '/Users/you/dev/frontend' })
await daemonCall('sessions.launch', { path: '/Users/you/dev/api' })
// Tile them side by side
const sessions = await daemonCall('tmux.list')
const fe = sessions.find(s => s.name.startsWith('frontend'))
const api = sessions.find(s => s.name.startsWith('api'))
await daemonCall('windows.place', { session: fe.name, placement: 'left' })
await daemonCall('windows.place', { session: api.name, placement: 'right' })Reactive event pattern
Subscribe to events to react to workspace changes:
import WebSocket from 'ws'
import { readFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
const runtime = process.env.LATTICES_RUNTIME_DIR || join(homedir(), '.lattices/run')
const token = readFileSync(join(runtime, 'lattices.token'), 'utf8').trim()
const ws = new WebSocket('ws://localhost', {
socketPath: join(runtime, 'lattices.sock'),
headers: { Host: 'localhost', 'x-lattices-token': token },
})
ws.on('message', (raw) => {
const msg = JSON.parse(raw)
if (msg.event === 'tmux.changed') {
console.log('Sessions:', msg.data.sessions.join(', '))
}
if (msg.event === 'windows.changed') {
console.log('Windows:', msg.data.windowCount, 'total')
}
if (msg.event === 'layer.switched') {
console.log('Switched to layer', msg.data.index)
}
})
// You can also send RPC calls on the same connection
ws.on('open', () => {
ws.send(JSON.stringify({ id: '1', method: 'daemon.status' }))
})Health check before use
Always verify the daemon is running before making calls:
import { isDaemonRunning, daemonCall } from '@lattices/lattices'
if (!(await isDaemonRunning())) {
console.error('lats daemon is not running — start it with: lats app')
process.exit(1)
}
const status = await daemonCall('daemon.status')
console.log(`Daemon up for ${Math.round(status.uptime)}s, tracking ${status.windowCount} windows`)Pi extension
Pi users can install the @arach/pi-lattices package in
packages/pi-lattices/ to expose the daemon as typed lattices_* tools:
pi install ./packages/pi-lattices --local
lats appThe extension wraps the existing daemon and keeps Lattices' macOS-native
runtime, run artifacts, action receipts, and computer-use treatment semantics.
It does not bundle cua-driver or enable browser automation. See
Pi Lattices Extension for the tool list and smoke checks.
Machines and visiting cursor
The Machines page uses these existing endpoints: visit.status, visit.pair,
visit.arm, visit.end, visit.screens, and visit.elsewhere.
| Method | Parameters | Result |
|---|---|---|
visit.forget |
host: string |
{ok: boolean}; false if absent. Ends an active visit to that host. |
visit.side |
host: string, side: left|right|top|bottom |
{ok: true}; errors for unknown hosts or invalid sides. Occupied sides swap, preserving both keys. |
mouse.home |
none | Ends the current visit and returns the cursor to this Mac. |
visit.pair requires host-side approval of the returned code. A new pairing
cannot replace an occupied side; move or forget its occupant first.
Machines and displays
lats @<host> <command...> routes daemon-backed CLI commands through callHost
using ~/.lattices/hosts.json and LATTICES_HOSTS. @local selects the local
endpoint. No SSH is used. Commands requiring local files, application lifecycle
or tmux actions report local-only; remote RPC failures never fall back to local
actions. lats @host call <method> [params-json] exposes the host's API directly.
A host only supports methods present in its installed version.
lats machines [--json] merges configured hosts, outbound visit pairings and
this machine by name/address. It probes host.describe and reports reachability,
OS, version/commit, display count, pairing and placement. Offline/unknown values
remain explicit. Pairing inventory falls back to the saved visit host list when
the local daemon is unavailable. JSON returns {machines, pairingError?}.
| Method | Parameters | Behavior |
|---|---|---|
host.describe |
none | Mac/Linux identity: platform, hostname, version, build (version/commit), displays, capabilities and methods. Unknown build fields are null. |
bring |
`display: int | string, or undo: true` |
main |
`display: int | string, keep?: bool` |
elsewhere |
`display: int | string, name?: string` |
here |
`display: int | string` |
visit.start |
host: string |
Starts an explicit visit at the midpoint of a paired machine's touching span, without arming edge crossing. Missing pairing, no touching span, overlap or capture failure is an error. End the current visit before starting another. |
home |
none | Alias of mouse.home: ends the visit and returns the cursor home. |
CLI forms: bring dell, bring --undo, main u32 --keep,
elsewhere u32 archie, here u32, visit archie, and home.
Display selectors use the display.gather resolver: display index from
display list, or a case-insensitive name fragment (first match). The existing
visit.main and visit.elsewhere screen parameters accept these same selectors;
CLI --display flags and capture display also accept names.
The Machines page, command bar and Machines menu use these same operations. The page shows version/commit and marks a lower numeric release version Behind. A differing commit without proven version ordering is Different build, not a claim about commit ancestry. The Machines page's Open action selects the existing still/live host view. No deployment, pairing, arming or visit occurs on page open.