@lattices/cli 0.4.5 → 0.4.6

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 (130) hide show
  1. package/app/Info.plist +2 -2
  2. package/app/Lattices.app/Contents/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  4. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +4 -0
  5. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
  6. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +2 -1
  7. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
  8. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  9. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  10. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  11. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
  12. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  13. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  14. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  15. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  16. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -108
  17. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  18. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +488 -55
  19. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  20. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  21. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  22. package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
  23. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  24. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -1
  25. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
  26. package/docs/component-extraction-roadmap.md +392 -0
  27. package/package.json +3 -1
  28. package/app/Sources/CommandPaletteWindow.swift +0 -134
  29. package/app/Sources/OmniSearchWindow.swift +0 -165
  30. /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
  31. /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
  32. /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
  33. /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
  34. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  35. /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
  36. /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
  37. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  38. /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
  39. /package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -0
  40. /package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +0 -0
  41. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  42. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  43. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  44. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  45. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  46. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  47. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  48. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  49. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  50. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  51. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  52. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  53. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  54. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  55. /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
  56. /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
  57. /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
  58. /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
  59. /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
  60. /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
  61. /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
  62. /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
  63. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  64. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  65. /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
  66. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  67. /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
  68. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  69. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  70. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  71. /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
  72. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  73. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  74. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  75. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  76. /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
  77. /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
  78. /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
  79. /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
  80. /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
  81. /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
  82. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  83. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  84. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  85. /package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +0 -0
  86. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  87. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  88. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  89. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  90. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  91. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  92. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  93. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  94. /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
  95. /package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +0 -0
  96. /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
  97. /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
  98. /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
  99. /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
  100. /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
  101. /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
  102. /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
  103. /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
  104. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  105. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  106. /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
  107. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  108. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  109. /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
  110. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  111. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  112. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  113. /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
  114. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  115. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  116. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  117. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  118. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  119. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  120. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  121. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  122. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  123. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  124. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  125. /package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +0 -0
  126. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  127. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  128. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  129. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  130. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -0,0 +1,113 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ final class WindowPreviewStore: ObservableObject {
5
+ static let shared = WindowPreviewStore()
6
+
7
+ @Published private var images: [UInt32: NSImage] = [:]
8
+ @Published private var loading: Set<UInt32> = []
9
+
10
+ private var lastAttemptAt: [UInt32: Date] = [:]
11
+ private var accessOrder: [UInt32] = []
12
+ private let maxCached = 15
13
+ private let queue = DispatchQueue(label: "com.arach.lattices.window-preview", qos: .userInitiated)
14
+ private let previewMaxSize = NSSize(width: 360, height: 190)
15
+
16
+ func image(for wid: UInt32) -> NSImage? {
17
+ if images[wid] != nil {
18
+ touchLRU(wid)
19
+ }
20
+ return images[wid]
21
+ }
22
+
23
+ func hasSettled(_ wid: UInt32) -> Bool {
24
+ images[wid] != nil || (lastAttemptAt[wid] != nil && !loading.contains(wid))
25
+ }
26
+
27
+ func isLoading(_ wid: UInt32) -> Bool {
28
+ loading.contains(wid)
29
+ }
30
+
31
+ func prewarm(windows: [WindowEntry], limit: Int = 4) {
32
+ for window in windows.prefix(limit) {
33
+ load(window: window)
34
+ }
35
+ }
36
+
37
+ func load(window: WindowEntry) {
38
+ if images[window.wid] != nil || loading.contains(window.wid) {
39
+ return
40
+ }
41
+
42
+ let now = Date()
43
+ if let lastAttemptAt = lastAttemptAt[window.wid], now.timeIntervalSince(lastAttemptAt) < 1.0 {
44
+ return
45
+ }
46
+ lastAttemptAt[window.wid] = now
47
+
48
+ loading.insert(window.wid)
49
+ let wid = window.wid
50
+ let frame = window.frame
51
+ let startedAt = Date()
52
+
53
+ queue.async { [weak self] in
54
+ guard let self else { return }
55
+
56
+ let cgImage = CGWindowListCreateImage(
57
+ .null,
58
+ .optionIncludingWindow,
59
+ CGWindowID(wid),
60
+ [.boundsIgnoreFraming, .nominalResolution]
61
+ )
62
+
63
+ let image = cgImage.map {
64
+ NSImage(
65
+ cgImage: $0,
66
+ size: self.previewSize(for: frame)
67
+ )
68
+ }
69
+
70
+ DispatchQueue.main.async {
71
+ self.loading.remove(wid)
72
+ let elapsedMs = Int(Date().timeIntervalSince(startedAt) * 1000)
73
+ if let image {
74
+ self.images[wid] = image
75
+ self.touchLRU(wid)
76
+ self.evictIfNeeded()
77
+ if elapsedMs >= 80 {
78
+ DiagnosticLog.shared.info("HUDPreview: captured wid=\(wid) in \(elapsedMs)ms")
79
+ }
80
+ } else {
81
+ DiagnosticLog.shared.info("HUDPreview: capture unavailable wid=\(wid) after \(elapsedMs)ms")
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ private func touchLRU(_ wid: UInt32) {
88
+ accessOrder.removeAll { $0 == wid }
89
+ accessOrder.append(wid)
90
+ }
91
+
92
+ private func evictIfNeeded() {
93
+ while images.count > maxCached, let oldest = accessOrder.first {
94
+ accessOrder.removeFirst()
95
+ images.removeValue(forKey: oldest)
96
+ lastAttemptAt.removeValue(forKey: oldest)
97
+ }
98
+ }
99
+
100
+ private func previewSize(for frame: WindowFrame) -> NSSize {
101
+ let width = max(CGFloat(frame.w), CGFloat(1))
102
+ let height = max(CGFloat(frame.h), CGFloat(1))
103
+ let scale = min(
104
+ previewMaxSize.width / width,
105
+ previewMaxSize.height / height,
106
+ CGFloat(1)
107
+ )
108
+ return NSSize(
109
+ width: max(CGFloat(1), width * scale),
110
+ height: max(CGFloat(1), height * scale)
111
+ )
112
+ }
113
+ }
@@ -0,0 +1,76 @@
1
+ import Combine
2
+ import Foundation
3
+
4
+ struct SelectedWindowSummary: Identifiable, Equatable {
5
+ let wid: UInt32
6
+ let app: String
7
+ let title: String
8
+ let latticesSession: String?
9
+
10
+ var id: UInt32 { wid }
11
+
12
+ var displayTitle: String {
13
+ if !title.isEmpty { return title }
14
+ if let latticesSession, !latticesSession.isEmpty { return latticesSession }
15
+ return app
16
+ }
17
+ }
18
+
19
+ final class WindowSelectionStore: ObservableObject {
20
+ static let shared = WindowSelectionStore()
21
+
22
+ @Published private(set) var windows: [SelectedWindowSummary] = []
23
+ @Published private(set) var source: String?
24
+ @Published private(set) var updatedAt: Date?
25
+
26
+ private init() {}
27
+
28
+ var isActive: Bool { !windows.isEmpty }
29
+ var windowIds: [UInt32] { windows.map(\.wid) }
30
+ var count: Int { windows.count }
31
+ var sourceLabel: String? {
32
+ switch source {
33
+ case "desktop-inventory": return "window selector"
34
+ case "screen-map": return "screen map"
35
+ case let value?: return value
36
+ case nil: return nil
37
+ }
38
+ }
39
+
40
+ func setSelection(_ windows: [SelectedWindowSummary], source: String) {
41
+ let unique = Array(
42
+ Dictionary(uniqueKeysWithValues: windows.map { ($0.wid, $0) }).values
43
+ ).sorted { lhs, rhs in
44
+ if lhs.app == rhs.app { return lhs.wid < rhs.wid }
45
+ return lhs.app.localizedCaseInsensitiveCompare(rhs.app) == .orderedAscending
46
+ }
47
+
48
+ DispatchQueue.main.async {
49
+ self.windows = unique
50
+ self.source = source
51
+ self.updatedAt = Date()
52
+ }
53
+ }
54
+
55
+ func clear(source: String? = nil) {
56
+ DispatchQueue.main.async {
57
+ if let source, let current = self.source, current != source {
58
+ return
59
+ }
60
+ self.windows = []
61
+ self.source = source ?? self.source
62
+ self.updatedAt = Date()
63
+ }
64
+ }
65
+
66
+ func summary(maxItems: Int = 3) -> String {
67
+ guard !windows.isEmpty else { return "No selection" }
68
+ let titles = windows.prefix(maxItems).map { item in
69
+ item.app == item.displayTitle ? item.app : "\(item.app): \(item.displayTitle)"
70
+ }
71
+ if windows.count > maxItems {
72
+ return titles.joined(separator: " • ") + " +\(windows.count - maxItems)"
73
+ }
74
+ return titles.joined(separator: " • ")
75
+ }
76
+ }
@@ -671,18 +671,7 @@ enum WindowTiler {
671
671
 
672
672
  /// Find a window by its title tag and return its CGWindowID and owner PID
673
673
  static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
674
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
675
- return nil
676
- }
677
- for info in windowList {
678
- if let name = info[kCGWindowName as String] as? String,
679
- name.contains(tag),
680
- let wid = info[kCGWindowNumber as String] as? UInt32,
681
- let pid = info[kCGWindowOwnerPID as String] as? pid_t {
682
- return (wid, pid)
683
- }
684
- }
685
- return nil
674
+ SessionWindowLocator.findCGWindow(tag: tag).map { ($0.wid, $0.pid) }
686
675
  }
687
676
 
688
677
  /// Get the space ID(s) a window is on
@@ -760,13 +749,9 @@ enum WindowTiler {
760
749
 
761
750
  // Find the window — CG first, then AX→CG fallback
762
751
  let wid: UInt32
763
- if let (w, _) = findWindow(tag: tag) {
764
- wid = w
765
- diag.info("moveWindowToSpace: found via CG wid=\(w)")
766
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
767
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
768
- wid = w
769
- diag.info("moveWindowToSpace: found via AX→CG wid=\(w)")
752
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
753
+ wid = match.wid
754
+ diag.info("moveWindowToSpace: located wid=\(match.wid) pid=\(match.pid)")
770
755
  } else {
771
756
  diag.warn("moveWindowToSpace: window not found for tag \(tag) — switching view only")
772
757
  switchToSpace(spaceId: spaceId)
@@ -834,39 +819,31 @@ enum WindowTiler {
834
819
  let tag = Terminal.windowTag(for: session)
835
820
 
836
821
  // Path 1: CG window lookup (needs Screen Recording permission for window names)
837
- if let (wid, pid) = findWindow(tag: tag) {
838
- diag.success("Path 1 (CG): found wid=\(wid) pid=\(pid)")
839
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
822
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
823
+ diag.success("Path 1/2 (locator): found wid=\(match.wid) pid=\(match.pid)")
824
+ navigateToKnownWindow(wid: match.wid, pid: match.pid, tag: tag, session: session, terminal: terminal)
840
825
  diag.finish(t)
841
826
  return
842
827
  }
843
- diag.warn("Path 1 (CG): findWindow failed — no Screen Recording?")
828
+ diag.warn("SessionWindowLocator failed — trying direct AX fallback")
844
829
 
845
- // Path 2: AX API fallback (needs Accessibility permission)
846
- if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag) {
847
- diag.success("Path 2 (AX): found window for \(terminal.rawValue) pid=\(pid)")
848
- // Try to match AX window → CG window for space switching
849
- if let wid = matchCGWindow(pid: pid, axWindow: axWindow) {
850
- diag.success("Path 2 (AX→CG): matched CG wid=\(wid)")
851
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
830
+ if let (pid, axWindow) = SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag) {
831
+ diag.success("Direct AX fallback: found window for \(terminal.rawValue) pid=\(pid)")
832
+ AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
833
+ AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
834
+ if let app = NSRunningApplication(processIdentifier: pid) {
835
+ app.activate()
836
+ }
837
+ if let frame = axWindowFrame(axWindow) {
838
+ diag.info("Highlighting via AX frame: \(frame)")
839
+ DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
852
840
  } else {
853
- diag.warn("Path 2 (AX): no CG match raising without space switch")
854
- AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
855
- AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
856
- if let app = NSRunningApplication(processIdentifier: pid) {
857
- app.activate()
858
- }
859
- if let frame = axWindowFrame(axWindow) {
860
- diag.info("Highlighting via AX frame: \(frame)")
861
- DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
862
- } else {
863
- diag.error("axWindowFrame returned nil — no highlight")
864
- }
841
+ diag.error("axWindowFrame returned nilno highlight")
865
842
  }
866
843
  diag.finish(t)
867
844
  return
868
845
  }
869
- diag.warn("Path 2 (AX): findWindowViaAX failed — no Accessibility?")
846
+ diag.warn("Direct AX fallback failed — no Accessibility?")
870
847
 
871
848
  // Path 3: AppleScript / bare activate fallback
872
849
  diag.warn("Path 3: falling back to AppleScript/activate")
@@ -894,70 +871,12 @@ enum WindowTiler {
894
871
  }
895
872
  }
896
873
 
897
- /// Find a terminal window by title tag using AX API (requires Accessibility permission)
898
874
  private static func findWindowViaAX(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
899
- let diag = DiagnosticLog.shared
900
- guard let app = NSWorkspace.shared.runningApplications.first(where: {
901
- $0.bundleIdentifier == terminal.bundleId
902
- }) else {
903
- diag.error("findWindowViaAX: \(terminal.rawValue) (\(terminal.bundleId)) not running")
904
- return nil
905
- }
906
-
907
- let pid = app.processIdentifier
908
- let appRef = AXUIElementCreateApplication(pid)
909
- var windowsRef: CFTypeRef?
910
- let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
911
- guard err == .success, let windows = windowsRef as? [AXUIElement] else {
912
- diag.error("findWindowViaAX: AX error \(err.rawValue) — Accessibility not granted?")
913
- return nil
914
- }
915
-
916
- diag.info("findWindowViaAX: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
917
- for win in windows {
918
- var titleRef: CFTypeRef?
919
- AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
920
- let title = titleRef as? String ?? "<no title>"
921
- if title.contains(tag) {
922
- diag.success("findWindowViaAX: matched \"\(title)\"")
923
- return (pid, win)
924
- } else {
925
- diag.info(" skip: \"\(title)\"")
926
- }
927
- }
928
- diag.warn("findWindowViaAX: no window matched tag \(tag)")
929
- return nil
875
+ SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag)
930
876
  }
931
877
 
932
- /// Match an AX window to its CG window ID using PID + bounds comparison
933
878
  private static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
934
- var posRef: CFTypeRef?
935
- var sizeRef: CFTypeRef?
936
- AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
937
- AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
938
- guard let pv = posRef, let sv = sizeRef else { return nil }
939
-
940
- var pos = CGPoint.zero
941
- var size = CGSize.zero
942
- AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
943
- AXValueGetValue(sv as! AXValue, .cgSize, &size)
944
-
945
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
946
-
947
- for info in windowList {
948
- guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
949
- wPid == pid,
950
- let wid = info[kCGWindowNumber as String] as? UInt32,
951
- let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
952
- var rect = CGRect.zero
953
- if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
954
- if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
955
- abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
956
- return wid
957
- }
958
- }
959
- }
960
- return nil
879
+ SessionWindowLocator.matchCGWindow(pid: pid, axWindow: axWindow)
961
880
  }
962
881
 
963
882
  /// Get NSRect from an AX window element (AX uses top-left origin, convert to NS bottom-left)
@@ -1107,11 +1026,8 @@ enum WindowTiler {
1107
1026
 
1108
1027
  // Find the window
1109
1028
  let wid: UInt32
1110
- if let (w, _) = findWindow(tag: tag) {
1111
- wid = w
1112
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
1113
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
1114
- wid = w
1029
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
1030
+ wid = match.wid
1115
1031
  } else {
1116
1032
  return nil
1117
1033
  }
@@ -91,11 +91,14 @@ final class CommandModeState: ObservableObject {
91
91
  @Published var inventory = CommandModeInventory(activeLayer: nil, layerCount: 0, items: [])
92
92
  @Published var chords: [Chord] = []
93
93
  @Published var desktopSnapshot: DesktopInventorySnapshot?
94
- @Published var selectedWindowIds: Set<UInt32> = []
94
+ @Published var selectedWindowIds: Set<UInt32> = [] {
95
+ didSet { syncSharedSelection() }
96
+ }
95
97
  @Published var desktopMode: DesktopInventoryMode = .browsing
96
98
  @Published var activePreset: FilterPreset? = nil
97
99
  @Published var searchQuery: String = ""
98
100
  @Published var isSearching: Bool = false
101
+ @Published var gridPreviewPlacement: PlacementSpec? = nil
99
102
 
100
103
  // MARK: - Marquee Drag State
101
104
  @Published var isDragging: Bool = false
@@ -140,6 +143,26 @@ final class CommandModeState: ObservableObject {
140
143
  selectedWindowIds.first
141
144
  }
142
145
 
146
+ var selectedWindowSummaryText: String {
147
+ let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
148
+ let labels = windows.compactMap { $0.appName }.uniquePrefix(3)
149
+ guard !labels.isEmpty else { return "" }
150
+ if windows.count > labels.count {
151
+ return labels.joined(separator: " • ") + " +\(windows.count - labels.count)"
152
+ }
153
+ return labels.joined(separator: " • ")
154
+ }
155
+
156
+ var gridPreviewRegionLabel: String {
157
+ guard let placement = gridPreviewPlacement else { return "Full Screen" }
158
+ switch placement {
159
+ case .tile(let position):
160
+ return position.label
161
+ default:
162
+ return placement.wireValue
163
+ }
164
+ }
165
+
143
166
  func isSelected(_ id: UInt32) -> Bool {
144
167
  selectedWindowIds.contains(id)
145
168
  }
@@ -344,6 +367,7 @@ final class CommandModeState: ObservableObject {
344
367
  desktopSnapshot = buildDesktopInventory()
345
368
  clearSelection()
346
369
  desktopMode = .browsing
370
+ gridPreviewPlacement = nil
347
371
  phase = .desktopInventory
348
372
  // Don't call onPanelResize here — caller handles initial sizing
349
373
  }
@@ -535,6 +559,7 @@ final class CommandModeState: ObservableObject {
535
559
  if isSearching && selectedWindowIds.isEmpty { return false }
536
560
  if isSearching { deactivateSearch() }
537
561
  if !selectedWindowIds.isEmpty {
562
+ gridPreviewPlacement = nil
538
563
  desktopMode = .gridPreview
539
564
  }
540
565
  return true
@@ -614,15 +639,54 @@ final class CommandModeState: ObservableObject {
614
639
 
615
640
  private func handleGridPreviewKey(_ keyCode: UInt16) -> Bool {
616
641
  switch keyCode {
617
- case 53: // Escape — always dismiss
618
- onDismiss?()
642
+ case 53: // Escape — cancel preview, keep selection
643
+ gridPreviewPlacement = nil
644
+ desktopMode = .browsing
619
645
  return true
620
646
 
621
647
  case 36, 1: // Enter or s → apply the layout
622
- showAndDistributeSelected()
648
+ showAndDistributeSelected(in: gridPreviewPlacement)
649
+ gridPreviewPlacement = nil
623
650
  desktopMode = .browsing
624
651
  return true
625
652
 
653
+ case 123:
654
+ gridPreviewPlacement = .tile(.left)
655
+ return true
656
+ case 124:
657
+ gridPreviewPlacement = .tile(.right)
658
+ return true
659
+ case 126:
660
+ gridPreviewPlacement = .tile(.top)
661
+ return true
662
+ case 125:
663
+ gridPreviewPlacement = .tile(.bottom)
664
+ return true
665
+ case 18:
666
+ gridPreviewPlacement = .tile(.topLeft)
667
+ return true
668
+ case 19:
669
+ gridPreviewPlacement = .tile(.topRight)
670
+ return true
671
+ case 20:
672
+ gridPreviewPlacement = .tile(.bottomLeft)
673
+ return true
674
+ case 21:
675
+ gridPreviewPlacement = .tile(.bottomRight)
676
+ return true
677
+ case 23:
678
+ gridPreviewPlacement = .tile(.leftThird)
679
+ return true
680
+ case 22:
681
+ gridPreviewPlacement = .tile(.centerThird)
682
+ return true
683
+ case 26:
684
+ gridPreviewPlacement = .tile(.rightThird)
685
+ return true
686
+ case 8:
687
+ gridPreviewPlacement = .tile(.center)
688
+ return true
689
+
626
690
  default:
627
691
  return true
628
692
  }
@@ -795,20 +859,8 @@ final class CommandModeState: ObservableObject {
795
859
  private func tileAllSelected(to position: TilePosition) {
796
860
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
797
861
  guard !windows.isEmpty else { return }
798
-
799
- // For left/right with 2+ windows: distribute evenly across width
800
- if windows.count >= 2 && (position == .left || position == .right) {
801
- distributeSelectedHorizontally()
802
- return
803
- }
804
-
805
- DiagnosticLog.shared.info("Tile all \(windows.count): \(position.rawValue)")
806
- for win in windows {
807
- WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: position)
808
- }
809
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
810
- self?.desktopSnapshot = self?.buildDesktopInventory()
811
- }
862
+ DiagnosticLog.shared.info("Grid selected \(windows.count): \(position.rawValue)")
863
+ showAndDistributeSelected(in: .tile(position))
812
864
  }
813
865
 
814
866
  private func distributeSelectedHorizontally() {
@@ -849,28 +901,36 @@ final class CommandModeState: ObservableObject {
849
901
  }
850
902
 
851
903
  /// Show all selected windows AND distribute in smart grid — single batch operation
852
- func showAndDistributeSelected() {
904
+ func showAndDistributeSelected(in placement: PlacementSpec? = nil) {
853
905
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
854
906
  guard !windows.isEmpty else { return }
855
907
  savePositions(for: windows)
856
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
908
+ WindowTiler.batchRaiseAndDistribute(
909
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
910
+ region: placement?.fractions
911
+ )
857
912
  let shape = WindowTiler.gridShape(for: windows.count)
858
913
  let grid = shape.map(String.init).joined(separator: "+")
859
- flash("\(windows.count) windows [\(grid)]")
914
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
915
+ flash("\(windows.count) windows\(region) [\(grid)]")
860
916
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
861
917
  self?.desktopSnapshot = self?.buildDesktopInventory()
862
918
  }
863
919
  }
864
920
 
865
921
  /// Distribute selected in smart grid without raising
866
- func distributeSelected() {
922
+ func distributeSelected(in placement: PlacementSpec? = nil) {
867
923
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
868
924
  guard !windows.isEmpty else { return }
869
925
  savePositions(for: windows)
870
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
926
+ WindowTiler.batchRaiseAndDistribute(
927
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
928
+ region: placement?.fractions
929
+ )
871
930
  let shape = WindowTiler.gridShape(for: windows.count)
872
931
  let grid = shape.map(String.init).joined(separator: "+")
873
- flash("\(windows.count) windows [\(grid)]")
932
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
933
+ flash("\(windows.count) windows\(region) [\(grid)]")
874
934
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
875
935
  self?.desktopSnapshot = self?.buildDesktopInventory()
876
936
  }
@@ -960,6 +1020,36 @@ final class CommandModeState: ObservableObject {
960
1020
  onDismiss?()
961
1021
  }
962
1022
 
1023
+ private func syncSharedSelection() {
1024
+ guard !selectedWindowIds.isEmpty else {
1025
+ WindowSelectionStore.shared.clear(source: "desktop-inventory")
1026
+ return
1027
+ }
1028
+
1029
+ let summaries = flatWindowList
1030
+ .filter { selectedWindowIds.contains($0.id) }
1031
+ .map {
1032
+ SelectedWindowSummary(
1033
+ wid: $0.id,
1034
+ app: $0.appName ?? "Window",
1035
+ title: $0.title,
1036
+ latticesSession: $0.latticesSession
1037
+ )
1038
+ }
1039
+
1040
+ guard !summaries.isEmpty else { return }
1041
+ WindowSelectionStore.shared.setSelection(summaries, source: "desktop-inventory")
1042
+ }
1043
+
1044
+ private func regionLabel(for placement: PlacementSpec) -> String {
1045
+ switch placement {
1046
+ case .tile(let position):
1047
+ return position.label
1048
+ default:
1049
+ return placement.wireValue
1050
+ }
1051
+ }
1052
+
963
1053
  // MARK: - Inventory Builder
964
1054
 
965
1055
  private func buildInventory() -> CommandModeInventory {
@@ -1360,3 +1450,16 @@ final class CommandModeState: ObservableObject {
1360
1450
  return chords
1361
1451
  }
1362
1452
  }
1453
+
1454
+ private extension Sequence where Element == String {
1455
+ func uniquePrefix(_ count: Int) -> [String] {
1456
+ var seen = Set<String>()
1457
+ var result: [String] = []
1458
+ for item in self where !seen.contains(item) {
1459
+ seen.insert(item)
1460
+ result.append(item)
1461
+ if result.count == count { break }
1462
+ }
1463
+ return result
1464
+ }
1465
+ }