@lattices/cli 0.4.2 → 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 (146) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/AppShell/App.swift +20 -0
  7. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
  8. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
  9. package/app/Sources/AppShell/AppUpdater.swift +92 -0
  10. package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
  11. package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
  12. package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
  13. package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
  14. package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
  15. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
  16. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
  17. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
  18. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
  19. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  20. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  21. package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
  22. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  23. package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
  24. package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
  25. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
  26. package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
  27. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
  28. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
  29. package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
  30. package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
  31. package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
  32. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
  33. package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
  34. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  35. package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
  36. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  37. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  38. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  39. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
  40. package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
  41. package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
  42. package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
  43. package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
  44. package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
  45. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  46. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
  47. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  48. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  49. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  50. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  51. package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
  52. package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
  53. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  54. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
  55. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
  56. package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
  57. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
  58. package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
  59. package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
  60. package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
  61. package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
  62. package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
  63. package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
  64. package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
  65. package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
  66. package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
  67. package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
  68. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
  69. package/bin/assistant-intelligence.ts +874 -0
  70. package/bin/handsoff-infer.ts +16 -209
  71. package/bin/handsoff-worker.ts +45 -258
  72. package/bin/lattices-app.ts +62 -0
  73. package/bin/lattices-dev +4 -0
  74. package/bin/lattices.ts +125 -14
  75. package/docs/agents.md +14 -0
  76. package/docs/api.md +55 -0
  77. package/docs/app.md +3 -0
  78. package/docs/companion-deck.md +180 -0
  79. package/docs/component-extraction-roadmap.md +392 -0
  80. package/docs/config.md +25 -0
  81. package/docs/tiling-reference.md +55 -0
  82. package/docs/voice-error-model.md +73 -0
  83. package/package.json +4 -1
  84. package/app/Sources/App.swift +0 -10
  85. package/app/Sources/CommandPaletteWindow.swift +0 -134
  86. package/app/Sources/MouseFinder.swift +0 -222
  87. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  88. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  89. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  90. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  91. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  92. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  93. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  94. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  95. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  96. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  97. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  98. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  99. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  100. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  101. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  102. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  103. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  104. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  105. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  106. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  107. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  108. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  109. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  110. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  111. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  112. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  113. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  114. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  115. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  116. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  117. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  118. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  119. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  120. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  121. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  122. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  123. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  124. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  125. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  126. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  127. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  128. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  129. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  130. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  131. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  132. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  133. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  134. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  135. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  136. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  137. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  138. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  139. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  140. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  141. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  142. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  143. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  144. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  145. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  146. /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
@@ -121,23 +229,6 @@ struct CommandModeView: View {
121
229
 
122
230
  Spacer()
123
231
 
124
- if let layer = state.inventory.activeLayer {
125
- HStack(spacing: 4) {
126
- Text("Layer: \(layer)")
127
- .font(Typo.mono(10))
128
- .foregroundColor(Palette.running)
129
-
130
- Text("[\(state.inventory.layerCount > 0 ? "\(WorkspaceManager.shared.activeLayerIndex + 1)/\(state.inventory.layerCount)" : "—")]")
131
- .font(Typo.mono(10))
132
- .foregroundColor(Palette.textMuted)
133
- }
134
- .padding(.horizontal, 6)
135
- .padding(.vertical, 2)
136
- .background(
137
- RoundedRectangle(cornerRadius: 3)
138
- .fill(Palette.running.opacity(0.10))
139
- )
140
- }
141
232
  }
142
233
  .padding(.horizontal, 16)
143
234
  .padding(.vertical, 10)
@@ -155,15 +246,12 @@ struct CommandModeView: View {
155
246
  private var inventoryGrid: some View {
156
247
  ScrollView {
157
248
  LazyVStack(alignment: .leading, spacing: 0) {
158
- let grouped = groupedItems
159
- if grouped.isEmpty {
249
+ let items = state.inventory.items
250
+ if items.isEmpty {
160
251
  emptyState
161
252
  } else {
162
- ForEach(grouped, id: \.0) { section, items in
163
- sectionHeader(section)
164
- ForEach(Array(items.enumerated()), id: \.offset) { _, item in
165
- inventoryRow(item)
166
- }
253
+ ForEach(Array(items.enumerated()), id: \.offset) { _, item in
254
+ inventoryRow(item)
167
255
  }
168
256
  }
169
257
  }
@@ -185,7 +273,7 @@ struct CommandModeView: View {
185
273
 
186
274
  // MARK: - Desktop Inventory Content
187
275
 
188
- private var desktopInventoryContent: some View {
276
+ private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
189
277
  VStack(spacing: 0) {
190
278
  if state.isSearching {
191
279
  searchBar
@@ -197,20 +285,7 @@ struct CommandModeView: View {
197
285
  ZStack {
198
286
  Group {
199
287
  if let snapshot = state.filteredSnapshot, !snapshot.displays.isEmpty {
200
- ScrollView(.horizontal, showsIndicators: false) {
201
- HStack(alignment: .top, spacing: 0) {
202
- let total = snapshot.displays.count
203
- ForEach(Array(snapshot.displays.enumerated()), id: \.element.id) { idx, display in
204
- if idx > 0 {
205
- Rectangle()
206
- .fill(Palette.border)
207
- .frame(width: 0.5)
208
- }
209
- displayColumn(display, index: idx, total: total)
210
- .frame(width: displayColumnWidth)
211
- }
212
- }
213
- }
288
+ inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
214
289
  } else {
215
290
  desktopEmptyState
216
291
  }
@@ -236,6 +311,53 @@ struct CommandModeView: View {
236
311
  }
237
312
  }
238
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
+
239
361
  private var filterPillBar: some View {
240
362
  HStack(spacing: 6) {
241
363
  ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
@@ -398,6 +520,210 @@ struct CommandModeView: View {
398
520
  .padding(.vertical, 8)
399
521
  }
400
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
+
401
727
  private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
402
728
  HStack(spacing: 5) {
403
729
  Text("Space \(space.index)")
@@ -630,15 +956,7 @@ struct CommandModeView: View {
630
956
  Menu("Tile All (\(selCount))") {
631
957
  ForEach(TilePosition.allCases) { tile in
632
958
  Button {
633
- let windows = state.flatWindowList.filter { state.selectedWindowIds.contains($0.id) }
634
- for (i, win) in windows.enumerated() {
635
- DispatchQueue.main.asyncAfter(deadline: .now() + Double(i) * 0.1) {
636
- WindowTiler.tileWindowById(wid: win.id, pid: win.pid, to: tile)
637
- }
638
- }
639
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3 + Double(windows.count) * 0.1) {
640
- state.desktopSnapshot = nil
641
- }
959
+ state.showAndDistributeSelected(in: .tile(tile))
642
960
  } label: {
643
961
  Label(tile.label, systemImage: tile.icon)
644
962
  }
@@ -703,9 +1021,6 @@ struct CommandModeView: View {
703
1021
  private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
704
1022
  let title = window.title
705
1023
  if title.isEmpty { return "(untitled)" }
706
- if title.count > 30 {
707
- return String(title.prefix(27)) + "..."
708
- }
709
1024
  return title
710
1025
  }
711
1026
 
@@ -713,6 +1028,57 @@ struct CommandModeView: View {
713
1028
  "\(Int(frame.w))×\(Int(frame.h))"
714
1029
  }
715
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
+
716
1082
  /// Group items by their group label
717
1083
  private var groupedItems: [(String, [CommandModeInventory.Item])] {
718
1084
  var result: [(String, [CommandModeInventory.Item])] = []
@@ -855,10 +1221,16 @@ struct CommandModeView: View {
855
1221
  if isDesktopInventory && state.desktopMode == .gridPreview {
856
1222
  // Grid preview hints
857
1223
  HStack(spacing: 12) {
1224
+ chordHint(key: "←→↑↓", label: "region")
1225
+ chordHint(key: "1-7", label: "corners/thirds")
1226
+ chordHint(key: "c", label: "center")
858
1227
  chordHint(key: "↩", label: "apply layout")
859
1228
  chordHint(key: "s", label: "apply layout")
860
1229
  chordHint(key: "esc", label: "cancel")
861
1230
  Spacer()
1231
+ Text(state.gridPreviewRegionLabel.uppercased())
1232
+ .font(Typo.mono(9))
1233
+ .foregroundColor(Palette.textDim)
862
1234
  let shape = state.gridPreviewShape
863
1235
  Text(shape.map(String.init).joined(separator: " + "))
864
1236
  .font(Typo.monoBold(9))
@@ -910,13 +1282,19 @@ struct CommandModeView: View {
910
1282
  } else if isDesktopInventory && state.selectedWindowIds.count > 1 {
911
1283
  // Multi-selection active
912
1284
  HStack(spacing: 12) {
913
- chordHint(key: "s", label: "show")
1285
+ chordHint(key: "s", label: "grid preview")
914
1286
  chordHint(key: "↩", label: "front")
915
- chordHint(key: "t", label: "tile")
1287
+ chordHint(key: "t", label: "grid region")
916
1288
  chordHint(key: "f", label: "focus")
917
1289
  chordHint(key: "h", label: "highlight")
918
1290
  chordHint(key: "esc", label: "clear")
919
1291
  Spacer()
1292
+ if !state.selectedWindowSummaryText.isEmpty {
1293
+ Text(state.selectedWindowSummaryText)
1294
+ .font(Typo.mono(9))
1295
+ .foregroundColor(Palette.textDim)
1296
+ .lineLimit(1)
1297
+ }
920
1298
  Text("\(state.selectedWindowIds.count) selected")
921
1299
  .font(Typo.mono(9))
922
1300
  .foregroundColor(Palette.running)
@@ -1102,6 +1480,9 @@ struct CommandModeView: View {
1102
1480
  Text("LAYOUT PREVIEW")
1103
1481
  .font(Typo.monoBold(10))
1104
1482
  .foregroundColor(Palette.textDim)
1483
+ Text(state.gridPreviewRegionLabel.uppercased())
1484
+ .font(Typo.mono(9))
1485
+ .foregroundColor(Palette.textMuted)
1105
1486
  Text(gridDesc)
1106
1487
  .font(Typo.monoBold(10))
1107
1488
  .foregroundColor(Palette.running)
@@ -1116,7 +1497,7 @@ struct CommandModeView: View {
1116
1497
  divider
1117
1498
 
1118
1499
  // Screen map: current positions (dimmed) + target grid (bright)
1119
- screenMap(windows: windows, shape: shape)
1500
+ screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
1120
1501
  .frame(height: 160)
1121
1502
  .padding(.horizontal, 12)
1122
1503
  .padding(.vertical, 8)
@@ -1145,7 +1526,11 @@ struct CommandModeView: View {
1145
1526
  // MARK: - Grid Preview Screen Map
1146
1527
 
1147
1528
  /// Miniature proportional map of the screen showing current window positions and target grid slots
1148
- 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 {
1149
1534
  GeometryReader { geo in
1150
1535
  let availW = geo.size.width
1151
1536
  let availH = geo.size.height
@@ -1174,6 +1559,14 @@ struct CommandModeView: View {
1174
1559
  )
1175
1560
  .frame(width: mapW, height: mapH)
1176
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
+
1177
1570
  // Current positions (dimmed)
1178
1571
  ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
1179
1572
  let f = win.frame
@@ -1193,7 +1586,13 @@ struct CommandModeView: View {
1193
1586
  }
1194
1587
 
1195
1588
  // Target grid slots (bright)
1196
- 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
+ )
1197
1596
  ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
1198
1597
  let win = idx < windows.count ? windows[idx] : nil
1199
1598
  RoundedRectangle(cornerRadius: 2)
@@ -1224,15 +1623,30 @@ struct CommandModeView: View {
1224
1623
  }
1225
1624
 
1226
1625
  /// Compute grid slots scaled to the mini map dimensions
1227
- 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)
1228
1637
  let rowCount = shape.count
1229
- let rowH = mapH / CGFloat(rowCount)
1638
+ let rowH = regionH / CGFloat(rowCount)
1230
1639
  var slots: [CGRect] = []
1231
1640
  for (row, cols) in shape.enumerated() {
1232
- let colW = mapW / CGFloat(cols)
1233
- let y = CGFloat(row) * rowH
1641
+ let colW = regionW / CGFloat(cols)
1642
+ let y = regionY + CGFloat(row) * rowH
1234
1643
  for col in 0..<cols {
1235
- 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
+ ))
1236
1650
  }
1237
1651
  }
1238
1652
  return slots
@@ -1402,4 +1816,3 @@ struct CommandModeView: View {
1402
1816
  }
1403
1817
  }
1404
1818
  }
1405
-