@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
@@ -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,13 +121,79 @@ 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
@@ -165,7 +273,7 @@ struct CommandModeView: View {
165
273
 
166
274
  // MARK: - Desktop Inventory Content
167
275
 
168
- private var desktopInventoryContent: some View {
276
+ private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
169
277
  VStack(spacing: 0) {
170
278
  if state.isSearching {
171
279
  searchBar
@@ -177,20 +285,7 @@ struct CommandModeView: View {
177
285
  ZStack {
178
286
  Group {
179
287
  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
- }
288
+ inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
194
289
  } else {
195
290
  desktopEmptyState
196
291
  }
@@ -216,6 +311,53 @@ struct CommandModeView: View {
216
311
  }
217
312
  }
218
313
 
314
+ @ViewBuilder
315
+ private func inventoryColumns(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> some View {
316
+ if shouldShowEmbeddedSidebar(snapshot: snapshot, contentWidth: contentWidth),
317
+ let display = snapshot.displays.first {
318
+ embeddedSingleDisplayLayout(display: display, contentWidth: contentWidth)
319
+ } else {
320
+ ScrollView(.horizontal, showsIndicators: false) {
321
+ HStack(alignment: .top, spacing: 0) {
322
+ let total = snapshot.displays.count
323
+ ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
324
+ if idx > 0 {
325
+ Rectangle()
326
+ .fill(Palette.border)
327
+ .frame(width: 0.5)
328
+ }
329
+ displayColumn(display, index: idx, total: total)
330
+ .frame(width: displayColumnWidth(for: contentWidth))
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ private func shouldShowEmbeddedSidebar(snapshot: DesktopInventorySnapshot, contentWidth: CGFloat) -> Bool {
338
+ isEmbedded && snapshot.displays.count == 1 && contentWidth >= 900
339
+ }
340
+
341
+ private func embeddedSingleDisplayLayout(
342
+ display: DesktopInventorySnapshot.DisplayInfo,
343
+ contentWidth: CGFloat
344
+ ) -> some View {
345
+ let sidebarWidth = min(max(contentWidth * 0.28, 250), 330)
346
+ let mainWidth = max(contentWidth - sidebarWidth - 0.5, 620)
347
+
348
+ return HStack(alignment: .top, spacing: 0) {
349
+ displayColumn(display, index: 0, total: 1)
350
+ .frame(width: mainWidth)
351
+
352
+ Rectangle()
353
+ .fill(Palette.border)
354
+ .frame(width: 0.5)
355
+
356
+ embeddedInventorySidebar(display: display)
357
+ .frame(width: sidebarWidth)
358
+ }
359
+ }
360
+
219
361
  private var filterPillBar: some View {
220
362
  HStack(spacing: 6) {
221
363
  ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
@@ -378,6 +520,210 @@ struct CommandModeView: View {
378
520
  .padding(.vertical, 8)
379
521
  }
380
522
 
523
+ private func embeddedInventorySidebar(display: DesktopInventorySnapshot.DisplayInfo) -> some View {
524
+ ScrollView {
525
+ VStack(alignment: .leading, spacing: 12) {
526
+ sidebarCard(title: "Overview") {
527
+ sidebarMetric(label: "Display", value: display.name)
528
+ sidebarMetric(
529
+ label: "Visible",
530
+ value: "\(display.visibleFrame.w)×\(display.visibleFrame.h)"
531
+ )
532
+ sidebarMetric(
533
+ label: "Current Space",
534
+ value: "Space \(display.currentSpaceIndex)"
535
+ )
536
+ sidebarMetric(
537
+ label: "Windows",
538
+ value: "\(windowCount(in: display)) total"
539
+ )
540
+ sidebarMetric(
541
+ label: "Apps",
542
+ value: "\(uniqueAppCount(in: display)) active"
543
+ )
544
+ sidebarMetric(
545
+ label: "Lattices",
546
+ value: "\(latticesWindowCount(in: display)) tagged"
547
+ )
548
+ }
549
+
550
+ sidebarCard(title: "Spaces") {
551
+ VStack(alignment: .leading, spacing: 8) {
552
+ ForEach(display.spaces) { space in
553
+ HStack(alignment: .top, spacing: 8) {
554
+ VStack(alignment: .leading, spacing: 2) {
555
+ HStack(spacing: 5) {
556
+ Text("Space \(space.index)")
557
+ .font(Typo.monoBold(10))
558
+ .foregroundColor(space.isCurrent ? Palette.running : Palette.text)
559
+ if space.isCurrent {
560
+ Text("active")
561
+ .font(Typo.mono(8))
562
+ .foregroundColor(Palette.running.opacity(0.75))
563
+ }
564
+ }
565
+ Text("\(spaceWindowCount(space)) windows across \(space.apps.count) apps")
566
+ .font(Typo.mono(9))
567
+ .foregroundColor(Palette.textDim)
568
+ }
569
+ Spacer()
570
+ Text("\(spaceLatticesCount(space))")
571
+ .font(Typo.mono(9))
572
+ .foregroundColor(Palette.textMuted)
573
+ }
574
+ }
575
+ }
576
+ }
577
+
578
+ sidebarCard(title: state.selectedWindowIds.isEmpty ? "Top Apps" : "Selection") {
579
+ if state.selectedWindowIds.isEmpty {
580
+ VStack(alignment: .leading, spacing: 8) {
581
+ Text("Select a window to inspect it here.")
582
+ .font(Typo.mono(9))
583
+ .foregroundColor(Palette.textDim)
584
+
585
+ ForEach(topApps(in: display), id: \.name) { app in
586
+ HStack(spacing: 8) {
587
+ Text(app.name)
588
+ .font(Typo.monoBold(10))
589
+ .foregroundColor(Palette.text)
590
+ .lineLimit(1)
591
+ Spacer()
592
+ Text("\(app.count)")
593
+ .font(Typo.mono(9))
594
+ .foregroundColor(Palette.textMuted)
595
+ }
596
+ }
597
+ }
598
+ } else {
599
+ selectionSidebarContent
600
+ }
601
+ }
602
+
603
+ sidebarCard(title: "Keys") {
604
+ VStack(alignment: .leading, spacing: 7) {
605
+ sidebarShortcut("Arrows", "move through windows")
606
+ sidebarShortcut("/", "search by title or OCR")
607
+ sidebarShortcut("M", "jump to Screen Map")
608
+ sidebarShortcut("T", "tile selected window")
609
+ sidebarShortcut("Esc", "back or clear selection")
610
+ }
611
+ }
612
+ }
613
+ .padding(12)
614
+ }
615
+ }
616
+
617
+ @ViewBuilder
618
+ private var selectionSidebarContent: some View {
619
+ let selected = selectedWindows
620
+
621
+ if selected.count > 1 {
622
+ VStack(alignment: .leading, spacing: 8) {
623
+ Text("\(selected.count) windows selected")
624
+ .font(Typo.monoBold(10))
625
+ .foregroundColor(Palette.text)
626
+ if !state.selectedWindowSummaryText.isEmpty {
627
+ Text(state.selectedWindowSummaryText)
628
+ .font(Typo.mono(9))
629
+ .foregroundColor(Palette.textDim)
630
+ }
631
+ ForEach(Array(selected.prefix(5)), id: \.id) { window in
632
+ HStack(spacing: 8) {
633
+ Text(window.appName ?? "Unknown")
634
+ .font(Typo.monoBold(9))
635
+ .foregroundColor(window.isLattices ? Palette.running : Palette.text)
636
+ .lineLimit(1)
637
+ Spacer()
638
+ Text(sizeText(window.frame))
639
+ .font(Typo.mono(9))
640
+ .foregroundColor(Palette.textMuted)
641
+ }
642
+ }
643
+ }
644
+ } else if let window = selected.first {
645
+ VStack(alignment: .leading, spacing: 8) {
646
+ Text(window.appName ?? "Unknown")
647
+ .font(Typo.monoBold(10))
648
+ .foregroundColor(window.isLattices ? Palette.running : Palette.text)
649
+ Text(window.title.isEmpty ? "(untitled)" : window.title)
650
+ .font(Typo.mono(9))
651
+ .foregroundColor(Palette.textDim)
652
+ .fixedSize(horizontal: false, vertical: true)
653
+ sidebarMetric(label: "Size", value: sizeText(window.frame))
654
+ if let tile = window.tilePosition?.label {
655
+ sidebarMetric(label: "Tile", value: tile)
656
+ }
657
+ if let session = window.latticesSession {
658
+ sidebarMetric(label: "Session", value: session)
659
+ }
660
+ if let path = window.inventoryPath {
661
+ Text(path.description)
662
+ .font(Typo.mono(8))
663
+ .foregroundColor(Palette.textMuted)
664
+ .fixedSize(horizontal: false, vertical: true)
665
+ }
666
+ }
667
+ }
668
+ }
669
+
670
+ private func sidebarCard<Content: View>(
671
+ title: String,
672
+ @ViewBuilder content: () -> Content
673
+ ) -> some View {
674
+ VStack(alignment: .leading, spacing: 10) {
675
+ Text(title.uppercased())
676
+ .font(Typo.mono(9))
677
+ .foregroundColor(Palette.textMuted)
678
+ content()
679
+ }
680
+ .padding(12)
681
+ .frame(maxWidth: .infinity, alignment: .leading)
682
+ .background(
683
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
684
+ .fill(Palette.surface.opacity(0.55))
685
+ .overlay(
686
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
687
+ .strokeBorder(Palette.border, lineWidth: 0.5)
688
+ )
689
+ )
690
+ }
691
+
692
+ private func sidebarMetric(label: String, value: String) -> some View {
693
+ HStack(alignment: .firstTextBaseline, spacing: 10) {
694
+ Text(label.uppercased())
695
+ .font(Typo.mono(8))
696
+ .foregroundColor(Palette.textMuted)
697
+ .frame(width: 74, alignment: .leading)
698
+ Text(value)
699
+ .font(Typo.mono(9))
700
+ .foregroundColor(Palette.text)
701
+ .fixedSize(horizontal: false, vertical: true)
702
+ Spacer(minLength: 0)
703
+ }
704
+ }
705
+
706
+ private func sidebarShortcut(_ key: String, _ label: String) -> some View {
707
+ HStack(spacing: 8) {
708
+ Text(key)
709
+ .font(Typo.monoBold(9))
710
+ .foregroundColor(Palette.text)
711
+ .padding(.horizontal, 6)
712
+ .padding(.vertical, 3)
713
+ .background(
714
+ RoundedRectangle(cornerRadius: 4, style: .continuous)
715
+ .fill(Palette.bg.opacity(0.8))
716
+ .overlay(
717
+ RoundedRectangle(cornerRadius: 4, style: .continuous)
718
+ .strokeBorder(Palette.border, lineWidth: 0.5)
719
+ )
720
+ )
721
+ Text(label)
722
+ .font(Typo.mono(9))
723
+ .foregroundColor(Palette.textDim)
724
+ }
725
+ }
726
+
381
727
  private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
382
728
  HStack(spacing: 5) {
383
729
  Text("Space \(space.index)")
@@ -610,15 +956,7 @@ struct CommandModeView: View {
610
956
  Menu("Tile All (\(selCount))") {
611
957
  ForEach(TilePosition.allCases) { tile in
612
958
  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
- }
959
+ state.showAndDistributeSelected(in: .tile(tile))
622
960
  } label: {
623
961
  Label(tile.label, systemImage: tile.icon)
624
962
  }
@@ -683,9 +1021,6 @@ struct CommandModeView: View {
683
1021
  private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
684
1022
  let title = window.title
685
1023
  if title.isEmpty { return "(untitled)" }
686
- if title.count > 30 {
687
- return String(title.prefix(27)) + "..."
688
- }
689
1024
  return title
690
1025
  }
691
1026
 
@@ -693,6 +1028,57 @@ struct CommandModeView: View {
693
1028
  "\(Int(frame.w))×\(Int(frame.h))"
694
1029
  }
695
1030
 
1031
+ private var selectedWindows: [DesktopInventorySnapshot.InventoryWindowInfo] {
1032
+ state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
1033
+ }
1034
+
1035
+ private func windowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1036
+ display.spaces.reduce(0) { total, space in
1037
+ total + spaceWindowCount(space)
1038
+ }
1039
+ }
1040
+
1041
+ private func uniqueAppCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1042
+ Set(display.spaces.flatMap { $0.apps.map(\.appName) }).count
1043
+ }
1044
+
1045
+ private func latticesWindowCount(in display: DesktopInventorySnapshot.DisplayInfo) -> Int {
1046
+ display.spaces.reduce(0) { total, space in
1047
+ total + spaceLatticesCount(space)
1048
+ }
1049
+ }
1050
+
1051
+ private func spaceWindowCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
1052
+ space.apps.reduce(0) { total, app in
1053
+ total + app.windows.count
1054
+ }
1055
+ }
1056
+
1057
+ private func spaceLatticesCount(_ space: DesktopInventorySnapshot.SpaceGroup) -> Int {
1058
+ space.apps.reduce(0) { total, app in
1059
+ total + app.windows.filter(\.isLattices).count
1060
+ }
1061
+ }
1062
+
1063
+ private func topApps(
1064
+ in display: DesktopInventorySnapshot.DisplayInfo
1065
+ ) -> [(name: String, count: Int)] {
1066
+ var counts: [String: Int] = [:]
1067
+ for space in display.spaces {
1068
+ for app in space.apps {
1069
+ counts[app.appName, default: 0] += app.windows.count
1070
+ }
1071
+ }
1072
+ return counts
1073
+ .map { (name: $0.key, count: $0.value) }
1074
+ .sorted { lhs, rhs in
1075
+ if lhs.count == rhs.count { return lhs.name < rhs.name }
1076
+ return lhs.count > rhs.count
1077
+ }
1078
+ .prefix(5)
1079
+ .map { $0 }
1080
+ }
1081
+
696
1082
  /// Group items by their group label
697
1083
  private var groupedItems: [(String, [CommandModeInventory.Item])] {
698
1084
  var result: [(String, [CommandModeInventory.Item])] = []
@@ -835,10 +1221,16 @@ struct CommandModeView: View {
835
1221
  if isDesktopInventory && state.desktopMode == .gridPreview {
836
1222
  // Grid preview hints
837
1223
  HStack(spacing: 12) {
1224
+ chordHint(key: "←→↑↓", label: "region")
1225
+ chordHint(key: "1-7", label: "corners/thirds")
1226
+ chordHint(key: "c", label: "center")
838
1227
  chordHint(key: "↩", label: "apply layout")
839
1228
  chordHint(key: "s", label: "apply layout")
840
1229
  chordHint(key: "esc", label: "cancel")
841
1230
  Spacer()
1231
+ Text(state.gridPreviewRegionLabel.uppercased())
1232
+ .font(Typo.mono(9))
1233
+ .foregroundColor(Palette.textDim)
842
1234
  let shape = state.gridPreviewShape
843
1235
  Text(shape.map(String.init).joined(separator: " + "))
844
1236
  .font(Typo.monoBold(9))
@@ -890,13 +1282,19 @@ struct CommandModeView: View {
890
1282
  } else if isDesktopInventory && state.selectedWindowIds.count > 1 {
891
1283
  // Multi-selection active
892
1284
  HStack(spacing: 12) {
893
- chordHint(key: "s", label: "show")
1285
+ chordHint(key: "s", label: "grid preview")
894
1286
  chordHint(key: "↩", label: "front")
895
- chordHint(key: "t", label: "tile")
1287
+ chordHint(key: "t", label: "grid region")
896
1288
  chordHint(key: "f", label: "focus")
897
1289
  chordHint(key: "h", label: "highlight")
898
1290
  chordHint(key: "esc", label: "clear")
899
1291
  Spacer()
1292
+ if !state.selectedWindowSummaryText.isEmpty {
1293
+ Text(state.selectedWindowSummaryText)
1294
+ .font(Typo.mono(9))
1295
+ .foregroundColor(Palette.textDim)
1296
+ .lineLimit(1)
1297
+ }
900
1298
  Text("\(state.selectedWindowIds.count) selected")
901
1299
  .font(Typo.mono(9))
902
1300
  .foregroundColor(Palette.running)
@@ -1082,6 +1480,9 @@ struct CommandModeView: View {
1082
1480
  Text("LAYOUT PREVIEW")
1083
1481
  .font(Typo.monoBold(10))
1084
1482
  .foregroundColor(Palette.textDim)
1483
+ Text(state.gridPreviewRegionLabel.uppercased())
1484
+ .font(Typo.mono(9))
1485
+ .foregroundColor(Palette.textMuted)
1085
1486
  Text(gridDesc)
1086
1487
  .font(Typo.monoBold(10))
1087
1488
  .foregroundColor(Palette.running)
@@ -1096,7 +1497,7 @@ struct CommandModeView: View {
1096
1497
  divider
1097
1498
 
1098
1499
  // Screen map: current positions (dimmed) + target grid (bright)
1099
- screenMap(windows: windows, shape: shape)
1500
+ screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
1100
1501
  .frame(height: 160)
1101
1502
  .padding(.horizontal, 12)
1102
1503
  .padding(.vertical, 8)
@@ -1125,7 +1526,11 @@ struct CommandModeView: View {
1125
1526
  // MARK: - Grid Preview Screen Map
1126
1527
 
1127
1528
  /// 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 {
1529
+ private func screenMap(
1530
+ windows: [DesktopInventorySnapshot.InventoryWindowInfo],
1531
+ shape: [Int],
1532
+ placement: PlacementSpec?
1533
+ ) -> some View {
1129
1534
  GeometryReader { geo in
1130
1535
  let availW = geo.size.width
1131
1536
  let availH = geo.size.height
@@ -1154,6 +1559,14 @@ struct CommandModeView: View {
1154
1559
  )
1155
1560
  .frame(width: mapW, height: mapH)
1156
1561
 
1562
+ if let placement {
1563
+ let region = placement.fractions
1564
+ RoundedRectangle(cornerRadius: 4)
1565
+ .strokeBorder(Palette.running.opacity(0.35), style: StrokeStyle(lineWidth: 1, dash: [6, 4]))
1566
+ .frame(width: mapW * region.2, height: mapH * region.3)
1567
+ .offset(x: mapW * region.0, y: mapH * region.1)
1568
+ }
1569
+
1157
1570
  // Current positions (dimmed)
1158
1571
  ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
1159
1572
  let f = win.frame
@@ -1173,7 +1586,13 @@ struct CommandModeView: View {
1173
1586
  }
1174
1587
 
1175
1588
  // Target grid slots (bright)
1176
- let slots = computeMapSlots(count: windows.count, shape: shape, mapW: mapW, mapH: mapH)
1589
+ let slots = computeMapSlots(
1590
+ count: windows.count,
1591
+ shape: shape,
1592
+ mapW: mapW,
1593
+ mapH: mapH,
1594
+ region: placement?.fractions
1595
+ )
1177
1596
  ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
1178
1597
  let win = idx < windows.count ? windows[idx] : nil
1179
1598
  RoundedRectangle(cornerRadius: 2)
@@ -1204,15 +1623,30 @@ struct CommandModeView: View {
1204
1623
  }
1205
1624
 
1206
1625
  /// Compute grid slots scaled to the mini map dimensions
1207
- private func computeMapSlots(count: Int, shape: [Int], mapW: CGFloat, mapH: CGFloat) -> [CGRect] {
1626
+ private func computeMapSlots(
1627
+ count: Int,
1628
+ shape: [Int],
1629
+ mapW: CGFloat,
1630
+ mapH: CGFloat,
1631
+ region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
1632
+ ) -> [CGRect] {
1633
+ let regionX = mapW * (region?.0 ?? 0)
1634
+ let regionY = mapH * (region?.1 ?? 0)
1635
+ let regionW = mapW * (region?.2 ?? 1)
1636
+ let regionH = mapH * (region?.3 ?? 1)
1208
1637
  let rowCount = shape.count
1209
- let rowH = mapH / CGFloat(rowCount)
1638
+ let rowH = regionH / CGFloat(rowCount)
1210
1639
  var slots: [CGRect] = []
1211
1640
  for (row, cols) in shape.enumerated() {
1212
- let colW = mapW / CGFloat(cols)
1213
- let y = CGFloat(row) * rowH
1641
+ let colW = regionW / CGFloat(cols)
1642
+ let y = regionY + CGFloat(row) * rowH
1214
1643
  for col in 0..<cols {
1215
- slots.append(CGRect(x: CGFloat(col) * colW, y: y, width: colW, height: rowH))
1644
+ slots.append(CGRect(
1645
+ x: regionX + CGFloat(col) * colW,
1646
+ y: y,
1647
+ width: colW,
1648
+ height: rowH
1649
+ ))
1216
1650
  }
1217
1651
  }
1218
1652
  return slots
@@ -1382,4 +1816,3 @@ struct CommandModeView: View {
1382
1816
  }
1383
1817
  }
1384
1818
  }
1385
-