@lattices/cli 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/app/Info.plist +2 -2
  2. package/app/Lattices.app/Contents/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  4. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +4 -0
  5. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +10 -1
  6. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +2 -1
  7. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +44 -26
  8. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  9. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  10. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  11. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +2 -8
  12. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  13. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  14. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  15. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  16. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +24 -108
  17. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  18. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +488 -55
  19. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  20. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  21. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  22. package/app/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +94 -0
  23. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  24. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +25 -1
  25. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -74
  26. package/docs/component-extraction-roadmap.md +392 -0
  27. package/package.json +3 -1
  28. package/app/Sources/CommandPaletteWindow.swift +0 -134
  29. package/app/Sources/OmniSearchWindow.swift +0 -165
  30. /package/app/Sources/{App.swift → AppShell/App.swift} +0 -0
  31. /package/app/Sources/{AppUpdater.swift → AppShell/AppUpdater.swift} +0 -0
  32. /package/app/Sources/{CliActionLauncher.swift → AppShell/CliActionLauncher.swift} +0 -0
  33. /package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +0 -0
  34. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  35. /package/app/Sources/{LatticesRuntime.swift → AppShell/LatticesRuntime.swift} +0 -0
  36. /package/app/Sources/{MainView.swift → AppShell/MainView.swift} +0 -0
  37. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  38. /package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +0 -0
  39. /package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +0 -0
  40. /package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +0 -0
  41. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  42. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  43. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  44. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  45. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  46. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  47. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  48. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  49. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  50. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  51. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  52. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  53. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  54. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  55. /package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +0 -0
  56. /package/app/Sources/{CompanionActivityLog.swift → Core/Companion/CompanionActivityLog.swift} +0 -0
  57. /package/app/Sources/{CompanionKeyboardController.swift → Core/Companion/CompanionKeyboardController.swift} +0 -0
  58. /package/app/Sources/{LatticesCompanionBridgeServer.swift → Core/Companion/LatticesCompanionBridgeServer.swift} +0 -0
  59. /package/app/Sources/{LatticesCompanionCockpit.swift → Core/Companion/LatticesCompanionCockpit.swift} +0 -0
  60. /package/app/Sources/{LatticesCompanionSecurityCoordinator.swift → Core/Companion/LatticesCompanionSecurityCoordinator.swift} +0 -0
  61. /package/app/Sources/{LatticesCompanionTrackpadController.swift → Core/Companion/LatticesCompanionTrackpadController.swift} +0 -0
  62. /package/app/Sources/{LatticesDeckHost.swift → Core/Companion/LatticesDeckHost.swift} +0 -0
  63. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  64. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  65. /package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +0 -0
  66. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  67. /package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +0 -0
  68. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  69. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  70. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  71. /package/app/Sources/{MouseFinder.swift → Core/Desktop/MouseFinder.swift} +0 -0
  72. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  73. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  74. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  75. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  76. /package/app/Sources/{WindowDragSnapController.swift → Core/Desktop/WindowDragSnapController.swift} +0 -0
  77. /package/app/Sources/{MouseGestureConfig.swift → Core/Input/MouseGestureConfig.swift} +0 -0
  78. /package/app/Sources/{MouseGestureController.swift → Core/Input/MouseGestureController.swift} +0 -0
  79. /package/app/Sources/{MouseInputDeviceStore.swift → Core/Input/MouseInputDeviceStore.swift} +0 -0
  80. /package/app/Sources/{MouseInputEventViewer.swift → Core/Input/MouseInputEventViewer.swift} +0 -0
  81. /package/app/Sources/{MouseShortcutStore.swift → Core/Input/MouseShortcutStore.swift} +0 -0
  82. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  83. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  84. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  85. /package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +0 -0
  86. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  87. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  88. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  89. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  90. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  91. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  92. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  93. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  94. /package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +0 -0
  95. /package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +0 -0
  96. /package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +0 -0
  97. /package/app/Sources/{PiAuthNextStepCard.swift → Core/Pi/PiAuthNextStepCard.swift} +0 -0
  98. /package/app/Sources/{PiAuthPromptCard.swift → Core/Pi/PiAuthPromptCard.swift} +0 -0
  99. /package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +0 -0
  100. /package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +0 -0
  101. /package/app/Sources/{PiInstallCallout.swift → Core/Pi/PiInstallCallout.swift} +0 -0
  102. /package/app/Sources/{PiProviderSetupCallout.swift → Core/Pi/PiProviderSetupCallout.swift} +0 -0
  103. /package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +0 -0
  104. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  105. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  106. /package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +0 -0
  107. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  108. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  109. /package/app/Sources/{SystemTelemetryMonitor.swift → Core/System/SystemTelemetryMonitor.swift} +0 -0
  110. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  111. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  112. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  113. /package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +0 -0
  114. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  115. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  116. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  117. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  118. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  119. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  120. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  121. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  122. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  123. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  124. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  125. /package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +0 -0
  126. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  127. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  128. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  129. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  130. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -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 })
@@ -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...") {