@lattices/cli 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -4
- package/apps/mac/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -2
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +11 -0
- package/apps/mac/Lattices.entitlements +6 -0
- package/bin/assistant-intelligence.ts +41 -3
- package/bin/cli/capture.ts +252 -0
- package/bin/cli/daemon.ts +22 -0
- package/bin/cli/helpers.ts +105 -0
- package/bin/cli/layer.ts +178 -0
- package/bin/cli/runs.ts +43 -0
- package/bin/cli/search.ts +141 -0
- package/bin/cli/session.ts +32 -0
- package/bin/client.ts +2 -1
- package/bin/cua.ts +26 -0
- package/bin/infer.ts +22 -4
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +111 -12
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +29 -2
- package/bin/lattices.ts +729 -769
- package/docs/api.md +496 -3
- package/docs/app.md +5 -4
- package/docs/assistant-knowledge.md +130 -0
- package/docs/config.md +5 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +53 -0
- package/docs/mouse-gestures.md +40 -3
- package/docs/ocr.md +3 -0
- package/docs/prompts/hands-off-system.md +9 -1
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/{LAT-006-mira-in-lattices.md → LAT-006-runs-and-capture-in-lattices.md} +83 -70
- package/docs/quickstart.md +3 -1
- package/docs/reference/dewey.config.ts +1 -1
- package/docs/release.md +4 -3
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +5 -3
- package/docs/voice.md +3 -3
- package/package.json +27 -5
- package/packages/npm/sdk/cua.d.mts +1 -0
- package/packages/npm/sdk/cua.d.ts +188 -0
- package/packages/npm/sdk/cua.mjs +376 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Assistant Knowledge Base
|
|
3
|
+
title: Lattices — Assistant Knowledge
|
|
4
|
+
description: Orientation + capability map the in-app Workspace Assistant uses to explain Lattices and point to the right feature or doc
|
|
5
|
+
audience: assistant
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
> You are reading the Workspace Assistant's knowledge base. It summarizes what
|
|
9
|
+
> Lattices can do and links to the deeper docs. Treat the **structured context**
|
|
10
|
+
> in your prompt (current settings, file paths, CLI commands) as ground truth for
|
|
11
|
+
> *this user's* configuration; treat this file as ground truth for *how Lattices
|
|
12
|
+
> works*. When a question goes deeper than this summary, name the relevant doc
|
|
13
|
+
> (see [References](#references)) instead of guessing.
|
|
14
|
+
|
|
15
|
+
## What Lattices is
|
|
16
|
+
|
|
17
|
+
Lattices is an **agentic window manager for macOS** — a programmable workspace
|
|
18
|
+
that pairs a native menu bar app with managed tmux sessions and a scriptable
|
|
19
|
+
agent API. Three layers, one product:
|
|
20
|
+
|
|
21
|
+
1. **Programmable workspace** — a CLI and a WebSocket agent API (`ws://127.0.0.1:9399`,
|
|
22
|
+
35+ methods, real-time events) that let scripts and AI agents observe and drive
|
|
23
|
+
the desktop the same way a person does.
|
|
24
|
+
2. **Smart layout manager** — the menu bar app tracks every window across all
|
|
25
|
+
monitors: tiling, switchable layers, snap zones, and screen-text indexing.
|
|
26
|
+
3. **Managed tmux sessions** — declare a dev environment in `.lattices.json`;
|
|
27
|
+
Lattices builds it, runs it, and keeps it alive across reboots.
|
|
28
|
+
|
|
29
|
+
Requirements: macOS 26+, Node 18+; tmux only for session management.
|
|
30
|
+
See [Overview](/docs/overview) and [Concepts](/docs/concepts).
|
|
31
|
+
|
|
32
|
+
## Capability map
|
|
33
|
+
|
|
34
|
+
Each area below is a one-paragraph summary plus the doc to cite for detail.
|
|
35
|
+
|
|
36
|
+
### Window tiling & placement
|
|
37
|
+
Snap windows to preset positions — halves, quarters, thirds, maximize, center —
|
|
38
|
+
from the command palette or `lattices tile <position>`. There is also a grid
|
|
39
|
+
placement primitive: compact `CxR:c,r` starts at 1 for command entry, while
|
|
40
|
+
canonical `grid:CxR:c,r` starts at 0 for APIs. → [Tiling reference](/docs/tiling-reference), positions in [Configuration](/docs/config).
|
|
41
|
+
|
|
42
|
+
### Workspace layers & tab groups
|
|
43
|
+
Group projects into named **layers** you can switch between, and tab-group related
|
|
44
|
+
windows. `workspace.json` layers launch/focus/tile projects. Studio layers are
|
|
45
|
+
rule-backed live window sets persisted in `~/.lattices/layers.json`; their clauses
|
|
46
|
+
support app/title/session exact, substring, regex, Space, visibility, and exclusion
|
|
47
|
+
matches. → [Layers](/docs/layers).
|
|
48
|
+
|
|
49
|
+
### Command palette & menu bar app
|
|
50
|
+
The palette (**Cmd+Shift+M**) is the app's primary surface: launch projects, tile,
|
|
51
|
+
sync, restart, open settings — all searchable. → [Menu Bar App](/docs/app).
|
|
52
|
+
|
|
53
|
+
### tmux sessions (`.lattices.json`)
|
|
54
|
+
Declare panes, commands, and layout per project. `lattices start` builds/attaches a
|
|
55
|
+
persistent session named `<basename>-<hash>`; `lattices sync` reconciles a running
|
|
56
|
+
session to its config. **Ensure** re-runs exited commands on reattach; **prefill**
|
|
57
|
+
types them and waits. → [Concepts](/docs/concepts), [Configuration](/docs/config).
|
|
58
|
+
|
|
59
|
+
### Screen OCR & search
|
|
60
|
+
The app reads on-screen text via the Accessibility API (~60s) and Apple Vision OCR
|
|
61
|
+
on background windows (~2h), indexing everything with FTS5. Search across titles,
|
|
62
|
+
app names, session tags, and OCR with `lattices search <query>` (add `--deep` or
|
|
63
|
+
`--all` to inspect terminal tabs by cwd). → [Screen OCR & Search](/docs/ocr).
|
|
64
|
+
|
|
65
|
+
### Voice commands
|
|
66
|
+
Natural-language voice control for window management ("put the browser on the
|
|
67
|
+
right", "switch to the backend layer"). → [Voice Commands](/docs/voice).
|
|
68
|
+
|
|
69
|
+
### Mouse gestures
|
|
70
|
+
Hold a mouse button, draw a direction or shape, release — runs the matched action.
|
|
71
|
+
Configured via `mouseGestures.enabled` plus `~/.lattices/mouse-shortcuts.json`.
|
|
72
|
+
→ [Mouse Gestures](/docs/mouse-gestures).
|
|
73
|
+
|
|
74
|
+
### Agent API & CLI
|
|
75
|
+
Agents connect over WebSocket and get the same control as a person: list
|
|
76
|
+
windows/projects, launch sessions, tile, switch layers, read screen text, and
|
|
77
|
+
subscribe to events (`windows.changed`, `tmux.changed`, `layer.switched`).
|
|
78
|
+
→ [Agent Guide](/docs/agents), [Agent API](/docs/api).
|
|
79
|
+
|
|
80
|
+
### Project twins
|
|
81
|
+
Pi-backed project "twins" for mediated, persistent agent execution scoped to a
|
|
82
|
+
project. → [Project Twins](/docs/twins).
|
|
83
|
+
|
|
84
|
+
## Key shortcuts
|
|
85
|
+
|
|
86
|
+
| Shortcut | Action |
|
|
87
|
+
|----------|--------|
|
|
88
|
+
| **Cmd+Shift+M** | Open the command palette |
|
|
89
|
+
| `lattices tile <position>` | Tile the focused window (CLI) |
|
|
90
|
+
| `lattices layer [name\|index]` | Switch workspace layer (CLI) |
|
|
91
|
+
| **Ctrl+B** then `D` / `Z` / arrows | tmux: detach / zoom / move pane (inside a session) |
|
|
92
|
+
|
|
93
|
+
Tiling and grid hotkeys are user-configurable — for the live set, point the user to
|
|
94
|
+
Settings or the [Tiling reference](/docs/tiling-reference) rather than asserting one.
|
|
95
|
+
|
|
96
|
+
## CLI quick reference
|
|
97
|
+
|
|
98
|
+
`lattices` · `lattices init` · `lattices sync` · `lattices start` ·
|
|
99
|
+
`lattices restart [pane]` · `lattices tile <position>` · `lattices group [id]` ·
|
|
100
|
+
`lattices layer [name|index]` · `lattices windows --json` ·
|
|
101
|
+
`lattices search <query> [--deep|--all] [--json] [--wid]` · `lattices place <query> [position]` ·
|
|
102
|
+
`lattices app restart`. Full flags: [Configuration](/docs/config).
|
|
103
|
+
|
|
104
|
+
## Config & file locations
|
|
105
|
+
|
|
106
|
+
- **Per project:** `.lattices.json` in the project root (panes, commands, layout, ensure/prefill).
|
|
107
|
+
- **User config (`~/.lattices/`):** `workspace.json`, `layers.json`, `mouse-shortcuts.json`,
|
|
108
|
+
`snap-zones.json`, `clusters.json`, `ocr.db`, `lattices.log`.
|
|
109
|
+
- **Defaults domain:** `dev.lattices.app` (read/write app settings via `defaults`).
|
|
110
|
+
|
|
111
|
+
The exact current values and paths for *this* machine arrive in your structured
|
|
112
|
+
context — prefer those over the generic paths above when answering.
|
|
113
|
+
|
|
114
|
+
## References
|
|
115
|
+
|
|
116
|
+
| Topic | Doc |
|
|
117
|
+
|-------|-----|
|
|
118
|
+
| What it is / who it's for | [Overview](/docs/overview) |
|
|
119
|
+
| Install & first run | [Quickstart](/docs/quickstart) |
|
|
120
|
+
| Architecture, glossary, internals | [Concepts](/docs/concepts) |
|
|
121
|
+
| `.lattices.json`, CLI, tile positions | [Configuration](/docs/config) |
|
|
122
|
+
| Command palette, tiling, sessions | [Menu Bar App](/docs/app) |
|
|
123
|
+
| Tiling & grid placement | [Tiling reference](/docs/tiling-reference) |
|
|
124
|
+
| Layers & tab groups | [Layers](/docs/layers) |
|
|
125
|
+
| Screen OCR & full-text search | [Screen OCR & Search](/docs/ocr) |
|
|
126
|
+
| Voice control | [Voice Commands](/docs/voice) |
|
|
127
|
+
| Mouse gestures | [Mouse Gestures](/docs/mouse-gestures) |
|
|
128
|
+
| Agent contracts (voice/CLI/daemon) | [Agent Guide](/docs/agents) |
|
|
129
|
+
| WebSocket RPC method reference | [Agent API](/docs/api) |
|
|
130
|
+
| Project twins | [Project Twins](/docs/twins) |
|
package/docs/config.md
CHANGED
|
@@ -152,6 +152,7 @@ Run `lattices init` in your project directory to generate a starter
|
|
|
152
152
|
| `lattices hud sync` | Publish all registered HUD actors |
|
|
153
153
|
| `lattices search <query>` | Search windows by title, app, session, OCR |
|
|
154
154
|
| `lattices search <q> --deep` | Deep search: index + live terminal inspection |
|
|
155
|
+
| `lattices search <q> --all` | Same as `--deep` (all search sources) |
|
|
155
156
|
| `lattices search <q> --wid` | Print matching window IDs only (pipeable) |
|
|
156
157
|
| `lattices place <query> [pos]` | Deep search + focus + tile (default: bottom-right)|
|
|
157
158
|
| `lattices focus <session>` | Focus a session's window and switch Spaces |
|
|
@@ -281,6 +282,10 @@ Aliases: `left-half`/`left`, `right-half`/`right`, `top-half`/`top`,
|
|
|
281
282
|
Tiling respects the menu bar and dock. It uses the visible desktop
|
|
282
283
|
area, not the full screen.
|
|
283
284
|
|
|
285
|
+
For arbitrary cells, use compact `CxR:c,r` with 1-indexed coordinates
|
|
286
|
+
from the top-left, or canonical `grid:CxR:c,r` with 0-indexed coordinates.
|
|
287
|
+
Example: `lattices tile 4x4:1,2`.
|
|
288
|
+
|
|
284
289
|
### Smart app tiling
|
|
285
290
|
|
|
286
291
|
Use `lattices tile family` when you want lattices to arrange a whole
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Hyper+G in-place grid — snappiness & satisfaction brief
|
|
2
|
+
|
|
3
|
+
Context: `relayoutGroup()` already switched from sequential `RealWindowAnimator.setFrameRobust`
|
|
4
|
+
to `WindowTiler.batchMoveAndRaiseWindows` (SLS freeze + one AX pass/app), with UI refresh
|
|
5
|
+
deferred and a tap on commit. This is the "make it even better" pass.
|
|
6
|
+
|
|
7
|
+
Code anchors:
|
|
8
|
+
- Grid path: `WindowMotionMode.swift:1100 relayoutGroup()` → `:1138 distributeGroup()` (G = keycode 5, `:833`)
|
|
9
|
+
- Batch move: `WindowTiler.swift:1753 batchMoveAndRaiseWindows` (freeze `:1767`, AX enum `:1776`, activate `:1818`, unfreeze `:1825`)
|
|
10
|
+
- Sound: `DiagnosticLog.swift:162 AppFeedback.playTap`
|
|
11
|
+
- Optional anim: `RealWindowAnimator.swift` (Timer 60fps, 0.28s — NOT on the batch path)
|
|
12
|
+
- Chrome flash: `WindowTiler.swift:54 WindowHighlight.flash` (single-window only)
|
|
13
|
+
- No haptics anywhere in the codebase yet → green field.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## The governing principle
|
|
18
|
+
|
|
19
|
+
Perceived latency is bound to the **earliest** feedback the brain receives, not the moment
|
|
20
|
+
pixels finish moving. The AX move is 40–120ms of work we can't fully erase. So the play is:
|
|
21
|
+
**acknowledge on key-down (haptic + sound + overlay), move under cover, let the windows catch
|
|
22
|
+
up.** Every quick win below is a variant of "fire feedback before the slow thing finishes."
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick wins (ship this week, low risk)
|
|
27
|
+
|
|
28
|
+
### Q1 — Haptic on key-down (biggest bang, ~10 lines)
|
|
29
|
+
There is zero haptic feedback today. `NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)`
|
|
30
|
+
is *exactly* the Loop/Magnet "snap" feel on trackpad/Force Touch. Fire it the instant `G` is
|
|
31
|
+
matched in `keyDown` (`:833`), **before** `distributeGroup()` runs. Costs nothing, lands on the
|
|
32
|
+
keypress, and the windows snapping ~80ms later reads as "instant + tactile."
|
|
33
|
+
|
|
34
|
+
### Q2 — Decouple + pre-warm the tap sound
|
|
35
|
+
`playTap` does `DispatchQueue.main.async { stop(); play() }`. Three problems:
|
|
36
|
+
- the async hop delays the sound ~1 runloop turn past the keypress,
|
|
37
|
+
- `stop()` then `play()` adds a tiny dead gap,
|
|
38
|
+
- NSSound's *first* play in a session stutters (codec spin-up).
|
|
39
|
+
|
|
40
|
+
Fixes: (a) prime the sound once on motion-mode entry by playing `tap.wav` at volume 0; (b) fire
|
|
41
|
+
it synchronously on key-down (same site as the haptic), not after the move; (c) consider a
|
|
42
|
+
prepared `AVAudioPlayer` (`prepareToPlay()`) or `AudioServicesPlaySystemSound` for sub-frame
|
|
43
|
+
latency. Net: the *thunk* and the *tap* (haptic) coincide with the keypress, not the landing.
|
|
44
|
+
|
|
45
|
+
### Q3 — Shrink the SLS freeze window
|
|
46
|
+
`batchMoveAndRaiseWindows` does the slow `kAXWindowsAttribute` enumeration **inside** the
|
|
47
|
+
`SLSDisableUpdate` freeze (`:1776`). The screen is frozen while we do AX queries. Resolve the
|
|
48
|
+
AX elements *before* the freeze, freeze only around the set-frame writes, unfreeze immediately.
|
|
49
|
+
Bonus: `relayoutGroup` already resolves each element via `recordOriginal`'s `ax(for: m)` (`:1113`)
|
|
50
|
+
and then the batch path re-resolves the whole window list — pass the resolved elements in and
|
|
51
|
+
skip the second AX pass entirely. Shorter freeze = less black-flash, snappier reveal.
|
|
52
|
+
|
|
53
|
+
### Q4 — Stop activating every app
|
|
54
|
+
`batchMoveAndRaiseWindows` calls `app.activate()` once per pid (`:1818`). With windows from N
|
|
55
|
+
apps that's N activations → focus churn, Space flicker, and the overlay can lose key. AX
|
|
56
|
+
`kAXRaiseAction` (already issued at `:1806`) reorders without activating. Activate **only** the
|
|
57
|
+
app that should end up frontmost (the last-picked / aimed window), once. Raise the rest.
|
|
58
|
+
|
|
59
|
+
### Q5 — Overlay "snap-pop" on the selection chrome
|
|
60
|
+
The real move is a teleport, so add the *sense* of motion in the cheap overlay layer (no AX
|
|
61
|
+
cost). When the grid lands, give each selection border a tiny scale overshoot (1.0→1.04→1.0,
|
|
62
|
+
~120ms, Core Animation) + a one-shot green edge flash converging on the slot. This is the
|
|
63
|
+
Arc/Raycast "it clicked into place" pop. Overlay-only, runs off the AX path, can't add input
|
|
64
|
+
latency.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Bigger bets (more design, higher ceiling)
|
|
69
|
+
|
|
70
|
+
### B1 — Ghost-slot anticipation (kills perceived latency outright)
|
|
71
|
+
On `G`, **instantly** paint the target grid as outlined ghost slots (you already compute
|
|
72
|
+
`balancedGrid` rects → `tileFrame`). Animate the slots filling (or the picked windows' chrome
|
|
73
|
+
sliding toward their slots) over ~140ms while the real `batchMove` runs underneath. The user
|
|
74
|
+
sees motion begin on the same frame as the keypress; the real windows arrive under the
|
|
75
|
+
animation. This fully decouples felt-speed from AX latency and is the single highest-ceiling
|
|
76
|
+
change. The ghost overlay is also the natural home for Q5's pop.
|
|
77
|
+
|
|
78
|
+
### B2 — Precompute on selection, commit = pure writes
|
|
79
|
+
Every pluck/unpluck currently recomputes `balancedGrid` + `tileFrame` and re-resolves AX. Cache
|
|
80
|
+
(a) the AX element per picked wid and (b) the target frame map, updated incrementally as the
|
|
81
|
+
selection changes. By the time `G` fires, commit is nothing but the frozen set-frame loop —
|
|
82
|
+
nothing to compute, nothing to resolve, minimal freeze.
|
|
83
|
+
|
|
84
|
+
### B3 — Staggered cascade fill (the "deliberate fast" feel)
|
|
85
|
+
Real windows must move simultaneously (per-window AX animation = jank — see "avoid"). But the
|
|
86
|
+
*overlay* tiles/borders can land on a micro-stagger in reading order (~10–14ms/cell). The eye
|
|
87
|
+
reads a fast left-to-right cascade as more intentional and premium than a flat simultaneous
|
|
88
|
+
snap, while the actual work stays a single batch. Pure overlay timing.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What to avoid (these ADD latency or jank)
|
|
93
|
+
|
|
94
|
+
- **Don't animate many real windows via per-tick AX writes.** `RealWindowAnimator`'s 0.28s
|
|
95
|
+
Timer loop is fine for one window; across a group it's 60fps × N AX position+size writes —
|
|
96
|
+
AX is slow and serializes, so it stutters and feels *slower* than a clean teleport. Keep the
|
|
97
|
+
batch teleport; animate the overlay, not the windows.
|
|
98
|
+
- **Don't do AX enumeration / `DesktopModel.poll()` inside the freeze or on the commit path.**
|
|
99
|
+
Poll/rebuild are already deferred (`refreshAfterGridMove`, `:1125`) — keep it that way; don't
|
|
100
|
+
let new chrome work creep back onto the hot path.
|
|
101
|
+
- **Don't async-dispatch or `stop()`-then-`play()` the commit sound** (see Q2) — both push the
|
|
102
|
+
thunk past the keypress.
|
|
103
|
+
- **Don't `app.activate()` per app** (Q4) — multi-app activation is the main source of flicker
|
|
104
|
+
and overlay focus loss.
|
|
105
|
+
- **Avoid Timer-driven overlay animation.** Use Core Animation / `CADisplayLink`; `Timer` at
|
|
106
|
+
1/60 drifts and can hitch under load.
|
|
107
|
+
- **Don't add dwell.** `WindowHighlight.flash` defaults to a 0.9s dwell + 0.3s fade — fine for a
|
|
108
|
+
one-shot locate, wrong for a snap pop. Snap feedback wants ~120–180ms total. Long fades read
|
|
109
|
+
as lag, not polish.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Recommended ship order
|
|
114
|
+
|
|
115
|
+
1. **Q1 + Q2** together — one small change at the `G` key site: haptic + synchronous pre-warmed
|
|
116
|
+
sound on key-down. Instant tactile upgrade, ~20 lines, no risk.
|
|
117
|
+
2. **Q3 + Q4** — tighten `batchMoveAndRaiseWindows` (pre-resolve AX, shorter freeze, single
|
|
118
|
+
activate). Real latency reduction.
|
|
119
|
+
3. **Q5** — overlay snap-pop on the chrome. First "ooh" moment.
|
|
120
|
+
4. **B1** — ghost-slot anticipation once Q5's overlay exists to build on. This is the headliner.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Implementation sketches
|
|
125
|
+
|
|
126
|
+
### Sketch 1 — Haptic + tactile commit on key-down (Q1+Q2)
|
|
127
|
+
At `WindowMotionMode.swift:833`, before dispatching the grid:
|
|
128
|
+
```swift
|
|
129
|
+
case 5: // G
|
|
130
|
+
if exposed { gatherInPlace() }
|
|
131
|
+
else {
|
|
132
|
+
AppFeedback.shared.commitTactile() // haptic + sound, synchronous, NOW
|
|
133
|
+
distributeGroup() // remove the playTapSound() from relayoutGroup
|
|
134
|
+
}
|
|
135
|
+
return
|
|
136
|
+
```
|
|
137
|
+
New in `AppFeedback`:
|
|
138
|
+
```swift
|
|
139
|
+
func warmUp() { // call on motion-mode entry
|
|
140
|
+
tapSound?.volume = 0; tapSound?.play(); tapSound?.stop(); tapSound?.volume = 1
|
|
141
|
+
}
|
|
142
|
+
func commitTactile() { // called on the keypress, on main
|
|
143
|
+
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .now)
|
|
144
|
+
tapSound?.currentTime = 0
|
|
145
|
+
tapSound?.play() // no async hop, no stop() gap
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
Remove `AppFeedback.shared.playTapSound()` from `relayoutGroup()` (`:1120`) so the feedback is
|
|
149
|
+
owned by the keypress, not the move completion.
|
|
150
|
+
|
|
151
|
+
### Sketch 2 — Pre-resolved, minimal-freeze batch (Q3+Q4)
|
|
152
|
+
New overload that trusts caller-resolved elements and freezes only the writes:
|
|
153
|
+
```swift
|
|
154
|
+
static func batchSetFrames(_ moves: [(el: AXUIElement, pid: Int32, frame: CGRect, raise: Bool)],
|
|
155
|
+
frontmostPid: Int32?) {
|
|
156
|
+
// app -> enhanced-UI off, BEFORE freeze
|
|
157
|
+
let pids = Set(moves.map(\.pid))
|
|
158
|
+
let appRefs = Dictionary(uniqueKeysWithValues: pids.map { ($0, AXUIElementCreateApplication($0)) })
|
|
159
|
+
appRefs.values.forEach { AXUIElementSetAttributeValue($0, "AXEnhancedUserInterface" as CFString, false as CFTypeRef) }
|
|
160
|
+
|
|
161
|
+
let cid = _SLSMainConnectionID?()
|
|
162
|
+
if let cid { _ = _SLSDisableUpdate?(cid) } // freeze ONLY the writes
|
|
163
|
+
for m in moves {
|
|
164
|
+
setFrameTriplet(m.el, m.frame) // size→pos→size
|
|
165
|
+
if m.raise { AXUIElementPerformAction(m.el, kAXRaiseAction as CFString) }
|
|
166
|
+
}
|
|
167
|
+
if let cid { _ = _SLSReenableUpdate?(cid) } // unfreeze immediately
|
|
168
|
+
|
|
169
|
+
appRefs.values.forEach { AXUIElementSetAttributeValue($0, "AXEnhancedUserInterface" as CFString, true as CFTypeRef) }
|
|
170
|
+
if let frontmostPid { NSRunningApplication(processIdentifier: frontmostPid)?.activate() } // ONE activate
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
`relayoutGroup` already has `el` in hand from `recordOriginal` — collect it into `moves` and
|
|
174
|
+
call this; no second `kAXWindows` enumeration, freeze shrinks to just the set-frame loop.
|
|
175
|
+
|
|
176
|
+
### Sketch 3 — Overlay snap-pop on selection chrome (Q5)
|
|
177
|
+
On grid landing, per slot, run a layer animation on the existing selection border view (overlay,
|
|
178
|
+
not the window):
|
|
179
|
+
```swift
|
|
180
|
+
let pop = CAKeyframeAnimation(keyPath: "transform.scale")
|
|
181
|
+
pop.values = [1.0, 1.045, 1.0]; pop.keyTimes = [0, 0.45, 1]
|
|
182
|
+
pop.duration = 0.13; pop.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
|
183
|
+
borderLayer.add(pop, forKey: "snapPop")
|
|
184
|
+
// + a one-shot edge tint that fades over the same 0.13s
|
|
185
|
+
```
|
|
186
|
+
No dwell, no AX. Fire it from `refreshAfterGridMove` (`:1125`) so it rides the deferred turn.
|
|
187
|
+
|
|
188
|
+
### Sketch 4 — Ghost-slot anticipation (B1)
|
|
189
|
+
On `G`, before the move, draw target slots in the motion overlay and animate the picked windows'
|
|
190
|
+
*chrome* (or ghost rects) from current → slot, while `batchSetFrames` runs underneath:
|
|
191
|
+
```swift
|
|
192
|
+
func previewGrid(_ rects: [CGRect]) { // rects already from balancedGrid → tileFrame
|
|
193
|
+
for (i, r) in rects.enumerated() {
|
|
194
|
+
let slot = ghostLayer(at: currentChromeFrame(i))
|
|
195
|
+
slot.frame = currentChromeFrame(i)
|
|
196
|
+
CATransaction.begin()
|
|
197
|
+
CATransaction.setAnimationDuration(0.14)
|
|
198
|
+
slot.frame = r // slides to target as the real window teleports under it
|
|
199
|
+
CATransaction.commit()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
Sequence: keypress → `commitTactile()` (Sketch 1) + `previewGrid()` same frame → `batchSetFrames()`
|
|
204
|
+
→ ghosts fade as real windows land → `refreshAfterGridMove`. Felt latency ≈ 0; the move hides
|
|
205
|
+
under 140ms of overlay motion.
|
|
206
|
+
|
|
207
|
+
### Sketch 5 — Staggered cascade (B3, optional flourish)
|
|
208
|
+
In `previewGrid`/snap-pop, offset each cell's animation `beginTime` by `index * 0.012` in
|
|
209
|
+
row-major order. Overlay-only; the real batch stays simultaneous. Toggle behind a setting if you
|
|
210
|
+
want a "calm" vs "playful" feel.
|
package/docs/layers.md
CHANGED
|
@@ -315,6 +315,59 @@ event is broadcast to all connected clients.
|
|
|
315
315
|
|
|
316
316
|
More methods in the [Agent API reference](/docs/api).
|
|
317
317
|
|
|
318
|
+
## Rule-backed Studio layers
|
|
319
|
+
|
|
320
|
+
Studio layers are live window rules stored in `~/.lattices/layers.json`.
|
|
321
|
+
They are separate from `workspace.json` launch-and-tile layers: Studio
|
|
322
|
+
layers do not launch projects. They resolve matching desktop windows,
|
|
323
|
+
then recall or scope those windows in Studio and Screen Map.
|
|
324
|
+
|
|
325
|
+
Each layer has a `match` array. A window joins the layer when it matches
|
|
326
|
+
any clause in that array. Inside one clause, every present positive field
|
|
327
|
+
must match, and every clause in `not` must fail.
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
[
|
|
331
|
+
{
|
|
332
|
+
"id": "review",
|
|
333
|
+
"name": "Review",
|
|
334
|
+
"match": [
|
|
335
|
+
{
|
|
336
|
+
"appEquals": "Google Chrome",
|
|
337
|
+
"titleRegex": "(GitHub|Pull Request)",
|
|
338
|
+
"not": [
|
|
339
|
+
{ "titleContains": "Actions" }
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
"sessionContains": "lattices",
|
|
344
|
+
"isOnScreen": true
|
|
345
|
+
}
|
|
346
|
+
]
|
|
347
|
+
}
|
|
348
|
+
]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Supported clause fields:
|
|
352
|
+
|
|
353
|
+
| Field | Match |
|
|
354
|
+
|-------|-------|
|
|
355
|
+
| `app` | App name contains this string |
|
|
356
|
+
| `appEquals` | App name exactly equals this string |
|
|
357
|
+
| `appRegex` | App name matches this regular expression |
|
|
358
|
+
| `titleContains` | Window title contains this string |
|
|
359
|
+
| `titleEquals` | Window title exactly equals this string |
|
|
360
|
+
| `titleRegex` | Window title matches this regular expression |
|
|
361
|
+
| `session` | Parsed lattices tmux session exactly equals this string |
|
|
362
|
+
| `sessionContains` | Parsed lattices tmux session contains this string |
|
|
363
|
+
| `isOnScreen` | Window is, or is not, visible on the current Space |
|
|
364
|
+
| `spaceId` | Window belongs to this macOS Space id |
|
|
365
|
+
| `not` | Exclusion clauses; any match rejects the window |
|
|
366
|
+
|
|
367
|
+
`app` and `titleContains` are the original substring fields, so older
|
|
368
|
+
`layers.json` files continue to work. New layers created from plucked
|
|
369
|
+
windows use `appEquals` by default to avoid accidental substring matches.
|
|
370
|
+
|
|
318
371
|
### Layer bar
|
|
319
372
|
|
|
320
373
|
When a workspace config is loaded, a layer bar appears between the
|
package/docs/mouse-gestures.md
CHANGED
|
@@ -12,8 +12,9 @@ Mouse gestures are a user-level shortcut system for the macOS app. Hold a
|
|
|
12
12
|
configured mouse button, draw a direction or shape, then release to run the
|
|
13
13
|
matched action.
|
|
14
14
|
|
|
15
|
-
The app code owns the recognizer, action dispatcher, and JSON schema.
|
|
16
|
-
|
|
15
|
+
The app code owns the recognizer, action dispatcher, and JSON schema. The
|
|
16
|
+
gesture mappings are data, so agents can define or change hotkeys without
|
|
17
|
+
adding a named action in Swift. Your actual gesture mappings live in:
|
|
17
18
|
|
|
18
19
|
```bash
|
|
19
20
|
~/.lattices/mouse-shortcuts.json
|
|
@@ -36,6 +37,16 @@ Do not add personal shortcuts by changing `MouseGestureConfig.swift`. Add them
|
|
|
36
37
|
to the user JSON file instead. The Settings UI can open that file from
|
|
37
38
|
**Settings -> Shortcuts -> Mouse Gestures -> Configure...**.
|
|
38
39
|
|
|
40
|
+
Agents should usually write these rules through the daemon:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
await daemonCall('mouse.shortcuts.upsert', { rule })
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The daemon persists the rule, snapshots the old config, and refreshes the
|
|
47
|
+
running app immediately. If a tool edits `~/.lattices/mouse-shortcuts.json`
|
|
48
|
+
directly, call `mouse.shortcuts.reload` afterward; no app restart is required.
|
|
49
|
+
|
|
39
50
|
## Lightweight History
|
|
40
51
|
|
|
41
52
|
Lattices keeps local snapshots of your mouse shortcut config before risky
|
|
@@ -100,6 +111,32 @@ The circle recognizer is intentionally generous: it looks for broad circular
|
|
|
100
111
|
motion, a reasonably closed path, and enough turn coverage rather than a
|
|
101
112
|
geometrically perfect circle.
|
|
102
113
|
|
|
114
|
+
## Default: Dictation Gesture
|
|
115
|
+
|
|
116
|
+
New default configs use a plain shortcut action for dictation. The middle-button
|
|
117
|
+
up gesture sends the Lattices voice command hotkey, currently Hyper+D:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"id": "dictation",
|
|
122
|
+
"enabled": true,
|
|
123
|
+
"device": "any",
|
|
124
|
+
"trigger": {
|
|
125
|
+
"button": "middle",
|
|
126
|
+
"kind": "drag",
|
|
127
|
+
"direction": "up"
|
|
128
|
+
},
|
|
129
|
+
"action": {
|
|
130
|
+
"type": "shortcut.send",
|
|
131
|
+
"shortcut": {
|
|
132
|
+
"key": "d",
|
|
133
|
+
"keyCode": 2,
|
|
134
|
+
"modifiers": ["control", "option", "shift", "command"]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
103
140
|
## Default: Screenshot Area Gesture
|
|
104
141
|
|
|
105
142
|
New default configs include a back-button circle gesture for the macOS
|
|
@@ -139,7 +176,7 @@ Supported action types include:
|
|
|
139
176
|
| `space.previous` | Switch to the previous macOS Space |
|
|
140
177
|
| `space.next` | Switch to the next macOS Space |
|
|
141
178
|
| `screenmap.toggle` | Open the Screen Map overview |
|
|
142
|
-
| `dictation.start` |
|
|
179
|
+
| `dictation.start` | Legacy compatibility alias that presses the configured Voice Command hotkey |
|
|
143
180
|
| `shortcut.send` | Send a keyboard shortcut |
|
|
144
181
|
| `app.activate` | Activate an app by name |
|
|
145
182
|
|
package/docs/ocr.md
CHANGED
|
@@ -65,6 +65,9 @@ lattices search "error"
|
|
|
65
65
|
# Deep search — also inspects terminal tabs and processes
|
|
66
66
|
lattices search "myproject" --deep
|
|
67
67
|
|
|
68
|
+
# Same as --deep (all search sources)
|
|
69
|
+
lattices search "myproject" --all
|
|
70
|
+
|
|
68
71
|
# Search + focus + tile the top result
|
|
69
72
|
lattices place "myproject" left
|
|
70
73
|
|
|
@@ -62,6 +62,11 @@ User: "put my terminals in a grid on the right"
|
|
|
62
62
|
{"actions": [{"intent": "distribute", "slots": {"app": "iTerm2", "region": "right"}}], "spoken": "Gridding your terminals on the right half."}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
User: "put chrome in the top-right cell of a 4x4 grid"
|
|
66
|
+
```json
|
|
67
|
+
{"actions": [{"intent": "tile_window", "slots": {"wid": 12345, "position": "grid:4x4:3,0"}}], "spoken": "Chrome to the top-right of a 4×4."}
|
|
68
|
+
```
|
|
69
|
+
|
|
65
70
|
User: "organize my chrome windows on the left"
|
|
66
71
|
```json
|
|
67
72
|
{"actions": [{"intent": "distribute", "slots": {"app": "Google Chrome", "region": "left"}}], "spoken": "Arranging your Chrome windows on the left."}
|
|
@@ -174,8 +179,11 @@ Grid-based tiling. Every position is a cell in a cols×rows grid.
|
|
|
174
179
|
**3x2 (sixths):** top-left-third, top-center-third, top-right-third, bottom-left-third, bottom-center-third, bottom-right-third
|
|
175
180
|
**4x1 (fourths):** first-fourth, second-fourth, third-fourth, last-fourth
|
|
176
181
|
**4x2 (eighths):** top-first-fourth, top-second-fourth, top-third-fourth, top-last-fourth, bottom-first-fourth, bottom-second-fourth, bottom-third-fourth, bottom-last-fourth
|
|
182
|
+
**4x4 (sixteenths):** no named cells — address them directly with `grid:4x4:C,R` (top-left `grid:4x4:0,0`, top-right `grid:4x4:3,0`, bottom-left `grid:4x4:0,3`, bottom-right `grid:4x4:3,3`, near-center `grid:4x4:1,1`)
|
|
183
|
+
|
|
184
|
+
For arbitrary grids, use the syntax `grid:CxR:C,R` where C=columns, R=rows, then col,row (0-indexed). Example: `grid:5x3:2,1` = center cell of a 5×3 grid; `grid:4x4:3,0` = top-right of a 4×4 (each cell a sixteenth of the screen).
|
|
177
185
|
|
|
178
|
-
|
|
186
|
+
To span a window across a rectangular block of cells, address two inclusive corners: `grid:CxR:c0,r0-c1,r1`. Example: `grid:4x4:0,0-1,1` = the top-left 2×2 block (a quarter) of a 4×4 grid; `grid:4x4:0,0-3,1` = the top half. Order doesn't matter — the corners are normalized.
|
|
179
187
|
|
|
180
188
|
When the user says "quarter" they mean a 2×2 cell (top-left, top-right, etc.), not a 4×1 fourth.
|
|
181
189
|
When they say "third" they usually mean a 3×1 column, but "top third" means the 3×2 row.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# LAT-006 Follow-up: Next Use Cases & Gaps (Load / Voice / Type / Click)
|
|
2
|
+
|
|
3
|
+
Assessment date: 2026-06-17. Scope: current working tree on `main` (uncommitted LAT-006
|
|
4
|
+
runs/capture/computer-use slice). Focus: root-cause product/API shape, not workarounds.
|
|
5
|
+
|
|
6
|
+
## What exists today (grounded)
|
|
7
|
+
|
|
8
|
+
A real **observe → act → capture → trace** layer landed (LAT-006 Phase 2 + a computer-use
|
|
9
|
+
extension), wired end to end:
|
|
10
|
+
|
|
11
|
+
- **Daemon** (`apps/mac/Sources/Core/Daemon/LatticesApi.swift`):
|
|
12
|
+
- `runs.create | runs.list | runs.get | runs.artifacts`
|
|
13
|
+
- `capture.screenshotWindow`
|
|
14
|
+
- `computer.prepare | computer.focusWindow | computer.typeText | computer.showCursor | computer.demoTerminal`
|
|
15
|
+
- `settings.cursorAppearance.get | .set`
|
|
16
|
+
- **Controllers**: `Core/Actions/ComputerUseController.swift`, `Core/Capture/CaptureController.swift`,
|
|
17
|
+
`Core/Runs/RunStore.swift` + `RunModels.swift` (persists to `~/Library/Application Support/Lattices/Runs/`, `runs.json` index).
|
|
18
|
+
- **CLI** (`bin/lattices.ts`): `lattices computer|capture|runs|terminals`.
|
|
19
|
+
- **Palette** (`PaletteCommand.swift`): "Screenshot Current Window", "Review Runs" under a new `.run` category → `ScreenMapWindowController.showPage(.runs)` + `RunsReviewView.swift`.
|
|
20
|
+
- **Safety model**: `ComputerTreatment` = observe/stage/present/execute. Typing only targets
|
|
21
|
+
scored *safe* terminals (avoids claude/codex/vim, requires idle shell for Enter), with a transport
|
|
22
|
+
ladder: tmux `send-keys` → iTerm session `write text` → pasteboard/key-events (active tab only).
|
|
23
|
+
|
|
24
|
+
This is a strong foundation. The gaps below are about **shape**, not patching.
|
|
25
|
+
|
|
26
|
+
## Gaps by focus area (root-cause)
|
|
27
|
+
|
|
28
|
+
### 1. Loading apps — not a first-class, composable operation
|
|
29
|
+
App/project launch lives only inside the voice `launch` intent → `session.launch`
|
|
30
|
+
(`Intents/LaunchIntent.swift`), with a brittle fallback (capitalize first letter, `NSWorkspace`
|
|
31
|
+
name match; no bundle-id resolution). Root cause: **there is no `apps.launch` daemon verb and no
|
|
32
|
+
"wait until window exists" precondition.** So launching can't be wrapped as a Run, can't be composed
|
|
33
|
+
with capture/type/click, and the computer-use layer can't open a target app before acting on it.
|
|
34
|
+
→ Add `apps.launch` (name/bundleId/project) returning a `RunSession` with surfaces, plus a shared
|
|
35
|
+
`waitForWindow` primitive that `computer.focusWindow/typeText/click` reuse as a precondition.
|
|
36
|
+
|
|
37
|
+
### 2. Voice/talk flows — the new capabilities are invisible to voice
|
|
38
|
+
`IntentEngine.swift` vocabulary stops at workspace control: tile_window, focus, launch,
|
|
39
|
+
switch_layer, search, list_*, distribute, create_layer, kill, scan, swap, hide, highlight,
|
|
40
|
+
move_to_display, find_mouse, summon_mouse, undo. Root cause: **none of `computer.*`, `capture.*`,
|
|
41
|
+
or `runs.*` is registered as an intent.** Voice can move/observe windows but cannot drive the proof
|
|
42
|
+
loop (screenshot, type, review a run). The slot/dispatch plumbing already exists, so this is additive.
|
|
43
|
+
→ Register `screenshot` → `capture.screenshotWindow`; `type` → `computer.typeText` (default
|
|
44
|
+
`treatment=stage`, require explicit confirm before execute — matches the HandsOff "don't act on
|
|
45
|
+
questions" preference); `show_cursor`/`click` → cursor/click methods; `review_run` → runs page.
|
|
46
|
+
|
|
47
|
+
### 3. Typing — solid, but terminal-only
|
|
48
|
+
Strongest area. The transport ladder + safety scoring is the right shape. Root-cause limits:
|
|
49
|
+
- Typing targets **terminals exclusively** (`TerminalCandidate` / `ProcessModel.synthesizeTerminals`).
|
|
50
|
+
There is no "type into the focused text field of app X" (browser URL bar, native field).
|
|
51
|
+
- The pasteboard path saves/restores the clipboard but requires the tab already active; iTerm is
|
|
52
|
+
explicitly excluded from the keyboard transport, so non-tmux iTerm has no fallback.
|
|
53
|
+
→ Add an AX-based `computer.typeInto` that resolves the focused element / a target text field, keeping
|
|
54
|
+
the terminal path as the specialized *safe* case. This generalizes typing without weakening the
|
|
55
|
+
terminal safety model.
|
|
56
|
+
|
|
57
|
+
### 4. Clicking around — the verb does not exist
|
|
58
|
+
**There is no click action.** `computer.showCursor` only renders a visual marker; it posts no mouse
|
|
59
|
+
event. The only `CGEvent` mouse code (`Core/Input/MouseGestureController.swift`) *recognizes* gestures
|
|
60
|
+
— it does not synthesize targeted clicks. So the "act" half of the loop for non-terminal targets
|
|
61
|
+
(buttons, links, menu items) is entirely missing. This is the single biggest missing primitive.
|
|
62
|
+
→ Add `computer.click` (+ `computer.moveCursor`) that resolves a target (coordinate, AX element, or
|
|
63
|
+
window+role) and posts left/right down/up via `CGEvent`, wrapped in a Run with before/after capture,
|
|
64
|
+
gated by the same treatment model (observe/stage/present/execute).
|
|
65
|
+
|
|
66
|
+
### 5. Cross-cutting: the proof loop is open at both ends
|
|
67
|
+
- **Recording not implemented.** Proposal lists `capture.recordWindow/recordRegion` + an AppKit
|
|
68
|
+
`--recording-probe` (Phase 3); only screenshots exist (`WindowCapture` uses `SCStream` for stills).
|
|
69
|
+
Also missing: `runs.start`, `runs.stop`.
|
|
70
|
+
- **No verify step.** `typeText` captures before/after screenshots but never asserts the text landed.
|
|
71
|
+
An OCR/AX diff (reusing the existing `ocr.search`) would close observe → act → **verify** and feed
|
|
72
|
+
LAT-005 receipts.
|
|
73
|
+
|
|
74
|
+
## Suggested implementation order (highest leverage first)
|
|
75
|
+
|
|
76
|
+
1. **`computer.click` + `computer.moveCursor`** — fills the only missing computer-use verb; reuse
|
|
77
|
+
treatment model + Run wrapper + before/after capture.
|
|
78
|
+
2. **Voice intents over existing `computer.*`/`capture.*`/`runs.*`** — additive, low risk; stage-by-default
|
|
79
|
+
for any execute, explicit confirm gate.
|
|
80
|
+
3. **`apps.launch` (Run-wrapped) + `waitForWindow` precondition** — unblocks "load app, then act".
|
|
81
|
+
4. **`computer.typeInto` (AX focused-element)** — generalize typing beyond terminals.
|
|
82
|
+
5. **LAT-006 Phase 3 recording probe + `runs.stop`**, then an **OCR/AX verify** step on type/click runs.
|
|
83
|
+
|
|
84
|
+
## Testing steps
|
|
85
|
+
|
|
86
|
+
- CLI dogfood (per project convention — test via `lattices`, not raw daemon):
|
|
87
|
+
`lattices computer prepare`, `lattices computer type --text "ls" --dry-run`, `lattices capture window`,
|
|
88
|
+
`lattices runs list`, `lattices runs <id> --json`.
|
|
89
|
+
- Treatment matrix: observe/stage/present/execute each produce a Run with correct artifacts and never
|
|
90
|
+
over-act (stage/observe must not focus or type).
|
|
91
|
+
- Safety regression: `computer.typeText` refuses claude/codex/vim targets; refuses Enter on non-idle shells.
|
|
92
|
+
- Click (new): coordinate click + AX-element click on a known button → before/after artifacts in the run
|
|
93
|
+
dir; assert no click when `treatment != execute`.
|
|
94
|
+
- Voice: "screenshot this window" / "type ls in my terminal" resolve to the right daemon method; execute
|
|
95
|
+
paths require confirm.
|
|
96
|
+
- Persistence: confirm `~/Library/Application Support/Lattices/Runs/` + `runs.json` survive an app restart.
|
|
97
|
+
|
|
98
|
+
## Owner / next move
|
|
99
|
+
|
|
100
|
+
This is an answer, not a handoff — no other agent needs waking. The clearest single next step that
|
|
101
|
+
unblocks the most use cases is **`computer.click`** (closes the "act" gap for non-terminal targets),
|
|
102
|
+
immediately followed by **exposing the existing computer/capture/run methods to voice**. Both are owned
|
|
103
|
+
by the Lattices macOS app. Recording + verify are the right Phase-3 follow-ups once click + voice land.
|