Presentation Execution Review
Presentation And Execution Review
This note reviews the current Lattices presentation layer, the tiling and intent-to-outcome pipeline behind it, and a direction for tightening execution logic. It also pulls in relevant ideas from Hyprland and Wayland, using primary sources only.
Why this document exists
Lattices already has strong operator-facing surfaces:
- HUD and keyboard-driven interaction
- local voice mode
- hands-off voice mode
- command palette
- desktop inventory and search
What is still under-shaped is the execution core behind those surfaces. Today the UX is ahead of the action model.
The root issue is not "tiling quality" in isolation. The deeper issue is that Lattices has multiple control surfaces, multiple intent grammars, and multiple direct execution paths that do not converge on a single canonical planner/executor.
Review scope
This review is based on:
apps/mac/Sources/WindowTiler.swiftapps/mac/Sources/WorkspaceManager.swiftapps/mac/Sources/IntentEngine.swiftapps/mac/Sources/VoiceIntentResolver.swiftapps/mac/Sources/LatticesApi.swiftapps/mac/Sources/PaletteCommand.swiftapps/mac/Sources/HUDController.swiftapps/mac/Sources/DesktopModel.swiftapps/mac/Sources/HandsOffSession.swiftdocs/voice-command-protocol.md
Current Architecture
1. Presentation surfaces are rich, but execution is fragmented
Lattices currently has at least five operator surfaces:
| Surface | Main files | How it resolves intent | How it executes |
|---|---|---|---|
| Command palette | PaletteCommand.swift, CommandPaletteView.swift |
No semantic resolution | Calls SessionManager, WindowTiler, WorkspaceManager directly |
| HUD | HUDController.swift |
Hardcoded key routing | Calls WindowTiler and HandsOffSession directly |
| Local voice | VoiceCommandWindow.swift, VoiceIntentResolver.swift, IntentEngine.swift |
NL embedding + heuristics + slot extraction | Mix of LatticesApi dispatch and direct WindowTiler calls |
| Hands-off voice | HandsOffSession.swift |
External worker returns action list | Replays actions through PhraseMatcher / IntentEngine |
| Daemon / API | LatticesApi.swift, DaemonServer.swift |
Structured request only | Calls subsystems directly |
The result is a split brain:
- presentation is centralized in the app
- execution is not centralized in one action engine
2. Tiling itself is reasonably capable
WindowTiler.swift already has solid building blocks:
- named positions from halves up through sixths and eighths
- generic
grid:CxR:C,Rsupport - frame derivation against
visibleFrame - fast path via
DesktopModel - AX fallback
- AppleScript fallback
- batch moves and raises
- space discovery and limited space movement
That is a good substrate. The problem is not the absence of tiling primitives. The problem is that these primitives are reached through different decision systems.
3. Layer tiling is the closest thing to an orchestrator
WorkspaceManager.tileLayer(...) is the most complete execution pipeline in the app today. It already does:
- inventory refresh
- classification of running vs missing windows
- batched moves for known windows
- fallbacks for untracked windows
- launch queue for missing projects/apps
- delayed post-launch tiling
This is useful, but it is still a per-feature orchestrator, not the shared execution model for the whole product.
4. Intent conversion currently exists in multiple incompatible forms
There are several distinct ways to convert a user goal into motion:
- HUD key map in
HUDController - local voice extraction in
VoiceIntentResolver - intent handlers in
IntentEngine - direct daemon calls in
LatticesApi - hands-off worker action generation in
HandsOffSession
These systems overlap, but they are not the same system.
Root-Cause Findings
1. There is no canonical action schema
The core missing layer is a first-class action model. Right now Lattices mostly jumps from:
- UI event -> subsystem method
instead of:
- UI event -> canonical action -> planner -> validated execution -> result
That is why each surface grows its own grammar and shortcuts.
2. Intent resolution and execution are coupled too early
IntentEngine mixes:
- slot validation
- target resolution
- direct side effects
That makes it harder to:
- preview what will happen
- batch actions coherently
- deduplicate targets
- rollback or retry
- explain why a request failed before side effects begin
3. Presentation layers still own execution semantics
Examples:
- command palette actions directly call
WindowTiler/SessionManager - HUD tile mode computes its own ad hoc grid and applies it immediately
- local voice can go through
LatticesApior bypass it - hands-off voice replays actions one by one rather than committing a single execution plan
This means the product does not yet have one source of truth for "what operation means" or "what order side effects should occur in."
4. The system is not transactional
Wayland's most useful idea here is not Linux-specific rendering. It is commit discipline.
Lattices currently performs many operations incrementally:
- poll desktop
- start moving known windows
- navigate to missing windows
- launch apps
- tile launched apps later
That produces useful behavior, but not an explicit transaction model. The user intent is "arrange my workspace", but implementation today is "execute a staggered set of best-effort side effects."
5. Window identity is still mostly heuristic
The current system relies on:
[lattices:session-name]title tags for terminal windowsapp + title substringmatching for non-terminal windows- a
DesktopModelcache plus CG/AX fallbacks
This is workable, but it is not yet elevated into a durable target-resolution layer with confidence, ambiguity handling, or plan-time diagnostics.
6. The product has multiple position grammars
At minimum there are separate position systems in:
TilePositionparseGridString(...)VoiceIntentResolver.resolvePosition(...)- HUD key routing
- hands-off worker output
- daemon
window.tile
The existence of TilePosition suggests there should be one canonical placement language. In practice there are several.
What Hyprland Gets Right
1. One mutation surface: dispatchers
Hyprland exposes a consistent mutation model: hyprctl dispatch ... calls compositor dispatchers rather than inventing a new command path for each UI. That is the right mental model for Lattices too.
For Lattices, the equivalent should be:
- one canonical action registry
- many presentation surfaces
- one executor
2. Separate control from live observation
Hyprland separates:
- control/info requests via
hyprctl/ request socket - live events via
socket2
That split is useful for Lattices. The app already has an EventBus, but execution still often assumes fresh polling instead of treating live state and mutations as distinct first-class channels.
3. Rules are first-class, not hidden glue
Hyprland's window rules are useful not because Lattices should copy them literally, but because they formalize policy:
- what should open where
- when rules are evaluated
- which rules are static vs dynamic
Lattices currently has pieces of this spread across:
- layer project specs
- companion
windowsentries - session naming
- title matching
- ad hoc fallback behavior
These want to become an explicit rule engine.
4. Layout engines are explicit and addressable
Hyprland treats layout strategy as a named concept:
masterdwindle- per-layout config
- layout-specific dispatchers / layout messages
Lattices currently has positions, grids, layer tiling, and distribute logic, but not named layout engines with their own semantics. That is why many arrangements still feel like clever commands rather than stable workspace behaviors.
What Wayland Gets Right
1. Clear object roles
Wayland's wl_surface becomes meaningful through roles such as toplevel and popup. That separation matters.
Lattices needs the same distinction between:
- raw windows
- lattices session windows
- companion app windows
- HUD utility windows
- layout targets
- execution results
Right now these concepts are present, but not formalized strongly enough in the execution model.
2. Requests and events are different things
Wayland's protocol model is built around objects with requests and events. Lattices should mirror that more explicitly:
- requests mutate state
- events report state changes
- snapshots are derived, not the mutation API
Today those concerns blur together in some places.
3. Pending state vs applied state
This is the biggest transferable idea.
Wayland's surface lifecycle makes state changes pending until commit. Lattices should adopt the same pattern for workspace arrangement:
- resolve targets
- compute plan
- validate plan
- commit plan
- publish result
Without this, "organize my windows" will always be less predictable than it should be.
4. Configure / acknowledge / commit
Wayland's configure-and-ack flow is a good model for coordination between planner and executor:
- planner proposes a layout/result
- executor acknowledges what can be applied
- final commit produces the visible outcome
For Lattices, this suggests explicit execution receipts, not just side effects plus logging.
Proposed Direction For Lattices
1. Introduce a canonical action model
Every surface should emit the same internal action envelope:
{
"intent": "window.tile",
"targets": [{ "kind": "wid", "value": 1234 }],
"args": { "position": "left" },
"source": "voice-local"
}Not all actions need to originate from voice. The important part is that command palette, HUD, daemon, and worker output all normalize into the same model.
2. Split the pipeline into four layers
A. Interpretation
Converts user input into canonical actions.
Examples:
- keyboard chord -> action
- transcript -> action
- daemon JSON -> action
B. Planning
Resolves targets and composes a layout plan.
Examples:
- which exact window does "Chrome" mean
- which screen should
left-thirdland on - whether a missing project should be launched
- whether multiple actions should be merged into one transaction
C. Execution
Applies the plan through one executor.
Examples:
- batch move these windows
- focus this session
- launch these apps
- wait for these windows to appear
- commit final arrangement
D. Presentation
Shows:
- preview
- progress
- result
- failure
The key point is that presentation should no longer define semantics.
3. Create a real arrangement transaction
Lattices should have a type along these lines:
ArrangementIntentArrangementPlanArrangementTransactionArrangementResult
An arrangement transaction should carry:
- requested actions
- resolved targets
- preconditions
- fallback strategy
- steps
- partial failures
- final applied state
That gives the app a place for:
- preview before commit
- retries
- rollback where possible
- voice confirmation
- better diagnostics than raw log lines
4. Unify the placement grammar
There should be exactly one placement language.
That language should cover:
- named positions
- grid cells
- screen/display selection
- optional grouping semantics
- optional relative semantics like "stack" or "master"
Everything else should compile into it, including:
- HUD keys
- voice phrases
- daemon requests
- future scripting
VoiceIntentResolver should not own a separate smaller placement universe.
5. Promote rules to a first-class subsystem
Lattices should formalize:
- static rules: applied on launch or first attach
- dynamic rules: re-evaluated when window properties or workspace state changes
Examples:
- project session window belongs on display 2 left half
- browser matching title X belongs beside project Y
- voice/HUD utility panels are never layout targets
- session windows for a layer open silently until plan commit
This is the clearest Hyprland-inspired gap in the current design.
6. Treat layout strategy as a pluggable engine
Instead of "tile to coordinates" being the only real abstraction, Lattices should support named layout engines, for example:
stackmastergridbspfocus-ringreview
Each layout engine can expose:
- configuration
- planning logic
- compact mutation messages
That is the Hyprland lesson from master, dwindle, and layoutmsg: layout should be semantic, not just positional.
7. Make the daemon the canonical external executor boundary
The daemon already has enough shape to become the stable mutation boundary, but it should move from "RPC bag of commands" toward:
- state snapshot endpoints
- event stream
- action submission
- transaction status / receipts
The app can still call internals directly, but conceptually it should use the same execution core the daemon exposes.
What This Means For Existing Features
HUD
The HUD should remain fast and tactile, but its keys should emit canonical actions or layout messages, not direct tiler calls with private semantics.
Local voice
Local voice should only do:
- transcript normalization
- action extraction
It should not decide execution policy inline.
Hands-off voice
The worker should emit canonical actions only. Swift should not need to reinterpret those actions through a second matcher.
Command palette
Palette commands should become thin wrappers over canonical actions, which makes them previewable and scriptable for free.
Layers
WorkspaceManager.tileLayer(...) should evolve into a planner/executor user, not remain a special-case orchestration island.
Concrete Recommendation
If I had to reduce this to one architectural move, it would be:
Build a single action planner/executor and force every surface to go through it.
That one move solves the deepest problems:
- duplicate intent logic
- duplicate position grammars
- direct UI-owned side effects
- lack of previews and execution receipts
- hard-to-reason-about batch behavior
Suggested Implementation Order
- Define canonical action and target schemas.
- Extract a placement grammar module shared by voice, HUD, daemon, and layer logic.
- Introduce
ArrangementPlanplus an executor that can batch, defer, and report. - Migrate
window.tile,layout.distribute, and layer switching onto that executor first. - Make local voice and hands-off worker emit canonical actions only.
- Add preview/result receipts to HUD and voice surfaces.
- Add first-class rules and named layout engines after the core path is stable.