@lattices/cli 0.4.0 → 0.4.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/app/Info.plist +30 -0
- package/app/Lattices.app/Contents/Info.plist +8 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
- package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
- package/app/Lattices.entitlements +15 -0
- package/app/Resources/tap.wav +0 -0
- package/app/Sources/AppDelegate.swift +1 -0
- package/app/Sources/DesktopModel.swift +26 -2
- package/app/Sources/HandsOffSession.swift +83 -14
- package/app/Sources/HotkeyStore.swift +5 -1
- package/app/Sources/IntentEngine.swift +37 -0
- package/app/Sources/LatticesApi.swift +40 -0
- package/app/Sources/MouseFinder.swift +222 -0
- package/app/Sources/ProjectScanner.swift +57 -44
- package/app/Tests/StageDragTests.swift +333 -0
- package/app/Tests/StageJoinTests.swift +313 -0
- package/app/Tests/StageManagerTests.swift +280 -0
- package/app/Tests/StageTileTests.swift +353 -0
- package/assets/AppIcon.icns +0 -0
- package/bin/handsoff-worker.ts +10 -1
- package/bin/lattices-app.ts +120 -38
- package/bin/lattices-dev +51 -3
- package/bin/lattices.ts +181 -7
- package/docs/agent-layer-guide.md +207 -0
- package/package.json +10 -3
package/bin/lattices.ts
CHANGED
|
@@ -56,7 +56,7 @@ function requireTmux(command: string | undefined): void {
|
|
|
56
56
|
const isImplicitCreate = command && !tmuxRequiredCommands.has(command)
|
|
57
57
|
&& !["search", "s", "focus", "place", "tile", "t", "windows", "window",
|
|
58
58
|
"voice", "call", "layer", "layers", "diag", "diagnostics", "scan",
|
|
59
|
-
"ocr", "daemon", "dev", "app", "help", "-h", "--help"].includes(command);
|
|
59
|
+
"ocr", "daemon", "dev", "app", "mouse", "help", "-h", "--help"].includes(command);
|
|
60
60
|
|
|
61
61
|
if (command && !tmuxRequiredCommands.has(command) && !isImplicitCreate) return;
|
|
62
62
|
|
|
@@ -831,6 +831,18 @@ function restartPane(target?: string): void {
|
|
|
831
831
|
|
|
832
832
|
// ── Daemon-aware commands ────────────────────────────────────────────
|
|
833
833
|
|
|
834
|
+
async function mouseCommand(sub?: string): Promise<void> {
|
|
835
|
+
const { daemonCall } = await getDaemonClient();
|
|
836
|
+
if (sub === "summon") {
|
|
837
|
+
const result = await daemonCall("mouse.summon") as any;
|
|
838
|
+
console.log(`🎯 Mouse summoned to (${result.x}, ${result.y})`);
|
|
839
|
+
} else {
|
|
840
|
+
// Default: find
|
|
841
|
+
const result = await daemonCall("mouse.find") as any;
|
|
842
|
+
console.log(`🔍 Mouse at (${result.x}, ${result.y})`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
834
846
|
async function daemonStatusCommand(): Promise<void> {
|
|
835
847
|
try {
|
|
836
848
|
const { daemonCall } = await getDaemonClient();
|
|
@@ -1189,10 +1201,37 @@ async function callCommand(method?: string, ...rest: string[]): Promise<void> {
|
|
|
1189
1201
|
}
|
|
1190
1202
|
}
|
|
1191
1203
|
|
|
1192
|
-
async function layerCommand(
|
|
1204
|
+
async function layerCommand(sub?: string, ...rest: string[]): Promise<void> {
|
|
1193
1205
|
try {
|
|
1194
1206
|
const { daemonCall } = await getDaemonClient();
|
|
1195
|
-
|
|
1207
|
+
|
|
1208
|
+
// ── Subcommands ──
|
|
1209
|
+
if (sub === "create") {
|
|
1210
|
+
await layerCreateCommand(rest);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (sub === "snap") {
|
|
1214
|
+
await layerSnapCommand(rest[0]);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (sub === "session" || sub === "sessions") {
|
|
1218
|
+
await layerSessionCommand(rest[0]);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (sub === "clear") {
|
|
1222
|
+
await daemonCall("session.layers.clear");
|
|
1223
|
+
console.log("Cleared all session layers.");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (sub === "delete" || sub === "rm") {
|
|
1227
|
+
if (!rest[0]) { console.log("Usage: lattices layer delete <name>"); return; }
|
|
1228
|
+
await daemonCall("session.layers.delete", { name: rest[0] });
|
|
1229
|
+
console.log(`Deleted session layer "${rest[0]}".`);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ── List or switch (original behavior) ──
|
|
1234
|
+
if (sub === undefined || sub === null || sub === "") {
|
|
1196
1235
|
const result = await daemonCall("layers.list") as any;
|
|
1197
1236
|
if (!result.layers.length) {
|
|
1198
1237
|
console.log("No layers configured.");
|
|
@@ -1205,19 +1244,144 @@ async function layerCommand(index?: string): Promise<void> {
|
|
|
1205
1244
|
}
|
|
1206
1245
|
return;
|
|
1207
1246
|
}
|
|
1208
|
-
const idx = parseInt(
|
|
1247
|
+
const idx = parseInt(sub, 10);
|
|
1209
1248
|
if (!isNaN(idx)) {
|
|
1210
1249
|
await daemonCall("layer.activate", { index: idx, mode: "launch" });
|
|
1211
1250
|
console.log(`Activated layer ${idx}`);
|
|
1212
1251
|
} else {
|
|
1213
|
-
await daemonCall("layer.activate", { name:
|
|
1214
|
-
console.log(`Activated layer "${
|
|
1252
|
+
await daemonCall("layer.activate", { name: sub, mode: "launch" });
|
|
1253
|
+
console.log(`Activated layer "${sub}"`);
|
|
1215
1254
|
}
|
|
1216
1255
|
} catch (e: unknown) {
|
|
1217
1256
|
console.log(`Error: ${(e as Error).message}`);
|
|
1218
1257
|
}
|
|
1219
1258
|
}
|
|
1220
1259
|
|
|
1260
|
+
// ── Layer create: build a session layer from window specs ────────────
|
|
1261
|
+
// Usage: lattices layer create <name> [wid:123 wid:456 ...]
|
|
1262
|
+
// lattices layer create <name> --json '[{"app":"Chrome","tile":"left"},...]'
|
|
1263
|
+
async function layerCreateCommand(args: string[]): Promise<void> {
|
|
1264
|
+
const { daemonCall } = await getDaemonClient();
|
|
1265
|
+
const name = args[0];
|
|
1266
|
+
if (!name) {
|
|
1267
|
+
console.log("Usage: lattices layer create <name> [wid:123 ...] [--json '<specs>']");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const jsonIdx = args.indexOf("--json");
|
|
1272
|
+
if (jsonIdx !== -1 && args[jsonIdx + 1]) {
|
|
1273
|
+
// JSON mode: parse window specs with tile positions
|
|
1274
|
+
const specs = JSON.parse(args[jsonIdx + 1]) as Array<{
|
|
1275
|
+
wid?: number; app?: string; title?: string; tile?: string;
|
|
1276
|
+
}>;
|
|
1277
|
+
|
|
1278
|
+
// Collect wids, resolve app-based specs
|
|
1279
|
+
const windowIds: number[] = [];
|
|
1280
|
+
const windows: Array<{ app: string; contentHint?: string }> = [];
|
|
1281
|
+
const tiles: Array<{ wid?: number; app?: string; title?: string; tile: string }> = [];
|
|
1282
|
+
|
|
1283
|
+
for (const spec of specs) {
|
|
1284
|
+
if (spec.wid) {
|
|
1285
|
+
windowIds.push(spec.wid);
|
|
1286
|
+
if (spec.tile) tiles.push({ wid: spec.wid, tile: spec.tile });
|
|
1287
|
+
} else if (spec.app) {
|
|
1288
|
+
windows.push({ app: spec.app, contentHint: spec.title });
|
|
1289
|
+
if (spec.tile) tiles.push({ app: spec.app, title: spec.title, tile: spec.tile });
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const result = await daemonCall("session.layers.create", {
|
|
1294
|
+
name,
|
|
1295
|
+
...(windowIds.length ? { windowIds } : {}),
|
|
1296
|
+
...(windows.length ? { windows } : {}),
|
|
1297
|
+
}) as any;
|
|
1298
|
+
|
|
1299
|
+
console.log(`Created session layer "${name}" with ${specs.length} window(s).`);
|
|
1300
|
+
|
|
1301
|
+
// Apply tile positions
|
|
1302
|
+
for (const t of tiles) {
|
|
1303
|
+
try {
|
|
1304
|
+
await daemonCall("window.place", {
|
|
1305
|
+
...(t.wid ? { wid: t.wid } : { app: t.app, title: t.title }),
|
|
1306
|
+
placement: t.tile,
|
|
1307
|
+
});
|
|
1308
|
+
} catch { /* window may not be resolved yet */ }
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
if (tiles.length) console.log(`Tiled ${tiles.length} window(s).`);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Simple wid mode: lattices layer create <name> wid:123 wid:456
|
|
1316
|
+
const wids = args.slice(1)
|
|
1317
|
+
.filter(a => a.startsWith("wid:"))
|
|
1318
|
+
.map(a => parseInt(a.slice(4), 10))
|
|
1319
|
+
.filter(n => !isNaN(n));
|
|
1320
|
+
|
|
1321
|
+
const result = await daemonCall("session.layers.create", {
|
|
1322
|
+
name,
|
|
1323
|
+
...(wids.length ? { windowIds: wids } : {}),
|
|
1324
|
+
}) as any;
|
|
1325
|
+
|
|
1326
|
+
console.log(`Created session layer "${name}"${wids.length ? ` with ${wids.length} window(s)` : ""}.`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// ── Layer snap: snapshot current visible windows into a session layer ─
|
|
1330
|
+
async function layerSnapCommand(name?: string): Promise<void> {
|
|
1331
|
+
const { daemonCall } = await getDaemonClient();
|
|
1332
|
+
const layerName = name || `snap-${new Date().toISOString().slice(11, 19).replace(/:/g, "")}`;
|
|
1333
|
+
|
|
1334
|
+
// Get all current windows
|
|
1335
|
+
const windows = await daemonCall("windows.list") as any[];
|
|
1336
|
+
const visibleWids = windows
|
|
1337
|
+
.filter((w: any) => !w.isMinimized && w.app !== "lattices")
|
|
1338
|
+
.map((w: any) => w.wid);
|
|
1339
|
+
|
|
1340
|
+
if (!visibleWids.length) {
|
|
1341
|
+
console.log("No visible windows to snapshot.");
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
await daemonCall("session.layers.create", {
|
|
1346
|
+
name: layerName,
|
|
1347
|
+
windowIds: visibleWids,
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
console.log(`Snapped ${visibleWids.length} window(s) → session layer "${layerName}".`);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ── Layer session: list or switch session layers ─────────────────────
|
|
1354
|
+
async function layerSessionCommand(nameOrIndex?: string): Promise<void> {
|
|
1355
|
+
const { daemonCall } = await getDaemonClient();
|
|
1356
|
+
const result = await daemonCall("session.layers.list") as any;
|
|
1357
|
+
|
|
1358
|
+
if (!nameOrIndex) {
|
|
1359
|
+
// List session layers
|
|
1360
|
+
if (!result.layers.length) {
|
|
1361
|
+
console.log("No session layers. Create one with: lattices layer create <name>");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
console.log("Session layers:\n");
|
|
1365
|
+
for (let i = 0; i < result.layers.length; i++) {
|
|
1366
|
+
const l = result.layers[i];
|
|
1367
|
+
const active = i === result.activeIndex ? " \x1b[32m● active\x1b[0m" : "";
|
|
1368
|
+
const winCount = l.windows?.length || 0;
|
|
1369
|
+
console.log(` [${i}] ${l.name} (${winCount} windows)${active}`);
|
|
1370
|
+
}
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Switch by index or name
|
|
1375
|
+
const idx = parseInt(nameOrIndex, 10);
|
|
1376
|
+
if (!isNaN(idx)) {
|
|
1377
|
+
await daemonCall("session.layers.switch", { index: idx });
|
|
1378
|
+
console.log(`Switched to session layer ${idx}.`);
|
|
1379
|
+
} else {
|
|
1380
|
+
await daemonCall("session.layers.switch", { name: nameOrIndex });
|
|
1381
|
+
console.log(`Switched to session layer "${nameOrIndex}".`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1221
1385
|
async function diagCommand(limit?: string): Promise<void> {
|
|
1222
1386
|
try {
|
|
1223
1387
|
const { daemonCall } = await getDaemonClient();
|
|
@@ -1549,6 +1713,11 @@ Usage:
|
|
|
1549
1713
|
lattices tile <position> Tile the frontmost window (left, right, top, etc.)
|
|
1550
1714
|
lattices distribute Smart-grid all visible windows (daemon required)
|
|
1551
1715
|
lattices layer [name|index] List layers or switch by name/index (daemon required)
|
|
1716
|
+
lattices layer create <name> [wid:N ...] [--json '<specs>'] Create a session layer
|
|
1717
|
+
lattices layer snap [name] Snapshot visible windows into a session layer
|
|
1718
|
+
lattices layer session [n] List or switch session layers (runtime, no restart)
|
|
1719
|
+
lattices layer delete <name> Delete a session layer
|
|
1720
|
+
lattices layer clear Clear all session layers
|
|
1552
1721
|
lattices voice status Voice provider status
|
|
1553
1722
|
lattices voice simulate <t> Parse and execute a voice command
|
|
1554
1723
|
lattices voice intents List all available intents
|
|
@@ -1563,6 +1732,8 @@ Usage:
|
|
|
1563
1732
|
lattices dev build Build the project (swift/node/rust/go/make)
|
|
1564
1733
|
lattices dev restart Build + restart (swift app) or just build
|
|
1565
1734
|
lattices dev type Print detected project type
|
|
1735
|
+
lattices mouse Find mouse — sonar pulse at cursor position
|
|
1736
|
+
lattices mouse summon Summon mouse to screen center
|
|
1566
1737
|
lattices daemon status Show daemon status
|
|
1567
1738
|
lattices diag [limit] Show diagnostic log entries
|
|
1568
1739
|
lattices app Launch the menu bar companion app
|
|
@@ -1975,7 +2146,7 @@ switch (command) {
|
|
|
1975
2146
|
break;
|
|
1976
2147
|
case "layer":
|
|
1977
2148
|
case "layers":
|
|
1978
|
-
await layerCommand(args[1]);
|
|
2149
|
+
await layerCommand(args[1], ...args.slice(2));
|
|
1979
2150
|
break;
|
|
1980
2151
|
case "diag":
|
|
1981
2152
|
case "diagnostics":
|
|
@@ -1985,6 +2156,9 @@ switch (command) {
|
|
|
1985
2156
|
case "ocr":
|
|
1986
2157
|
await scanCommand(args[1], ...args.slice(2));
|
|
1987
2158
|
break;
|
|
2159
|
+
case "mouse":
|
|
2160
|
+
await mouseCommand(args[1]);
|
|
2161
|
+
break;
|
|
1988
2162
|
case "daemon":
|
|
1989
2163
|
if (args[1] === "status") {
|
|
1990
2164
|
await daemonStatusCommand();
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Agent Guide: Generating Layers
|
|
2
|
+
|
|
3
|
+
How to create and manage Lattices workspace layers programmatically. This guide is for AI agents (Claude Code, etc.) that want to generate layers from high-level user descriptions.
|
|
4
|
+
|
|
5
|
+
## Quick Reference
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# See what's on screen
|
|
9
|
+
lattices windows --json
|
|
10
|
+
|
|
11
|
+
# Create a layer with tiling
|
|
12
|
+
lattices layer create "Design" --json '[
|
|
13
|
+
{"app": "Figma", "tile": "left"},
|
|
14
|
+
{"app": "Google Chrome", "title": "Tailwind", "tile": "right"}
|
|
15
|
+
]'
|
|
16
|
+
|
|
17
|
+
# Snapshot current windows as a layer
|
|
18
|
+
lattices layer snap "my-context"
|
|
19
|
+
|
|
20
|
+
# List / switch / delete session layers
|
|
21
|
+
lattices layer session
|
|
22
|
+
lattices layer session "Design"
|
|
23
|
+
lattices layer delete "Design"
|
|
24
|
+
lattices layer clear
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## How It Works
|
|
28
|
+
|
|
29
|
+
There are two kinds of layers:
|
|
30
|
+
|
|
31
|
+
| Type | Storage | Requires restart? | How to create |
|
|
32
|
+
|------|---------|-------------------|---------------|
|
|
33
|
+
| **Config layers** | `~/.lattices/workspace.json` | Yes (or refresh) | Edit JSON file |
|
|
34
|
+
| **Session layers** | In-memory (daemon) | No | CLI or daemon API |
|
|
35
|
+
|
|
36
|
+
**Session layers are what you want.** They're created via TypeScript CLI commands, take effect immediately, and don't require restarting anything.
|
|
37
|
+
|
|
38
|
+
## Step-by-Step: Generating a Layer
|
|
39
|
+
|
|
40
|
+
### 1. Discover what's available
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
lattices windows --json
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Returns an array of window objects:
|
|
47
|
+
```json
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
"wid": 1234,
|
|
51
|
+
"app": "iTerm2",
|
|
52
|
+
"title": "lattices — zsh",
|
|
53
|
+
"latticesSession": "lattices-abc123",
|
|
54
|
+
"frame": { "x": 0, "y": 25, "w": 960, "h": 1050 },
|
|
55
|
+
"spaceIds": [1]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"wid": 5678,
|
|
59
|
+
"app": "Google Chrome",
|
|
60
|
+
"title": "GitHub - arach/lattices",
|
|
61
|
+
"frame": { "x": 960, "y": 25, "w": 960, "h": 1050 },
|
|
62
|
+
"spaceIds": [1]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Key fields for matching:
|
|
68
|
+
- `wid` — unique window ID (most precise)
|
|
69
|
+
- `app` — application name
|
|
70
|
+
- `title` — window title (use for disambiguation when multiple windows of same app)
|
|
71
|
+
- `latticesSession` — tmux session name (for terminal windows)
|
|
72
|
+
|
|
73
|
+
### 2. Decide on a layout
|
|
74
|
+
|
|
75
|
+
Pick tile positions based on how many windows and what makes sense:
|
|
76
|
+
|
|
77
|
+
| Windows | Good layout | Tile values |
|
|
78
|
+
|---------|-------------|-------------|
|
|
79
|
+
| 2 | Side by side | `left`, `right` |
|
|
80
|
+
| 2 | Stacked | `top`, `bottom` |
|
|
81
|
+
| 3 | Main + sidebar | `left` (60%), `top-right`, `bottom-right` |
|
|
82
|
+
| 3 | Columns | `left-third`, `center-third`, `right-third` |
|
|
83
|
+
| 4 | Quadrants | `top-left`, `top-right`, `bottom-left`, `bottom-right` |
|
|
84
|
+
| 1 | Focused | `maximize` or `center` |
|
|
85
|
+
|
|
86
|
+
Full position reference:
|
|
87
|
+
- **Halves**: `left`, `right`, `top`, `bottom`
|
|
88
|
+
- **Quarters**: `top-left`, `top-right`, `bottom-left`, `bottom-right`
|
|
89
|
+
- **Thirds**: `left-third`, `center-third`, `right-third`
|
|
90
|
+
- **Sixths**: `top-left-third`, `top-center-third`, `top-right-third`, `bottom-left-third`, `bottom-center-third`, `bottom-right-third`
|
|
91
|
+
- **Fourths**: `first-fourth`, `second-fourth`, `third-fourth`, `last-fourth`
|
|
92
|
+
- **Special**: `maximize`, `center`
|
|
93
|
+
- **Custom grid**: `grid:CxR:C,R` (e.g. `grid:5x3:2,1`)
|
|
94
|
+
|
|
95
|
+
### 3. Create the layer
|
|
96
|
+
|
|
97
|
+
**Option A: By window ID (most reliable)**
|
|
98
|
+
```bash
|
|
99
|
+
lattices layer create "Coding" --json '[
|
|
100
|
+
{"wid": 1234, "tile": "left"},
|
|
101
|
+
{"wid": 5678, "tile": "right"}
|
|
102
|
+
]'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Option B: By app name (survives window recreation)**
|
|
106
|
+
```bash
|
|
107
|
+
lattices layer create "Research" --json '[
|
|
108
|
+
{"app": "Google Chrome", "title": "docs", "tile": "left"},
|
|
109
|
+
{"app": "Notes", "tile": "right"}
|
|
110
|
+
]'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Option C: Simple wid list (no tiling)**
|
|
114
|
+
```bash
|
|
115
|
+
lattices layer create "Focus" wid:1234 wid:5678
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Option D: Snapshot everything visible**
|
|
119
|
+
```bash
|
|
120
|
+
lattices layer snap "Current Context"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 4. Switch between layers
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
lattices layer session # list all session layers
|
|
127
|
+
lattices layer session "Coding" # switch to "Coding"
|
|
128
|
+
lattices layer session 0 # switch by index
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Daemon API (Advanced)
|
|
132
|
+
|
|
133
|
+
For finer control, use raw daemon calls:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# Create layer with window IDs
|
|
137
|
+
lattices call session.layers.create '{"name":"Coding","windowIds":[1234,5678]}'
|
|
138
|
+
|
|
139
|
+
# Create layer with app references
|
|
140
|
+
lattices call session.layers.create '{"name":"Design","windows":[{"app":"Figma"},{"app":"Google Chrome","contentHint":"Tailwind"}]}'
|
|
141
|
+
|
|
142
|
+
# Tile a specific window
|
|
143
|
+
lattices call window.place '{"wid":1234,"placement":"left"}'
|
|
144
|
+
|
|
145
|
+
# Switch layer
|
|
146
|
+
lattices call session.layers.switch '{"name":"Coding"}'
|
|
147
|
+
|
|
148
|
+
# List session layers
|
|
149
|
+
lattices call session.layers.list
|
|
150
|
+
|
|
151
|
+
# Delete
|
|
152
|
+
lattices call session.layers.delete '{"name":"old-layer"}'
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Composing Layers from Intent
|
|
156
|
+
|
|
157
|
+
When a user says something high-level, here's how to think about it:
|
|
158
|
+
|
|
159
|
+
### "Make me a coding layer"
|
|
160
|
+
1. Find terminal windows (iTerm2, Terminal, Warp, etc.)
|
|
161
|
+
2. Find browser windows with dev-related titles (GitHub, docs, localhost)
|
|
162
|
+
3. Main editor/terminal on `left`, reference material on `right`
|
|
163
|
+
|
|
164
|
+
### "Set up a design layer"
|
|
165
|
+
1. Find design tools (Figma, Sketch, Adobe XD)
|
|
166
|
+
2. Find browser windows with design references
|
|
167
|
+
3. Design tool `left` (or `maximize`), references `right`
|
|
168
|
+
|
|
169
|
+
### "Create a writing layer"
|
|
170
|
+
1. Find text editors, notes apps (Notes, Obsidian, iA Writer, VS Code with .md)
|
|
171
|
+
2. Find research/reference windows
|
|
172
|
+
3. Writing app `left` or `center`, references `right`
|
|
173
|
+
|
|
174
|
+
### "Give me a communication layer"
|
|
175
|
+
1. Find messaging apps (Slack, Discord, Messages)
|
|
176
|
+
2. Find email (Mail, Gmail in browser)
|
|
177
|
+
3. Arrange by priority — primary tool `left`, secondary `right`
|
|
178
|
+
|
|
179
|
+
### "Split my work into layers by project"
|
|
180
|
+
1. Group windows by project (match on title keywords, session names, or app)
|
|
181
|
+
2. Create one layer per project group
|
|
182
|
+
3. Use the 3-window layout pattern: main `left`, support `top-right`, `bottom-right`
|
|
183
|
+
|
|
184
|
+
## App Grouping Heuristics
|
|
185
|
+
|
|
186
|
+
When deciding which windows go together:
|
|
187
|
+
|
|
188
|
+
| Category | Common apps | Goes well with |
|
|
189
|
+
|----------|-------------|----------------|
|
|
190
|
+
| **Code** | iTerm2, Terminal, VS Code, Xcode | Chrome (docs/GitHub), Simulator |
|
|
191
|
+
| **Design** | Figma, Sketch, Pixelmator | Chrome (design systems), Preview |
|
|
192
|
+
| **Writing** | Notes, Obsidian, iA Writer | Chrome (research), Preview |
|
|
193
|
+
| **Communication** | Slack, Discord, Messages, Mail | Calendar, Notes |
|
|
194
|
+
| **Media** | Spotify, Music, Podcasts | (background, no tile needed) |
|
|
195
|
+
| **Reference** | Chrome, Safari, Preview, Finder | (depends on content) |
|
|
196
|
+
|
|
197
|
+
Browser windows are chameleons — use `title` matching to assign them to the right layer based on their content.
|
|
198
|
+
|
|
199
|
+
## Tips
|
|
200
|
+
|
|
201
|
+
- Prefer `wid` when the windows are already open — it's unambiguous.
|
|
202
|
+
- Use `app` + `title` when you want the layer to survive window restarts.
|
|
203
|
+
- Don't put more than 4-5 windows in a single layer — it gets cramped.
|
|
204
|
+
- Background apps (music, etc.) usually don't need to be in any layer.
|
|
205
|
+
- The `snap` command is great for "save what I have now" scenarios.
|
|
206
|
+
- Session layers are ephemeral — they live until the daemon restarts. For permanent layers, edit `~/.lattices/workspace.json`.
|
|
207
|
+
- You can create multiple layers in sequence, then switch between them with `lattices layer session <name>`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattices/cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Agentic window manager for macOS — programmable workspace, smart layouts, managed tmux sessions, and a 35+-method agent API",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lattices": "./bin/lattices.ts",
|
|
@@ -27,15 +27,22 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "bun --cwd docs-site dev",
|
|
30
|
-
"typecheck": "tsc --noEmit"
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"build:app-bundle": "bash ./bin/lattices-dev build",
|
|
32
|
+
"prepack": "bash ./bin/lattices-dev build"
|
|
31
33
|
},
|
|
32
34
|
"type": "module",
|
|
33
35
|
"os": ["darwin"],
|
|
34
36
|
"files": [
|
|
35
37
|
"bin",
|
|
38
|
+
"app/Info.plist",
|
|
39
|
+
"app/Lattices.app",
|
|
40
|
+
"app/Lattices.entitlements",
|
|
36
41
|
"app/Package.swift",
|
|
42
|
+
"app/Resources",
|
|
37
43
|
"app/Sources",
|
|
38
|
-
"app/
|
|
44
|
+
"app/Tests",
|
|
45
|
+
"assets/AppIcon.icns",
|
|
39
46
|
"docs"
|
|
40
47
|
],
|
|
41
48
|
"devDependencies": {
|