@lattices/cli 0.4.13 → 0.5.0

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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +191 -63
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2271
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,1927 +0,0 @@
1
- import SwiftUI
2
- import AppKit
3
-
4
- // MARK: - Row Frame PreferenceKey
5
-
6
- struct WindowRowFrameKey: PreferenceKey {
7
- static var defaultValue: [UInt32: CGRect] = [:]
8
- static func reduce(value: inout [UInt32: CGRect], nextValue: () -> [UInt32: CGRect]) {
9
- value.merge(nextValue(), uniquingKeysWith: { _, new in new })
10
- }
11
- }
12
-
13
- // MARK: - Focus Ring Suppressor
14
-
15
- private struct FocusRingSuppressor: ViewModifier {
16
- func body(content: Content) -> some View {
17
- if #available(macOS 14, *) {
18
- content.focusEffectDisabled()
19
- } else {
20
- content
21
- }
22
- }
23
- }
24
-
25
- enum CommandModePresentation {
26
- case panel
27
- case embedded
28
- }
29
-
30
- struct CommandModeView: View {
31
- @ObservedObject var state: CommandModeState
32
- var presentation: CommandModePresentation = .panel
33
- @State private var eventMonitor: Any?
34
- @State private var mouseDownMonitor: Any?
35
- @State private var mouseDragMonitor: Any?
36
- @State private var mouseUpMonitor: Any?
37
- @State private var panelOriginY: CGFloat = 0
38
- @State private var hoveredWindowId: UInt32?
39
- @FocusState private var isSearchFieldFocused: Bool
40
-
41
- private var isDesktopInventory: Bool {
42
- state.phase == .desktopInventory
43
- }
44
-
45
- private var isEmbedded: Bool {
46
- presentation == .embedded
47
- }
48
-
49
- // Column widths for inventory table
50
- private static let sizeColW: CGFloat = 80
51
- private static let tileColW: CGFloat = 60
52
-
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())
75
- }
76
-
77
- private func resolvedContentWidth(in availableWidth: CGFloat) -> CGFloat {
78
- if isDesktopInventory {
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
86
- return min(ideal, screenWidth * 0.92)
87
- }
88
-
89
- if isEmbedded {
90
- return min(720, max(availableWidth - 32, 580))
91
- }
92
- return 580
93
- }
94
-
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 {
102
- VStack(spacing: 0) {
103
- header
104
- divider
105
- if isDesktopInventory && state.desktopMode == .gridPreview {
106
- gridPreviewContent
107
- } else if isDesktopInventory {
108
- desktopInventoryContent(contentWidth: contentWidth)
109
- } else {
110
- inventoryGrid
111
- }
112
- divider
113
- chordFooter
114
- }
115
- .frame(width: contentWidth)
116
- .background(Palette.bg)
117
- .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
118
- .overlay(
119
- RoundedRectangle(cornerRadius: 14, style: .continuous)
120
- .strokeBorder(Palette.borderLit, lineWidth: 0.5)
121
- )
122
- .overlay(executingOverlay)
123
- .overlay(flashOverlay)
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)
132
- }
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
- )
197
- }
198
-
199
- // MARK: - Header
200
-
201
- private var header: some View {
202
- HStack {
203
- Text(isDesktopInventory ? (state.isOrganizeFlow ? "ORGANIZE WINDOWS" : "DESKTOP INVENTORY") : "COMMAND MODE")
204
- .font(Typo.monoBold(11))
205
- .foregroundColor(Palette.text)
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
-
214
- if isDesktopInventory {
215
- Button(action: { state.copyInventoryToClipboard() }) {
216
- HStack(spacing: 3) {
217
- Image(systemName: "doc.on.doc")
218
- .font(.system(size: 9))
219
- Text("Copy")
220
- .font(Typo.mono(9))
221
- }
222
- .foregroundColor(Palette.textDim)
223
- .padding(.horizontal, 6)
224
- .padding(.vertical, 3)
225
- .background(
226
- RoundedRectangle(cornerRadius: 3)
227
- .fill(Palette.surface)
228
- .overlay(
229
- RoundedRectangle(cornerRadius: 3)
230
- .strokeBorder(Palette.border, lineWidth: 0.5)
231
- )
232
- )
233
- }
234
- .buttonStyle(.plain)
235
- }
236
-
237
- Spacer()
238
-
239
- }
240
- .padding(.horizontal, 16)
241
- .padding(.vertical, 10)
242
- .contentShape(Rectangle())
243
- .gesture(
244
- DragGesture()
245
- .onChanged { _ in
246
- CommandModeWindow.shared.panelWindow?.performDrag(with: NSApp.currentEvent!)
247
- }
248
- )
249
- }
250
-
251
- // MARK: - Inventory Grid
252
-
253
- private var inventoryGrid: some View {
254
- ScrollView {
255
- LazyVStack(alignment: .leading, spacing: 0) {
256
- let items = state.inventory.items
257
- if items.isEmpty {
258
- emptyState
259
- } else {
260
- ForEach(Array(items.enumerated()), id: \.offset) { _, item in
261
- inventoryRow(item)
262
- }
263
- }
264
- }
265
- .padding(.vertical, 6)
266
- }
267
- .frame(minHeight: 160, maxHeight: 240)
268
- }
269
-
270
- private var emptyState: some View {
271
- HStack {
272
- Spacer()
273
- Text("No sessions found")
274
- .font(Typo.mono(11))
275
- .foregroundColor(Palette.textMuted)
276
- Spacer()
277
- }
278
- .padding(.vertical, 24)
279
- }
280
-
281
- // MARK: - Desktop Inventory Content
282
-
283
- private func desktopInventoryContent(contentWidth: CGFloat) -> some View {
284
- VStack(spacing: 0) {
285
- if state.isOrganizeFlow {
286
- organizeBanner
287
- divider
288
- }
289
-
290
- if state.isSearching {
291
- searchBar
292
- } else {
293
- filterPillBar
294
- }
295
- divider
296
-
297
- ZStack {
298
- Group {
299
- if let snapshot = state.filteredSnapshot, !snapshot.displays.isEmpty {
300
- inventoryColumns(snapshot: snapshot, contentWidth: contentWidth)
301
- } else {
302
- desktopEmptyState
303
- }
304
- }
305
-
306
- marqueeOverlay
307
- }
308
- .coordinateSpace(name: "inventoryPanel")
309
- .background(
310
- GeometryReader { geo in
311
- Color.clear.onAppear {
312
- panelOriginY = geo.frame(in: .global).origin.y
313
- }
314
- .onChange(of: geo.frame(in: .global).origin.y) { newY in
315
- panelOriginY = newY
316
- }
317
- }
318
- )
319
- .onPreferenceChange(WindowRowFrameKey.self) { frames in
320
- state.rowFrames = frames
321
- }
322
- .frame(maxHeight: .infinity)
323
- }
324
- }
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
-
400
- private var filterPillBar: some View {
401
- HStack(spacing: 6) {
402
- ForEach(FilterPreset.allCases, id: \.rawValue) { preset in
403
- let isActive = state.activePreset == preset
404
- Button {
405
- if isActive {
406
- state.activePreset = nil
407
- } else {
408
- state.activePreset = preset
409
- state.clearSelection()
410
- }
411
- } label: {
412
- HStack(spacing: 3) {
413
- Text(preset.rawValue)
414
- .font(Typo.mono(9))
415
- if let idx = preset.keyIndex {
416
- Text("\(idx)")
417
- .font(Typo.mono(8))
418
- .foregroundColor(isActive ? Palette.text.opacity(0.7) : Palette.textMuted)
419
- }
420
- }
421
- .foregroundColor(isActive ? Palette.text : Palette.textDim)
422
- .padding(.horizontal, 8)
423
- .padding(.vertical, 4)
424
- .background(
425
- RoundedRectangle(cornerRadius: 10)
426
- .fill(isActive ? Palette.running.opacity(0.2) : Palette.surface)
427
- )
428
- .overlay(
429
- RoundedRectangle(cornerRadius: 10)
430
- .strokeBorder(isActive ? Palette.running.opacity(0.4) : Palette.border, lineWidth: 0.5)
431
- )
432
- }
433
- .buttonStyle(.plain)
434
- }
435
- Spacer()
436
- }
437
- .padding(.horizontal, 14)
438
- .padding(.vertical, 6)
439
- }
440
-
441
- private var searchBar: some View {
442
- HStack(spacing: 10) {
443
- Image(systemName: "magnifyingglass")
444
- .font(.system(size: 11))
445
- .foregroundColor(Palette.textDim)
446
- TextField("Search windows & content…", text: $state.searchQuery)
447
- .textFieldStyle(.plain)
448
- .font(Typo.mono(12))
449
- .foregroundColor(Palette.text)
450
- .focused($isSearchFieldFocused)
451
- if !state.searchQuery.isEmpty {
452
- let total = state.flatWindowList.count
453
- let ocrCount = state.ocrMatchSnippets.count
454
- Text(ocrCount > 0 ? "\(total) matches (\(ocrCount) by content)" : "\(total) matches")
455
- .font(Typo.mono(9))
456
- .foregroundColor(Palette.textMuted)
457
- }
458
- Button(action: { state.deactivateSearch() }) {
459
- Image(systemName: "xmark.circle.fill")
460
- .font(.system(size: 11))
461
- .foregroundColor(Palette.textDim)
462
- }
463
- .buttonStyle(.plain)
464
- }
465
- .padding(.horizontal, 14)
466
- .padding(.vertical, 8)
467
- .onAppear {
468
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
469
- isSearchFieldFocused = true
470
- }
471
- }
472
- }
473
-
474
- private func displayColumn(_ display: DesktopInventorySnapshot.DisplayInfo, index: Int, total: Int) -> some View {
475
- VStack(alignment: .leading, spacing: 0) {
476
- displayHeader(display, index: index, total: total)
477
- divider
478
-
479
- ScrollViewReader { proxy in
480
- ScrollView {
481
- LazyVStack(alignment: .leading, spacing: 0) {
482
- ForEach(display.spaces) { space in
483
- spaceHeader(space, display: display)
484
- columnHeaders
485
- ForEach(space.apps) { appGroup in
486
- appGroupRows(appGroup, dimmed: !space.isCurrent)
487
- }
488
- }
489
- }
490
- .padding(.vertical, 4)
491
- }
492
- .onChange(of: state.selectedWindowIds) { newIds in
493
- // Only scroll if the selected window is in this display
494
- guard let id = newIds.first else { return }
495
- let displayWindows = display.spaces.flatMap { $0.apps.flatMap { $0.windows } }
496
- if displayWindows.contains(where: { $0.id == id }) {
497
- withAnimation(.easeInOut(duration: 0.15)) {
498
- proxy.scrollTo(id, anchor: .center)
499
- }
500
- }
501
- }
502
- }
503
- }
504
- }
505
-
506
- private var desktopEmptyState: some View {
507
- HStack {
508
- Spacer()
509
- if state.isSearching && !state.searchQuery.isEmpty {
510
- Text("No matches for \"\(state.searchQuery)\"")
511
- .font(Typo.mono(11))
512
- .foregroundColor(Palette.textMuted)
513
- } else {
514
- Text("No windows found")
515
- .font(Typo.mono(11))
516
- .foregroundColor(Palette.textMuted)
517
- }
518
- Spacer()
519
- }
520
- .padding(.vertical, 24)
521
- }
522
-
523
- private func positionLabel(index: Int, total: Int) -> String {
524
- if total == 2 { return index == 0 ? "Left" : "Right" }
525
- if total == 3 { return ["Left", "Center", "Right"][index] }
526
- return "\(index + 1) of \(total)"
527
- }
528
-
529
- private func displayHeader(_ display: DesktopInventorySnapshot.DisplayInfo, index: Int, total: Int) -> some View {
530
- HStack(spacing: 6) {
531
- Text(display.name)
532
- .font(Typo.monoBold(11))
533
- .foregroundColor(Palette.text)
534
- if display.isMain {
535
- Text("main")
536
- .font(Typo.mono(8))
537
- .foregroundColor(Palette.running.opacity(0.7))
538
- .padding(.horizontal, 4)
539
- .padding(.vertical, 1)
540
- .background(
541
- RoundedRectangle(cornerRadius: 2)
542
- .fill(Palette.running.opacity(0.10))
543
- )
544
- }
545
- if total > 1 {
546
- Text(positionLabel(index: index, total: total))
547
- .font(Typo.mono(9))
548
- .foregroundColor(Palette.textDim)
549
- }
550
- Text("\(display.visibleFrame.w)×\(display.visibleFrame.h)")
551
- .font(Typo.mono(9))
552
- .foregroundColor(Palette.textDim)
553
- Spacer()
554
- Text("\(display.spaceCount) space\(display.spaceCount == 1 ? "" : "s")")
555
- .font(Typo.mono(9))
556
- .foregroundColor(Palette.textMuted)
557
- }
558
- .padding(.horizontal, 14)
559
- .padding(.vertical, 8)
560
- }
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
-
766
- private func spaceHeader(_ space: DesktopInventorySnapshot.SpaceGroup, display: DesktopInventorySnapshot.DisplayInfo) -> some View {
767
- HStack(spacing: 5) {
768
- Text("Space \(space.index)")
769
- .font(Typo.monoBold(10))
770
- .foregroundColor(space.isCurrent ? Palette.running : Palette.textDim)
771
- if space.isCurrent {
772
- Text("active")
773
- .font(Typo.mono(8))
774
- .foregroundColor(Palette.running.opacity(0.7))
775
- .padding(.horizontal, 4)
776
- .padding(.vertical, 1)
777
- .background(
778
- RoundedRectangle(cornerRadius: 2)
779
- .fill(Palette.running.opacity(0.10))
780
- )
781
- }
782
- Spacer()
783
- let windowCount = space.apps.reduce(0) { $0 + $1.windows.count }
784
- Text("\(windowCount)")
785
- .font(Typo.mono(9))
786
- .foregroundColor(Palette.textMuted)
787
- }
788
- .padding(.horizontal, 14)
789
- .padding(.top, 6)
790
- .padding(.bottom, 2)
791
- }
792
-
793
- private var columnHeaders: some View {
794
- HStack(spacing: 0) {
795
- Text("APP / WINDOW")
796
- .frame(maxWidth: .infinity, alignment: .leading)
797
- Text("SIZE")
798
- .frame(width: Self.sizeColW, alignment: .leading)
799
- Text("TILE")
800
- .frame(width: Self.tileColW, alignment: .trailing)
801
- }
802
- .font(Typo.mono(9))
803
- .foregroundColor(Palette.textMuted)
804
- .padding(.horizontal, 14)
805
- .padding(.vertical, 3)
806
- }
807
-
808
- private func appGroupRows(_ appGroup: DesktopInventorySnapshot.AppGroup, dimmed: Bool = false) -> some View {
809
- VStack(alignment: .leading, spacing: 0) {
810
- if appGroup.windows.count == 1, let win = appGroup.windows.first {
811
- inventoryRow(window: win, appLabel: appGroup.appName)
812
- ocrSnippetRow(for: win.id)
813
- if state.isSelected(win.id), let path = win.inventoryPath {
814
- inventoryPathLabel(path)
815
- }
816
- } else {
817
- Text(appGroup.appName)
818
- .font(Typo.monoBold(10))
819
- .foregroundColor(dimmed ? Palette.textDim : Palette.text)
820
- .padding(.horizontal, 14)
821
- .padding(.top, 4)
822
- .padding(.bottom, 1)
823
- ForEach(appGroup.windows) { win in
824
- inventoryRow(window: win, indented: true)
825
- ocrSnippetRow(for: win.id)
826
- if state.isSelected(win.id), let path = win.inventoryPath {
827
- inventoryPathLabel(path)
828
- }
829
- }
830
- }
831
- }
832
- .opacity(dimmed ? 0.6 : 1.0)
833
- }
834
-
835
- private func inventoryPathLabel(_ path: InventoryPath) -> some View {
836
- Text(path.description)
837
- .font(Typo.mono(8))
838
- .foregroundColor(Palette.textMuted)
839
- .padding(.horizontal, 28)
840
- .padding(.vertical, 2)
841
- }
842
-
843
- @ViewBuilder
844
- private func ocrSnippetRow(for windowId: UInt32) -> some View {
845
- if let snippet = state.ocrMatchSnippets[windowId] {
846
- HStack(spacing: 4) {
847
- Image(systemName: "text.magnifyingglass")
848
- .font(.system(size: 7))
849
- .foregroundColor(Palette.textMuted)
850
- Text(snippet)
851
- .font(Typo.mono(9).italic())
852
- .foregroundColor(Palette.textMuted)
853
- .lineLimit(1)
854
- .truncationMode(.tail)
855
- }
856
- .padding(.horizontal, 28)
857
- .padding(.vertical, 1)
858
- }
859
- }
860
-
861
- /// Unified inventory row — handles both single-app rows (with appLabel) and
862
- /// sub-rows under a multi-window app header (with indented).
863
- private func inventoryRow(
864
- window: DesktopInventorySnapshot.InventoryWindowInfo,
865
- appLabel: String? = nil,
866
- indented: Bool = false
867
- ) -> some View {
868
- let isSelected = state.isSelected(window.id)
869
- let isHovered = hoveredWindowId == window.id
870
- let isLattices = window.isLattices
871
-
872
- return HStack(spacing: 0) {
873
- HStack(spacing: 4) {
874
- if indented {
875
- Spacer().frame(width: 8)
876
- }
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
- }
888
- if let app = appLabel {
889
- Text(app)
890
- .font(Typo.monoBold(10))
891
- .foregroundColor(isLattices ? Palette.running : Palette.text)
892
- }
893
- Text(windowTitle(window))
894
- .font(Typo.mono(10))
895
- .foregroundColor(
896
- isLattices
897
- ? Palette.running.opacity(appLabel != nil && !isSelected ? 0.7 : 1.0)
898
- : (isSelected ? Palette.text : Palette.textDim)
899
- )
900
- .lineLimit(1)
901
- if isLattices, let session = window.latticesSession, appLabel == nil {
902
- Text("[\(session)]")
903
- .font(Typo.mono(9))
904
- .foregroundColor(Palette.running.opacity(isSelected ? 1.0 : 0.6))
905
- }
906
- }
907
- .frame(maxWidth: .infinity, alignment: .leading)
908
-
909
- Text(sizeText(window.frame))
910
- .font(Typo.mono(10))
911
- .foregroundColor(isSelected ? Palette.text : Palette.textDim)
912
- .frame(width: Self.sizeColW, alignment: .leading)
913
-
914
- Text(window.tilePosition?.label ?? "\u{2014}")
915
- .font(Typo.mono(10))
916
- .foregroundColor(window.tilePosition != nil ? (isSelected ? Palette.text : Palette.textDim) : Palette.textMuted)
917
- .frame(width: Self.tileColW, alignment: .trailing)
918
- }
919
- .padding(.horizontal, 14)
920
- .padding(.vertical, 3)
921
- .background(
922
- RoundedRectangle(cornerRadius: 4)
923
- .fill(isSelected ? Palette.surface : (isHovered ? Palette.surface.opacity(0.5) : Color.clear))
924
- .padding(.horizontal, 6)
925
- )
926
- .overlay(
927
- isSelected ?
928
- RoundedRectangle(cornerRadius: 4)
929
- .strokeBorder(Palette.borderLit, lineWidth: 0.5)
930
- .padding(.horizontal, 6)
931
- : nil
932
- )
933
- .background(
934
- GeometryReader { geo in
935
- Color.clear.preference(
936
- key: WindowRowFrameKey.self,
937
- value: [window.id: geo.frame(in: .named("inventoryPanel"))]
938
- )
939
- }
940
- )
941
- .contentShape(Rectangle())
942
- .onTapGesture(count: 2) {
943
- WindowTiler.navigateToWindowById(wid: window.id, pid: window.pid)
944
- }
945
- .onTapGesture(count: 1) {
946
- let mods = NSEvent.modifierFlags
947
- if mods.contains(.shift) {
948
- state.selectRange(to: window.id)
949
- } else if mods.contains(.command) {
950
- state.toggleSelection(window.id)
951
- } else {
952
- state.selectSingle(window.id)
953
- }
954
- }
955
- .contextMenu { windowContextMenu(for: window) }
956
- .onHover { hovering in hoveredWindowId = hovering ? window.id : nil }
957
- .id(window.id)
958
- }
959
-
960
- // MARK: - Context Menu
961
-
962
- @ViewBuilder
963
- private func windowContextMenu(for window: DesktopInventorySnapshot.InventoryWindowInfo) -> some View {
964
- let multiSelected = state.selectedWindowIds.count > 1 && state.isSelected(window.id)
965
- let selCount = state.selectedWindowIds.count
966
-
967
- if multiSelected {
968
- // Multi-select context menu
969
- Button {
970
- state.showAndDistributeSelected()
971
- } label: {
972
- Label("Show & Distribute (\(selCount))", systemImage: "rectangle.3.group")
973
- }
974
-
975
- Button {
976
- state.showAllSelected()
977
- } label: {
978
- Label("Show All (\(selCount))", systemImage: "macwindow.on.rectangle")
979
- }
980
-
981
- Button {
982
- state.distributeSelected()
983
- } label: {
984
- Label("Distribute (\(selCount))", systemImage: "rectangle.split.3x1")
985
- }
986
-
987
- Divider()
988
-
989
- Button {
990
- state.focusAllSelected()
991
- } label: {
992
- Label("Focus All (\(selCount))", systemImage: "eye")
993
- }
994
-
995
- Button {
996
- state.highlightAllSelected()
997
- } label: {
998
- Label("Highlight All (\(selCount))", systemImage: "sparkle")
999
- }
1000
-
1001
- Divider()
1002
-
1003
- Menu("Tile All (\(selCount))") {
1004
- ForEach(TilePosition.allCases) { tile in
1005
- Button {
1006
- state.showAndDistributeSelected(in: .tile(tile))
1007
- } label: {
1008
- Label(tile.label, systemImage: tile.icon)
1009
- }
1010
- }
1011
- }
1012
-
1013
- Divider()
1014
-
1015
- Button {
1016
- state.clearSelection()
1017
- } label: {
1018
- Label("Deselect All", systemImage: "xmark.circle")
1019
- }
1020
- } else {
1021
- // Single window context menu
1022
- Button {
1023
- WindowTiler.navigateToWindowById(wid: window.id, pid: window.pid)
1024
- } label: {
1025
- Label("Bring to Front", systemImage: "macwindow")
1026
- }
1027
-
1028
- Button {
1029
- WindowTiler.highlightWindowById(wid: window.id)
1030
- } label: {
1031
- Label("Highlight", systemImage: "sparkle")
1032
- }
1033
-
1034
- Divider()
1035
-
1036
- Menu("Tile Window") {
1037
- ForEach(TilePosition.allCases) { tile in
1038
- Button {
1039
- WindowTiler.tileWindowById(wid: window.id, pid: window.pid, to: tile)
1040
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
1041
- state.desktopSnapshot = nil
1042
- }
1043
- } label: {
1044
- Label(tile.label, systemImage: tile.icon)
1045
- }
1046
- }
1047
- }
1048
-
1049
- Divider()
1050
-
1051
- Button {
1052
- let info: String
1053
- if let path = window.inventoryPath {
1054
- info = path.description
1055
- } else {
1056
- let app = window.appName ?? "Unknown"
1057
- let title = window.title.isEmpty ? "(untitled)" : window.title
1058
- info = "[\(app)] \(title) wid=\(window.id)"
1059
- }
1060
- NSPasteboard.general.clearContents()
1061
- NSPasteboard.general.setString(info, forType: .string)
1062
- } label: {
1063
- Label("Copy Info", systemImage: "doc.on.doc")
1064
- }
1065
- }
1066
- }
1067
-
1068
- private func windowTitle(_ window: DesktopInventorySnapshot.InventoryWindowInfo) -> String {
1069
- let title = window.title
1070
- if title.isEmpty { return "(untitled)" }
1071
- return title
1072
- }
1073
-
1074
- private func sizeText(_ frame: WindowFrame) -> String {
1075
- "\(Int(frame.w))×\(Int(frame.h))"
1076
- }
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
-
1129
- /// Group items by their group label
1130
- private var groupedItems: [(String, [CommandModeInventory.Item])] {
1131
- var result: [(String, [CommandModeInventory.Item])] = []
1132
- var seen = Set<String>()
1133
- for item in state.inventory.items {
1134
- if !seen.contains(item.group) {
1135
- seen.insert(item.group)
1136
- result.append((item.group, state.inventory.items.filter { $0.group == item.group }))
1137
- }
1138
- }
1139
- return result
1140
- }
1141
-
1142
- private func sectionHeader(_ title: String) -> some View {
1143
- Text(title.uppercased())
1144
- .font(Typo.mono(9))
1145
- .foregroundColor(Palette.textMuted)
1146
- .padding(.horizontal, 16)
1147
- .padding(.top, 10)
1148
- .padding(.bottom, 4)
1149
- }
1150
-
1151
- private func inventoryRow(_ item: CommandModeInventory.Item) -> some View {
1152
- HStack(spacing: 0) {
1153
- // Name
1154
- Text(item.name)
1155
- .font(Typo.mono(11))
1156
- .foregroundColor(statusColor(item.status))
1157
- .lineLimit(1)
1158
- .frame(width: 160, alignment: .leading)
1159
-
1160
- // Pane count
1161
- Text(item.paneCount > 0 ? "\(item.paneCount) pane\(item.paneCount == 1 ? "" : "s")" : "—")
1162
- .font(Typo.mono(10))
1163
- .foregroundColor(Palette.textDim)
1164
- .frame(width: 70, alignment: .leading)
1165
-
1166
- // Status dot + label
1167
- HStack(spacing: 4) {
1168
- Circle()
1169
- .fill(statusColor(item.status))
1170
- .frame(width: 5, height: 5)
1171
- Text(statusLabel(item.status))
1172
- .font(Typo.mono(10))
1173
- .foregroundColor(statusColor(item.status))
1174
- }
1175
- .frame(width: 80, alignment: .leading)
1176
-
1177
- // Tile hint
1178
- Text(item.tileHint ?? "\u{2014}")
1179
- .font(Typo.mono(10))
1180
- .foregroundColor(Palette.textMuted)
1181
- .frame(width: 60, alignment: .leading)
1182
-
1183
- Spacer()
1184
- }
1185
- .padding(.horizontal, 16)
1186
- .padding(.vertical, 3)
1187
- }
1188
-
1189
- private func statusColor(_ status: CommandModeInventory.Status) -> Color {
1190
- switch status {
1191
- case .running: return Palette.running
1192
- case .attached: return Palette.running
1193
- case .stopped: return Palette.textMuted
1194
- }
1195
- }
1196
-
1197
- private func statusLabel(_ status: CommandModeInventory.Status) -> String {
1198
- switch status {
1199
- case .running: return "running"
1200
- case .attached: return "attached"
1201
- case .stopped: return "stopped"
1202
- }
1203
- }
1204
-
1205
- // MARK: - Chord Footer
1206
-
1207
- private var chordFooter: some View {
1208
- VStack(spacing: 4) {
1209
- // Restore banner — shown when positions are saved
1210
- if isDesktopInventory && state.savedPositions != nil {
1211
- HStack(spacing: 10) {
1212
- Text("Layout changed")
1213
- .font(Typo.mono(10))
1214
- .foregroundColor(Palette.text)
1215
- Spacer()
1216
- Button {
1217
- state.restorePositions()
1218
- } label: {
1219
- HStack(spacing: 3) {
1220
- Image(systemName: "arrow.uturn.backward")
1221
- .font(.system(size: 9))
1222
- Text("Restore")
1223
- .font(Typo.mono(9))
1224
- }
1225
- .foregroundColor(Palette.text)
1226
- .padding(.horizontal, 8)
1227
- .padding(.vertical, 4)
1228
- .background(
1229
- RoundedRectangle(cornerRadius: 4)
1230
- .fill(Palette.surface)
1231
- .overlay(
1232
- RoundedRectangle(cornerRadius: 4)
1233
- .strokeBorder(Palette.border, lineWidth: 0.5)
1234
- )
1235
- )
1236
- }
1237
- .buttonStyle(.plain)
1238
-
1239
- Button {
1240
- state.discardSavedPositions()
1241
- } label: {
1242
- HStack(spacing: 3) {
1243
- Image(systemName: "checkmark")
1244
- .font(.system(size: 9))
1245
- Text("Keep")
1246
- .font(Typo.mono(9))
1247
- }
1248
- .foregroundColor(Palette.running)
1249
- .padding(.horizontal, 8)
1250
- .padding(.vertical, 4)
1251
- .background(
1252
- RoundedRectangle(cornerRadius: 4)
1253
- .fill(Palette.running.opacity(0.1))
1254
- .overlay(
1255
- RoundedRectangle(cornerRadius: 4)
1256
- .strokeBorder(Palette.running.opacity(0.3), lineWidth: 0.5)
1257
- )
1258
- )
1259
- }
1260
- .buttonStyle(.plain)
1261
- }
1262
- .padding(.horizontal, 16)
1263
- .padding(.vertical, 6)
1264
- .background(Palette.running.opacity(0.05))
1265
- divider
1266
- }
1267
-
1268
- if isDesktopInventory && state.desktopMode == .gridPreview {
1269
- // Grid preview hints
1270
- HStack(spacing: 12) {
1271
- chordHint(key: "←→↑↓", label: "region")
1272
- chordHint(key: "1-7", label: "corners/thirds")
1273
- chordHint(key: "c", label: "center")
1274
- chordHint(key: "↩", label: "apply layout")
1275
- chordHint(key: "s", label: "apply layout")
1276
- chordHint(key: "esc", label: "cancel")
1277
- Spacer()
1278
- Text(state.gridPreviewRegionLabel.uppercased())
1279
- .font(Typo.mono(9))
1280
- .foregroundColor(Palette.textDim)
1281
- let shape = state.gridPreviewShape
1282
- Text(shape.map(String.init).joined(separator: " + "))
1283
- .font(Typo.monoBold(9))
1284
- .foregroundColor(Palette.running)
1285
- }
1286
- } else if isDesktopInventory && state.isSearching {
1287
- // Search mode hints
1288
- HStack(spacing: 12) {
1289
- chordHint(key: "↩", label: "select & front")
1290
- chordHint(key: "⌘A", label: "select all")
1291
- chordHint(key: "⇧↑↓", label: "multi-select")
1292
- if state.isOrganizeFlow && state.selectedWindowIds.count > 1 {
1293
- chordHint(key: "d", label: "organize")
1294
- }
1295
- if !state.selectedWindowIds.isEmpty {
1296
- chordHint(key: "t", label: "tile")
1297
- }
1298
- chordHint(key: "esc", label: "exit search")
1299
- Spacer()
1300
- if state.selectedWindowIds.count > 1 {
1301
- Text("\(state.selectedWindowIds.count) selected")
1302
- .font(Typo.mono(9))
1303
- .foregroundColor(Palette.running)
1304
- }
1305
- }
1306
- } else if isDesktopInventory && state.desktopMode == .tiling {
1307
- // Tiling sub-mode hints
1308
- HStack(spacing: 12) {
1309
- if state.selectedWindowIds.count == 2 {
1310
- chordHint(key: "←→", label: "split L/R")
1311
- } else {
1312
- chordHint(key: "←", label: "left")
1313
- chordHint(key: "→", label: "right")
1314
- }
1315
- chordHint(key: "↑", label: "top")
1316
- chordHint(key: "↓", label: "bottom")
1317
- chordHint(key: "⇧↑", label: "max")
1318
- chordHint(key: "1-4", label: "quad")
1319
- chordHint(key: "5-7", label: "thirds")
1320
- chordHint(key: "c", label: "center")
1321
- if state.selectedWindowIds.count >= 2 {
1322
- chordHint(key: "d", label: "distribute")
1323
- }
1324
- chordHint(key: "esc", label: "back")
1325
- Spacer()
1326
- if state.selectedWindowIds.count > 1 {
1327
- Text("\(state.selectedWindowIds.count) windows")
1328
- .font(Typo.mono(9))
1329
- .foregroundColor(Palette.running)
1330
- }
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
- }
1363
- } else if isDesktopInventory && state.selectedWindowIds.count > 1 {
1364
- // Multi-selection active
1365
- HStack(spacing: 12) {
1366
- chordHint(key: "s", label: "grid preview")
1367
- chordHint(key: "d", label: "distribute")
1368
- chordHint(key: "s", label: "grid preview")
1369
- chordHint(key: "↩", label: "front")
1370
- chordHint(key: "t", label: "grid region")
1371
- chordHint(key: "f", label: "focus")
1372
- chordHint(key: "h", label: "highlight")
1373
- chordHint(key: "esc", label: "clear")
1374
- Spacer()
1375
- if !state.selectedWindowSummaryText.isEmpty {
1376
- Text(state.selectedWindowSummaryText)
1377
- .font(Typo.mono(9))
1378
- .foregroundColor(Palette.textDim)
1379
- .lineLimit(1)
1380
- }
1381
- Text("\(state.selectedWindowIds.count) selected")
1382
- .font(Typo.mono(9))
1383
- .foregroundColor(Palette.running)
1384
- }
1385
- } else if isDesktopInventory && !state.selectedWindowIds.isEmpty {
1386
- // Single selection active — browsing hints with direct shortcuts
1387
- HStack(spacing: 12) {
1388
- chordHint(key: "d", label: "organize")
1389
- chordHint(key: "s", label: "show")
1390
- chordHint(key: "↩", label: "front")
1391
- chordHint(key: "f", label: "focus+close")
1392
- chordHint(key: "t", label: "tile")
1393
- chordHint(key: "h", label: "highlight")
1394
- chordHint(key: "esc", label: "deselect")
1395
- Spacer()
1396
- }
1397
- } else if isDesktopInventory {
1398
- // No selection — browsing hints
1399
- HStack(spacing: 12) {
1400
- chordHint(key: "↑↓", label: "navigate")
1401
- chordHint(key: "←→", label: "display")
1402
- chordHint(key: "m", label: "map")
1403
- chordHint(key: "/", label: "search")
1404
- chordHint(key: "`", label: "chords")
1405
- chordHint(key: "esc", label: "back")
1406
- Spacer()
1407
- }
1408
- } else {
1409
- // First row: action chords
1410
- HStack(spacing: 12) {
1411
- chordHint(key: "`", label: "desktop")
1412
- ForEach(state.chords.prefix(3), id: \.key) { chord in
1413
- chordHint(key: chord.key, label: chord.label)
1414
- }
1415
- Spacer()
1416
- }
1417
-
1418
- // Second row: layer chords + utility
1419
- HStack(spacing: 12) {
1420
- ForEach(state.chords.dropFirst(3), id: \.key) { chord in
1421
- chordHint(key: chord.key, label: chord.label)
1422
- }
1423
- chordHint(key: "esc", label: "dismiss")
1424
- Spacer()
1425
- }
1426
- }
1427
- }
1428
- .padding(.horizontal, 16)
1429
- .padding(.vertical, 8)
1430
- .background(Palette.surface.opacity(0.4))
1431
- }
1432
-
1433
- private func chordHint(key: String, label: String) -> some View {
1434
- HStack(spacing: 4) {
1435
- Text(key)
1436
- .font(Typo.mono(9))
1437
- .foregroundColor(Palette.text)
1438
- .padding(.horizontal, 4)
1439
- .padding(.vertical, 2)
1440
- .background(
1441
- RoundedRectangle(cornerRadius: 3)
1442
- .fill(Palette.surface)
1443
- .overlay(
1444
- RoundedRectangle(cornerRadius: 3)
1445
- .strokeBorder(Palette.border, lineWidth: 0.5)
1446
- )
1447
- )
1448
- Text(label)
1449
- .font(Typo.mono(9))
1450
- .foregroundColor(Palette.textMuted)
1451
- }
1452
- }
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
-
1479
- private func actionButton(key: String, label: String, action: @escaping () -> Void) -> some View {
1480
- Button(action: action) {
1481
- HStack(spacing: 4) {
1482
- Text(key)
1483
- .font(Typo.mono(9))
1484
- .foregroundColor(Palette.text)
1485
- .padding(.horizontal, 4)
1486
- .padding(.vertical, 2)
1487
- .background(
1488
- RoundedRectangle(cornerRadius: 3)
1489
- .fill(Palette.surface)
1490
- .overlay(
1491
- RoundedRectangle(cornerRadius: 3)
1492
- .strokeBorder(Palette.border, lineWidth: 0.5)
1493
- )
1494
- )
1495
- Text(label)
1496
- .font(Typo.mono(9))
1497
- .foregroundColor(Palette.textMuted)
1498
- }
1499
- .padding(.horizontal, 4)
1500
- .padding(.vertical, 2)
1501
- .background(
1502
- RoundedRectangle(cornerRadius: 4)
1503
- .fill(Color.white.opacity(0.001))
1504
- )
1505
- .contentShape(Rectangle())
1506
- }
1507
- .buttonStyle(.plain)
1508
- .onHover { hovering in
1509
- if hovering {
1510
- NSCursor.pointingHand.push()
1511
- } else {
1512
- NSCursor.pop()
1513
- }
1514
- }
1515
- }
1516
-
1517
- // MARK: - Executing Overlay
1518
-
1519
- @ViewBuilder
1520
- private var executingOverlay: some View {
1521
- if case .executing(let label) = state.phase {
1522
- ZStack {
1523
- Palette.bg.opacity(0.85)
1524
- HStack(spacing: 8) {
1525
- Image(systemName: "checkmark.circle.fill")
1526
- .font(.system(size: 16))
1527
- .foregroundColor(Palette.running)
1528
- Text(label)
1529
- .font(Typo.monoBold(13))
1530
- .foregroundColor(Palette.running)
1531
- }
1532
- }
1533
- .clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
1534
- .transition(.opacity)
1535
- }
1536
- }
1537
-
1538
- // MARK: - Flash Overlay
1539
-
1540
- @ViewBuilder
1541
- private var flashOverlay: some View {
1542
- if let msg = state.flashMessage {
1543
- VStack {
1544
- Spacer()
1545
- HStack(spacing: 6) {
1546
- Image(systemName: "rectangle.3.group")
1547
- .font(.system(size: 11))
1548
- Text(msg)
1549
- .font(Typo.monoBold(11))
1550
- }
1551
- .foregroundColor(Palette.text)
1552
- .padding(.horizontal, 14)
1553
- .padding(.vertical, 8)
1554
- .background(
1555
- RoundedRectangle(cornerRadius: 8, style: .continuous)
1556
- .fill(Palette.surface)
1557
- .overlay(
1558
- RoundedRectangle(cornerRadius: 8, style: .continuous)
1559
- .strokeBorder(Palette.running.opacity(0.3), lineWidth: 0.5)
1560
- )
1561
- .shadow(color: .black.opacity(0.2), radius: 8, y: 2)
1562
- )
1563
- .padding(.bottom, 60)
1564
- }
1565
- .transition(.opacity.combined(with: .move(edge: .bottom)))
1566
- .animation(.easeOut(duration: 0.2), value: state.flashMessage)
1567
- .allowsHitTesting(false)
1568
- }
1569
- }
1570
-
1571
- // MARK: - Divider
1572
-
1573
- private var divider: some View {
1574
- Rectangle()
1575
- .fill(Palette.border)
1576
- .frame(height: 0.5)
1577
- }
1578
-
1579
- // MARK: - Grid Preview
1580
-
1581
- private var gridPreviewContent: some View {
1582
- let windows = state.gridPreviewWindows
1583
- let shape = state.gridPreviewShape
1584
- let gridDesc = shape.map(String.init).joined(separator: " + ")
1585
-
1586
- return VStack(spacing: 0) {
1587
- // Title bar
1588
- HStack {
1589
- Text("LAYOUT PREVIEW")
1590
- .font(Typo.monoBold(10))
1591
- .foregroundColor(Palette.textDim)
1592
- Text(state.gridPreviewRegionLabel.uppercased())
1593
- .font(Typo.mono(9))
1594
- .foregroundColor(Palette.textMuted)
1595
- Text(gridDesc)
1596
- .font(Typo.monoBold(10))
1597
- .foregroundColor(Palette.running)
1598
- Spacer()
1599
- Text("\(windows.count) window\(windows.count == 1 ? "" : "s")")
1600
- .font(Typo.mono(9))
1601
- .foregroundColor(Palette.textMuted)
1602
- }
1603
- .padding(.horizontal, 16)
1604
- .padding(.vertical, 8)
1605
-
1606
- divider
1607
-
1608
- // Screen map: current positions (dimmed) + target grid (bright)
1609
- screenMap(windows: windows, shape: shape, placement: state.gridPreviewPlacement)
1610
- .frame(height: 160)
1611
- .padding(.horizontal, 12)
1612
- .padding(.vertical, 8)
1613
-
1614
- divider
1615
-
1616
- // Grid cells with window details
1617
- VStack(spacing: 2) {
1618
- ForEach(Array(shape.enumerated()), id: \.offset) { rowIdx, colCount in
1619
- HStack(spacing: 2) {
1620
- ForEach(0..<colCount, id: \.self) { colIdx in
1621
- let idx = shape[0..<rowIdx].reduce(0, +) + colIdx
1622
- if idx < windows.count {
1623
- gridCell(windows[idx], index: idx + 1)
1624
- }
1625
- }
1626
- }
1627
- }
1628
- }
1629
- .padding(8)
1630
- .frame(maxWidth: .infinity, maxHeight: .infinity)
1631
- }
1632
- }
1633
-
1634
-
1635
- // MARK: - Grid Preview Screen Map
1636
-
1637
- /// Miniature proportional map of the screen showing current window positions and target grid slots
1638
- private func screenMap(
1639
- windows: [DesktopInventorySnapshot.InventoryWindowInfo],
1640
- shape: [Int],
1641
- placement: PlacementSpec?
1642
- ) -> some View {
1643
- GeometryReader { geo in
1644
- let availW = geo.size.width
1645
- let availH = geo.size.height
1646
-
1647
- // Get screen dimensions from snapshot
1648
- let display = state.filteredSnapshot?.displays.first
1649
- let screenW = CGFloat(display?.visibleFrame.w ?? 3440)
1650
- let screenH = CGFloat(display?.visibleFrame.h ?? 1440)
1651
-
1652
- // Scale to fit
1653
- let scaleX = availW / screenW
1654
- let scaleY = availH / screenH
1655
- let scale = min(scaleX, scaleY)
1656
- let mapW = screenW * scale
1657
- let mapH = screenH * scale
1658
- let offsetX = (availW - mapW) / 2
1659
- let offsetY = (availH - mapH) / 2
1660
-
1661
- ZStack(alignment: .topLeading) {
1662
- // Screen background
1663
- RoundedRectangle(cornerRadius: 4)
1664
- .fill(Palette.bg.opacity(0.5))
1665
- .overlay(
1666
- RoundedRectangle(cornerRadius: 4)
1667
- .strokeBorder(Palette.border, lineWidth: 0.5)
1668
- )
1669
- .frame(width: mapW, height: mapH)
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
-
1679
- // Current positions (dimmed)
1680
- ForEach(Array(windows.enumerated()), id: \.element.id) { idx, win in
1681
- let f = win.frame
1682
- let x = CGFloat(f.x) * scale
1683
- let y = CGFloat(f.y) * scale
1684
- let w = max(CGFloat(f.w) * scale, 2)
1685
- let h = max(CGFloat(f.h) * scale, 2)
1686
-
1687
- RoundedRectangle(cornerRadius: 2)
1688
- .fill(Palette.textMuted.opacity(0.15))
1689
- .overlay(
1690
- RoundedRectangle(cornerRadius: 2)
1691
- .strokeBorder(Palette.textMuted.opacity(0.3), lineWidth: 0.5)
1692
- )
1693
- .frame(width: w, height: h)
1694
- .offset(x: x, y: y)
1695
- }
1696
-
1697
- // Target grid slots (bright)
1698
- let slots = computeMapSlots(
1699
- count: windows.count,
1700
- shape: shape,
1701
- mapW: mapW,
1702
- mapH: mapH,
1703
- region: placement?.fractions
1704
- )
1705
- ForEach(Array(slots.enumerated()), id: \.offset) { idx, slot in
1706
- let win = idx < windows.count ? windows[idx] : nil
1707
- RoundedRectangle(cornerRadius: 2)
1708
- .fill(Palette.running.opacity(0.12))
1709
- .overlay(
1710
- RoundedRectangle(cornerRadius: 2)
1711
- .strokeBorder(Palette.running.opacity(0.5), lineWidth: 1)
1712
- )
1713
- .overlay {
1714
- VStack(spacing: 1) {
1715
- Text("\(idx + 1)")
1716
- .font(Typo.monoBold(9))
1717
- .foregroundColor(Palette.running)
1718
- if let win = win {
1719
- Text(win.appName ?? "")
1720
- .font(Typo.mono(7))
1721
- .foregroundColor(Palette.running.opacity(0.7))
1722
- .lineLimit(1)
1723
- }
1724
- }
1725
- }
1726
- .frame(width: slot.width - 2, height: slot.height - 2)
1727
- .offset(x: slot.origin.x + 1, y: slot.origin.y + 1)
1728
- }
1729
- }
1730
- .offset(x: offsetX, y: offsetY)
1731
- }
1732
- }
1733
-
1734
- /// Compute grid slots scaled to the mini map dimensions
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)
1746
- let rowCount = shape.count
1747
- let rowH = regionH / CGFloat(rowCount)
1748
- var slots: [CGRect] = []
1749
- for (row, cols) in shape.enumerated() {
1750
- let colW = regionW / CGFloat(cols)
1751
- let y = regionY + CGFloat(row) * rowH
1752
- for col in 0..<cols {
1753
- slots.append(CGRect(
1754
- x: regionX + CGFloat(col) * colW,
1755
- y: y,
1756
- width: colW,
1757
- height: rowH
1758
- ))
1759
- }
1760
- }
1761
- return slots
1762
- }
1763
-
1764
- private func gridCell(_ window: DesktopInventorySnapshot.InventoryWindowInfo, index: Int) -> some View {
1765
- VStack(spacing: 3) {
1766
- // App name
1767
- Text(window.appName ?? "Unknown")
1768
- .font(Typo.monoBold(10))
1769
- .foregroundColor(window.isLattices ? Palette.running : Palette.text)
1770
- .lineLimit(1)
1771
-
1772
- // Window title
1773
- Text(windowTitle(window))
1774
- .font(Typo.mono(9))
1775
- .foregroundColor(Palette.textDim)
1776
- .lineLimit(2)
1777
- .multilineTextAlignment(.center)
1778
-
1779
- // Size
1780
- Text(sizeText(window.frame))
1781
- .font(Typo.mono(8))
1782
- .foregroundColor(Palette.textMuted)
1783
- }
1784
- .frame(maxWidth: .infinity, maxHeight: .infinity)
1785
- .padding(.vertical, 8)
1786
- .padding(.horizontal, 6)
1787
- .background(
1788
- RoundedRectangle(cornerRadius: 6, style: .continuous)
1789
- .fill(Palette.surface)
1790
- )
1791
- .overlay(
1792
- RoundedRectangle(cornerRadius: 6, style: .continuous)
1793
- .strokeBorder(window.isLattices ? Palette.running.opacity(0.3) : Palette.border, lineWidth: 0.5)
1794
- )
1795
- .overlay(alignment: .topLeading) {
1796
- Text("\(index)")
1797
- .font(Typo.mono(8))
1798
- .foregroundColor(Palette.textMuted)
1799
- .padding(4)
1800
- }
1801
- }
1802
-
1803
- // MARK: - Marquee Overlay
1804
-
1805
- @ViewBuilder
1806
- private var marqueeOverlay: some View {
1807
- if state.isDragging {
1808
- let rect = state.marqueeRect
1809
- Rectangle()
1810
- .fill(Palette.running.opacity(0.08))
1811
- .overlay(
1812
- Rectangle()
1813
- .strokeBorder(Palette.running.opacity(0.4), lineWidth: 1)
1814
- )
1815
- .frame(width: rect.width, height: rect.height)
1816
- .position(x: rect.midX, y: rect.midY)
1817
- .allowsHitTesting(false)
1818
- }
1819
- }
1820
-
1821
- // MARK: - Key Handler
1822
-
1823
- private func installKeyHandler() {
1824
- eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
1825
- guard state.phase == .inventory || state.phase == .desktopInventory else { return event }
1826
- // Only handle keys when our panel is the key window
1827
- guard let panel = CommandModeWindow.shared.panelWindow,
1828
- panel.isKeyWindow else { return event }
1829
- let consumed = state.handleKey(event.keyCode, modifiers: event.modifierFlags)
1830
- return consumed ? nil : event
1831
- }
1832
- }
1833
-
1834
- // MARK: - Mouse Monitors (marquee drag + screen map drag)
1835
-
1836
- private func installMouseMonitors() {
1837
- let dragThreshold: CGFloat = 4
1838
-
1839
- mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { event in
1840
- guard let eventWindow = event.window,
1841
- eventWindow === CommandModeWindow.shared.panelWindow else { return event }
1842
- guard state.phase == .desktopInventory else { return event }
1843
-
1844
- state.dragStartPoint = event.locationInWindow
1845
- return event
1846
- }
1847
-
1848
- mouseDragMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDragged) { event in
1849
- guard state.phase == .desktopInventory else { return event }
1850
-
1851
- guard let startPt = state.dragStartPoint else { return event }
1852
-
1853
- let currentPt = event.locationInWindow
1854
-
1855
- if !state.isDragging {
1856
- // Check threshold before starting drag
1857
- let dx = currentPt.x - startPt.x
1858
- let dy = currentPt.y - startPt.y
1859
- let dist = sqrt(dx * dx + dy * dy)
1860
- guard dist >= dragThreshold else { return event }
1861
-
1862
- // Convert NSEvent bottom-left → SwiftUI top-left in inventoryPanel space
1863
- let additive = event.modifierFlags.contains(.command)
1864
- let swiftUIStart = convertToPanel(startPt, event: event)
1865
- state.beginDrag(at: swiftUIStart, additive: additive)
1866
- }
1867
-
1868
- let swiftUICurrent = convertToPanel(currentPt, event: event)
1869
- state.updateDrag(to: swiftUICurrent)
1870
-
1871
- return nil // consume to prevent ScrollView scrolling during drag
1872
- }
1873
-
1874
- mouseUpMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseUp) { event in
1875
- if state.isDragging {
1876
- state.endDrag()
1877
- }
1878
- state.dragStartPoint = nil
1879
- return event
1880
- }
1881
-
1882
- }
1883
-
1884
-
1885
-
1886
- /// Convert NSEvent window coordinates (bottom-left origin) to SwiftUI inventoryPanel coordinates (top-left origin)
1887
- private func convertToPanel(_ windowPoint: NSPoint, event: NSEvent) -> CGPoint {
1888
- guard let nsWindow = event.window else { return .zero }
1889
- // Convert to screen coordinates
1890
- let screenPoint = nsWindow.convertPoint(toScreen: windowPoint)
1891
- // Convert to SwiftUI top-left: screen Y is bottom-up, SwiftUI Y is top-down
1892
- let screenHeight = NSScreen.main?.frame.height ?? 0
1893
- let flippedY = screenHeight - screenPoint.y
1894
- // Subtract the panel's global origin to get panel-local coordinates
1895
- let panelY = flippedY - panelOriginY
1896
- // X is relative to window — we need global X minus panel X
1897
- // For simplicity, use the window point X directly since the panel fills the window width
1898
- return CGPoint(x: windowPoint.x, y: panelY)
1899
- }
1900
-
1901
- /// Convert NSEvent to flipped window-local coordinates (Y=0 at top of window content)
1902
- /// This matches SwiftUI GeometryReader's `.global` coordinate space inside NSHostingView
1903
- private func flippedScreenPoint(_ event: NSEvent) -> CGPoint {
1904
- guard let nsWindow = event.window else { return .zero }
1905
- let loc = event.locationInWindow // bottom-left origin
1906
- let windowHeight = nsWindow.contentView?.frame.height ?? nsWindow.frame.height
1907
- return CGPoint(x: loc.x, y: windowHeight - loc.y)
1908
- }
1909
-
1910
- private func removeMouseMonitors() {
1911
- if let m = mouseDownMonitor { NSEvent.removeMonitor(m); mouseDownMonitor = nil }
1912
- if let m = mouseDragMonitor { NSEvent.removeMonitor(m); mouseDragMonitor = nil }
1913
- if let m = mouseUpMonitor { NSEvent.removeMonitor(m); mouseUpMonitor = nil }
1914
- }
1915
-
1916
- // Clear hover when leaving desktop inventory
1917
- private func clearDesktopState() {
1918
- hoveredWindowId = nil
1919
- }
1920
-
1921
- private func removeKeyHandler() {
1922
- if let monitor = eventMonitor {
1923
- NSEvent.removeMonitor(monitor)
1924
- eventMonitor = nil
1925
- }
1926
- }
1927
- }