Tiling Reference
Tiling Reference
Complete reference for Lattices window tiling — positions, grids, execution paths, and voice interpretation.
Position System
Every tile position is a cell in a cols × rows grid, expressed as fractional (x, y, w, h) of the screen's visible area (excluding menu bar and dock).
Named Positions
All valid position strings that TilePosition accepts:
| Position string | Grid | Cell (col, row) | Description |
|---|---|---|---|
maximize |
1×1 | full | Full screen (100% × 100%) |
center |
— | — | Centered floating (70% × 80%, offset 15%/10%) |
| Halves (2×1, full height) | |||
left |
2×1 | 0,0 | Left 50% |
right |
2×1 | 1,0 | Right 50% |
| Halves (1×2, full width) | |||
top |
1×2 | 0,0 | Top 50% |
bottom |
1×2 | 0,1 | Bottom 50% |
| Quarters (2×2) | |||
top-left |
2×2 | 0,0 | Top-left 25% |
top-right |
2×2 | 1,0 | Top-right 25% |
bottom-left |
2×2 | 0,1 | Bottom-left 25% |
bottom-right |
2×2 | 1,1 | Bottom-right 25% |
| Thirds (3×1, full height) | |||
left-third |
3×1 | 0,0 | Left 33% column |
center-third |
3×1 | 1,0 | Center 33% column |
right-third |
3×1 | 2,0 | Right 33% column |
| Sixths (3×2) | |||
top-left-third |
3×2 | 0,0 | Top-left sixth |
top-center-third |
3×2 | 1,0 | Top-center sixth |
top-right-third |
3×2 | 2,0 | Top-right sixth |
bottom-left-third |
3×2 | 0,1 | Bottom-left sixth |
bottom-center-third |
3×2 | 1,1 | Bottom-center sixth |
bottom-right-third |
3×2 | 2,1 | Bottom-right sixth |
| Fourths (4×1, full height) | |||
first-fourth |
4×1 | 0,0 | Leftmost 25% column |
second-fourth |
4×1 | 1,0 | Second 25% column |
third-fourth |
4×1 | 2,0 | Third 25% column |
last-fourth |
4×1 | 3,0 | Rightmost 25% column |
| Eighths (4×2) | |||
top-first-fourth |
4×2 | 0,0 | Top row, 1st column |
top-second-fourth |
4×2 | 1,0 | Top row, 2nd column |
top-third-fourth |
4×2 | 2,0 | Top row, 3rd column |
top-last-fourth |
4×2 | 3,0 | Top row, 4th column |
bottom-first-fourth |
4×2 | 0,1 | Bottom row, 1st column |
bottom-second-fourth |
4×2 | 1,1 | Bottom row, 2nd column |
bottom-third-fourth |
4×2 | 2,1 | Bottom row, 3rd column |
bottom-last-fourth |
4×2 | 3,1 | Bottom row, 4th column |
| Horizontal thirds (1×3) | |||
top-third |
1×3 | 0,0 | Top 33% row |
middle-third |
1×3 | 0,1 | Middle 33% row |
bottom-third |
1×3 | 0,2 | Bottom 33% row |
| Edge quarters | |||
left-quarter |
4×1 | 0,0 | Leftmost 25% column |
right-quarter |
4×1 | 3,0 | Rightmost 25% column |
top-quarter |
1×4 | 0,0 | Top 25% row |
bottom-quarter |
1×4 | 0,3 | Bottom 25% row |
Custom Grid Syntax
For arbitrary grids: grid:CxR:C,R
C= total columns,R= total rowsC,R= target cell (0-indexed position)- Example:
grid:5x3:2,1= center cell of a 5×3 grid
Parsed by PlacementSpec / parseGridString() into fractional (x, y, w, h).
Placement Contract
Placement strings are convenient at the boundary, but the daemon uses a typed placement model internally:
- named tile positions
- arbitrary grid cells
- raw fractional rectangles
That is what keeps CLI, daemon, voice, and hands-off execution aligned.
Drag Snap Zones
The menu bar app can also use placement specs as drag-to-snap targets.
When you drag a window, Lattices shows faint landing zones plus a live
preview of the resulting frame. Releasing over a zone tiles the dragged
window to that placement. Hold Command while dragging to reveal snap
mode, and release Command to drop back to a normal free drag without
ending the gesture.
The recommended agent-owned config lives in ~/.lattices/snap-zones.json:
{
"enabled": true,
"modifier": "command",
"zoneOpacity": 0.08,
"highlightOpacity": 0.18,
"previewOpacity": 0.14,
"rules": [
{
"id": "left-edge",
"label": "Left",
"placement": "left",
"trigger": { "x": 0.0, "y": 0.18, "w": 0.12, "h": 0.64 },
"priority": 10
},
{
"id": "notes-rail",
"label": "Notes",
"placement": { "x": 0.68, "y": 0.0, "w": 0.32, "h": 1.0 },
"trigger": { "x": 0.88, "y": 0.18, "w": 0.12, "h": 0.64 },
"priority": 30
}
]
}Notes:
rulesis the preferred list key.zonesis still accepted for backward compatibility.modifieracceptscommand,option,control, orshift.placementcan be a named placement/preset string or raw fractions.triggeruses normalized(x, y, w, h)fractions of the screen's visible area, withy = 0at the top.prioritybreaks ties when trigger regions overlap.triggercan also be a named placement or preset string if you want the trigger region itself to reuse an existing tile definition.- The older
~/.lattices/grid.jsonsnapZonessection still works, but~/.lattices/snap-zones.jsonis the cleaner file for agents to edit.
Execution Paths
The old split-brain tiling logic has been collapsed toward a shared path. The canonical mutation is now:
{ "method": "window.place", "params": { "placement": "left" } }All higher-level surfaces should compile into the same placement model:
- Daemon / CLI:
window.placeis the canonical mutation - Compatibility:
window.tilemaps towindow.place - Voice / hands-off: parse natural language, then emit a placement spec
- HUD: still exposes a smaller shortcut set, but should target the same placement executor
The important change is that placement resolution now happens through
PlacementSpec, not through separate ad hoc parsers per surface.
Frame Calculation
All paths eventually call one of:
WindowTiler.tileFrame(for:on:)— takes aTilePosition+NSScreen, returns aCGRectin AX coordinates (origin = top-left of primary display)WindowTiler.tileFrame(fractions:inDisplay:)— takes raw(x, y, w, h)fractions + display rect
The math:
visible = screen.visibleFrame (excludes menu bar + dock)
primaryH = primary screen height
axTop = primaryH - visible.maxY (flip from AppKit bottom-left to AX top-left)
frame.x = visible.x + visible.width × fx
frame.y = axTop + visible.height × fy
frame.w = visible.width × fw
frame.h = visible.height × fhWindow Targeting
The tile_window intent resolves the target window in this priority:
sessionslot →LatticesApi.window.place/window.tilecompatibility wrapperwidslot →DesktopModel.shared.windows[wid](direct window ID lookup)appslot → first matching window bylocalizedCaseInsensitiveContains, excluding recently-tiled windows (prevents double-matching in batch commands like "Chrome left, Chrome right")- No target → tiles the frontmost window
HandsOff-specific targeting
The system prompt instructs the LLM to always use wid from the desktop snapshot, never app. This avoids ambiguity when multiple windows of the same app exist. In speech, the LLM says the app name; in the JSON action, it uses the wid.
Common Layouts (multi-action)
These are composed from multiple tile_window actions:
| Layout | Actions |
|---|---|
| Split screen | left + right |
| Stack | top + bottom |
| Thirds | left-third + center-third + right-third |
| Quadrants | top-left + top-right + bottom-left + bottom-right |
| Six-up (3×2) | All six *-*-third positions |
| Eight-up (4×2) | All eight *-*-fourth positions |
| Distribute | Single distribute intent (auto-grid) |
CLI shortcuts compile into the same distributor:
lattices tile family→ smart-grid the frontmost app's visible windowslattices distribute iTerm2 right→ smart-grid visible iTerm windows inside the right half
HandsOff Smart Distribution
When the LLM sends multiple tile_window actions targeting the same position, HandsOffSession.distributeTileActions() subdivides:
- 2+ windows → "left" becomes top-left, left, bottom-left
- 2+ windows → "right" becomes top-right, right, bottom-right
- 2+ windows → "maximize" fans out to quadrants then halves
Guardrails
- Typed placement validation: invalid placement strings or objects are rejected at the daemon boundary.
- Recently-tiled dedup:
IntentEngine.recentlyTiledWidsprevents the same window from being matched twice within 2 seconds during batch operations. - Compatibility wrappers:
window.tilestill works, but routes through the same placement machinery.
Current Gaps
- Voice extraction still needs to catch up: the canonical executor understands horizontal thirds and edge quarters, but the local voice resolver still needs broader phrase coverage.
- HUD coverage is narrower than the executor: keyboard tiling exposes a small subset of the full placement vocabulary.
- Optimization and layer actions are still wrapper-level:
space.optimizeandlayer.activateare now stable action IDs, but they currently wrap existing distributor and layer-switching behavior rather than a full planner.