@lattices/cli 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/app/Info.plist +2 -2
  2. package/app/Lattices.app/Contents/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  4. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +9 -0
  5. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
  6. package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +1 -1
  7. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -2
  8. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +27 -2
  9. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +15 -2
  10. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
  11. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  12. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  13. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  14. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
  15. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  16. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  17. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  18. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  19. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -110
  20. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +228 -24
  21. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +601 -59
  22. package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +9 -5
  23. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  24. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  25. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  26. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  27. package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
  28. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  29. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -2
  30. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +20 -7
  31. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
  32. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +59 -4
  33. package/docs/component-extraction-roadmap.md +392 -0
  34. package/package.json +3 -1
  35. package/app/Sources/CommandPaletteWindow.swift +0 -134
  36. package/app/Sources/OmniSearchWindow.swift +0 -165
  37. /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
  38. /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
  39. /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
  40. /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
  41. /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
  42. /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
  43. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  44. /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
  45. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  46. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  47. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  48. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  49. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  50. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  51. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  52. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  53. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  54. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  55. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  56. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  57. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  58. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  59. /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
  60. /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
  61. /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
  62. /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
  63. /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
  64. /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
  65. /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
  66. /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
  67. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  68. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  69. /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
  70. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  71. /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
  72. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  73. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  74. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  75. /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
  76. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  77. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  78. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  79. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  80. /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
  81. /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
  82. /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
  83. /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
  84. /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
  85. /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
  86. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  87. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  88. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  89. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  90. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  91. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  92. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  93. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  94. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  95. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  96. /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
  97. /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
  98. /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
  99. /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
  100. /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
  101. /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
  102. /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
  103. /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
  104. /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
  105. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  106. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  107. /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
  108. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  109. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  110. /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
  111. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  112. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  113. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  114. /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
  115. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  116. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  117. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  118. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  119. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  120. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  121. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  122. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  123. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  124. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  125. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  126. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  127. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  128. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  129. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  130. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -0,0 +1,241 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ final class OverlayPanel: NSPanel {
5
+ var activatesOnMouseDown = false
6
+ var onKeyDown: ((NSEvent) -> Void)?
7
+ var onFlagsChanged: ((NSEvent) -> Void)?
8
+
9
+ override var canBecomeKey: Bool { true }
10
+ override var canBecomeMain: Bool { true }
11
+
12
+ override func sendEvent(_ event: NSEvent) {
13
+ if activatesOnMouseDown,
14
+ event.type == .leftMouseDown || event.type == .rightMouseDown {
15
+ if !NSApp.isActive {
16
+ NSApp.activate(ignoringOtherApps: true)
17
+ }
18
+ if !isKeyWindow {
19
+ makeKey()
20
+ }
21
+ }
22
+ super.sendEvent(event)
23
+ }
24
+
25
+ override func keyDown(with event: NSEvent) {
26
+ if let onKeyDown {
27
+ onKeyDown(event)
28
+ } else {
29
+ super.keyDown(with: event)
30
+ }
31
+ }
32
+
33
+ override func flagsChanged(with event: NSEvent) {
34
+ if let onFlagsChanged {
35
+ onFlagsChanged(event)
36
+ } else {
37
+ super.flagsChanged(with: event)
38
+ }
39
+ }
40
+ }
41
+
42
+ private final class OverlayHostingView<Content: View>: NSHostingView<Content> {
43
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
44
+ override var focusRingType: NSFocusRingType { get { .none } set {} }
45
+ }
46
+
47
+ struct OverlayPanelShell {
48
+ enum Background {
49
+ case clear
50
+ case solid(NSColor)
51
+ case material(NSVisualEffectView.Material)
52
+ }
53
+
54
+ enum Placement {
55
+ case centered(yOffsetRatio: CGFloat = 0)
56
+ case mouseScreenCentered(yOffsetRatio: CGFloat = 0)
57
+ case topCenter(margin: CGFloat = 40)
58
+ }
59
+
60
+ struct Config {
61
+ var size: NSSize
62
+ var styleMask: NSWindow.StyleMask = [.nonactivatingPanel]
63
+ var title: String = ""
64
+ var titleVisible: NSWindow.TitleVisibility = .hidden
65
+ var titlebarAppearsTransparent = false
66
+ var background: Background = .clear
67
+ var cornerRadius: CGFloat? = nil
68
+ var level: NSWindow.Level = .floating
69
+ var hasShadow = true
70
+ var hidesOnDeactivate = false
71
+ var isReleasedWhenClosed = false
72
+ var isMovableByWindowBackground = false
73
+ var collectionBehavior: NSWindow.CollectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
74
+ var minSize: NSSize? = nil
75
+ var maxSize: NSSize? = nil
76
+ var activatesOnMouseDown = false
77
+ var onKeyDown: ((NSEvent) -> Void)? = nil
78
+ var onFlagsChanged: ((NSEvent) -> Void)? = nil
79
+ var appearance: NSAppearance? = NSAppearance(named: .darkAqua)
80
+ }
81
+
82
+ static func makePanel<Content: View>(config: Config, rootView: Content) -> OverlayPanel {
83
+ let hosting = OverlayHostingView(rootView: rootView)
84
+ hosting.translatesAutoresizingMaskIntoConstraints = false
85
+
86
+ let panel = OverlayPanel(
87
+ contentRect: NSRect(origin: .zero, size: config.size),
88
+ styleMask: config.styleMask,
89
+ backing: .buffered,
90
+ defer: false
91
+ )
92
+ panel.title = config.title
93
+ panel.titleVisibility = config.titleVisible
94
+ panel.titlebarAppearsTransparent = config.titlebarAppearsTransparent
95
+ panel.isOpaque = false
96
+ panel.backgroundColor = backgroundColor(for: config.background)
97
+ panel.level = config.level
98
+ panel.hasShadow = config.hasShadow
99
+ panel.hidesOnDeactivate = config.hidesOnDeactivate
100
+ panel.isReleasedWhenClosed = config.isReleasedWhenClosed
101
+ panel.isMovableByWindowBackground = config.isMovableByWindowBackground
102
+ panel.collectionBehavior = config.collectionBehavior
103
+ panel.activatesOnMouseDown = config.activatesOnMouseDown
104
+ panel.onKeyDown = config.onKeyDown
105
+ panel.onFlagsChanged = config.onFlagsChanged
106
+ if let minSize = config.minSize {
107
+ panel.minSize = minSize
108
+ }
109
+ if let maxSize = config.maxSize {
110
+ panel.maxSize = maxSize
111
+ }
112
+ if let appearance = config.appearance {
113
+ panel.appearance = appearance
114
+ }
115
+
116
+ install(hosting: hosting, on: panel, background: config.background, cornerRadius: config.cornerRadius)
117
+ return panel
118
+ }
119
+
120
+ static func position(_ window: NSWindow, placement: Placement) {
121
+ let screen: NSScreen
122
+ switch placement {
123
+ case .mouseScreenCentered, .topCenter:
124
+ screen = mouseScreen()
125
+ case .centered:
126
+ screen = NSScreen.main ?? mouseScreen()
127
+ }
128
+
129
+ let visibleFrame = screen.visibleFrame
130
+ let size = window.frame.size
131
+ let origin: NSPoint
132
+
133
+ switch placement {
134
+ case .centered(let yOffsetRatio), .mouseScreenCentered(let yOffsetRatio):
135
+ origin = NSPoint(
136
+ x: visibleFrame.midX - size.width / 2,
137
+ y: visibleFrame.midY - size.height / 2 + (visibleFrame.height * yOffsetRatio)
138
+ )
139
+ case .topCenter(let margin):
140
+ origin = NSPoint(
141
+ x: visibleFrame.midX - size.width / 2,
142
+ y: visibleFrame.maxY - size.height - margin
143
+ )
144
+ }
145
+
146
+ window.setFrameOrigin(origin)
147
+ }
148
+
149
+ static func present(
150
+ _ panel: NSPanel,
151
+ activate: Bool = true,
152
+ makeKey: Bool = true,
153
+ orderFrontRegardless: Bool = false
154
+ ) {
155
+ if orderFrontRegardless {
156
+ panel.orderFrontRegardless()
157
+ } else if makeKey {
158
+ panel.makeKeyAndOrderFront(nil)
159
+ } else {
160
+ panel.orderFront(nil)
161
+ }
162
+
163
+ if makeKey {
164
+ panel.makeKey()
165
+ }
166
+
167
+ if activate {
168
+ NSApp.activate(ignoringOtherApps: true)
169
+ }
170
+ }
171
+
172
+ private static func install(
173
+ hosting: NSView,
174
+ on panel: NSPanel,
175
+ background: Background,
176
+ cornerRadius: CGFloat?
177
+ ) {
178
+ switch background {
179
+ case .material(let material):
180
+ let effectView = NSVisualEffectView()
181
+ effectView.blendingMode = .behindWindow
182
+ effectView.material = material
183
+ effectView.state = .active
184
+ effectView.wantsLayer = true
185
+ if let cornerRadius {
186
+ effectView.maskImage = maskImage(cornerRadius: cornerRadius)
187
+ }
188
+ panel.contentView = effectView
189
+ pin(hosting: hosting, to: effectView)
190
+ case .clear, .solid:
191
+ panel.contentView = hosting
192
+ }
193
+ }
194
+
195
+ private static func pin(hosting: NSView, to container: NSView) {
196
+ container.addSubview(hosting)
197
+ NSLayoutConstraint.activate([
198
+ hosting.leadingAnchor.constraint(equalTo: container.leadingAnchor),
199
+ hosting.trailingAnchor.constraint(equalTo: container.trailingAnchor),
200
+ hosting.topAnchor.constraint(equalTo: container.topAnchor),
201
+ hosting.bottomAnchor.constraint(equalTo: container.bottomAnchor),
202
+ ])
203
+ }
204
+
205
+ private static func mouseScreen() -> NSScreen {
206
+ let mouseLocation = NSEvent.mouseLocation
207
+ return NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) })
208
+ ?? NSScreen.main
209
+ ?? NSScreen.screens.first!
210
+ }
211
+
212
+ private static func backgroundColor(for background: Background) -> NSColor {
213
+ switch background {
214
+ case .clear, .material:
215
+ return .clear
216
+ case .solid(let color):
217
+ return color
218
+ }
219
+ }
220
+
221
+ private static func maskImage(cornerRadius: CGFloat) -> NSImage {
222
+ let edgeLength = 2.0 * cornerRadius + 1.0
223
+ let maskImage = NSImage(
224
+ size: NSSize(width: edgeLength, height: edgeLength),
225
+ flipped: false
226
+ ) { rect in
227
+ let path = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
228
+ NSColor.black.set()
229
+ path.fill()
230
+ return true
231
+ }
232
+ maskImage.capInsets = NSEdgeInsets(
233
+ top: cornerRadius,
234
+ left: cornerRadius,
235
+ bottom: cornerRadius,
236
+ right: cornerRadius
237
+ )
238
+ maskImage.resizingMode = .stretch
239
+ return maskImage
240
+ }
241
+ }
@@ -1388,7 +1388,9 @@ final class ScreenMapController: ObservableObject {
1388
1388
  @Published var editor: ScreenMapEditorState? {
1389
1389
  didSet { bindEditor() }
1390
1390
  }
1391
- @Published var selectedWindowIds: Set<UInt32> = []
1391
+ @Published var selectedWindowIds: Set<UInt32> = [] {
1392
+ didSet { syncSharedSelection() }
1393
+ }
1392
1394
  @Published var windowSets: [ScreenMapWindowSet] = []
1393
1395
  @Published var activeWindowSetID: UUID? = nil
1394
1396
  @Published var flashMessage: String? = nil
@@ -1484,6 +1486,28 @@ final class ScreenMapController: ObservableObject {
1484
1486
  activeWindowSetID = nil
1485
1487
  }
1486
1488
 
1489
+ private func syncSharedSelection() {
1490
+ guard !selectedWindowIds.isEmpty else {
1491
+ WindowSelectionStore.shared.clear(source: "screen-map")
1492
+ return
1493
+ }
1494
+
1495
+ guard let editor else { return }
1496
+ let summaries = editor.windows
1497
+ .filter { selectedWindowIds.contains($0.id) }
1498
+ .map {
1499
+ SelectedWindowSummary(
1500
+ wid: $0.id,
1501
+ app: $0.app,
1502
+ title: $0.title,
1503
+ latticesSession: $0.latticesSession
1504
+ )
1505
+ }
1506
+
1507
+ guard !summaries.isEmpty else { return }
1508
+ WindowSelectionStore.shared.setSelection(summaries, source: "screen-map")
1509
+ }
1510
+
1487
1511
  func selectNextWindow() {
1488
1512
  guard let ed = editor else { return }
1489
1513
  let wins = ed.focusedVisibleWindows.sorted(by: { $0.zIndex < $1.zIndex })
@@ -2011,7 +2035,6 @@ final class ScreenMapController: ObservableObject {
2011
2035
 
2012
2036
  func handleKey(_ keyCode: UInt16, modifiers: NSEvent.ModifierFlags = []) -> Bool {
2013
2037
  let diag = DiagnosticLog.shared
2014
- diag.info("[ScreenMap] key: \(keyCode)")
2015
2038
 
2016
2039
  // Tiling mode intercepts keys before anything else
2017
2040
  if editor?.isTilingMode == true {
@@ -1805,6 +1805,7 @@ struct ScreenMapView: View {
1805
1805
  let displays = editor?.displays ?? []
1806
1806
  let zoomLevel = editor?.zoomLevel ?? 1.0
1807
1807
  let panOffset = editor?.panOffset ?? .zero
1808
+ let canvasShape = RoundedRectangle(cornerRadius: 6, style: .continuous)
1808
1809
 
1809
1810
  return GeometryReader { geo in
1810
1811
  let metrics = CanvasMetrics(editor: editor, displays: displays, viewportSize: geo.size)
@@ -1852,12 +1853,14 @@ struct ScreenMapView: View {
1852
1853
  }
1853
1854
  .padding(8)
1854
1855
  .frame(maxWidth: .infinity, maxHeight: .infinity)
1856
+ .contentShape(canvasShape)
1857
+ .clipShape(canvasShape)
1855
1858
  .clipped()
1856
1859
  .background(
1857
1860
  ZStack {
1858
- RoundedRectangle(cornerRadius: 6)
1861
+ canvasShape
1859
1862
  .fill(Color.black.opacity(0.25))
1860
- RoundedRectangle(cornerRadius: 6)
1863
+ canvasShape
1861
1864
  .strokeBorder(Color.white.opacity(0.06), lineWidth: 0.5)
1862
1865
  Canvas { context, size in
1863
1866
  let spacing: CGFloat = 20
@@ -2742,11 +2745,14 @@ struct ScreenMapView: View {
2742
2745
  }
2743
2746
  // Track space key for canvas drag-to-pan
2744
2747
  if event.keyCode == 49 && !controller.isSearchActive {
2745
- if event.type == .keyDown && !event.isARepeat {
2746
- isSpaceHeld = true
2747
- NSCursor.openHand.push()
2748
+ if event.type == .keyDown {
2749
+ if !isSpaceHeld {
2750
+ isSpaceHeld = true
2751
+ NSCursor.openHand.push()
2752
+ }
2748
2753
  return nil
2749
2754
  } else if event.type == .keyUp {
2755
+ guard isSpaceHeld else { return nil }
2750
2756
  isSpaceHeld = false
2751
2757
  spaceDragStart = nil
2752
2758
  NSCursor.pop()
@@ -2788,16 +2794,18 @@ struct ScreenMapView: View {
2788
2794
  mouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: .leftMouseDown) { event in
2789
2795
  guard let eventWindow = event.window,
2790
2796
  eventWindow === ScreenMapWindowController.shared.nsWindow else { return event }
2797
+ let flippedPt = flippedScreenPoint(event)
2791
2798
 
2792
2799
  // Space+click → begin canvas pan
2793
- if isSpaceHeld, let editor = controller.editor {
2800
+ if isSpaceHeld,
2801
+ isCanvasPoint(flippedPt),
2802
+ let editor = controller.editor {
2794
2803
  spaceDragStart = event.locationInWindow
2795
2804
  spaceDragPanStart = editor.panOffset
2796
2805
  NSCursor.closedHand.push()
2797
2806
  return nil
2798
2807
  }
2799
2808
 
2800
- let flippedPt = flippedScreenPoint(event)
2801
2809
  if let editor = controller.editor,
2802
2810
  let hit = canvasHit(flippedScreenPt: flippedPt, editor: editor),
2803
2811
  hoveredWindowId == hit.id {
@@ -3042,6 +3050,7 @@ struct ScreenMapView: View {
3042
3050
  // MARK: - Hit Test / Coordinate Conversion
3043
3051
 
3044
3052
  private func canvasHit(flippedScreenPt: CGPoint, editor: ScreenMapEditorState) -> CanvasHit? {
3053
+ guard isCanvasPoint(flippedScreenPt) else { return nil }
3045
3054
  let projection = CanvasProjection(editor: editor)
3046
3055
  guard projection.scale > 0 else { return nil }
3047
3056
  let canvasLocal = CGPoint(
@@ -3059,6 +3068,10 @@ struct ScreenMapView: View {
3059
3068
  return nil
3060
3069
  }
3061
3070
 
3071
+ private func isCanvasPoint(_ point: CGPoint) -> Bool {
3072
+ CGRect(origin: screenMapCanvasOrigin, size: screenMapCanvasSize).contains(point)
3073
+ }
3074
+
3062
3075
  private func detectDragMode(mapPoint: CGPoint, windowMapRect: CGRect) -> CanvasDragMode {
3063
3076
  let w = windowMapRect.width
3064
3077
  let h = windowMapRect.height
@@ -2,55 +2,12 @@ import AppKit
2
2
  import Combine
3
3
  import SwiftUI
4
4
 
5
- // MARK: - Panel subclass (handles keyDown when focused)
6
-
7
- final class VoicePanel: NSPanel {
8
- var onKeyDown: ((NSEvent) -> Void)?
9
- var onFlagsChanged: ((NSEvent) -> Void)?
10
-
11
- override var canBecomeKey: Bool { true }
12
- override var canBecomeMain: Bool { true }
13
-
14
- override func sendEvent(_ event: NSEvent) {
15
- if event.type == .leftMouseDown || event.type == .rightMouseDown {
16
- if !NSApp.isActive {
17
- NSApp.activate(ignoringOtherApps: true)
18
- }
19
- if !isKeyWindow {
20
- makeKey()
21
- }
22
- }
23
- super.sendEvent(event)
24
- }
25
-
26
- override func keyDown(with event: NSEvent) {
27
- if let handler = onKeyDown {
28
- handler(event)
29
- } else {
30
- super.keyDown(with: event)
31
- }
32
- }
33
-
34
- override func flagsChanged(with event: NSEvent) {
35
- if let handler = onFlagsChanged {
36
- handler(event)
37
- } else {
38
- super.flagsChanged(with: event)
39
- }
40
- }
41
- }
42
-
43
- private final class VoiceHostingView<Content: View>: NSHostingView<Content> {
44
- override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
45
- override var focusRingType: NSFocusRingType { get { .none } set {} }
46
- }
47
-
48
5
  // MARK: - Window Controller
49
6
 
50
7
  final class VoiceCommandWindow {
51
8
  static let shared = VoiceCommandWindow()
52
9
 
53
- private(set) var panel: VoicePanel?
10
+ private(set) var panel: OverlayPanel?
54
11
  private var keyMonitor: Any?
55
12
  private var state: VoiceCommandState?
56
13
 
@@ -66,7 +23,7 @@ final class VoiceCommandWindow {
66
23
 
67
24
  func show() {
68
25
  // If panel exists but is hidden, just re-show it
69
- if let p = panel, let s = state {
26
+ if let p = panel, state != nil {
70
27
  p.alphaValue = 0
71
28
  p.orderFrontRegardless()
72
29
  NSAnimationContext.runAnimationGroup { ctx in
@@ -87,42 +44,32 @@ final class VoiceCommandWindow {
87
44
  .preferredColorScheme(.dark)
88
45
 
89
46
  let mouseLocation = NSEvent.mouseLocation
90
- let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) ?? NSScreen.main ?? NSScreen.screens.first!
47
+ let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) })
48
+ ?? NSScreen.main
49
+ ?? NSScreen.screens.first!
91
50
  let visible = screen.visibleFrame
92
-
93
51
  let panelWidth: CGFloat = min(900, visible.width - 80)
94
52
  let panelHeight: CGFloat = min(560, visible.height - 80)
95
53
 
96
- let p = VoicePanel(
97
- contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight),
98
- styleMask: [.titled, .nonactivatingPanel],
99
- backing: .buffered,
100
- defer: false
54
+ let p = OverlayPanelShell.makePanel(
55
+ config: .init(
56
+ size: NSSize(width: panelWidth, height: panelHeight),
57
+ styleMask: [.titled, .nonactivatingPanel],
58
+ titleVisible: .hidden,
59
+ titlebarAppearsTransparent: true,
60
+ background: .clear,
61
+ hidesOnDeactivate: false,
62
+ isMovableByWindowBackground: true,
63
+ activatesOnMouseDown: true,
64
+ onKeyDown: { [weak self] event in self?.handleKey(event) },
65
+ onFlagsChanged: { [weak self] event in self?.handleFlags(event) }
66
+ ),
67
+ rootView: view
101
68
  )
102
- p.onKeyDown = { [weak self] event in self?.handleKey(event) }
103
- p.onFlagsChanged = { [weak self] event in self?.handleFlags(event) }
104
- p.titlebarAppearsTransparent = true
105
- p.titleVisibility = .hidden
106
- p.isOpaque = false
107
- p.backgroundColor = .clear
108
- p.level = .floating
109
- p.hasShadow = true
110
- p.hidesOnDeactivate = false
111
- p.isReleasedWhenClosed = false
112
- p.isMovableByWindowBackground = true
113
- let hosting = VoiceHostingView(rootView: view)
114
- hosting.translatesAutoresizingMaskIntoConstraints = false
115
- p.contentView = hosting
116
-
117
- // Position: top-center of screen
118
- let x = visible.midX - panelWidth / 2
119
- let y = visible.maxY - panelHeight - 40
120
- p.setFrameOrigin(NSPoint(x: x, y: y))
69
+ OverlayPanelShell.position(p, placement: .topCenter(margin: 40))
121
70
 
122
71
  p.alphaValue = 0
123
- p.orderFrontRegardless()
124
- p.makeKey()
125
- NSApp.activate(ignoringOtherApps: true)
72
+ OverlayPanelShell.present(p, activate: true, makeKey: true, orderFrontRegardless: true)
126
73
 
127
74
  NSAnimationContext.runAnimationGroup { ctx in
128
75
  ctx.duration = 0.15
@@ -560,6 +507,7 @@ final class VoiceCommandState: ObservableObject {
560
507
 
561
508
  struct VoiceCommandView: View {
562
509
  @ObservedObject var state: VoiceCommandState
510
+ @ObservedObject private var activeSelection = WindowSelectionStore.shared
563
511
  let onDismiss: () -> Void
564
512
 
565
513
  private let docsURL = "https://lattices.dev/docs/voice"
@@ -884,6 +832,30 @@ struct VoiceCommandView: View {
884
832
  VStack(alignment: .leading, spacing: 14) {
885
833
  // Zero-height spacer forces VStack to fill ScrollView width
886
834
  Color.clear.frame(maxWidth: .infinity, maxHeight: 0)
835
+ if activeSelection.isActive {
836
+ commandSection("selection") {
837
+ VStack(alignment: .leading, spacing: 6) {
838
+ HStack(spacing: 6) {
839
+ Text("\(activeSelection.count) window\(activeSelection.count == 1 ? "" : "s")")
840
+ .font(Typo.geistMonoBold(11))
841
+ .foregroundColor(Palette.running)
842
+ if let source = activeSelection.sourceLabel {
843
+ Text(source)
844
+ .font(Typo.geistMono(10))
845
+ .foregroundColor(Palette.textMuted)
846
+ }
847
+ }
848
+ Text(activeSelection.summary(maxItems: 4))
849
+ .font(Typo.geistMono(11))
850
+ .foregroundColor(Palette.textDim)
851
+ .lineLimit(3)
852
+ Text("Try: grid that in the bottom half")
853
+ .font(Typo.geistMono(10))
854
+ .foregroundColor(Palette.textMuted)
855
+ }
856
+ }
857
+ }
858
+
887
859
  // Partial transcript (while listening)
888
860
  if state.phase == .listening, !state.partialText.isEmpty {
889
861
  commandSection("hearing...") {
@@ -66,12 +66,40 @@ struct GridFile: Codable {
66
66
  let snapZones: SnapZonesConfig?
67
67
  }
68
68
 
69
- enum SnapModifierKey: String, Codable, Equatable {
69
+ enum SnapModifierKey: String, Codable, Equatable, CaseIterable, Identifiable {
70
70
  case command
71
71
  case option
72
72
  case control
73
73
  case shift
74
74
 
75
+ var id: String { rawValue }
76
+
77
+ var label: String {
78
+ switch self {
79
+ case .command:
80
+ return "Command"
81
+ case .option:
82
+ return "Option"
83
+ case .control:
84
+ return "Control"
85
+ case .shift:
86
+ return "Shift"
87
+ }
88
+ }
89
+
90
+ var shortLabel: String {
91
+ switch self {
92
+ case .command:
93
+ return "Cmd"
94
+ case .option:
95
+ return "Opt"
96
+ case .control:
97
+ return "Ctrl"
98
+ case .shift:
99
+ return "Shift"
100
+ }
101
+ }
102
+
75
103
  var eventFlags: NSEvent.ModifierFlags {
76
104
  switch self {
77
105
  case .command:
@@ -98,9 +126,6 @@ enum SnapModifierKey: String, Codable, Equatable {
98
126
  }
99
127
  }
100
128
 
101
- var label: String {
102
- rawValue.capitalized
103
- }
104
129
  }
105
130
 
106
131
  enum SnapZoneTriggerSpec: Codable, Equatable {
@@ -459,6 +484,36 @@ class WorkspaceManager: ObservableObject {
459
484
  self.snapZonesConfig = snapZones
460
485
  }
461
486
 
487
+ func updateSnapModifier(_ modifier: SnapModifierKey) {
488
+ let updated = SnapZonesConfig(
489
+ enabled: snapZonesConfig.enabled,
490
+ modifier: modifier,
491
+ zoneOpacity: snapZonesConfig.zoneOpacity,
492
+ highlightOpacity: snapZonesConfig.highlightOpacity,
493
+ previewOpacity: snapZonesConfig.previewOpacity,
494
+ cornerRadius: snapZonesConfig.cornerRadius,
495
+ rules: snapZonesConfig.rules
496
+ )
497
+
498
+ do {
499
+ let url = URL(fileURLWithPath: snapZonesConfigPath)
500
+ try FileManager.default.createDirectory(
501
+ at: url.deletingLastPathComponent(),
502
+ withIntermediateDirectories: true
503
+ )
504
+
505
+ let encoder = JSONEncoder()
506
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
507
+ let data = try encoder.encode(updated)
508
+ try data.write(to: url, options: .atomic)
509
+
510
+ loadGridConfig()
511
+ DiagnosticLog.shared.info("WorkspaceManager: updated snap modifier to \(modifier.rawValue)")
512
+ } catch {
513
+ DiagnosticLog.shared.error("WorkspaceManager: failed to write snap-zones.json — \(error.localizedDescription)")
514
+ }
515
+ }
516
+
462
517
  /// Resolve a tile string to fractions: check user presets first, then built-in TilePosition
463
518
  func resolveTileFractions(_ tile: String) -> (CGFloat, CGFloat, CGFloat, CGFloat)? {
464
519
  resolvePlacement(tile)?.fractions