@arach/lattices 0.2.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/LICENSE +21 -0
- package/README.md +172 -86
- package/apps/mac/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/Info.plist +43 -0
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/apps/mac/Lattices.app/Contents/Resources/docs/assistant-knowledge.md +130 -0
- package/apps/mac/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/apps/mac/Lattices.app/Contents/_CodeSignature/CodeResources +150 -0
- package/apps/mac/Lattices.entitlements +21 -0
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Resources/tap.wav +0 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/assistant-intelligence.ts +912 -0
- 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 +17 -0
- package/bin/cua.ts +26 -0
- package/bin/{daemon-client.js → daemon-client.ts} +49 -30
- package/bin/handsoff-infer.ts +96 -0
- package/bin/handsoff-worker.ts +531 -0
- package/bin/infer.ts +424 -0
- package/bin/keychain.ts +75 -0
- package/bin/lattices-app.ts +655 -0
- package/bin/lattices-build +125 -0
- package/bin/lattices-build-env.ts +77 -0
- package/bin/lattices-dev +362 -0
- package/bin/lattices.ts +3260 -0
- package/bin/project-twin.ts +645 -0
- package/docs/agent-execution-plan.md +562 -0
- package/docs/agent-layer-guide.md +207 -0
- package/docs/agents.md +233 -0
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +1041 -47
- package/docs/app.md +96 -13
- package/docs/assistant-knowledge.md +130 -0
- package/docs/companion-deck.md +209 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/concepts.md +13 -12
- package/docs/config.md +83 -10
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/handsoff-test-scenarios.md +84 -0
- package/docs/hyperspace-grid-snappiness.md +210 -0
- package/docs/layers.md +176 -28
- package/docs/mouse-gestures.md +244 -0
- package/docs/ocr.md +21 -9
- package/docs/overview.md +42 -23
- package/docs/presentation-execution-review.md +491 -0
- package/docs/prompts/hands-off-system.md +382 -0
- package/docs/prompts/hands-off-turn.md +30 -0
- package/docs/prompts/voice-advisor.md +31 -0
- package/docs/prompts/voice-fallback.md +23 -0
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-followup-gaps.md +103 -0
- package/docs/proposals/LAT-006-runs-and-capture-in-lattices.md +566 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/quickstart.md +8 -12
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/release.md +172 -0
- package/docs/repo-structure.md +100 -0
- package/docs/terminal-kit.md +87 -0
- package/docs/tiling-reference.md +224 -0
- package/docs/twins.md +138 -0
- package/docs/voice-command-protocol.md +278 -0
- package/docs/voice-error-model.md +73 -0
- package/docs/voice.md +221 -0
- package/package.json +69 -16
- 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
- package/app/Lattices.app/Contents/Info.plist +0 -24
- package/app/Package.swift +0 -13
- package/app/Sources/ActionRow.swift +0 -61
- package/app/Sources/App.swift +0 -10
- package/app/Sources/AppDelegate.swift +0 -234
- package/app/Sources/AppShellView.swift +0 -62
- package/app/Sources/AppTypeClassifier.swift +0 -70
- package/app/Sources/AppWindowShell.swift +0 -63
- package/app/Sources/CheatSheetHUD.swift +0 -332
- package/app/Sources/CommandModeState.swift +0 -1362
- package/app/Sources/CommandModeView.swift +0 -1405
- package/app/Sources/CommandModeWindow.swift +0 -192
- package/app/Sources/CommandPaletteView.swift +0 -307
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/DaemonProtocol.swift +0 -101
- package/app/Sources/DaemonServer.swift +0 -414
- package/app/Sources/DesktopModel.swift +0 -121
- package/app/Sources/DesktopModelTypes.swift +0 -71
- package/app/Sources/DiagnosticLog.swift +0 -271
- package/app/Sources/EventBus.swift +0 -30
- package/app/Sources/HotkeyManager.swift +0 -250
- package/app/Sources/HotkeyStore.swift +0 -338
- package/app/Sources/InventoryManager.swift +0 -35
- package/app/Sources/InventoryPath.swift +0 -43
- package/app/Sources/KeyRecorderView.swift +0 -210
- package/app/Sources/LatticesApi.swift +0 -1125
- package/app/Sources/MainView.swift +0 -467
- package/app/Sources/MainWindow.swift +0 -83
- package/app/Sources/OcrModel.swift +0 -309
- package/app/Sources/OcrStore.swift +0 -295
- package/app/Sources/OmniSearchState.swift +0 -283
- package/app/Sources/OmniSearchView.swift +0 -288
- package/app/Sources/OmniSearchWindow.swift +0 -105
- package/app/Sources/OrphanRow.swift +0 -129
- package/app/Sources/PaletteCommand.swift +0 -419
- package/app/Sources/PermissionChecker.swift +0 -125
- package/app/Sources/Preferences.swift +0 -92
- package/app/Sources/ProcessModel.swift +0 -199
- package/app/Sources/ProcessQuery.swift +0 -151
- package/app/Sources/Project.swift +0 -28
- package/app/Sources/ProjectRow.swift +0 -368
- package/app/Sources/ProjectScanner.swift +0 -121
- package/app/Sources/ScreenMapState.swift +0 -2387
- package/app/Sources/ScreenMapView.swift +0 -2820
- package/app/Sources/ScreenMapWindowController.swift +0 -89
- package/app/Sources/SessionManager.swift +0 -72
- package/app/Sources/SettingsView.swift +0 -1053
- package/app/Sources/SettingsWindow.swift +0 -20
- package/app/Sources/TabGroupRow.swift +0 -178
- package/app/Sources/Terminal.swift +0 -259
- package/app/Sources/TerminalQuery.swift +0 -156
- package/app/Sources/TerminalSynthesizer.swift +0 -200
- package/app/Sources/Theme.swift +0 -163
- package/app/Sources/TilePickerView.swift +0 -209
- package/app/Sources/TmuxModel.swift +0 -53
- package/app/Sources/TmuxQuery.swift +0 -81
- package/app/Sources/WindowTiler.swift +0 -1755
- package/app/Sources/WorkspaceManager.swift +0 -434
- package/bin/lattices-app.js +0 -221
- package/bin/lattices.js +0 -1418
|
@@ -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
|
@@ -28,13 +28,13 @@ Add `groups` to `~/.lattices/workspace.json`:
|
|
|
28
28
|
"name": "my-setup",
|
|
29
29
|
"groups": [
|
|
30
30
|
{
|
|
31
|
-
"id": "
|
|
32
|
-
"label": "
|
|
31
|
+
"id": "vox",
|
|
32
|
+
"label": "Vox",
|
|
33
33
|
"tabs": [
|
|
34
|
-
{ "path": "/Users/you/dev/
|
|
35
|
-
{ "path": "/Users/you/dev/
|
|
36
|
-
{ "path": "/Users/you/dev/
|
|
37
|
-
{ "path": "/Users/you/dev/
|
|
34
|
+
{ "path": "/Users/you/dev/vox-ios", "label": "iOS" },
|
|
35
|
+
{ "path": "/Users/you/dev/vox-macos", "label": "macOS" },
|
|
36
|
+
{ "path": "/Users/you/dev/vox-web", "label": "Website" },
|
|
37
|
+
{ "path": "/Users/you/dev/vox-api", "label": "API" }
|
|
38
38
|
]
|
|
39
39
|
}
|
|
40
40
|
]
|
|
@@ -46,10 +46,10 @@ to per-project configs.
|
|
|
46
46
|
|
|
47
47
|
### How it works
|
|
48
48
|
|
|
49
|
-
- Session name follows the pattern `lattices-group-<id>` (e.g. `lattices-group-
|
|
49
|
+
- Session name follows the pattern `lattices-group-<id>` (e.g. `lattices-group-vox`)
|
|
50
50
|
- 1 group = 1 tmux session. Each tab is a tmux window, and each window
|
|
51
51
|
gets its own panes from that project's `.lattices.json`
|
|
52
|
-
- You can still launch projects independently: `cd
|
|
52
|
+
- You can still launch projects independently: `cd vox-ios && lattices start`
|
|
53
53
|
creates its own standalone session as before
|
|
54
54
|
|
|
55
55
|
### Tab group fields
|
|
@@ -73,9 +73,9 @@ lattices tab <group> [tab] # Switch tab by label or index
|
|
|
73
73
|
Examples:
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
lattices group
|
|
77
|
-
lattices tab
|
|
78
|
-
lattices tab
|
|
76
|
+
lattices group vox # Launch all Vox tabs
|
|
77
|
+
lattices tab vox iOS # Switch to the iOS tab
|
|
78
|
+
lattices tab vox 0 # Switch to first tab (by index)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
### Menu bar app
|
|
@@ -136,6 +136,42 @@ Add `layers` to `~/.lattices/workspace.json`:
|
|
|
136
136
|
}
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
### App windows in layers
|
|
140
|
+
|
|
141
|
+
Layers aren't limited to terminal sessions. You can include any
|
|
142
|
+
application window by using the `app`, `title`, `url`, and `launch`
|
|
143
|
+
fields instead of `path`:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"name": "hudson",
|
|
148
|
+
"layers": [
|
|
149
|
+
{
|
|
150
|
+
"id": "main",
|
|
151
|
+
"label": "Main",
|
|
152
|
+
"projects": [
|
|
153
|
+
{ "app": "Google Chrome", "title": "GitHub", "tile": "left" },
|
|
154
|
+
{ "app": "Vox", "tile": "top-right", "launch": "open -a Vox" },
|
|
155
|
+
{ "path": "/Users/you/dev/frontend", "tile": "bottom-right" }
|
|
156
|
+
]
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"id": "docs",
|
|
160
|
+
"label": "Docs",
|
|
161
|
+
"projects": [
|
|
162
|
+
{ "app": "Google Chrome", "url": "https://docs.example.com", "tile": "left" },
|
|
163
|
+
{ "app": "Notes", "title": "Sprint Notes", "tile": "right" }
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
When switching to a layer, lattices matches windows by `app` name and
|
|
171
|
+
optionally filters by `title` substring or `url` prefix. If `launch`
|
|
172
|
+
is provided and no matching window is found, the command is executed
|
|
173
|
+
to open the app.
|
|
174
|
+
|
|
139
175
|
### Using groups in layers
|
|
140
176
|
|
|
141
177
|
Layer projects can reference a tab group instead of a single path.
|
|
@@ -146,11 +182,11 @@ This lets you tile a whole group into a screen position:
|
|
|
146
182
|
"name": "my-setup",
|
|
147
183
|
"groups": [
|
|
148
184
|
{
|
|
149
|
-
"id": "
|
|
150
|
-
"label": "
|
|
185
|
+
"id": "vox",
|
|
186
|
+
"label": "Vox",
|
|
151
187
|
"tabs": [
|
|
152
|
-
{ "path": "/Users/you/dev/
|
|
153
|
-
{ "path": "/Users/you/dev/
|
|
188
|
+
{ "path": "/Users/you/dev/vox-ios", "label": "iOS" },
|
|
189
|
+
{ "path": "/Users/you/dev/vox-web", "label": "Website" }
|
|
154
190
|
]
|
|
155
191
|
}
|
|
156
192
|
],
|
|
@@ -159,7 +195,7 @@ This lets you tile a whole group into a screen position:
|
|
|
159
195
|
"id": "main",
|
|
160
196
|
"label": "Main",
|
|
161
197
|
"projects": [
|
|
162
|
-
{ "group": "
|
|
198
|
+
{ "group": "vox", "tile": "top-left" },
|
|
163
199
|
{ "path": "/Users/you/dev/design-system", "tile": "right" }
|
|
164
200
|
]
|
|
165
201
|
}
|
|
@@ -168,7 +204,7 @@ This lets you tile a whole group into a screen position:
|
|
|
168
204
|
```
|
|
169
205
|
|
|
170
206
|
When switching to this layer, lattices launches (or focuses) the
|
|
171
|
-
"
|
|
207
|
+
"vox" group session and tiles it to the top-left quarter, alongside
|
|
172
208
|
the design-system project on the right.
|
|
173
209
|
|
|
174
210
|
### Layer fields
|
|
@@ -182,9 +218,13 @@ the design-system project on the right.
|
|
|
182
218
|
| `layers[].projects` | array | Projects in this layer |
|
|
183
219
|
| `projects[].path` | string? | Absolute path to project directory |
|
|
184
220
|
| `projects[].group`| string? | Group ID (alternative to `path`) |
|
|
221
|
+
| `projects[].app` | string? | Application name (for non-terminal windows) |
|
|
222
|
+
| `projects[].title`| string? | Window title substring to match |
|
|
223
|
+
| `projects[].url` | string? | URL prefix to match (browser windows) |
|
|
224
|
+
| `projects[].launch`| string? | Shell command to launch the app if not found |
|
|
185
225
|
| `projects[].tile` | string? | Tile position (optional, see below) |
|
|
186
226
|
|
|
187
|
-
Each project entry must have either `path` or `
|
|
227
|
+
Each project entry must have either `path`, `group`, or `app` — pick one.
|
|
188
228
|
|
|
189
229
|
### Tile values
|
|
190
230
|
|
|
@@ -195,29 +235,68 @@ works: `left`, `right`, `top`, `bottom`, `top-left`, `top-right`,
|
|
|
195
235
|
|
|
196
236
|
### Switching layers
|
|
197
237
|
|
|
198
|
-
|
|
238
|
+
Four ways to switch:
|
|
199
239
|
|
|
200
240
|
| Method | How |
|
|
201
241
|
|----------------------|------------------------------------------|
|
|
202
242
|
| **Hotkey** | Cmd+Option+1, Cmd+Option+2, Cmd+Option+3... |
|
|
203
243
|
| **Layer bar** | Click a layer pill in the menu bar panel |
|
|
204
244
|
| **Command palette** | Search "Switch to Layer" in Cmd+Shift+M |
|
|
245
|
+
| **CLI** | `lattices layer <name\|index>` |
|
|
205
246
|
|
|
206
247
|
When you switch to a layer:
|
|
207
248
|
|
|
208
|
-
1. Each project's
|
|
209
|
-
2.
|
|
210
|
-
3.
|
|
211
|
-
4.
|
|
249
|
+
1. Each project's window is **raised and focused**
|
|
250
|
+
2. App windows are matched by `app` / `title` / `url`
|
|
251
|
+
3. If a project isn't running yet, it gets **launched** automatically
|
|
252
|
+
4. Windows with a `tile` value are **tiled** to that position
|
|
253
|
+
5. The previous layer's windows stay open behind the new ones
|
|
212
254
|
|
|
213
255
|
The app remembers which layer was last active across restarts.
|
|
214
256
|
|
|
257
|
+
### Named layer switching
|
|
258
|
+
|
|
259
|
+
You can switch layers by name from the CLI:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
lattices layer hudson # Switch to the layer named "hudson"
|
|
263
|
+
lattices layer 0 # Switch to the first layer (by index)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
This is useful for scripting — you don't need to know the index,
|
|
267
|
+
just the layer's `id` or `label`.
|
|
268
|
+
|
|
269
|
+
### Window tagging
|
|
270
|
+
|
|
271
|
+
You can manually assign any window to a layer, even if it's not
|
|
272
|
+
declared in `workspace.json`. This is useful for ad-hoc windows
|
|
273
|
+
that you want to move with a layer:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
lattices window assign <wid> <layer> # Tag a window to a layer
|
|
277
|
+
lattices window map # Show all window→layer assignments
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Tagged windows behave like declared ones — they're raised and tiled
|
|
281
|
+
when their layer activates. Remove a tag by reassigning or with:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Via the agent API
|
|
285
|
+
await daemonCall('window.removeLayer', { wid: 1234 })
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Layer bezel
|
|
289
|
+
|
|
290
|
+
When you switch layers via hotkey, a translucent HUD pill appears
|
|
291
|
+
briefly at the top of the screen showing the new layer's name.
|
|
292
|
+
This provides instant visual feedback without interrupting your flow.
|
|
293
|
+
|
|
215
294
|
### Programmatic switching
|
|
216
295
|
|
|
217
|
-
Agents and scripts can switch layers via the
|
|
296
|
+
Agents and scripts can switch layers via the agent API:
|
|
218
297
|
|
|
219
298
|
```js
|
|
220
|
-
import { daemonCall } from '@
|
|
299
|
+
import { daemonCall } from '@lattices/cli'
|
|
221
300
|
|
|
222
301
|
// List available layers
|
|
223
302
|
const { layers, active } = await daemonCall('layers.list')
|
|
@@ -225,13 +304,69 @@ console.log(`Active: ${layers[active].label}`)
|
|
|
225
304
|
|
|
226
305
|
// Switch to a layer by index
|
|
227
306
|
await daemonCall('layer.switch', { index: 0 })
|
|
307
|
+
|
|
308
|
+
// Switch to a layer by name
|
|
309
|
+
await daemonCall('layer.switch', { name: 'hudson' })
|
|
228
310
|
```
|
|
229
311
|
|
|
230
312
|
The `layer.switch` call focuses and tiles all windows in the target
|
|
231
313
|
layer, just like the hotkey or command palette. A `layer.switched`
|
|
232
314
|
event is broadcast to all connected clients.
|
|
233
315
|
|
|
234
|
-
More methods in the [
|
|
316
|
+
More methods in the [Agent API reference](/docs/api).
|
|
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.
|
|
235
370
|
|
|
236
371
|
### Layer bar
|
|
237
372
|
|
|
@@ -258,7 +393,7 @@ header and search field in the menu bar panel:
|
|
|
258
393
|
```json
|
|
259
394
|
{
|
|
260
395
|
"projects": [
|
|
261
|
-
{ "path": "/Users/you/dev/
|
|
396
|
+
{ "path": "/Users/you/dev/vox" }
|
|
262
397
|
]
|
|
263
398
|
}
|
|
264
399
|
```
|
|
@@ -276,12 +411,23 @@ No `tile` — just focuses the window wherever it is.
|
|
|
276
411
|
}
|
|
277
412
|
```
|
|
278
413
|
|
|
414
|
+
### Mixed: apps + terminals
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{
|
|
418
|
+
"projects": [
|
|
419
|
+
{ "app": "Google Chrome", "title": "GitHub", "tile": "left" },
|
|
420
|
+
{ "path": "/Users/you/dev/api", "tile": "right" }
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
279
425
|
### Group + project
|
|
280
426
|
|
|
281
427
|
```json
|
|
282
428
|
{
|
|
283
429
|
"projects": [
|
|
284
|
-
{ "group": "
|
|
430
|
+
{ "group": "vox", "tile": "left" },
|
|
285
431
|
{ "path": "/Users/you/dev/api", "tile": "right" }
|
|
286
432
|
]
|
|
287
433
|
}
|
|
@@ -305,6 +451,8 @@ No `tile` — just focuses the window wherever it is.
|
|
|
305
451
|
- Projects don't need a `.lattices.json` config to be in a layer — any
|
|
306
452
|
directory path works. If the project has a config, lattices uses it; if
|
|
307
453
|
not, it opens a plain terminal in that directory.
|
|
454
|
+
- App windows don't need any config at all — just specify `app` and
|
|
455
|
+
optionally `title` or `url` to match the right window.
|
|
308
456
|
- You can have up to 9 layers (Cmd+Option+1 through Cmd+Option+9).
|
|
309
457
|
- Edit `workspace.json` by hand — the app re-reads it on launch. Use
|
|
310
458
|
the Refresh Projects button or restart the app to pick up changes.
|