@lattices/cli 0.4.1 → 0.4.5

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 (71) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/ActionRow.swift +43 -26
  7. package/app/Sources/App.swift +10 -0
  8. package/app/Sources/AppDelegate.swift +91 -30
  9. package/app/Sources/AppShellView.swift +2 -0
  10. package/app/Sources/AppTypeClassifier.swift +36 -0
  11. package/app/Sources/AppUpdater.swift +92 -0
  12. package/app/Sources/CheatSheetHUD.swift +1 -0
  13. package/app/Sources/CliActionLauncher.swift +50 -0
  14. package/app/Sources/CommandModeView.swift +4 -24
  15. package/app/Sources/CompanionActivityLog.swift +70 -0
  16. package/app/Sources/CompanionKeyboardController.swift +141 -0
  17. package/app/Sources/DesktopModel.swift +4 -0
  18. package/app/Sources/HandsOffSession.swift +53 -16
  19. package/app/Sources/HomeDashboardView.swift +18 -10
  20. package/app/Sources/HotkeyStore.swift +8 -5
  21. package/app/Sources/IntentEngine.swift +7 -1
  22. package/app/Sources/LatticesApi.swift +125 -4
  23. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  24. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  25. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  26. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  27. package/app/Sources/LatticesDeckHost.swift +1463 -0
  28. package/app/Sources/LatticesRuntime.swift +61 -0
  29. package/app/Sources/MainView.swift +398 -186
  30. package/app/Sources/MouseFinder.swift +335 -30
  31. package/app/Sources/MouseGestureConfig.swift +364 -0
  32. package/app/Sources/MouseGestureController.swift +1203 -0
  33. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  34. package/app/Sources/MouseInputEventViewer.swift +272 -0
  35. package/app/Sources/MouseShortcutStore.swift +107 -0
  36. package/app/Sources/OmniSearchView.swift +136 -2
  37. package/app/Sources/OmniSearchWindow.swift +65 -5
  38. package/app/Sources/OnboardingView.swift +30 -16
  39. package/app/Sources/PaletteCommand.swift +26 -6
  40. package/app/Sources/PermissionChecker.swift +76 -2
  41. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  42. package/app/Sources/PiAuthPromptCard.swift +90 -0
  43. package/app/Sources/PiChatDock.swift +137 -74
  44. package/app/Sources/PiChatSession.swift +608 -108
  45. package/app/Sources/PiInstallCallout.swift +86 -0
  46. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  47. package/app/Sources/PiWorkspaceView.swift +174 -77
  48. package/app/Sources/Preferences.swift +78 -0
  49. package/app/Sources/ScreenMapState.swift +91 -31
  50. package/app/Sources/ScreenMapView.swift +510 -524
  51. package/app/Sources/ScreenMapWindowController.swift +12 -4
  52. package/app/Sources/SettingsView.swift +869 -152
  53. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  54. package/app/Sources/VoiceCommandWindow.swift +23 -2
  55. package/app/Sources/WindowDragSnapController.swift +628 -0
  56. package/app/Sources/WindowTiler.swift +328 -65
  57. package/app/Sources/WorkspaceManager.swift +288 -0
  58. package/bin/assistant-intelligence.ts +874 -0
  59. package/bin/handsoff-infer.ts +16 -209
  60. package/bin/handsoff-worker.ts +45 -258
  61. package/bin/lattices-app.ts +65 -1
  62. package/bin/lattices-dev +4 -0
  63. package/bin/lattices.ts +125 -14
  64. package/docs/agents.md +14 -0
  65. package/docs/api.md +55 -0
  66. package/docs/app.md +3 -0
  67. package/docs/companion-deck.md +180 -0
  68. package/docs/config.md +25 -0
  69. package/docs/tiling-reference.md +55 -0
  70. package/docs/voice-error-model.md +73 -0
  71. package/package.json +4 -2
@@ -51,10 +51,10 @@ final class WindowHighlight {
51
51
  private var fadeTimer: Timer?
52
52
 
53
53
  /// Flash a green border overlay at the given screen frame
54
- func flash(frame: NSRect, duration: TimeInterval = 1.2) {
54
+ func flash(frame: NSRect, duration: TimeInterval = 0.9) {
55
55
  dismiss()
56
56
 
57
- let inset: CGFloat = -8 // slightly larger than the window
57
+ let inset: CGFloat = -6 // slightly larger than the window
58
58
  let expandedFrame = frame.insetBy(dx: inset, dy: inset)
59
59
 
60
60
  let window = NSWindow(
@@ -110,22 +110,28 @@ final class WindowHighlight {
110
110
 
111
111
  private class HighlightBorderView: NSView {
112
112
  override func draw(_ dirtyRect: NSRect) {
113
- let borderWidth: CGFloat = 4
113
+ let borderWidth: CGFloat = 3
114
114
  let cornerRadius: CGFloat = 12
115
115
 
116
116
  // Outer glow
117
117
  let glowRect = bounds.insetBy(dx: 1, dy: 1)
118
118
  let glowPath = NSBezierPath(roundedRect: glowRect, xRadius: cornerRadius + 2, yRadius: cornerRadius + 2)
119
- glowPath.lineWidth = borderWidth + 4
120
- NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.15).setStroke()
119
+ glowPath.lineWidth = borderWidth + 2
120
+ NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.07).setStroke()
121
121
  glowPath.stroke()
122
122
 
123
123
  // Main border
124
124
  let rect = bounds.insetBy(dx: borderWidth / 2 + 2, dy: borderWidth / 2 + 2)
125
125
  let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
126
126
  path.lineWidth = borderWidth
127
- NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.9).setStroke()
127
+ NSColor(calibratedRed: 0.2, green: 0.9, blue: 0.4, alpha: 0.58).setStroke()
128
128
  path.stroke()
129
+
130
+ let innerRect = rect.insetBy(dx: 3, dy: 3)
131
+ let innerPath = NSBezierPath(roundedRect: innerRect, xRadius: max(cornerRadius - 3, 6), yRadius: max(cornerRadius - 3, 6))
132
+ innerPath.lineWidth = 1
133
+ NSColor.white.withAlphaComponent(0.10).setStroke()
134
+ innerPath.stroke()
129
135
  }
130
136
  }
131
137
 
@@ -588,6 +594,81 @@ enum WindowTiler {
588
594
  return Int(getActive(mainConn()))
589
595
  }
590
596
 
597
+ static func adjacentSpace(in spaces: [SpaceInfo], currentSpaceId: Int, offset: Int) -> SpaceInfo? {
598
+ guard let currentIndex = spaces.firstIndex(where: { $0.id == currentSpaceId }) else { return nil }
599
+ let targetIndex = currentIndex + offset
600
+ guard spaces.indices.contains(targetIndex) else { return nil }
601
+ return spaces[targetIndex]
602
+ }
603
+
604
+ private struct AdjacentSpaceContext {
605
+ let point: CGPoint
606
+ let display: DisplaySpaces
607
+ let activeSpaceId: Int
608
+ let currentSpaceId: Int
609
+ let target: SpaceInfo?
610
+ }
611
+
612
+ private static func adjacentSpaceContext(offset: Int, from cgPoint: CGPoint? = nil) -> AdjacentSpaceContext? {
613
+ let point = cgPoint ?? currentMouseCGPoint()
614
+ guard let display = displaySpaces(containing: point) else { return nil }
615
+ let activeSpaceId = getCurrentSpace()
616
+ let currentSpaceId = resolvedCurrentSpaceId(for: display, activeSpaceId: activeSpaceId)
617
+ let target = adjacentSpace(in: display.spaces, currentSpaceId: currentSpaceId, offset: offset)
618
+ return AdjacentSpaceContext(
619
+ point: point,
620
+ display: display,
621
+ activeSpaceId: activeSpaceId,
622
+ currentSpaceId: currentSpaceId,
623
+ target: target
624
+ )
625
+ }
626
+
627
+ static func adjacentSpaceTarget(offset: Int, from cgPoint: CGPoint? = nil) -> SpaceInfo? {
628
+ adjacentSpaceContext(offset: offset, from: cgPoint)?.target
629
+ }
630
+
631
+ @discardableResult
632
+ static func switchToAdjacentSpace(offset: Int, from cgPoint: CGPoint? = nil) -> Bool {
633
+ guard let context = adjacentSpaceContext(offset: offset, from: cgPoint) else {
634
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: no adjacent space for offset \(offset) from \(formatCGPoint(cgPoint ?? currentMouseCGPoint()))")
635
+ return false
636
+ }
637
+
638
+ let spaces = context.display.spaces.map(\.id)
639
+ let targetText = context.target.map { String($0.id) } ?? "none"
640
+ DiagnosticLog.shared.info(
641
+ "switchToAdjacentSpace: offset=\(offset) point=\(formatCGPoint(context.point)) displayId=\(context.display.displayId) active=\(context.activeSpaceId) displayCurrent=\(context.display.currentSpaceId) resolved=\(context.currentSpaceId) target=\(targetText) spaces=\(spaces)"
642
+ )
643
+
644
+ if getDisplaySpaces().count == 1 {
645
+ if let finalSpaceId = switchToAdjacentSpaceViaSystemShortcut(
646
+ offset: offset,
647
+ displayId: context.display.displayId,
648
+ initialSpaceId: context.currentSpaceId
649
+ ) {
650
+ if let target = context.target, finalSpaceId != target.id {
651
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId) (expected \(target.id))")
652
+ } else {
653
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut changed \(context.currentSpaceId) → \(finalSpaceId)")
654
+ }
655
+ return true
656
+ }
657
+
658
+ guard let target = context.target else {
659
+ DiagnosticLog.shared.info("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId) and there is no adjacent space")
660
+ return false
661
+ }
662
+
663
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut stayed on \(context.currentSpaceId), falling back to SkyLight target \(target.id)")
664
+ }
665
+
666
+ guard let target = context.target else { return false }
667
+ let switched = switchToSpace(spaceId: target.id)
668
+ DiagnosticLog.shared.info("switchToAdjacentSpace: SkyLight \(switched ? "reached" : "missed") target \(target.id)")
669
+ return switched
670
+ }
671
+
591
672
  /// Find a window by its title tag and return its CGWindowID and owner PID
592
673
  static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
593
674
  guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
@@ -614,10 +695,12 @@ enum WindowTiler {
614
695
  return result.map { $0.intValue }
615
696
  }
616
697
 
617
- /// Switch a display to a specific Space
618
- static func switchToSpace(spaceId: Int) {
698
+ /// Switch a display to a specific Space.
699
+ /// Returns true once the requested Space becomes current.
700
+ @discardableResult
701
+ static func switchToSpace(spaceId: Int, verify: Bool = true) -> Bool {
619
702
  guard let mainConn = CGS.mainConnectionID,
620
- let setSpace = CGS.setCurrentSpace else { return }
703
+ let setSpace = CGS.setCurrentSpace else { return false }
621
704
 
622
705
  let cid = mainConn()
623
706
 
@@ -625,10 +708,37 @@ enum WindowTiler {
625
708
  let allDisplays = getDisplaySpaces()
626
709
  for display in allDisplays {
627
710
  if display.spaces.contains(where: { $0.id == spaceId }) {
711
+ let initialSpace = display.currentSpaceId
712
+ if initialSpace == spaceId {
713
+ DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) already on target \(spaceId)")
714
+ return true
715
+ }
716
+
717
+ DiagnosticLog.shared.info(
718
+ "switchToSpace: requesting \(spaceId) on display \(display.displayIndex) id=\(display.displayId) from \(initialSpace)"
719
+ )
628
720
  setSpace(cid, display.displayId as CFString, UInt64(spaceId))
629
- return
721
+ guard verify else { return true }
722
+
723
+ let deadline = Date().addingTimeInterval(0.45)
724
+ while Date() < deadline {
725
+ usleep(30_000)
726
+ let current = getDisplaySpaces().first(where: { $0.displayId == display.displayId })?.currentSpaceId ?? 0
727
+ if current == spaceId {
728
+ DiagnosticLog.shared.info("switchToSpace: display \(display.displayIndex) confirmed target \(spaceId)")
729
+ return true
730
+ }
731
+ }
732
+
733
+ DiagnosticLog.shared.warn(
734
+ "switchToSpace: requested \(spaceId) on display \(display.displayIndex) from \(initialSpace), but current Space did not change"
735
+ )
736
+ return false
630
737
  }
631
738
  }
739
+
740
+ DiagnosticLog.shared.warn("switchToSpace: couldn't resolve display for space \(spaceId)")
741
+ return false
632
742
  }
633
743
 
634
744
  // MARK: - Move Window Between Spaces
@@ -1206,14 +1316,11 @@ enum WindowTiler {
1206
1316
  }
1207
1317
 
1208
1318
  /// Distribute ALL visible non-Lattices windows into a smart grid on the screen with the most windows.
1209
- static func distributeVisible() {
1319
+ static func distributeVisible(reactivateLattices: Bool = true) {
1210
1320
  let diag = DiagnosticLog.shared
1211
1321
  let t = diag.startTimed("distributeVisible")
1212
1322
 
1213
- let allEntries = DesktopModel.shared.allWindows()
1214
- let visible = allEntries.filter { entry in
1215
- entry.isOnScreen && entry.app != "Lattices" && entry.frame.w > 50 && entry.frame.h > 50
1216
- }
1323
+ let visible = visibleDistributableWindows()
1217
1324
 
1218
1325
  guard !visible.isEmpty else {
1219
1326
  diag.info("distributeVisible: no visible windows to distribute")
@@ -1223,7 +1330,59 @@ enum WindowTiler {
1223
1330
 
1224
1331
  let windows = visible.map { (wid: $0.wid, pid: $0.pid) }
1225
1332
  diag.info("distributeVisible: \(windows.count) windows")
1226
- batchRaiseAndDistribute(windows: windows)
1333
+ batchRaiseAndDistribute(windows: windows, reactivateLattices: reactivateLattices)
1334
+ diag.finish(t)
1335
+ }
1336
+
1337
+ /// Distribute visible windows matching the frontmost app's broader type.
1338
+ /// Example: when the active app is a terminal, grid all visible terminal windows on that display.
1339
+ static func distributeVisibleByFrontmostType(reactivateLattices: Bool = true) {
1340
+ let diag = DiagnosticLog.shared
1341
+ let t = diag.startTimed("distributeVisibleByFrontmostType")
1342
+
1343
+ let visible = visibleDistributableWindows()
1344
+ guard !visible.isEmpty else {
1345
+ diag.info("distributeVisibleByFrontmostType: no visible windows to distribute")
1346
+ diag.finish(t)
1347
+ return
1348
+ }
1349
+
1350
+ let frontmostAppName = NSWorkspace.shared.frontmostApplication?.localizedName
1351
+ let anchor = frontmostAppName.flatMap { name in
1352
+ visible.first { $0.app.localizedCaseInsensitiveCompare(name) == .orderedSame }
1353
+ } ?? visible.first
1354
+
1355
+ guard let anchor else {
1356
+ diag.info("distributeVisibleByFrontmostType: no anchor window resolved")
1357
+ diag.finish(t)
1358
+ return
1359
+ }
1360
+
1361
+ let grouping = AppTypeClassifier.grouping(for: anchor.app)
1362
+ let anchorScreen = screenForWindowFrame(anchor.frame)
1363
+ let anchorScreenId = screenID(for: anchorScreen)
1364
+
1365
+ let sameScreenMatches = visible.filter { entry in
1366
+ AppTypeClassifier.matches(entry.app, grouping: grouping) &&
1367
+ screenID(for: screenForWindowFrame(entry.frame)) == anchorScreenId
1368
+ }
1369
+
1370
+ let matches = sameScreenMatches.isEmpty
1371
+ ? visible.filter { AppTypeClassifier.matches($0.app, grouping: grouping) }
1372
+ : sameScreenMatches
1373
+
1374
+ guard !matches.isEmpty else {
1375
+ diag.info("distributeVisibleByFrontmostType: no matches for \(grouping.label)")
1376
+ diag.finish(t)
1377
+ return
1378
+ }
1379
+
1380
+ let ordered = sortWindowsForGrid(matches)
1381
+ diag.info("distributeVisibleByFrontmostType: grouping=\(grouping.label) count=\(ordered.count) screen=\(anchorScreen.localizedName)")
1382
+ batchRaiseAndDistribute(
1383
+ windows: ordered.map { (wid: $0.wid, pid: $0.pid) },
1384
+ reactivateLattices: reactivateLattices
1385
+ )
1227
1386
  diag.finish(t)
1228
1387
  }
1229
1388
 
@@ -1685,7 +1844,11 @@ enum WindowTiler {
1685
1844
 
1686
1845
  /// Raise multiple windows and arrange in smart grid — single CG query, single AX query per process.
1687
1846
  /// 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) {
1847
+ static func batchRaiseAndDistribute(
1848
+ windows: [(wid: UInt32, pid: Int32)],
1849
+ region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil,
1850
+ reactivateLattices: Bool = true
1851
+ ) {
1689
1852
  guard !windows.isEmpty else { return }
1690
1853
  let diag = DiagnosticLog.shared
1691
1854
 
@@ -1744,91 +1907,99 @@ enum WindowTiler {
1744
1907
  }
1745
1908
 
1746
1909
  // Group by pid for AX queries, keep slot mapping
1747
- var widToSlot: [UInt32: Int] = [:]
1748
- for (i, win) in windows.enumerated() { widToSlot[win.wid] = i }
1749
-
1750
- var byPid: [Int32: [(wid: UInt32, target: CGRect)]] = [:]
1751
- for (i, win) in windows.enumerated() {
1752
- byPid[win.pid, default: []].append((wid: win.wid, target: slots[i]))
1910
+ var byPid: [Int32: [(slotIdx: Int, wid: UInt32, target: CGRect)]] = [:]
1911
+ let moves: [(wid: UInt32, pid: Int32, frame: CGRect)] = windows.enumerated().map { index, win in
1912
+ let target = slots[index]
1913
+ byPid[win.pid, default: []].append((slotIdx: index, wid: win.wid, target: target))
1914
+ return (wid: win.wid, pid: win.pid, frame: target)
1753
1915
  }
1754
1916
 
1755
- struct AXWin { let el: AXUIElement; let pos: CGPoint; let size: CGSize }
1756
-
1757
- // Pass 1: Move all windows to target positions (no raise yet)
1917
+ // Pass 1: Move all windows using exact wid→AX mapping.
1758
1918
  var moved = 0
1759
1919
  var failed: [UInt32] = []
1760
1920
  var resolvedAXElements: [(slotIdx: Int, el: AXUIElement)] = [] // for raise pass
1921
+ var activatedPids = Set<Int32>()
1922
+
1923
+ let cid = _SLSMainConnectionID?()
1924
+ if let cid { _ = _SLSDisableUpdate?(cid) }
1761
1925
 
1762
1926
  for (pid, windowMoves) in byPid {
1763
1927
  let appRef = AXUIElementCreateApplication(pid)
1928
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, false as CFTypeRef)
1929
+
1764
1930
  var windowsRef: CFTypeRef?
1765
1931
  let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
1766
1932
  guard err == .success, let axWindows = windowsRef as? [AXUIElement] else {
1767
1933
  diag.warn(" AX query failed for pid=\(pid) err=\(err.rawValue)")
1768
1934
  failed.append(contentsOf: windowMoves.map(\.wid))
1935
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
1769
1936
  continue
1770
1937
  }
1771
1938
 
1772
- var axCache: [AXWin] = []
1939
+ var axByWid: [UInt32: AXUIElement] = [:]
1773
1940
  for axWin in axWindows {
1774
- var posRef: CFTypeRef?; var sizeRef: CFTypeRef?
1775
- AXUIElementCopyAttributeValue(axWin, kAXPositionAttribute as CFString, &posRef)
1776
- AXUIElementCopyAttributeValue(axWin, kAXSizeAttribute as CFString, &sizeRef)
1777
- guard let pv = posRef, let sv = sizeRef else { continue }
1778
- var pos = CGPoint.zero; var size = CGSize.zero
1779
- AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
1780
- AXValueGetValue(sv as! AXValue, .cgSize, &size)
1781
- axCache.append(AXWin(el: axWin, pos: pos, size: size))
1941
+ var windowId: CGWindowID = 0
1942
+ if _AXUIElementGetWindow(axWin, &windowId) == .success {
1943
+ axByWid[windowId] = axWin
1944
+ }
1782
1945
  }
1783
1946
 
1784
1947
  for wm in windowMoves {
1785
- guard let cgRect = cgFrames[wm.wid] else {
1786
- diag.warn(" wid=\(wm.wid): no CG frame, skipping")
1787
- failed.append(wm.wid)
1788
- continue
1789
- }
1790
- guard let ax = axCache.first(where: {
1791
- abs(cgRect.origin.x - $0.pos.x) < 2 && abs(cgRect.origin.y - $0.pos.y) < 2 &&
1792
- abs(cgRect.width - $0.size.width) < 2 && abs(cgRect.height - $0.size.height) < 2
1793
- }) else {
1794
- diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX match among \(axCache.count) AX windows")
1795
- for (j, axw) in axCache.enumerated() {
1796
- diag.info(" AX[\(j)]: pos=(\(Int(axw.pos.x)),\(Int(axw.pos.y))) size=\(Int(axw.size.width))x\(Int(axw.size.height))")
1948
+ guard let axWin = axByWid[wm.wid] else {
1949
+ if let cgRect = cgFrames[wm.wid] {
1950
+ diag.warn(" wid=\(wm.wid): CG frame (\(Int(cgRect.origin.x)),\(Int(cgRect.origin.y)) \(Int(cgRect.width))x\(Int(cgRect.height))) — no AX wid match")
1951
+ } else {
1952
+ diag.warn(" wid=\(wm.wid): no CG frame and no AX wid match")
1797
1953
  }
1798
1954
  failed.append(wm.wid)
1799
1955
  continue
1800
1956
  }
1801
1957
 
1802
- let slotIdx = widToSlot[wm.wid] ?? -1
1803
- // Move only — raise comes later
1804
1958
  var newPos = CGPoint(x: wm.target.origin.x, y: wm.target.origin.y)
1805
1959
  var newSize = CGSize(width: wm.target.width, height: wm.target.height)
1806
- let posOk = AXValueCreate(.cgPoint, &newPos).map {
1807
- AXUIElementSetAttributeValue(ax.el, kAXPositionAttribute as CFString, $0)
1960
+ let sizeErr1 = AXValueCreate(.cgSize, &newSize).map {
1961
+ AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
1962
+ }
1963
+ let posErr = AXValueCreate(.cgPoint, &newPos).map {
1964
+ AXUIElementSetAttributeValue(axWin, kAXPositionAttribute as CFString, $0)
1808
1965
  }
1809
- let sizeOk = AXValueCreate(.cgSize, &newSize).map {
1810
- AXUIElementSetAttributeValue(ax.el, kAXSizeAttribute as CFString, $0)
1966
+ let sizeErr2 = AXValueCreate(.cgSize, &newSize).map {
1967
+ AXUIElementSetAttributeValue(axWin, kAXSizeAttribute as CFString, $0)
1811
1968
  }
1812
- diag.info(" Move[\(slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) posErr=\(posOk?.rawValue ?? -1) sizeErr=\(sizeOk?.rawValue ?? -1)")
1813
- resolvedAXElements.append((slotIdx: slotIdx, el: ax.el))
1969
+ diag.info(" Move[\(wm.slotIdx)] wid=\(wm.wid): target=(\(Int(wm.target.origin.x)),\(Int(wm.target.origin.y))) \(Int(wm.target.width))x\(Int(wm.target.height)) sizeErr1=\(sizeErr1?.rawValue ?? -1) posErr=\(posErr?.rawValue ?? -1) sizeErr2=\(sizeErr2?.rawValue ?? -1)")
1970
+ resolvedAXElements.append((slotIdx: wm.slotIdx, el: axWin))
1814
1971
  moved += 1
1815
1972
  }
1973
+
1974
+ if !activatedPids.contains(pid) {
1975
+ if let app = NSRunningApplication(processIdentifier: pid) {
1976
+ app.activate()
1977
+ activatedPids.insert(pid)
1978
+ }
1979
+ }
1980
+
1981
+ AXUIElementSetAttributeValue(appRef, "AXEnhancedUserInterface" as CFString, true as CFTypeRef)
1816
1982
  }
1817
1983
 
1818
- // Pass 2: Raise all windows in slot order so they all come to front
1819
- // Sort by slot index so the layout order is predictable
1984
+ if let cid { _ = _SLSReenableUpdate?(cid) }
1985
+
1986
+ // Pass 2: Raise all windows in slot order after app activation so final z-order matches the grid.
1820
1987
  resolvedAXElements.sort { $0.slotIdx < $1.slotIdx }
1821
1988
  for item in resolvedAXElements {
1822
1989
  AXUIElementPerformAction(item.el, kAXRaiseAction as CFString)
1990
+ AXUIElementSetAttributeValue(item.el, kAXMainAttribute as CFString, kCFBooleanTrue)
1823
1991
  }
1824
1992
  diag.info(" Raised \(resolvedAXElements.count) windows in slot order")
1825
1993
 
1826
- // Pass 3: Activate all apps so windows come to front of other apps
1827
- var activatedPids = Set<Int32>()
1828
- for win in windows {
1829
- if !activatedPids.contains(win.pid) {
1830
- if let app = NSRunningApplication(processIdentifier: win.pid) { app.activate() }
1831
- activatedPids.insert(win.pid)
1994
+ // Verify and retry drifted windows once using the battle-tested batch mover.
1995
+ let drifted = verifyMoves(moves)
1996
+ if !drifted.isEmpty {
1997
+ diag.warn(" Drifted after distribute: \(drifted.map(\.wid)) — retrying exact move path")
1998
+ usleep(100_000)
1999
+ batchMoveAndRaiseWindows(drifted)
2000
+ let stillDrifted = verifyMoves(drifted)
2001
+ if !stillDrifted.isEmpty {
2002
+ diag.warn(" Still drifted after retry: \(stillDrifted.map(\.wid))")
1832
2003
  }
1833
2004
  }
1834
2005
 
@@ -1837,8 +2008,10 @@ enum WindowTiler {
1837
2008
  }
1838
2009
  DesktopModel.shared.markInteraction(wids: windows.map(\.wid))
1839
2010
  diag.success("batchRaiseAndDistribute: moved \(moved)/\(windows.count) [\(desc) grid]")
1840
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
1841
- NSApp.activate(ignoringOtherApps: true)
2011
+ if reactivateLattices {
2012
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
2013
+ NSApp.activate(ignoringOtherApps: true)
2014
+ }
1842
2015
  }
1843
2016
  }
1844
2017
 
@@ -1997,8 +2170,98 @@ enum WindowTiler {
1997
2170
  }) ?? NSScreen.main ?? primaryScreen
1998
2171
  }
1999
2172
 
2173
+ private static func currentMouseCGPoint() -> CGPoint {
2174
+ let mouseLocation = NSEvent.mouseLocation
2175
+ let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2176
+ return CGPoint(x: mouseLocation.x, y: primaryHeight - mouseLocation.y)
2177
+ }
2178
+
2179
+ private static func switchToAdjacentSpaceViaSystemShortcut(offset: Int, displayId: String, initialSpaceId: Int) -> Int? {
2180
+ let keyCode: CGKeyCode = offset < 0 ? 123 : 124
2181
+ let script = """
2182
+ tell application "System Events"
2183
+ key code \(keyCode) using control down
2184
+ end tell
2185
+ return "ok"
2186
+ """
2187
+ let result = ProcessQuery.shell(["/usr/bin/osascript", "-e", script])
2188
+ if result != "ok" {
2189
+ DiagnosticLog.shared.warn("switchToAdjacentSpace: system shortcut script did not complete for offset \(offset)")
2190
+ }
2191
+ return waitForSpaceChange(displayId: displayId, initialSpaceId: initialSpaceId, timeout: 1.2)
2192
+ }
2193
+
2194
+ private static func waitForSpaceChange(displayId: String, initialSpaceId: Int, timeout: TimeInterval) -> Int? {
2195
+ let deadline = Date().addingTimeInterval(timeout)
2196
+ while Date() < deadline {
2197
+ usleep(30_000)
2198
+ let current = getDisplaySpaces().first(where: { $0.displayId == displayId })?.currentSpaceId ?? 0
2199
+ if current != 0, current != initialSpaceId {
2200
+ return current
2201
+ }
2202
+ }
2203
+ return nil
2204
+ }
2205
+
2206
+ private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
2207
+ guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
2208
+ return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
2209
+ }
2210
+
2211
+ private static func resolvedCurrentSpaceId(for display: DisplaySpaces, activeSpaceId: Int) -> Int {
2212
+ if display.spaces.contains(where: { $0.id == activeSpaceId }) {
2213
+ return activeSpaceId
2214
+ }
2215
+ return display.currentSpaceId
2216
+ }
2217
+
2218
+ private static func formatCGPoint(_ point: CGPoint) -> String {
2219
+ "\(Int(point.x)),\(Int(point.y))"
2220
+ }
2221
+
2222
+ private static func screenIndex(for cgPoint: CGPoint) -> Int? {
2223
+ let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2224
+ let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)
2225
+ return NSScreen.screens.firstIndex(where: { $0.frame.contains(nsPoint) })
2226
+ ?? (NSScreen.main != nil ? 0 : nil)
2227
+ }
2228
+
2000
2229
  // MARK: - Private
2001
2230
 
2231
+ private static func visibleDistributableWindows() -> [WindowEntry] {
2232
+ DesktopModel.shared.allWindows().filter { entry in
2233
+ entry.isOnScreen &&
2234
+ entry.app != "Lattices" &&
2235
+ entry.frame.w > 50 &&
2236
+ entry.frame.h > 50
2237
+ }
2238
+ }
2239
+
2240
+ private static func sortWindowsForGrid(_ windows: [WindowEntry]) -> [WindowEntry] {
2241
+ windows.sorted { lhs, rhs in
2242
+ let rowTolerance = 40.0
2243
+ let yDelta = lhs.frame.y - rhs.frame.y
2244
+ if abs(yDelta) > rowTolerance {
2245
+ return lhs.frame.y < rhs.frame.y
2246
+ }
2247
+
2248
+ let xDelta = lhs.frame.x - rhs.frame.x
2249
+ if abs(xDelta) > rowTolerance {
2250
+ return lhs.frame.x < rhs.frame.x
2251
+ }
2252
+
2253
+ return lhs.zIndex < rhs.zIndex
2254
+ }
2255
+ }
2256
+
2257
+ private static func screenID(for screen: NSScreen) -> String {
2258
+ let key = NSDeviceDescriptionKey("NSScreenNumber")
2259
+ if let number = screen.deviceDescription[key] as? NSNumber {
2260
+ return number.stringValue
2261
+ }
2262
+ return screen.localizedName
2263
+ }
2264
+
2002
2265
  private static func tileAppleScript(app: String, tag: String, bounds: (Int, Int, Int, Int)) {
2003
2266
  let (x1, y1, x2, y2) = bounds
2004
2267
  let script = """