@lattices/cli 0.3.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.
Files changed (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -25,6 +25,13 @@ private let _SLSMoveWindow: SLSMoveWindowFunc? = {
25
25
  return unsafeBitCast(sym, to: SLSMoveWindowFunc.self)
26
26
  }()
27
27
 
28
+ private typealias SLSOrderWindowFunc = @convention(c) (Int32, UInt32, Int32, UInt32) -> CGError
29
+
30
+ private let _SLSOrderWindow: SLSOrderWindowFunc? = {
31
+ guard let sl = skyLight, let sym = dlsym(sl, "SLSOrderWindow") ?? dlsym(sl, "CGSOrderWindow") else { return nil }
32
+ return unsafeBitCast(sym, to: SLSOrderWindowFunc.self)
33
+ }()
34
+
28
35
  private let _SLSDisableUpdate: SLSDisableUpdateFunc? = {
29
36
  guard let sl = skyLight, let sym = dlsym(sl, "SLSDisableUpdate") else { return nil }
30
37
  return unsafeBitCast(sym, to: SLSDisableUpdateFunc.self)
@@ -122,25 +129,75 @@ private class HighlightBorderView: NSView {
122
129
  }
123
130
  }
124
131
 
132
+ // MARK: - Grid Tiling
133
+
134
+ /// Compute fractional (x, y, w, h) for a cell in a cols×rows grid.
135
+ func tileGrid(cols: Int, rows: Int, col: Int, row: Int) -> (CGFloat, CGFloat, CGFloat, CGFloat) {
136
+ let w = 1.0 / CGFloat(cols)
137
+ let h = 1.0 / CGFloat(rows)
138
+ return (CGFloat(col) * w, CGFloat(row) * h, w, h)
139
+ }
140
+
141
+ /// Parse a grid string like "grid:3x2:0,0" → fractional (x, y, w, h).
142
+ func parseGridString(_ str: String) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
143
+ GridPlacement.parse(str)?.fractions
144
+ }
145
+
125
146
  enum TilePosition: String, CaseIterable, Identifiable {
147
+ // 1x1
148
+ case maximize = "maximize"
149
+ case center = "center"
150
+ // 2x1 (halves, full height)
126
151
  case left = "left"
127
152
  case right = "right"
153
+ // 1x2 (halves, full width)
128
154
  case top = "top"
129
155
  case bottom = "bottom"
156
+ // 2x2 (quarters)
130
157
  case topLeft = "top-left"
131
158
  case topRight = "top-right"
132
159
  case bottomLeft = "bottom-left"
133
160
  case bottomRight = "bottom-right"
134
- case maximize = "maximize"
135
- case center = "center"
161
+ // 3x1 (thirds, full height)
136
162
  case leftThird = "left-third"
137
163
  case centerThird = "center-third"
138
164
  case rightThird = "right-third"
165
+ // 3x2 (sixths)
166
+ case topLeftThird = "top-left-third"
167
+ case topCenterThird = "top-center-third"
168
+ case topRightThird = "top-right-third"
169
+ case bottomLeftThird = "bottom-left-third"
170
+ case bottomCenterThird = "bottom-center-third"
171
+ case bottomRightThird = "bottom-right-third"
172
+ // 4x1 (fourths, full height)
173
+ case firstFourth = "first-fourth"
174
+ case secondFourth = "second-fourth"
175
+ case thirdFourth = "third-fourth"
176
+ case lastFourth = "last-fourth"
177
+ // 4x2 (eighths)
178
+ case topFirstFourth = "top-first-fourth"
179
+ case topSecondFourth = "top-second-fourth"
180
+ case topThirdFourth = "top-third-fourth"
181
+ case topLastFourth = "top-last-fourth"
182
+ case bottomFirstFourth = "bottom-first-fourth"
183
+ case bottomSecondFourth = "bottom-second-fourth"
184
+ case bottomThirdFourth = "bottom-third-fourth"
185
+ case bottomLastFourth = "bottom-last-fourth"
186
+ // Horizontal thirds / quarters
187
+ case topThird = "top-third"
188
+ case middleThird = "middle-third"
189
+ case bottomThird = "bottom-third"
190
+ case leftQuarter = "left-quarter"
191
+ case rightQuarter = "right-quarter"
192
+ case topQuarter = "top-quarter"
193
+ case bottomQuarter = "bottom-quarter"
139
194
 
140
195
  var id: String { rawValue }
141
196
 
142
197
  var label: String {
143
198
  switch self {
199
+ case .maximize: return "Max"
200
+ case .center: return "Center"
144
201
  case .left: return "Left"
145
202
  case .right: return "Right"
146
203
  case .top: return "Top"
@@ -149,11 +206,34 @@ enum TilePosition: String, CaseIterable, Identifiable {
149
206
  case .topRight: return "Top Right"
150
207
  case .bottomLeft: return "Bottom Left"
151
208
  case .bottomRight: return "Bottom Right"
152
- case .maximize: return "Max"
153
- case .center: return "Center"
154
- case .leftThird: return "Left Third"
155
- case .centerThird: return "Center Third"
156
- case .rightThird: return "Right Third"
209
+ case .leftThird: return "Left ⅓"
210
+ case .centerThird: return "Center"
211
+ case .rightThird: return "Right "
212
+ case .topLeftThird: return "Top Left ⅓"
213
+ case .topCenterThird: return "Top Center ⅓"
214
+ case .topRightThird: return "Top Right ⅓"
215
+ case .bottomLeftThird: return "Bottom Left ⅓"
216
+ case .bottomCenterThird: return "Bottom Center ⅓"
217
+ case .bottomRightThird: return "Bottom Right ⅓"
218
+ case .firstFourth: return "1st ¼"
219
+ case .secondFourth: return "2nd ¼"
220
+ case .thirdFourth: return "3rd ¼"
221
+ case .lastFourth: return "4th ¼"
222
+ case .topFirstFourth: return "Top 1st ¼"
223
+ case .topSecondFourth: return "Top 2nd ¼"
224
+ case .topThirdFourth: return "Top 3rd ¼"
225
+ case .topLastFourth: return "Top 4th ¼"
226
+ case .bottomFirstFourth: return "Bottom 1st ¼"
227
+ case .bottomSecondFourth: return "Bottom 2nd ¼"
228
+ case .bottomThirdFourth: return "Bottom 3rd ¼"
229
+ case .bottomLastFourth: return "Bottom 4th ¼"
230
+ case .topThird: return "Top ⅓"
231
+ case .middleThird: return "Middle ⅓"
232
+ case .bottomThird: return "Bottom ⅓"
233
+ case .leftQuarter: return "Left ¼"
234
+ case .rightQuarter: return "Right ¼"
235
+ case .topQuarter: return "Top ¼"
236
+ case .bottomQuarter: return "Bottom ¼"
157
237
  }
158
238
  }
159
239
 
@@ -172,25 +252,66 @@ enum TilePosition: String, CaseIterable, Identifiable {
172
252
  case .leftThird: return "rectangle.leadingthird.inset.filled"
173
253
  case .centerThird: return "rectangle.center.inset.filled"
174
254
  case .rightThird: return "rectangle.trailingthird.inset.filled"
255
+ case .topThird: return "rectangle.topthird.inset.filled"
256
+ case .middleThird: return "rectangle.center.inset.filled"
257
+ case .bottomThird: return "rectangle.bottomthird.inset.filled"
258
+ case .leftQuarter: return "rectangle.leadinghalf.inset.filled"
259
+ case .rightQuarter:return "rectangle.trailinghalf.inset.filled"
260
+ case .topQuarter: return "rectangle.tophalf.inset.filled"
261
+ case .bottomQuarter:return "rectangle.bottomhalf.inset.filled"
262
+ default: return "rectangle.split.3x3.fill"
175
263
  }
176
264
  }
177
265
 
178
266
  /// Returns (x, y, w, h) as fractions of screen
179
267
  var rect: (CGFloat, CGFloat, CGFloat, CGFloat) {
180
268
  switch self {
181
- case .left: return (0, 0, 0.5, 1.0)
182
- case .right: return (0.5, 0, 0.5, 1.0)
183
- case .top: return (0, 0, 1.0, 0.5)
184
- case .bottom: return (0, 0.5, 1.0, 0.5)
185
- case .topLeft: return (0, 0, 0.5, 0.5)
186
- case .topRight: return (0.5, 0, 0.5, 0.5)
187
- case .bottomLeft: return (0, 0.5, 0.5, 0.5)
188
- case .bottomRight: return (0.5, 0.5, 0.5, 0.5)
269
+ // 1x1
189
270
  case .maximize: return (0, 0, 1.0, 1.0)
190
271
  case .center: return (0.15, 0.1, 0.7, 0.8)
191
- case .leftThird: return (0, 0, 0.333, 1.0)
192
- case .centerThird: return (0.333, 0, 0.334, 1.0)
193
- case .rightThird: return (0.667, 0, 0.333, 1.0)
272
+ // 2x1
273
+ case .left: return tileGrid(cols: 2, rows: 1, col: 0, row: 0)
274
+ case .right: return tileGrid(cols: 2, rows: 1, col: 1, row: 0)
275
+ // 1x2
276
+ case .top: return tileGrid(cols: 1, rows: 2, col: 0, row: 0)
277
+ case .bottom: return tileGrid(cols: 1, rows: 2, col: 0, row: 1)
278
+ // 2x2
279
+ case .topLeft: return tileGrid(cols: 2, rows: 2, col: 0, row: 0)
280
+ case .topRight: return tileGrid(cols: 2, rows: 2, col: 1, row: 0)
281
+ case .bottomLeft: return tileGrid(cols: 2, rows: 2, col: 0, row: 1)
282
+ case .bottomRight: return tileGrid(cols: 2, rows: 2, col: 1, row: 1)
283
+ // 3x1
284
+ case .leftThird: return tileGrid(cols: 3, rows: 1, col: 0, row: 0)
285
+ case .centerThird: return tileGrid(cols: 3, rows: 1, col: 1, row: 0)
286
+ case .rightThird: return tileGrid(cols: 3, rows: 1, col: 2, row: 0)
287
+ // 3x2
288
+ case .topLeftThird: return tileGrid(cols: 3, rows: 2, col: 0, row: 0)
289
+ case .topCenterThird: return tileGrid(cols: 3, rows: 2, col: 1, row: 0)
290
+ case .topRightThird: return tileGrid(cols: 3, rows: 2, col: 2, row: 0)
291
+ case .bottomLeftThird: return tileGrid(cols: 3, rows: 2, col: 0, row: 1)
292
+ case .bottomCenterThird: return tileGrid(cols: 3, rows: 2, col: 1, row: 1)
293
+ case .bottomRightThird: return tileGrid(cols: 3, rows: 2, col: 2, row: 1)
294
+ // 4x1
295
+ case .firstFourth: return tileGrid(cols: 4, rows: 1, col: 0, row: 0)
296
+ case .secondFourth: return tileGrid(cols: 4, rows: 1, col: 1, row: 0)
297
+ case .thirdFourth: return tileGrid(cols: 4, rows: 1, col: 2, row: 0)
298
+ case .lastFourth: return tileGrid(cols: 4, rows: 1, col: 3, row: 0)
299
+ // 4x2
300
+ case .topFirstFourth: return tileGrid(cols: 4, rows: 2, col: 0, row: 0)
301
+ case .topSecondFourth: return tileGrid(cols: 4, rows: 2, col: 1, row: 0)
302
+ case .topThirdFourth: return tileGrid(cols: 4, rows: 2, col: 2, row: 0)
303
+ case .topLastFourth: return tileGrid(cols: 4, rows: 2, col: 3, row: 0)
304
+ case .bottomFirstFourth: return tileGrid(cols: 4, rows: 2, col: 0, row: 1)
305
+ case .bottomSecondFourth: return tileGrid(cols: 4, rows: 2, col: 1, row: 1)
306
+ case .bottomThirdFourth: return tileGrid(cols: 4, rows: 2, col: 2, row: 1)
307
+ case .bottomLastFourth: return tileGrid(cols: 4, rows: 2, col: 3, row: 1)
308
+ case .topThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 0)
309
+ case .middleThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 1)
310
+ case .bottomThird: return tileGrid(cols: 1, rows: 3, col: 0, row: 2)
311
+ case .leftQuarter: return tileGrid(cols: 4, rows: 1, col: 0, row: 0)
312
+ case .rightQuarter: return tileGrid(cols: 4, rows: 1, col: 3, row: 0)
313
+ case .topQuarter: return tileGrid(cols: 1, rows: 4, col: 0, row: 0)
314
+ case .bottomQuarter: return tileGrid(cols: 1, rows: 4, col: 0, row: 3)
194
315
  }
195
316
  }
196
317
  }
@@ -272,6 +393,10 @@ enum WindowTiler {
272
393
  /// Convert fractional rect to AppleScript bounds {left, top, right, bottom}
273
394
  /// AppleScript uses top-left origin; NSScreen uses bottom-left origin
274
395
  private static func appleScriptBounds(for position: TilePosition, screen: NSScreen? = nil) -> (Int, Int, Int, Int) {
396
+ appleScriptBounds(for: position.rect, screen: screen)
397
+ }
398
+
399
+ private static func appleScriptBounds(for fractions: (CGFloat, CGFloat, CGFloat, CGFloat), screen: NSScreen? = nil) -> (Int, Int, Int, Int) {
275
400
  let targetScreen = screen ?? NSScreen.main
276
401
  guard let targetScreen else { return (0, 0, 960, 540) }
277
402
  let full = targetScreen.frame
@@ -282,7 +407,7 @@ enum WindowTiler {
282
407
  let visW = Int(visible.width)
283
408
  let visH = Int(visible.height)
284
409
 
285
- let (fx, fy, fw, fh) = position.rect
410
+ let (fx, fy, fw, fh) = fractions
286
411
  let x1 = visLeft + Int(CGFloat(visW) * fx)
287
412
  let y1 = visTop + Int(CGFloat(visH) * fy)
288
413
  let x2 = x1 + Int(CGFloat(visW) * fw)
@@ -290,13 +415,23 @@ enum WindowTiler {
290
415
  return (x1, y1, x2, y2)
291
416
  }
292
417
 
418
+ /// Get the main screen's frame (safe to call from main thread only).
419
+ static func mainScreenFrame() -> CGRect {
420
+ return NSScreen.main?.frame ?? NSScreen.screens.first?.frame ?? CGRect(x: 0, y: 0, width: 1920, height: 1080)
421
+ }
422
+
293
423
  /// Compute AX-coordinate frame for a tile position on a given screen
294
424
  static func tileFrame(for position: TilePosition, on screen: NSScreen) -> CGRect {
425
+ tileFrame(fractions: position.rect, on: screen)
426
+ }
427
+
428
+ /// Compute AX-coordinate frame for arbitrary fractional placement on a given screen.
429
+ static func tileFrame(fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on screen: NSScreen) -> CGRect {
295
430
  let visible = screen.visibleFrame
296
431
  guard let primary = NSScreen.screens.first else { return .zero }
297
432
  let primaryH = primary.frame.height
298
433
  let axTop = primaryH - visible.maxY
299
- let (fx, fy, fw, fh) = position.rect
434
+ let (fx, fy, fw, fh) = fractions
300
435
  return CGRect(
301
436
  x: visible.origin.x + visible.width * fx,
302
437
  y: axTop + visible.height * fy,
@@ -305,6 +440,10 @@ enum WindowTiler {
305
440
  )
306
441
  }
307
442
 
443
+ static func tileFrame(for placement: PlacementSpec, on screen: NSScreen) -> CGRect {
444
+ tileFrame(fractions: placement.fractions, on: screen)
445
+ }
446
+
308
447
  /// Compute AX-coordinate frame for a tile position within a raw display CGRect (CG/AX coords)
309
448
  static func tileFrame(for position: TilePosition, inDisplay displayRect: CGRect) -> CGRect {
310
449
  let (fx, fy, fw, fh) = position.rect
@@ -330,12 +469,16 @@ enum WindowTiler {
330
469
  /// Tile a specific terminal window on a given screen.
331
470
  /// Fast path: DesktopModel → AX. Fallback: AX search → AppleScript last resort.
332
471
  static func tile(session: String, terminal: Terminal, to position: TilePosition, on screen: NSScreen) {
472
+ tile(session: session, terminal: terminal, to: .tile(position), on: screen)
473
+ }
474
+
475
+ static func tile(session: String, terminal: Terminal, to placement: PlacementSpec, on screen: NSScreen) {
333
476
  let diag = DiagnosticLog.shared
334
- let t = diag.startTimed("tile: \(session) → \(position.rawValue)")
477
+ let t = diag.startTimed("tile: \(session) → \(placement.wireValue)")
335
478
 
336
479
  // Fast path: use DesktopModel cache → single AX move
337
480
  if let entry = DesktopModel.shared.windowForSession(session) {
338
- let frame = tileFrame(for: position, on: screen)
481
+ let frame = tileFrame(for: placement, on: screen)
339
482
  batchMoveAndRaiseWindows([(wid: entry.wid, pid: entry.pid, frame: frame)])
340
483
  diag.success("tile fast path (DesktopModel): \(session)")
341
484
  diag.finish(t)
@@ -345,7 +488,7 @@ enum WindowTiler {
345
488
  // AX fallback: search terminal windows by title tag
346
489
  let tag = Terminal.windowTag(for: session)
347
490
  if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag) {
348
- let targetFrame = tileFrame(for: position, on: screen)
491
+ let targetFrame = tileFrame(for: placement, on: screen)
349
492
  var newPos = CGPoint(x: targetFrame.origin.x, y: targetFrame.origin.y)
350
493
  var newSize = CGSize(width: targetFrame.width, height: targetFrame.height)
351
494
  let win = axWindow
@@ -370,7 +513,7 @@ enum WindowTiler {
370
513
 
371
514
  // AppleScript last resort (slow, single-monitor)
372
515
  diag.warn("tile AppleScript last resort: \(session)")
373
- let bounds = appleScriptBounds(for: position, screen: screen)
516
+ let bounds = appleScriptBounds(for: placement.fractions, screen: screen)
374
517
  switch terminal {
375
518
  case .terminal:
376
519
  tileAppleScript(app: "Terminal", tag: tag, bounds: bounds)
@@ -385,8 +528,12 @@ enum WindowTiler {
385
528
  /// Tile a specific terminal window (found by lattices session tag) to a position.
386
529
  /// Uses the same fast path strategy as tile(session:terminal:to:on:) with main screen.
387
530
  static func tile(session: String, terminal: Terminal, to position: TilePosition) {
531
+ tile(session: session, terminal: terminal, to: .tile(position))
532
+ }
533
+
534
+ static func tile(session: String, terminal: Terminal, to placement: PlacementSpec) {
388
535
  let screen = NSScreen.main ?? NSScreen.screens[0]
389
- tile(session: session, terminal: terminal, to: position, on: screen)
536
+ tile(session: session, terminal: terminal, to: placement, on: screen)
390
537
  }
391
538
 
392
539
  /// Tile the frontmost window (works for any terminal)
@@ -537,7 +684,8 @@ enum WindowTiler {
537
684
  }
538
685
 
539
686
  /// Attempt CGS-based window move. Returns nil if APIs are unavailable.
540
- private static func moveViaCGS(wid: UInt32, fromSpaces: [Int], toSpace: Int) -> MoveResult? {
687
+ /// Move a window between spaces via CGS private APIs. Internal — used by present() and moveWindowToSpace().
688
+ internal static func moveViaCGS(wid: UInt32, fromSpaces: [Int], toSpace: Int) -> MoveResult? {
541
689
  let diag = DiagnosticLog.shared
542
690
  guard let mainConn = CGS.mainConnectionID,
543
691
  let addToSpaces = CGS.addWindowsToSpaces,
@@ -997,53 +1145,59 @@ enum WindowTiler {
997
1145
  DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
998
1146
  }
999
1147
 
1000
- /// Tile any window by its CG window ID to a position using AX API
1001
- static func tileWindowById(wid: UInt32, pid: Int32, to position: TilePosition) {
1002
- let diag = DiagnosticLog.shared
1003
- diag.info("tileWindowById: wid=\(wid) pid=\(pid) pos=\(position.rawValue)")
1004
-
1005
- // Find the screen the window is on
1006
- guard let windowFrame = cgWindowFrame(wid: wid) else {
1007
- diag.warn("tileWindowById: no frame for wid=\(wid)")
1008
- return
1148
+ /// Tile any window by its CG window ID to a position.
1149
+ /// Delegates to `batchMoveAndRaiseWindows` which is the battle-tested path:
1150
+ /// uses `_AXUIElementGetWindow` for direct wid→AX mapping, disables enhanced UI,
1151
+ /// freezes screen rendering, and verifies+retries drifted windows.
1152
+ /// Tile a window using raw fractional coordinates.
1153
+ static func tileWindowById(wid: UInt32, pid: Int32, fractions: (CGFloat, CGFloat, CGFloat, CGFloat), on targetScreen: NSScreen? = nil) {
1154
+ let screen = targetScreen ?? NSScreen.main ?? NSScreen.screens[0]
1155
+ let frame = tileFrame(fractions: fractions, on: screen)
1156
+ DiagnosticLog.shared.info("tileWindowById: wid=\(wid) fractions=\(fractions) frame=(\(Int(frame.origin.x)),\(Int(frame.origin.y))) \(Int(frame.width))x\(Int(frame.height))")
1157
+ if let app = NSRunningApplication(processIdentifier: pid) { app.activate() }
1158
+ let moves = [(wid: wid, pid: pid, frame: frame)]
1159
+ batchMoveAndRaiseWindows(moves)
1160
+ let drifted = verifyMoves(moves)
1161
+ if !drifted.isEmpty {
1162
+ usleep(100_000)
1163
+ batchMoveAndRaiseWindows(drifted.map { (wid: $0.wid, pid: $0.pid, frame: $0.frame) })
1009
1164
  }
1010
- let screen = NSScreen.screens.first(where: {
1011
- $0.frame.contains(NSPoint(x: windowFrame.midX, y: windowFrame.midY))
1012
- }) ?? NSScreen.main ?? NSScreen.screens[0]
1165
+ }
1013
1166
 
1014
- let visible = screen.visibleFrame
1015
- let (fx, fy, fw, fh) = position.rect
1167
+ static func tileWindowById(wid: UInt32, pid: Int32, to position: TilePosition, on targetScreen: NSScreen? = nil) {
1168
+ tileWindowById(wid: wid, pid: pid, to: .tile(position), on: targetScreen)
1169
+ }
1016
1170
 
1017
- // Calculate target in NS coordinates (bottom-left origin)
1018
- let targetX = visible.origin.x + visible.width * fx
1019
- let targetY = visible.origin.y + visible.height * (1.0 - fy - fh)
1020
- let targetW = visible.width * fw
1021
- let targetH = visible.height * fh
1171
+ static func tileWindowById(wid: UInt32, pid: Int32, to placement: PlacementSpec, on targetScreen: NSScreen? = nil) {
1172
+ let diag = DiagnosticLog.shared
1173
+ let screen = targetScreen ?? NSScreen.main ?? NSScreen.screens[0]
1174
+ let frame = tileFrame(for: placement, on: screen)
1022
1175
 
1023
- // Convert NS bottom-left AX top-left origin
1024
- guard let primaryScreen = NSScreen.screens.first else { return }
1025
- let primaryHeight = primaryScreen.frame.height
1026
- let axX = targetX
1027
- let axY = primaryHeight - targetY - targetH
1176
+ diag.info("tileWindowById: wid=\(wid) pid=\(pid) pos=\(placement.wireValue) screen=\(screen.localizedName) frame=(\(Int(frame.origin.x)),\(Int(frame.origin.y))) \(Int(frame.width))x\(Int(frame.height))")
1028
1177
 
1029
- // Find the AX window matching this CG wid by frame comparison
1030
- guard let axWindow = findAXWindowByFrame(wid: wid, pid: pid) else {
1031
- diag.warn("tileWindowById: couldn't match AX window for wid=\(wid)")
1032
- return
1178
+ // Focus the app so windows on other Spaces come to the current one
1179
+ if let app = NSRunningApplication(processIdentifier: pid) {
1180
+ app.activate()
1033
1181
  }
1034
1182
 
1035
- // Set position and size via AX
1036
- var newPos = CGPoint(x: axX, y: axY)
1037
- var newSize = CGSize(width: targetW, height: targetH)
1183
+ let moves = [(wid: wid, pid: pid, frame: frame)]
1184
+ batchMoveAndRaiseWindows(moves)
1038
1185
 
1039
- if let posValue = AXValueCreate(.cgPoint, &newPos) {
1040
- AXUIElementSetAttributeValue(axWindow, kAXPositionAttribute as CFString, posValue)
1041
- }
1042
- if let sizeValue = AXValueCreate(.cgSize, &newSize) {
1043
- AXUIElementSetAttributeValue(axWindow, kAXSizeAttribute as CFString, sizeValue)
1186
+ // Verify and retry once if needed
1187
+ let drifted = verifyMoves(moves)
1188
+ if !drifted.isEmpty {
1189
+ diag.info("tileWindowById: wid=\(wid) drifted, retrying...")
1190
+ usleep(100_000)
1191
+ batchMoveAndRaiseWindows(drifted)
1192
+ let stillDrifted = verifyMoves(drifted)
1193
+ if stillDrifted.isEmpty {
1194
+ diag.success("tileWindowById: wid=\(wid) retry succeeded")
1195
+ } else {
1196
+ diag.warn("tileWindowById: wid=\(wid) still drifted after retry")
1197
+ }
1198
+ } else {
1199
+ diag.success("tileWindowById: tiled wid=\(wid) to \(placement.wireValue)")
1044
1200
  }
1045
-
1046
- diag.success("tileWindowById: tiled wid=\(wid) to \(position.rawValue)")
1047
1201
  }
1048
1202
 
1049
1203
  /// Distribute windows in a smart grid layout (delegates to batch operation)
@@ -1074,7 +1228,7 @@ enum WindowTiler {
1074
1228
  }
1075
1229
 
1076
1230
  /// Get NSRect (bottom-left origin) for a known CG window ID
1077
- private static func cgWindowFrame(wid: UInt32) -> NSRect? {
1231
+ static func cgWindowFrame(wid: UInt32) -> NSRect? {
1078
1232
  guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
1079
1233
  for info in windowList {
1080
1234
  if let num = info[kCGWindowNumber as String] as? UInt32, num == wid,
@@ -1128,6 +1282,7 @@ enum WindowTiler {
1128
1282
  }
1129
1283
  }
1130
1284
  }
1285
+ DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
1131
1286
  diag.success("raiseWindowsAndReactivate: raised \(windows.count) windows")
1132
1287
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
1133
1288
  NSApp.activate(ignoringOtherApps: true)
@@ -1166,6 +1321,7 @@ enum WindowTiler {
1166
1321
  } else {
1167
1322
  doRaise()
1168
1323
  }
1324
+ DesktopModel.shared.markInteraction(wid: wid)
1169
1325
  }
1170
1326
 
1171
1327
  // MARK: - Batch Window Operations
@@ -1288,24 +1444,93 @@ enum WindowTiler {
1288
1444
  }
1289
1445
 
1290
1446
  /// Raise and focus a single window by its CGWindowID.
1291
- static func focusWindow(wid: UInt32, pid: Int32) {
1292
- let appRef = AXUIElementCreateApplication(pid)
1293
- var windowsRef: CFTypeRef?
1294
- guard AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef) == .success,
1295
- let axWindows = windowsRef as? [AXUIElement] else { return }
1447
+ @discardableResult
1448
+ static func focusWindow(wid: UInt32, pid: Int32) -> Bool {
1449
+ return present(wid: wid, pid: pid)
1450
+ }
1296
1451
 
1297
- for axWin in axWindows {
1298
- var windowId: CGWindowID = 0
1299
- if _AXUIElementGetWindow(axWin, &windowId) == .success, windowId == wid {
1300
- AXUIElementPerformAction(axWin, kAXRaiseAction as CFString)
1301
- AXUIElementSetAttributeValue(axWin, kAXMainAttribute as CFString, kCFBooleanTrue)
1302
- break
1452
+ // MARK: - Present
1453
+
1454
+ /// Present a window: move it to the current space, bring it to front, optionally position it.
1455
+ /// This is the single entry point for "show me this window right now."
1456
+ @discardableResult
1457
+ static func present(wid: UInt32, pid: Int32, frame: CGRect? = nil) -> Bool {
1458
+ let diag = DiagnosticLog.shared
1459
+
1460
+ // 1. Move to current space if needed
1461
+ let windowSpaces = getSpacesForWindow(wid)
1462
+ let currentSpace = getCurrentSpace()
1463
+ if currentSpace != 0, !windowSpaces.isEmpty, !windowSpaces.contains(currentSpace) {
1464
+ diag.info("present: wid \(wid) on space \(windowSpaces), moving to current space \(currentSpace)")
1465
+ _ = moveViaCGS(wid: wid, fromSpaces: windowSpaces, toSpace: currentSpace)
1466
+ }
1467
+
1468
+ // 2. Position if requested
1469
+ if let frame = frame {
1470
+ if let mainConn = _SLSMainConnectionID, let moveWindow = _SLSMoveWindow {
1471
+ let cid = mainConn()
1472
+ var origin = CGPoint(x: frame.origin.x, y: frame.origin.y)
1473
+ moveWindow(cid, wid, &origin)
1474
+ }
1475
+ // Resize via AX
1476
+ let appRef = AXUIElementCreateApplication(pid)
1477
+ var windowsRef: CFTypeRef?
1478
+ if AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef) == .success,
1479
+ let axWindows = windowsRef as? [AXUIElement] {
1480
+ for axWin in axWindows {
1481
+ var windowId: CGWindowID = 0
1482
+ if _AXUIElementGetWindow(axWin, &windowId) == .success, windowId == wid {
1483
+ var size = CGSize(width: frame.width, height: frame.height)
1484
+ let sizeValue = AXValueCreate(.cgSize, &size)!
1485
+ AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, sizeValue)
1486
+ break
1487
+ }
1488
+ }
1303
1489
  }
1304
1490
  }
1305
1491
 
1492
+ // 3. Activate the app first (this may bring the wrong window forward)
1306
1493
  if let app = NSRunningApplication(processIdentifier: pid) {
1307
1494
  app.activate()
1308
1495
  }
1496
+
1497
+ // 4. Then order OUR window to front — after activate so we get the last word
1498
+ if let mainConn = _SLSMainConnectionID, let orderWindow = _SLSOrderWindow {
1499
+ let cid = mainConn()
1500
+ let err = orderWindow(cid, wid, 1, 0)
1501
+ if err != .success {
1502
+ diag.warn("present: SLSOrderWindow failed for wid \(wid): \(err)")
1503
+ }
1504
+ }
1505
+
1506
+ // 5. Set as main window via AX
1507
+ let appRef = AXUIElementCreateApplication(pid)
1508
+ var windowsRef: CFTypeRef?
1509
+ if AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef) == .success,
1510
+ let axWindows = windowsRef as? [AXUIElement] {
1511
+ for axWin in axWindows {
1512
+ var windowId: CGWindowID = 0
1513
+ if _AXUIElementGetWindow(axWin, &windowId) == .success, windowId == wid {
1514
+ AXUIElementSetAttributeValue(axWin, kAXMainAttribute as CFString, kCFBooleanTrue)
1515
+ AXUIElementPerformAction(axWin, kAXRaiseAction as CFString)
1516
+ break
1517
+ }
1518
+ }
1519
+ }
1520
+
1521
+ // 6. Re-raise after delays to defeat focus stealing from the caller.
1522
+ // Two passes: early catch (200ms) and late catch (600ms) for slower responses.
1523
+ for delay in [0.2, 0.6] {
1524
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
1525
+ if let mainConn = _SLSMainConnectionID, let orderWindow = _SLSOrderWindow {
1526
+ let cid = mainConn()
1527
+ orderWindow(cid, wid, 1, 0)
1528
+ }
1529
+ }
1530
+ }
1531
+
1532
+ DesktopModel.shared.markInteraction(wid: wid)
1533
+ return true
1309
1534
  }
1310
1535
 
1311
1536
  /// Move AND raise windows in a single CG+AX pass (avoids duplicate lookups).
@@ -1383,6 +1608,7 @@ enum WindowTiler {
1383
1608
 
1384
1609
  // Unfreeze screen rendering
1385
1610
  if let cid { _ = _SLSReenableUpdate?(cid) }
1611
+ DesktopModel.shared.markInteraction(wids: moves.map(\.wid))
1386
1612
  diag.success("batchMoveAndRaiseWindows: processed \(processed)/\(moves.count) windows")
1387
1613
  }
1388
1614
 
@@ -1418,33 +1644,48 @@ enum WindowTiler {
1418
1644
  }
1419
1645
  }
1420
1646
 
1421
- /// Compute grid slot rects in AX coordinates (top-left origin) for N windows
1422
- static func computeGridSlots(count: Int, screen: NSScreen) -> [CGRect] {
1647
+ /// Compute grid slot rects in AX coordinates (top-left origin) for N windows.
1648
+ /// If `region` is provided (fractional x, y, w, h), slots are constrained to that sub-area of the screen.
1649
+ static func computeGridSlots(count: Int, screen: NSScreen, region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil) -> [CGRect] {
1423
1650
  guard count > 0 else { return [] }
1424
1651
  let visible = screen.visibleFrame
1425
1652
  guard let primaryScreen = NSScreen.screens.first else { return [] }
1426
1653
  let primaryHeight = primaryScreen.frame.height
1427
1654
 
1428
- // AX Y of visible top edge
1429
- let axTop = primaryHeight - visible.maxY
1655
+ // Compute the target area — full visible frame or a fractional sub-region
1656
+ let targetArea: CGRect
1657
+ if let (rx, ry, rw, rh) = region {
1658
+ targetArea = CGRect(
1659
+ x: visible.origin.x + visible.width * rx,
1660
+ y: visible.origin.y + visible.height * (1.0 - ry - rh), // NSRect is bottom-left origin
1661
+ width: visible.width * rw,
1662
+ height: visible.height * rh
1663
+ )
1664
+ } else {
1665
+ targetArea = visible
1666
+ }
1667
+
1668
+ // AX Y of target area's top edge
1669
+ let axTop = primaryHeight - targetArea.maxY
1430
1670
  let shape = gridShape(for: count)
1431
1671
  let rowCount = shape.count
1432
- let rowH = visible.height / CGFloat(rowCount)
1672
+ let rowH = targetArea.height / CGFloat(rowCount)
1433
1673
 
1434
1674
  var slots: [CGRect] = []
1435
1675
  for (row, cols) in shape.enumerated() {
1436
- let colW = visible.width / CGFloat(cols)
1676
+ let colW = targetArea.width / CGFloat(cols)
1437
1677
  let axY = axTop + CGFloat(row) * rowH
1438
1678
  for col in 0..<cols {
1439
- let x = visible.origin.x + CGFloat(col) * colW
1679
+ let x = targetArea.origin.x + CGFloat(col) * colW
1440
1680
  slots.append(CGRect(x: x, y: axY, width: colW, height: rowH))
1441
1681
  }
1442
1682
  }
1443
1683
  return slots
1444
1684
  }
1445
1685
 
1446
- /// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process
1447
- static func batchRaiseAndDistribute(windows: [(wid: UInt32, pid: Int32)]) {
1686
+ /// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process.
1687
+ /// If `region` is provided (fractional x, y, w, h), the grid is constrained to that sub-area.
1688
+ static func batchRaiseAndDistribute(windows: [(wid: UInt32, pid: Int32)], region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil) {
1448
1689
  guard !windows.isEmpty else { return }
1449
1690
  let diag = DiagnosticLog.shared
1450
1691
 
@@ -1466,10 +1707,11 @@ enum WindowTiler {
1466
1707
  diag.info("Grid layout: \(windows.count) windows → [\(desc)]")
1467
1708
  diag.info(" Screen: \(screen.localizedName) \(Int(screenFrame.width))x\(Int(screenFrame.height))")
1468
1709
  diag.info(" Visible: origin=(\(Int(visible.origin.x)),\(Int(visible.origin.y))) size=\(Int(visible.width))x\(Int(visible.height))")
1710
+ if let region { diag.info(" Region: x=\(region.0) y=\(region.1) w=\(region.2) h=\(region.3)") }
1469
1711
  diag.info(" Primary height: \(Int(primaryHeight))")
1470
1712
 
1471
1713
  // Pre-compute all target slots
1472
- let slots = computeGridSlots(count: windows.count, screen: screen)
1714
+ let slots = computeGridSlots(count: windows.count, screen: screen, region: region)
1473
1715
  guard slots.count == windows.count else {
1474
1716
  diag.warn(" Slot count mismatch: \(slots.count) slots for \(windows.count) windows")
1475
1717
  return
@@ -1593,6 +1835,7 @@ enum WindowTiler {
1593
1835
  if !failed.isEmpty {
1594
1836
  diag.warn("batchRaiseAndDistribute: failed wids=\(failed)")
1595
1837
  }
1838
+ DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
1596
1839
  diag.success("batchRaiseAndDistribute: moved \(moved)/\(windows.count) [\(desc) grid]")
1597
1840
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
1598
1841
  NSApp.activate(ignoringOtherApps: true)
@@ -1670,6 +1913,10 @@ enum WindowTiler {
1670
1913
  /// Tile the frontmost window of any app to a position using AX API.
1671
1914
  /// Works for any application (Finder, Chrome, etc.), not just terminals.
1672
1915
  static func tileFrontmostViaAX(to position: TilePosition) {
1916
+ tileFrontmostViaAX(to: .tile(position))
1917
+ }
1918
+
1919
+ static func tileFrontmostViaAX(to placement: PlacementSpec) {
1673
1920
  guard let frontApp = NSWorkspace.shared.frontmostApplication,
1674
1921
  frontApp.bundleIdentifier != "com.arach.lattices" else { return }
1675
1922
 
@@ -1680,7 +1927,7 @@ enum WindowTiler {
1680
1927
  let win = axWindow as! AXUIElement
1681
1928
 
1682
1929
  let screen = screenForAXWindow(win)
1683
- let target = tileFrame(for: position, on: screen)
1930
+ let target = tileFrame(for: placement, on: screen)
1684
1931
 
1685
1932
  // Disable enhanced UI on the APP element (not window) — breaks macOS tile lock
1686
1933
  AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, false as CFTypeRef)
@@ -1736,6 +1983,20 @@ enum WindowTiler {
1736
1983
  ?? NSScreen.screens[0]
1737
1984
  }
1738
1985
 
1986
+ static func screenForWindowFrame(_ frame: WindowFrame) -> NSScreen {
1987
+ guard let primaryScreen = NSScreen.screens.first else {
1988
+ return NSScreen.main ?? NSScreen.screens[0]
1989
+ }
1990
+
1991
+ let centerX = frame.x + frame.w / 2
1992
+ let centerY = frame.y + frame.h / 2
1993
+ let nsCenterY = primaryScreen.frame.height - centerY
1994
+
1995
+ return NSScreen.screens.first(where: {
1996
+ $0.frame.contains(NSPoint(x: centerX, y: nsCenterY))
1997
+ }) ?? NSScreen.main ?? primaryScreen
1998
+ }
1999
+
1739
2000
  // MARK: - Private
1740
2001
 
1741
2002
  private static func tileAppleScript(app: String, tag: String, bounds: (Int, Int, Int, Int)) {