@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
@@ -22,8 +22,14 @@ private struct FocusRingSuppressor: ViewModifier {
22
22
  }
23
23
  }
24
24
 
25
+ enum CommandModePresentation {
26
+ case panel
27
+ case embedded
28
+ }
29
+
25
30
  struct CommandModeView: View {
26
31
  @ObservedObject var state: CommandModeState
32
+ var presentation: CommandModePresentation = .panel
27
33
  @State private var eventMonitor: Any?
28
34
  @State private var mouseDownMonitor: Any?
29
35
  @State private var mouseDragMonitor: Any?
@@ -36,41 +42,77 @@ struct CommandModeView: View {
36
42
  state.phase == .desktopInventory
37
43
  }
38
44
 
45
+ private var isEmbedded: Bool {
46
+ presentation == .embedded
47
+ }
48
+
39
49
  // Column widths for inventory table
40
50
  private static let sizeColW: CGFloat = 80
41
51
  private static let tileColW: CGFloat = 60
42
52
 
43
- private var displayColumnWidth: CGFloat {
44
- let count = CGFloat(max(1, state.filteredSnapshot?.displays.count ?? 1))
45
- let available = panelWidth - 32 - (count - 1) * 0.5
46
- return max(360, (available / count).rounded(.down))
53
+ var body: some View {
54
+ GeometryReader { geo in
55
+ let availableWidth = max(geo.size.width, 580)
56
+ let contentWidth = resolvedContentWidth(in: availableWidth)
57
+
58
+ Group {
59
+ if isEmbedded && isDesktopInventory {
60
+ embeddedInventoryPage(contentWidth: contentWidth)
61
+ } else {
62
+ inventoryCard(contentWidth: contentWidth)
63
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
64
+ }
65
+ }
66
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
67
+ }
68
+ .onAppear { installKeyHandler(); installMouseMonitors() }
69
+ .onDisappear { removeKeyHandler(); removeMouseMonitors() }
70
+ .onChange(of: state.desktopMode) { mode in
71
+ CommandModeWindow.shared.panelWindow?.isMovableByWindowBackground = true
72
+ }
73
+ .animation(.easeInOut(duration: 0.2), value: isDesktopInventory)
74
+ .modifier(FocusRingSuppressor())
47
75
  }
48
76
 
49
- private var panelWidth: CGFloat {
77
+ private func resolvedContentWidth(in availableWidth: CGFloat) -> CGFloat {
50
78
  if isDesktopInventory {
51
- let displayCount = max(1, state.filteredSnapshot?.displays.count ?? 1)
52
- let ideal = CGFloat(displayCount) * 480 + CGFloat(displayCount - 1) + 32
53
- let screenWidth = NSScreen.main?.visibleFrame.width ?? 1920
79
+ if isEmbedded {
80
+ return min(max(availableWidth - 24, 840), 1560)
81
+ }
82
+
83
+ let displayCount = CGFloat(max(1, state.filteredSnapshot?.displays.count ?? 1))
84
+ let ideal = displayCount * 480 + CGFloat(max(0, Int(displayCount) - 1)) + 32
85
+ let screenWidth = NSScreen.main?.visibleFrame.width ?? availableWidth
54
86
  return min(ideal, screenWidth * 0.92)
55
87
  }
88
+
89
+ if isEmbedded {
90
+ return min(720, max(availableWidth - 32, 580))
91
+ }
56
92
  return 580
57
93
  }
58
94
 
59
- var body: some View {
95
+ private func displayColumnWidth(for contentWidth: CGFloat) -> CGFloat {
96
+ let count = CGFloat(max(1, state.filteredSnapshot?.displays.count ?? 1))
97
+ let available = contentWidth - 32 - (count - 1) * 0.5
98
+ return max(isEmbedded ? 400 : 360, (available / count).rounded(.down))
99
+ }
100
+
101
+ private func inventoryCard(contentWidth: CGFloat) -> some View {
60
102
  VStack(spacing: 0) {
61
103
  header
62
104
  divider
63
105
  if isDesktopInventory && state.desktopMode == .gridPreview {
64
106
  gridPreviewContent
65
107
  } else if isDesktopInventory {
66
- desktopInventoryContent
108
+ desktopInventoryContent(contentWidth: contentWidth)
67
109
  } else {
68
110
  inventoryGrid
69
111
  }
70
112
  divider
71
113
  chordFooter
72
114
  }
73
- .frame(width: panelWidth)
115
+ .frame(width: contentWidth)
74
116
  .background(Palette.bg)
75
117
  .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
76
118
  .overlay(
@@ -79,23 +121,96 @@ struct CommandModeView: View {
79
121
  )
80
122
  .overlay(executingOverlay)
81
123
  .overlay(flashOverlay)
82
- .onAppear { installKeyHandler(); installMouseMonitors() }
83
- .onDisappear { removeKeyHandler(); removeMouseMonitors() }
84
- .onChange(of: state.desktopMode) { mode in
85
- CommandModeWindow.shared.panelWindow?.isMovableByWindowBackground = true
124
+ }
125
+
126
+ private func embeddedInventoryPage(contentWidth: CGFloat) -> some View {
127
+ VStack(spacing: 12) {
128
+ embeddedInventorySummary
129
+ .frame(width: contentWidth, alignment: .leading)
130
+ inventoryCard(contentWidth: contentWidth)
131
+ .frame(maxHeight: .infinity)
86
132
  }
87
- .animation(.easeInOut(duration: 0.2), value: isDesktopInventory)
88
- .modifier(FocusRingSuppressor())
133
+ .padding(16)
134
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
135
+ }
136
+
137
+ private var embeddedInventorySummary: some View {
138
+ let snapshot = state.filteredSnapshot ?? state.desktopSnapshot
139
+ let displayCount = snapshot?.displays.count ?? 0
140
+ let spaceCount = snapshot?.displays.reduce(0) { total, display in
141
+ total + display.spaces.count
142
+ } ?? 0
143
+ let windowCount = snapshot?.allWindows.count ?? 0
144
+
145
+ return HStack(alignment: .center, spacing: 12) {
146
+ VStack(alignment: .leading, spacing: 4) {
147
+ Text("Grouped by display, Space, and app")
148
+ .font(Typo.heading(13))
149
+ .foregroundColor(Palette.text)
150
+ Text(state.isSearching
151
+ ? "Search results stay in place so the desktop reads like a map instead of a flat list."
152
+ : "Live window sizes, tiling hints, and OCR search in one balanced pass across the desktop.")
153
+ .font(Typo.mono(10))
154
+ .foregroundColor(Palette.textDim)
155
+ .fixedSize(horizontal: false, vertical: true)
156
+ }
157
+
158
+ Spacer(minLength: 12)
159
+
160
+ HStack(spacing: 8) {
161
+ inventoryStatPill(value: displayCount, label: "Displays")
162
+ inventoryStatPill(value: spaceCount, label: "Spaces")
163
+ inventoryStatPill(value: windowCount, label: "Windows")
164
+ }
165
+ }
166
+ .padding(.horizontal, 14)
167
+ .padding(.vertical, 12)
168
+ .background(
169
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
170
+ .fill(Palette.surface.opacity(0.65))
171
+ .overlay(
172
+ RoundedRectangle(cornerRadius: 12, style: .continuous)
173
+ .strokeBorder(Palette.border, lineWidth: 0.5)
174
+ )
175
+ )
176
+ }
177
+
178
+ private func inventoryStatPill(value: Int, label: String) -> some View {
179
+ VStack(alignment: .leading, spacing: 2) {
180
+ Text("\(value)")
181
+ .font(Typo.monoBold(12))
182
+ .foregroundColor(Palette.text)
183
+ Text(label.uppercased())
184
+ .font(Typo.mono(8))
185
+ .foregroundColor(Palette.textMuted)
186
+ }
187
+ .padding(.horizontal, 10)
188
+ .padding(.vertical, 8)
189
+ .background(
190
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
191
+ .fill(Palette.bg.opacity(0.75))
192
+ .overlay(
193
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
194
+ .strokeBorder(Palette.border, lineWidth: 0.5)
195
+ )
196
+ )
89
197
  }
90
198
 
91
199
  // MARK: - Header
92
200
 
93
201
  private var header: some View {
94
202
  HStack {
95
- Text(isDesktopInventory ? "DESKTOP INVENTORY" : "COMMAND MODE")
203
+ Text(isDesktopInventory ? (state.isOrganizeFlow ? "ORGANIZE WINDOWS" : "DESKTOP INVENTORY") : "COMMAND MODE")
96
204
  .font(Typo.monoBold(11))
97
205
  .foregroundColor(Palette.text)
98
206
 
207
+ if isDesktopInventory && state.isOrganizeFlow {
208
+ bannerBadge("Current Space", tone: .neutral)
209
+ if let appName = state.organizeSeedAppName, !appName.isEmpty {
210
+ bannerBadge(appName, tone: .accent)
211
+ }
212
+ }
213
+
99
214
  if isDesktopInventory {
100
215
  Button(action: { state.copyInventoryToClipboard() }) {
101
216
  HStack(spacing: 3) {
@@ -165,8 +280,13 @@ struct CommandModeView: View {
165
280
 
166
281
  // MARK: - Desktop Inventory Content
167
282
 
168
- private var desktopInventoryContent: some View {
283
+ private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
169
284
  VStack(spacing: 0) {
285
+ if state.isOrganizeFlow {
286
+ organizeBanner
287
+ divider
288
+ }
289
+
170
290
  if state.isSearching {
171
291
  searchBar
172
292
  } else {
@@ -177,20 +297,7 @@ struct CommandModeView: View {
177
297
  ZStack {
178
298
  Group {
179
299
  if let snapshot = state.filteredSnapshot, !snapshot.displays.isEmpty {
180
- ScrollView(.horizontal, showsIndicators: false) {
181
- HStack(alignment: .top, spacing: 0) {
182
- let total = snapshot.displays.count
183
- ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
184
- if idx > 0 {
185
- Rectangle()
186
- .fill(Palette.border)
187
- .frame(width: 0.5)
188
- }
189
- displayColumn(display, index: idx, total: total)
190
- .frame(width: displayColumnWidth)
191
- }
192
- }
193
- }
300
+ inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
194
301
  } else {
195
302
  desktopEmptyState
196
303
  }
@@ -216,6 +323,80 @@ struct CommandModeView: View {
216
323
  }
217
324
  }
218
325
 
326
+ @ViewBuilder
327
+ private func inventoryColumns(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> some View {
328
+ if shouldShowEmbeddedSidebar(snapshot: snapshot, contentWidth: contentWidth),
329
+ let display = snapshot.displays.first {
330
+ embeddedSingleDisplayLayout(display: display, contentWidth: contentWidth)
331
+ } else {
332
+ ScrollView(.horizontal, showsIndicators: false) {
333
+ HStack(alignment: .top, spacing: 0) {
334
+ let total = snapshot.displays.count
335
+ ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
336
+ if idx > 0 {
337
+ Rectangle()
338
+ .fill(Palette.border)
339
+ .frame(width: 0.5)
340
+ }
341
+ displayColumn(display, index: idx, total: total)
342
+ .frame(width: displayColumnWidth(for: contentWidth))
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+
349
+ private func shouldShowEmbeddedSidebar(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> Bool {
350
+ isEmbedded && snapshot.displays.count == 1 && contentWidth >= 900
351
+ }
352
+
353
+ private func embeddedSingleDisplayLayout(
354
+ display: DesktopInventorySnapshot.DisplayInfo,
355
+ contentWidth: CGFloat
356
+ ) -> some View {
357
+ let sidebarWidth = min(max(contentWidth * 0.28, 250), 330)
358
+ let mainWidth = max(contentWidth - sidebarWidth - 0.5, 620)
359
+
360
+ return HStack(alignment: .top, spacing: 0) {
361
+ displayColumn(display, index: 0, total: 1)
362
+ .frame(width: mainWidth)
363
+
364
+ Rectangle()
365
+ .fill(Palette.border)
366
+ .frame(width: 0.5)
367
+
368
+ embeddedInventorySidebar(display: display)
369
+ .frame(width: sidebarWidth)
370
+ }
371
+ }
372
+
373
+ private var organizeBanner: some View {
374
+ VStack(alignment: .leading, spacing: 6) {
375
+ HStack(alignment: .center, spacing: 8) {
376
+ Image(systemName: "rectangle.3.group")
377
+ .font(.system(size: 11, weight: .medium))
378
+ .foregroundColor(Palette.running)
379
+ Text(state.organizeSelectionSummary)
380
+ .font(Typo.monoBold(10))
381
+ .foregroundColor(Palette.text)
382
+ Spacer()
383
+ if state.selectedWindowIds.count > 1 {
384
+ bannerBadge("Ready", tone: .accent)
385
+ } else {
386
+ bannerBadge("Add More", tone: .neutral)
387
+ }
388
+ }
389
+
390
+ Text(state.organizeGuidance)
391
+ .font(Typo.mono(10))
392
+ .foregroundColor(Palette.textDim)
393
+ .lineLimit(2)
394
+ }
395
+ .padding(.horizontal, 14)
396
+ .padding(.vertical, 8)
397
+ .background(Palette.running.opacity(0.06))
398
+ }
399
+
219
400
  private var filterPillBar: some View {
220
401
  HStack(spacing: 6) {
221
402
  ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
@@ -378,6 +559,210 @@ struct CommandModeView: View {
378
559
  .padding(.vertical, 8)
379
560
  }
380
561
 
562
+ private func embeddedInventorySidebar(display: DesktopInventorySnapshot.DisplayInfo) -> some View {
563
+ ScrollView {
564
+ VStack(alignment: .leading, spacing: 12) {
565
+ sidebarCard(title: "Overview") {
566
+ sidebarMetric(label: "Display", value: display.name)
567
+ sidebarMetric(
568
+ label: "Visible",
569
+ value: "\(display.visibleFrame.w)×\(display.visibleFrame.h)"
570
+ )
571
+ sidebarMetric(
572
+ label: "Current Space",
573
+ value: "Space \(display.currentSpaceIndex)"
574
+ )
575
+ sidebarMetric(
576
+ label: "Windows",
577
+ value: "\(windowCount(in: display)) total"
578
+ )
579
+ sidebarMetric(
580
+ label: "Apps",
581
+ value: "\(uniqueAppCount(in: display)) active"
582
+ )
583
+ sidebarMetric(
584
+ label: "Lattices",
585
+ value: "\(latticesWindowCount(in: display)) tagged"
586
+ )
587
+ }
588
+
589
+ sidebarCard(title: "Spaces") {
590
+ VStack(alignment: .leading, spacing: 8) {
591
+ ForEach(display.spaces) { space in
592
+ HStack(alignment: .top, spacing: 8) {
593
+ VStack(alignment: .leading, spacing: 2) {
594
+ HStack(spacing: 5) {
595
+ Text("Space \(space.index)")
596
+ .font(Typo.monoBold(10))
597
+ .foregroundColor(space.isCurrent ? Palette.running : Palette.text)
598
+ if space.isCurrent {
599
+ Text("active")
600
+ .font(Typo.mono(8))
601
+ .foregroundColor(Palette.running.opacity(0.75))
602
+ }
603
+ }
604
+ Text("\(spaceWindowCount(space)) windows across \(space.apps.count) apps")
605
+ .font(Typo.mono(9))
606
+ .foregroundColor(Palette.textDim)
607
+ }
608
+ Spacer()
609
+ Text("\(spaceLatticesCount(space))")
610
+ .font(Typo.mono(9))
611
+ .foregroundColor(Palette.textMuted)
612
+ }
613
+ }
614
+ }
615
+ }
616
+
617
+ sidebarCard(title: state.selectedWindowIds.isEmpty ? "Top Apps" : "Selection") {
618
+ if state.selectedWindowIds.isEmpty {
619
+ VStack(alignment: .leading, spacing: 8) {
620
+ Text("Select a window to inspect it here.")
621
+ .font(Typo.mono(9))
622
+ .foregroundColor(Palette.textDim)
623
+
624
+ ForEach(topApps(in: display), id: \.name) { app in
625
+ HStack(spacing: 8) {
626
+ Text(app.name)
627
+ .font(Typo.monoBold(10))
628
+ .foregroundColor(Palette.text)
629
+ .lineLimit(1)
630
+ Spacer()
631
+ Text("\(app.count)")
632
+ .font(Typo.mono(9))
633
+ .foregroundColor(Palette.textMuted)
634
+ }
635
+ }
636
+ }
637
+ } else {
638
+ selectionSidebarContent
639
+ }
640
+ }
641
+
642
+ sidebarCard(title: "Keys") {
643
+ VStack(alignment: .leading, spacing: 7) {
644
+ sidebarShortcut("Arrows", "move through windows")
645
+ sidebarShortcut("/", "search by title or OCR")
646
+ sidebarShortcut("M", "jump to Screen Map")
647
+ sidebarShortcut("T", "tile selected window")
648
+ sidebarShortcut("Esc", "back or clear selection")
649
+ }
650
+ }
651
+ }
652
+ .padding(12)
653
+ }
654
+ }
655
+
656
+ @ViewBuilder
657
+ private var selectionSidebarContent: some View {
658
+ let selected = selectedWindows
659
+
660
+ if selected.count > 1 {
661
+ VStack(alignment: .leading, spacing: 8) {
662
+ Text("\(selected.count) windows selected")
663
+ .font(Typo.monoBold(10))
664
+ .foregroundColor(Palette.text)
665
+ if !state.selectedWindowSummaryText.isEmpty {
666
+ Text(state.selectedWindowSummaryText)
667
+ .font(Typo.mono(9))
668
+ .foregroundColor(Palette.textDim)
669
+ }
670
+ ForEach(Array(selected.prefix(5)), id: \.id) { window in
671
+ HStack(spacing: 8) {
672
+ Text(window.appName ?? "Unknown")
673
+ .font(Typo.monoBold(9))
674
+ .foregroundColor(window.isLattices ? Palette.running : Palette.text)
675
+ .lineLimit(1)
676
+ Spacer()
677
+ Text(sizeText(window.frame))
678
+ .font(Typo.mono(9))
679
+ .foregroundColor(Palette.textMuted)
680
+ }
681
+ }
682
+ }
683
+ } else if let window = selected.first {
684
+ VStack(alignment: .leading, spacing: 8) {
685
+ Text(window.appName ?? "Unknown")
686
+ .font(Typo.monoBold(10))
687
+ .foregroundColor(window.isLattices ? Palette.running : Palette.text)
688
+ Text(window.title.isEmpty ? "(untitled)" : window.title)
689
+ .font(Typo.mono(9))
690
+ .foregroundColor(Palette.textDim)
691
+ .fixedSize(horizontal: false, vertical: true)
692
+ sidebarMetric(label: "Size", value: sizeText(window.frame))
693
+ if let tile = window.tilePosition?.label {
694
+ sidebarMetric(label: "Tile", value: tile)
695
+ }
696
+ if let session = window.latticesSession {
697
+ sidebarMetric(label: "Session", value: session)
698
+ }
699
+ if let path = window.inventoryPath {
700
+ Text(path.description)
701
+ .font(Typo.mono(8))
702
+ .foregroundColor(Palette.textMuted)
703
+ .fixedSize(horizontal: false, vertical: true)
704
+ }
705
+ }
706
+ }
707
+ }
708
+
709
+ private func sidebarCard<Content: View>(
710
+ title: String,
711
+ @ViewBuilder content: () -> Content
712
+ ) -> some View {
713
+ VStack(alignment: .leading, spacing: 10) {
714
+ Text(title.uppercased())
715
+ .font(Typo.mono(9))
716
+ .foregroundColor(Palette.textMuted)
717
+ content()
718
+ }
719
+ .padding(12)
720
+ .frame(maxWidth: .infinity, alignment: .leading)
721
+ .background(
722
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
723
+ .fill(Palette.surface.opacity(0.55))
724
+ .overlay(
725
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
726
+ .strokeBorder(Palette.border, lineWidth: 0.5)
727
+ )
728
+ )
729
+ }
730
+
731
+ private func sidebarMetric(label: String, value: String) -> some View {
732
+ HStack(alignment: .firstTextBaseline, spacing: 10) {
733
+ Text(label.uppercased())
734
+ .font(Typo.mono(8))
735
+ .foregroundColor(Palette.textMuted)
736
+ .frame(width: 74, alignment: .leading)
737
+ Text(value)
738
+ .font(Typo.mono(9))
739
+ .foregroundColor(Palette.text)
740
+ .fixedSize(horizontal: false, vertical: true)
741
+ Spacer(minLength: 0)
742
+ }
743
+ }
744
+
745
+ private func sidebarShortcut(_ key: String, _ label: String) -> some View {
746
+ HStack(spacing: 8) {
747
+ Text(key)
748
+ .font(Typo.monoBold(9))
749
+ .foregroundColor(Palette.text)
750
+ .padding(.horizontal, 6)
751
+ .padding(.vertical, 3)
752
+ .background(
753
+ RoundedRectangle(cornerRadius: 4, style: .continuous)
754
+ .fill(Palette.bg.opacity(0.8))
755
+ .overlay(
756
+ RoundedRectangle(cornerRadius: 4, style: .continuous)
757
+ .strokeBorder(Palette.border, lineWidth: 0.5)
758
+ )
759
+ )
760
+ Text(label)
761
+ .font(Typo.mono(9))
762
+ .foregroundColor(Palette.textDim)
763
+ }
764
+ }
765
+
381
766
  private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
382
767
  HStack(spacing: 5) {
383
768
  Text("Space \(space.index)")
@@ -489,9 +874,17 @@ struct CommandModeView: View {
489
874
  if indented {
490
875
  Spacer().frame(width: 8)
491
876
  }
492
- Text(isLattices ? "●" : "•")
493
- .font(.system(size: 7))
494
- .foregroundColor(isLattices ? Palette.running : (isSelected ? Palette.text : Palette.textDim))
877
+ Group {
878
+ if isSelected {
879
+ Image(systemName: "checkmark.circle.fill")
880
+ .font(.system(size: 10, weight: .semibold))
881
+ .foregroundColor(Palette.running)
882
+ } else {
883
+ Text(isLattices ? "●" : "•")
884
+ .font(.system(size: 7))
885
+ .foregroundColor(isLattices ? Palette.running : Palette.textDim)
886
+ }
887
+ }
495
888
  if let app = appLabel {
496
889
  Text(app)
497
890
  .font(Typo.monoBold(10))
@@ -610,15 +1003,7 @@ struct CommandModeView: View {
610
1003
  Menu("Tile All (\(selCount))") {
611
1004
  ForEach(TilePosition.allCases) { tile in
612
1005
  Button {
613
- let windows = state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
614
- for (i, win) in windows.enumerated() {
615
- DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.1) {
616
- WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: tile)
617
- }
618
- }
619
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3 + Double(windows.count) * 0.1) {
620
- state.desktopSnapshot = nil
621
- }
1006
+ state.showAndDistributeSelected(in: .tile(tile))
622
1007
  } label: {
623
1008
  Label(tile.label, systemImage: tile.icon)
624
1009
  }
@@ -683,9 +1068,6 @@ struct CommandModeView: View {
683
1068
  private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
684
1069
  let title = window.title
685
1070
  if title.isEmpty { return "(untitled)" }
686
- if title.count > 30 {
687
- return String(title.prefix(27)) + "..."
688
- }
689
1071
  return title
690
1072
  }
691
1073
 
@@ -693,6 +1075,57 @@ struct CommandModeView: View {
693
1075
  "\(Int(frame.w))×\(Int(frame.h))"
694
1076
  }
695
1077
 
1078
+ private var selectedWindows: [DesktopInventorySnapshot.InventoryWindowInfo] {
1079
+ state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
1080
+ }
1081
+
1082
+ private func windowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1083
+ display.spaces.reduce(0) { total, space in
1084
+ total + spaceWindowCount(space)
1085
+ }
1086
+ }
1087
+
1088
+ private func uniqueAppCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1089
+ Set(display.spaces.flatMap { $0.apps.map(\.appName) }).count
1090
+ }
1091
+
1092
+ private func latticesWindowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1093
+ display.spaces.reduce(0) { total, space in
1094
+ total + spaceLatticesCount(space)
1095
+ }
1096
+ }
1097
+
1098
+ private func spaceWindowCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
1099
+ space.apps.reduce(0) { total, app in
1100
+ total + app.windows.count
1101
+ }
1102
+ }
1103
+
1104
+ private func spaceLatticesCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
1105
+ space.apps.reduce(0) { total, app in
1106
+ total + app.windows.filter(\.isLattices).count
1107
+ }
1108
+ }
1109
+
1110
+ private func topApps(
1111
+ in display: DesktopInventorySnapshot.DisplayInfo
1112
+ ) -> [(name: String, count: Int)] {
1113
+ var counts: [String: Int] = [:]
1114
+ for space in display.spaces {
1115
+ for app in space.apps {
1116
+ counts[app.appName, default: 0] += app.windows.count
1117
+ }
1118
+ }
1119
+ return counts
1120
+ .map { (name: $0.key, count: $0.value) }
1121
+ .sorted { lhs, rhs in
1122
+ if lhs.count == rhs.count { return lhs.name < rhs.name }
1123
+ return lhs.count > rhs.count
1124
+ }
1125
+ .prefix(5)
1126
+ .map { $0 }
1127
+ }
1128
+
696
1129
  /// Group items by their group label
697
1130
  private var groupedItems: [(String, [CommandModeInventory.Item])] {
698
1131
  var result: [(String, [CommandModeInventory.Item])] = []
@@ -835,10 +1268,16 @@ struct CommandModeView: View {
835
1268
  if isDesktopInventory && state.desktopMode == .gridPreview {
836
1269
  // Grid preview hints
837
1270
  HStack(spacing: 12) {
1271
+ chordHint(key: "←→↑↓", label: "region")
1272
+ chordHint(key: "1-7", label: "corners/thirds")
1273
+ chordHint(key: "c", label: "center")
838
1274
  chordHint(key: "↩", label: "apply layout")
839
1275
  chordHint(key: "s", label: "apply layout")
840
1276
  chordHint(key: "esc", label: "cancel")
841
1277
  Spacer()
1278
+ Text(state.gridPreviewRegionLabel.uppercased())
1279
+ .font(Typo.mono(9))
1280
+ .foregroundColor(Palette.textDim)
842
1281
  let shape = state.gridPreviewShape
843
1282
  Text(shape.map(String.init).joined(separator: " + "))
844
1283
  .font(Typo.monoBold(9))
@@ -850,6 +1289,9 @@ struct CommandModeView: View {
850
1289
  chordHint(key: "↩", label: "select & front")
851
1290
  chordHint(key: "⌘A", label: "select all")
852
1291
  chordHint(key: "⇧↑↓", label: "multi-select")
1292
+ if state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
1293
+ chordHint(key: "d", label: "organize")
1294
+ }
853
1295
  if !state.selectedWindowIds.isEmpty {
854
1296
  chordHint(key: "t", label: "tile")
855
1297
  }
@@ -887,16 +1329,55 @@ struct CommandModeView: View {
887
1329
  .foregroundColor(Palette.running)
888
1330
  }
889
1331
  }
1332
+ } else if isDesktopInventory && state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
1333
+ HStack(spacing: 12) {
1334
+ chordHint(key: "d", label: "organize")
1335
+ chordHint(key: "⌘-click", label: "add/remove")
1336
+ chordHint(key: "⇧-click", label: "range")
1337
+ chordHint(key: "↩", label: "front")
1338
+ chordHint(key: "esc", label: "cancel")
1339
+ Spacer()
1340
+ Text("\(state.selectedWindowIds.count) selected")
1341
+ .font(Typo.mono(9))
1342
+ .foregroundColor(Palette.running)
1343
+ }
1344
+ } else if isDesktopInventory && state.isOrganizeFlow && !state.selectedWindowIds.isEmpty {
1345
+ HStack(spacing: 12) {
1346
+ chordHint(key: "⌘-click", label: "add more")
1347
+ chordHint(key: "d", label: "need 2+")
1348
+ chordHint(key: "↩", label: "front")
1349
+ chordHint(key: "esc", label: "cancel")
1350
+ Spacer()
1351
+ Text(state.organizeSelectionSummary)
1352
+ .font(Typo.mono(9))
1353
+ .foregroundColor(Palette.textDim)
1354
+ }
1355
+ } else if isDesktopInventory && state.isOrganizeFlow {
1356
+ HStack(spacing: 12) {
1357
+ chordHint(key: "click", label: "select")
1358
+ chordHint(key: "⌘-click", label: "add/remove")
1359
+ chordHint(key: "/", label: "search")
1360
+ chordHint(key: "esc", label: "cancel")
1361
+ Spacer()
1362
+ }
890
1363
  } else if isDesktopInventory && state.selectedWindowIds.count > 1 {
891
1364
  // Multi-selection active
892
1365
  HStack(spacing: 12) {
893
- chordHint(key: "s", label: "show")
1366
+ chordHint(key: "s", label: "grid preview")
1367
+ chordHint(key: "d", label: "distribute")
1368
+ chordHint(key: "s", label: "grid preview")
894
1369
  chordHint(key: "↩", label: "front")
895
- chordHint(key: "t", label: "tile")
1370
+ chordHint(key: "t", label: "grid region")
896
1371
  chordHint(key: "f", label: "focus")
897
1372
  chordHint(key: "h", label: "highlight")
898
1373
  chordHint(key: "esc", label: "clear")
899
1374
  Spacer()
1375
+ if !state.selectedWindowSummaryText.isEmpty {
1376
+ Text(state.selectedWindowSummaryText)
1377
+ .font(Typo.mono(9))
1378
+ .foregroundColor(Palette.textDim)
1379
+ .lineLimit(1)
1380
+ }
900
1381
  Text("\(state.selectedWindowIds.count) selected")
901
1382
  .font(Typo.mono(9))
902
1383
  .foregroundColor(Palette.running)
@@ -904,6 +1385,7 @@ struct CommandModeView: View {
904
1385
  } else if isDesktopInventory && !state.selectedWindowIds.isEmpty {
905
1386
  // Single selection active — browsing hints with direct shortcuts
906
1387
  HStack(spacing: 12) {
1388
+ chordHint(key: "d", label: "organize")
907
1389
  chordHint(key: "s", label: "show")
908
1390
  chordHint(key: "↩", label: "front")
909
1391
  chordHint(key: "f", label: "focus+close")
@@ -969,6 +1451,31 @@ struct CommandModeView: View {
969
1451
  }
970
1452
  }
971
1453
 
1454
+ private enum BannerTone {
1455
+ case neutral
1456
+ case accent
1457
+ }
1458
+
1459
+ private func bannerBadge(_ text: String, tone: BannerTone) -> some View {
1460
+ let foreground = tone == .accent ? Palette.running : Palette.textDim
1461
+ let fill = tone == .accent ? Palette.running.opacity(0.10) : Palette.surface
1462
+ let stroke = tone == .accent ? Palette.running.opacity(0.30) : Palette.border
1463
+
1464
+ return Text(text)
1465
+ .font(Typo.mono(8))
1466
+ .foregroundColor(foreground)
1467
+ .padding(.horizontal, 6)
1468
+ .padding(.vertical, 3)
1469
+ .background(
1470
+ RoundedRectangle(cornerRadius: 8)
1471
+ .fill(fill)
1472
+ .overlay(
1473
+ RoundedRectangle(cornerRadius: 8)
1474
+ .strokeBorder(stroke, lineWidth: 0.5)
1475
+ )
1476
+ )
1477
+ }
1478
+
972
1479
  private func actionButton(key: String, label: String, action: @escaping () -> Void) -> some View {
973
1480
  Button(action: action) {
974
1481
  HStack(spacing: 4) {
@@ -1082,6 +1589,9 @@ struct CommandModeView: View {
1082
1589
  Text("LAYOUT PREVIEW")
1083
1590
  .font(Typo.monoBold(10))
1084
1591
  .foregroundColor(Palette.textDim)
1592
+ Text(state.gridPreviewRegionLabel.uppercased())
1593
+ .font(Typo.mono(9))
1594
+ .foregroundColor(Palette.textMuted)
1085
1595
  Text(gridDesc)
1086
1596
  .font(Typo.monoBold(10))
1087
1597
  .foregroundColor(Palette.running)
@@ -1096,7 +1606,7 @@ struct CommandModeView: View {
1096
1606
  divider
1097
1607
 
1098
1608
  // Screen map: current positions (dimmed) + target grid (bright)
1099
- screenMap(windows: windows, shape: shape)
1609
+ screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
1100
1610
  .frame(height: 160)
1101
1611
  .padding(.horizontal, 12)
1102
1612
  .padding(.vertical, 8)
@@ -1125,7 +1635,11 @@ struct CommandModeView: View {
1125
1635
  // MARK: - Grid Preview Screen Map
1126
1636
 
1127
1637
  /// Miniature proportional map of the screen showing current window positions and target grid slots
1128
- private func screenMap(windows: [DesktopInventorySnapshot.InventoryWindowInfo], shape: [Int]) -> some View {
1638
+ private func screenMap(
1639
+ windows: [DesktopInventorySnapshot.InventoryWindowInfo],
1640
+ shape: [Int],
1641
+ placement: PlacementSpec?
1642
+ ) -> some View {
1129
1643
  GeometryReader { geo in
1130
1644
  let availW = geo.size.width
1131
1645
  let availH = geo.size.height
@@ -1154,6 +1668,14 @@ struct CommandModeView: View {
1154
1668
  )
1155
1669
  .frame(width: mapW, height: mapH)
1156
1670
 
1671
+ if let placement {
1672
+ let region = placement.fractions
1673
+ RoundedRectangle(cornerRadius: 4)
1674
+ .strokeBorder(Palette.running.opacity(0.35), style: StrokeStyle(lineWidth: 1, dash: [6, 4]))
1675
+ .frame(width: mapW * region.2, height: mapH * region.3)
1676
+ .offset(x: mapW * region.0, y: mapH * region.1)
1677
+ }
1678
+
1157
1679
  // Current positions (dimmed)
1158
1680
  ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
1159
1681
  let f = win.frame
@@ -1173,7 +1695,13 @@ struct CommandModeView: View {
1173
1695
  }
1174
1696
 
1175
1697
  // Target grid slots (bright)
1176
- let slots = computeMapSlots(count: windows.count, shape: shape, mapW: mapW, mapH: mapH)
1698
+ let slots = computeMapSlots(
1699
+ count: windows.count,
1700
+ shape: shape,
1701
+ mapW: mapW,
1702
+ mapH: mapH,
1703
+ region: placement?.fractions
1704
+ )
1177
1705
  ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
1178
1706
  let win = idx < windows.count ? windows[idx] : nil
1179
1707
  RoundedRectangle(cornerRadius: 2)
@@ -1204,15 +1732,30 @@ struct CommandModeView: View {
1204
1732
  }
1205
1733
 
1206
1734
  /// Compute grid slots scaled to the mini map dimensions
1207
- private func computeMapSlots(count: Int, shape: [Int], mapW: CGFloat, mapH: CGFloat) -> [CGRect] {
1735
+ private func computeMapSlots(
1736
+ count: Int,
1737
+ shape: [Int],
1738
+ mapW: CGFloat,
1739
+ mapH: CGFloat,
1740
+ region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
1741
+ ) -> [CGRect] {
1742
+ let regionX = mapW * (region?.0 ?? 0)
1743
+ let regionY = mapH * (region?.1 ?? 0)
1744
+ let regionW = mapW * (region?.2 ?? 1)
1745
+ let regionH = mapH * (region?.3 ?? 1)
1208
1746
  let rowCount = shape.count
1209
- let rowH = mapH / CGFloat(rowCount)
1747
+ let rowH = regionH / CGFloat(rowCount)
1210
1748
  var slots: [CGRect] = []
1211
1749
  for (row, cols) in shape.enumerated() {
1212
- let colW = mapW / CGFloat(cols)
1213
- let y = CGFloat(row) * rowH
1750
+ let colW = regionW / CGFloat(cols)
1751
+ let y = regionY + CGFloat(row) * rowH
1214
1752
  for col in 0..<cols {
1215
- slots.append(CGRect(x: CGFloat(col) * colW, y: y, width: colW, height: rowH))
1753
+ slots.append(CGRect(
1754
+ x: regionX + CGFloat(col) * colW,
1755
+ y: y,
1756
+ width: colW,
1757
+ height: rowH
1758
+ ))
1216
1759
  }
1217
1760
  }
1218
1761
  return slots
@@ -1382,4 +1925,3 @@ struct CommandModeView: View {
1382
1925
  }
1383
1926
  }
1384
1927
  }
1385
-