@lattices/cli 0.4.5 → 0.4.7

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} +9 -0
  5. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
  6. package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +1 -1
  7. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -2
  8. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +27 -2
  9. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +15 -2
  10. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
  11. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  12. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  13. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  14. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
  15. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  16. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  17. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  18. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  19. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -110
  20. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +228 -24
  21. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +601 -59
  22. package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +9 -5
  23. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  24. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  25. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  26. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  27. package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
  28. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  29. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -2
  30. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +20 -7
  31. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
  32. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +59 -4
  33. package/docs/component-extraction-roadmap.md +392 -0
  34. package/package.json +3 -1
  35. package/app/Sources/CommandPaletteWindow.swift +0 -134
  36. package/app/Sources/OmniSearchWindow.swift +0 -165
  37. /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
  38. /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
  39. /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
  40. /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
  41. /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
  42. /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
  43. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  44. /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
  45. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  46. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  47. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  48. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  49. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  50. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  51. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  52. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  53. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  54. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  55. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  56. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  57. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  58. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  59. /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
  60. /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
  61. /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
  62. /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
  63. /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
  64. /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
  65. /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
  66. /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
  67. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  68. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  69. /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
  70. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  71. /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
  72. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  73. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  74. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  75. /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
  76. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  77. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  78. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  79. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  80. /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
  81. /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
  82. /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
  83. /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
  84. /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
  85. /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
  86. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  87. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  88. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  89. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  90. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  91. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  92. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  93. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  94. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  95. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  96. /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
  97. /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
  98. /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
  99. /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
  100. /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
  101. /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
  102. /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
  103. /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
  104. /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
  105. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  106. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  107. /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
  108. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  109. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  110. /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
  111. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  112. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  113. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  114. /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
  115. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  116. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  117. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  118. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  119. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  120. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  121. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  122. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  123. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  124. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  125. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.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
@@ -45,6 +45,11 @@ enum DesktopInventoryMode: Equatable {
45
45
  case screenMap // m → interactive screen map editor
46
46
  }
47
47
 
48
+ enum CommandModeLaunchMode: Equatable {
49
+ case normal
50
+ case organize(appName: String?)
51
+ }
52
+
48
53
  // DisplayGeometry, ScreenMapWindowEntry, ScreenMapEditorState, ScreenMapActionLog
49
54
  // are defined in ScreenMapState.swift
50
55
  // MARK: - Filter Presets
@@ -91,11 +96,14 @@ final class CommandModeState: ObservableObject {
91
96
  @Published var inventory = CommandModeInventory(activeLayer: nil, layerCount: 0, items: [])
92
97
  @Published var chords: [Chord] = []
93
98
  @Published var desktopSnapshot: DesktopInventorySnapshot?
94
- @Published var selectedWindowIds: Set<UInt32> = []
99
+ @Published var selectedWindowIds: Set<UInt32> = [] {
100
+ didSet { syncSharedSelection() }
101
+ }
95
102
  @Published var desktopMode: DesktopInventoryMode = .browsing
96
103
  @Published var activePreset: FilterPreset? = nil
97
104
  @Published var searchQuery: String = ""
98
105
  @Published var isSearching: Bool = false
106
+ @Published var gridPreviewPlacement: PlacementSpec? = nil
99
107
 
100
108
  // MARK: - Marquee Drag State
101
109
  @Published var isDragging: Bool = false
@@ -129,10 +137,43 @@ final class CommandModeState: ObservableObject {
129
137
 
130
138
  var onDismiss: (() -> Void)?
131
139
  var onPanelResize: ((_ width: CGFloat, _ height: CGFloat) -> Void)?
140
+ private let launchMode: CommandModeLaunchMode
132
141
 
133
142
  /// Tracks the last item navigated to, for consistent Shift+arrow multi-select
134
143
  private var cursorWindowId: UInt32?
135
144
 
145
+ init(launchMode: CommandModeLaunchMode = .normal) {
146
+ self.launchMode = launchMode
147
+ }
148
+
149
+ var isOrganizeFlow: Bool {
150
+ if case .organize = launchMode { return true }
151
+ return false
152
+ }
153
+
154
+ var organizeSeedAppName: String? {
155
+ if case .organize(let appName) = launchMode { return appName }
156
+ return nil
157
+ }
158
+
159
+ var organizeSelectionSummary: String {
160
+ let count = selectedWindowIds.count
161
+ if let appName = organizeSeedAppName, !appName.isEmpty {
162
+ return "\(count) \(appName) window\(count == 1 ? "" : "s") selected"
163
+ }
164
+ return "\(count) window\(count == 1 ? "" : "s") selected"
165
+ }
166
+
167
+ var organizeGuidance: String {
168
+ if selectedWindowIds.count > 1 {
169
+ return "Press D to organize. Cmd-click adds or removes windows. Shift-click extends the selection."
170
+ }
171
+ if selectedWindowIds.count == 1 {
172
+ return "Cmd-click another window to add it, then press D to organize the set."
173
+ }
174
+ return "Click windows to select them. Cmd-click adds or removes windows, and D organizes the selection."
175
+ }
176
+
136
177
  // MARK: - Selection Helpers
137
178
 
138
179
  /// Backwards-compat: returns single selected ID (first element)
@@ -140,6 +181,26 @@ final class CommandModeState: ObservableObject {
140
181
  selectedWindowIds.first
141
182
  }
142
183
 
184
+ var selectedWindowSummaryText: String {
185
+ let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
186
+ let labels = windows.compactMap { $0.appName }.uniquePrefix(3)
187
+ guard !labels.isEmpty else { return "" }
188
+ if windows.count > labels.count {
189
+ return labels.joined(separator: " • ") + " +\(windows.count - labels.count)"
190
+ }
191
+ return labels.joined(separator: " • ")
192
+ }
193
+
194
+ var gridPreviewRegionLabel: String {
195
+ guard let placement = gridPreviewPlacement else { return "Full Screen" }
196
+ switch placement {
197
+ case .tile(let position):
198
+ return position.label
199
+ default:
200
+ return placement.wireValue
201
+ }
202
+ }
203
+
143
204
  func isSelected(_ id: UInt32) -> Bool {
144
205
  selectedWindowIds.contains(id)
145
206
  }
@@ -344,7 +405,9 @@ final class CommandModeState: ObservableObject {
344
405
  desktopSnapshot = buildDesktopInventory()
345
406
  clearSelection()
346
407
  desktopMode = .browsing
408
+ gridPreviewPlacement = nil
347
409
  phase = .desktopInventory
410
+ configureLaunchMode()
348
411
  // Don't call onPanelResize here — caller handles initial sizing
349
412
  }
350
413
 
@@ -535,6 +598,7 @@ final class CommandModeState: ObservableObject {
535
598
  if isSearching && selectedWindowIds.isEmpty { return false }
536
599
  if isSearching { deactivateSearch() }
537
600
  if !selectedWindowIds.isEmpty {
601
+ gridPreviewPlacement = nil
538
602
  desktopMode = .gridPreview
539
603
  }
540
604
  return true
@@ -551,6 +615,20 @@ final class CommandModeState: ObservableObject {
551
615
  }
552
616
  return true
553
617
 
618
+ case 2: // d → distribute selected
619
+ if isSearching && selectedWindowIds.isEmpty { return false }
620
+ if isSearching { deactivateSearch() }
621
+ guard !selectedWindowIds.isEmpty else {
622
+ flash("Select 2+ windows to organize")
623
+ return true
624
+ }
625
+ guard selectedWindowIds.count > 1 else {
626
+ flash("Add another window, then press D to organize")
627
+ return true
628
+ }
629
+ distributeSelected()
630
+ return true
631
+
554
632
  case 46: // m → screen map editor (standalone window)
555
633
  if isSearching { deactivateSearch() }
556
634
  ScreenMapWindowController.shared.show()
@@ -614,15 +692,54 @@ final class CommandModeState: ObservableObject {
614
692
 
615
693
  private func handleGridPreviewKey(_ keyCode: UInt16) -> Bool {
616
694
  switch keyCode {
617
- case 53: // Escape — always dismiss
618
- onDismiss?()
695
+ case 53: // Escape — cancel preview, keep selection
696
+ gridPreviewPlacement = nil
697
+ desktopMode = .browsing
619
698
  return true
620
699
 
621
700
  case 36, 1: // Enter or s → apply the layout
622
- showAndDistributeSelected()
701
+ showAndDistributeSelected(in: gridPreviewPlacement)
702
+ gridPreviewPlacement = nil
623
703
  desktopMode = .browsing
624
704
  return true
625
705
 
706
+ case 123:
707
+ gridPreviewPlacement = .tile(.left)
708
+ return true
709
+ case 124:
710
+ gridPreviewPlacement = .tile(.right)
711
+ return true
712
+ case 126:
713
+ gridPreviewPlacement = .tile(.top)
714
+ return true
715
+ case 125:
716
+ gridPreviewPlacement = .tile(.bottom)
717
+ return true
718
+ case 18:
719
+ gridPreviewPlacement = .tile(.topLeft)
720
+ return true
721
+ case 19:
722
+ gridPreviewPlacement = .tile(.topRight)
723
+ return true
724
+ case 20:
725
+ gridPreviewPlacement = .tile(.bottomLeft)
726
+ return true
727
+ case 21:
728
+ gridPreviewPlacement = .tile(.bottomRight)
729
+ return true
730
+ case 23:
731
+ gridPreviewPlacement = .tile(.leftThird)
732
+ return true
733
+ case 22:
734
+ gridPreviewPlacement = .tile(.centerThird)
735
+ return true
736
+ case 26:
737
+ gridPreviewPlacement = .tile(.rightThird)
738
+ return true
739
+ case 8:
740
+ gridPreviewPlacement = .tile(.center)
741
+ return true
742
+
626
743
  default:
627
744
  return true
628
745
  }
@@ -795,20 +912,8 @@ final class CommandModeState: ObservableObject {
795
912
  private func tileAllSelected(to position: TilePosition) {
796
913
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
797
914
  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
- }
915
+ DiagnosticLog.shared.info("Grid selected \(windows.count): \(position.rawValue)")
916
+ showAndDistributeSelected(in: .tile(position))
812
917
  }
813
918
 
814
919
  private func distributeSelectedHorizontally() {
@@ -849,28 +954,36 @@ final class CommandModeState: ObservableObject {
849
954
  }
850
955
 
851
956
  /// Show all selected windows AND distribute in smart grid — single batch operation
852
- func showAndDistributeSelected() {
957
+ func showAndDistributeSelected(in placement: PlacementSpec? = nil) {
853
958
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
854
959
  guard !windows.isEmpty else { return }
855
960
  savePositions(for: windows)
856
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
961
+ WindowTiler.batchRaiseAndDistribute(
962
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
963
+ region: placement?.fractions
964
+ )
857
965
  let shape = WindowTiler.gridShape(for: windows.count)
858
966
  let grid = shape.map(String.init).joined(separator: "+")
859
- flash("\(windows.count) windows [\(grid)]")
967
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
968
+ flash("\(windows.count) windows\(region) [\(grid)]")
860
969
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
861
970
  self?.desktopSnapshot = self?.buildDesktopInventory()
862
971
  }
863
972
  }
864
973
 
865
974
  /// Distribute selected in smart grid without raising
866
- func distributeSelected() {
975
+ func distributeSelected(in placement: PlacementSpec? = nil) {
867
976
  let windows = flatWindowList.filter { selectedWindowIds.contains($0.id) }
868
977
  guard !windows.isEmpty else { return }
869
978
  savePositions(for: windows)
870
- WindowTiler.batchRaiseAndDistribute(windows: windows.map { (wid: $0.id, pid: $0.pid) })
979
+ WindowTiler.batchRaiseAndDistribute(
980
+ windows: windows.map { (wid: $0.id, pid: $0.pid) },
981
+ region: placement?.fractions
982
+ )
871
983
  let shape = WindowTiler.gridShape(for: windows.count)
872
984
  let grid = shape.map(String.init).joined(separator: "+")
873
- flash("\(windows.count) windows [\(grid)]")
985
+ let region = placement.map { " in \(self.regionLabel(for: $0))" } ?? ""
986
+ flash("\(windows.count) windows\(region) [\(grid)]")
874
987
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
875
988
  self?.desktopSnapshot = self?.buildDesktopInventory()
876
989
  }
@@ -960,6 +1073,84 @@ final class CommandModeState: ObservableObject {
960
1073
  onDismiss?()
961
1074
  }
962
1075
 
1076
+ private func syncSharedSelection() {
1077
+ guard !selectedWindowIds.isEmpty else {
1078
+ WindowSelectionStore.shared.clear(source: "desktop-inventory")
1079
+ return
1080
+ }
1081
+
1082
+ let summaries = flatWindowList
1083
+ .filter { selectedWindowIds.contains($0.id) }
1084
+ .map {
1085
+ SelectedWindowSummary(
1086
+ wid: $0.id,
1087
+ app: $0.appName ?? "Window",
1088
+ title: $0.title,
1089
+ latticesSession: $0.latticesSession
1090
+ )
1091
+ }
1092
+
1093
+ guard !summaries.isEmpty else { return }
1094
+ WindowSelectionStore.shared.setSelection(summaries, source: "desktop-inventory")
1095
+ }
1096
+
1097
+ private func regionLabel(for placement: PlacementSpec) -> String {
1098
+ switch placement {
1099
+ case .tile(let position):
1100
+ return position.label
1101
+ default:
1102
+ return placement.wireValue
1103
+ }
1104
+ }
1105
+
1106
+ private func configureLaunchMode() {
1107
+ switch launchMode {
1108
+ case .normal:
1109
+ return
1110
+ case .organize(let appName):
1111
+ activePreset = .currentSpace
1112
+ seedSelectionForOrganization(appName: appName)
1113
+ }
1114
+ }
1115
+
1116
+ private func seedSelectionForOrganization(appName: String?) {
1117
+ let visibleWindows = flatWindowList.filter(\.isOnScreen)
1118
+ let targetApp = appName ?? visibleWindows.first?.appName
1119
+ let initialSelection = visibleWindows.filter { window in
1120
+ guard let name = window.appName, let targetApp else { return false }
1121
+ return name.localizedCaseInsensitiveCompare(targetApp) == .orderedSame
1122
+ }
1123
+
1124
+ if initialSelection.isEmpty {
1125
+ flash("Select windows to organize. Cmd-click adds or removes windows; D distributes.")
1126
+ return
1127
+ }
1128
+
1129
+ selectedWindowIds = Set(initialSelection.map(\.id))
1130
+ cursorWindowId = initialSelection.first?.id
1131
+
1132
+ if let targetApp {
1133
+ if initialSelection.count > 1 {
1134
+ flash("Selected \(initialSelection.count) \(targetApp) windows. Press D to organize.")
1135
+ } else {
1136
+ flash("Selected the \(targetApp) window. Cmd-click more windows, then press D.")
1137
+ }
1138
+ } else if initialSelection.count > 1 {
1139
+ flash("Selected \(initialSelection.count) windows. Press D to organize.")
1140
+ } else {
1141
+ flash("Selected 1 window. Cmd-click more windows, then press D.")
1142
+ }
1143
+
1144
+ DispatchQueue.main.async { [weak self] in
1145
+ guard let self = self else { return }
1146
+ if self.selectedWindowIds.count > 1 {
1147
+ self.highlightAllSelected()
1148
+ } else {
1149
+ self.highlightSelectedWindow()
1150
+ }
1151
+ }
1152
+ }
1153
+
963
1154
  // MARK: - Inventory Builder
964
1155
 
965
1156
  private func buildInventory() -> CommandModeInventory {
@@ -1360,3 +1551,16 @@ final class CommandModeState: ObservableObject {
1360
1551
  return chords
1361
1552
  }
1362
1553
  }
1554
+
1555
+ private extension Sequence where Element == String {
1556
+ func uniquePrefix(_ count: Int) -> [String] {
1557
+ var seen = Set<String>()
1558
+ var result: [String] = []
1559
+ for item in self where !seen.contains(item) {
1560
+ seen.insert(item)
1561
+ result.append(item)
1562
+ if result.count == count { break }
1563
+ }
1564
+ return result
1565
+ }
1566
+ }