@lattices/cli 0.4.2 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/AppShell/App.swift +20 -0
  7. package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
  8. package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
  9. package/app/Sources/AppShell/AppUpdater.swift +92 -0
  10. package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
  11. package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
  12. package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
  13. package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
  14. package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
  15. package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
  16. package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
  17. package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
  18. package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
  19. package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
  20. package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
  21. package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
  22. package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
  23. package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
  24. package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
  25. package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
  26. package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
  27. package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
  28. package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
  29. package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
  30. package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
  31. package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
  32. package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
  33. package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
  34. package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
  35. package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
  36. package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
  37. package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
  38. package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
  39. package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
  40. package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
  41. package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
  42. package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
  43. package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
  44. package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
  45. package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
  46. package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
  47. package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
  48. package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
  49. package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
  50. package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
  51. package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
  52. package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
  53. package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
  54. package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
  55. package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
  56. package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
  57. package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
  58. package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
  59. package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
  60. package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
  61. package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
  62. package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
  63. package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
  64. package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
  65. package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
  66. package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
  67. package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
  68. package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
  69. package/bin/assistant-intelligence.ts +874 -0
  70. package/bin/handsoff-infer.ts +16 -209
  71. package/bin/handsoff-worker.ts +45 -258
  72. package/bin/lattices-app.ts +62 -0
  73. package/bin/lattices-dev +4 -0
  74. package/bin/lattices.ts +125 -14
  75. package/docs/agents.md +14 -0
  76. package/docs/api.md +55 -0
  77. package/docs/app.md +3 -0
  78. package/docs/companion-deck.md +180 -0
  79. package/docs/component-extraction-roadmap.md +392 -0
  80. package/docs/config.md +25 -0
  81. package/docs/tiling-reference.md +55 -0
  82. package/docs/voice-error-model.md +73 -0
  83. package/package.json +4 -1
  84. package/app/Sources/App.swift +0 -10
  85. package/app/Sources/CommandPaletteWindow.swift +0 -134
  86. package/app/Sources/MouseFinder.swift +0 -222
  87. /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
  88. /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
  89. /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
  90. /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
  91. /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
  92. /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
  93. /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
  94. /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
  95. /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
  96. /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
  97. /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
  98. /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
  99. /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
  100. /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
  101. /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
  102. /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
  103. /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
  104. /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
  105. /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
  106. /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
  107. /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
  108. /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
  109. /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
  110. /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
  111. /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
  112. /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
  113. /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
  114. /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
  115. /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
  116. /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
  117. /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
  118. /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
  119. /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
  120. /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
  121. /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
  122. /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
  123. /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
  124. /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
  125. /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
  126. /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
  127. /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
  128. /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
  129. /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
  130. /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
  131. /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
  132. /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
  133. /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
  134. /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
  135. /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
  136. /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
  137. /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
  138. /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
  139. /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
  140. /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
  141. /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
  142. /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
  143. /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
  144. /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
  145. /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
  146. /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
@@ -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
+ }
@@ -1,4 +1,5 @@
1
1
  import AppKit
2
+ import Combine
2
3
  import Foundation
3
4
  import SwiftUI
4
5
 
@@ -251,7 +252,8 @@ final class ScreenMapEditorState: ObservableObject {
251
252
  static let minZoom: CGFloat = 0.3
252
253
  static let maxZoom: CGFloat = 5.0
253
254
 
254
- var effectiveScale: CGFloat { scale * zoomLevel }
255
+ /// `scale` is the synced effective canvas scale (fit scale × zoom).
256
+ var effectiveScale: CGFloat { scale }
255
257
 
256
258
  func resetZoomPan() {
257
259
  zoomLevel = 1.0
@@ -330,6 +332,12 @@ final class ScreenMapEditorState: ObservableObject {
330
332
  effectiveLayers.count
331
333
  }
332
334
 
335
+ /// Total windows in the current display scope before any layer filter is applied.
336
+ var scopedWindowCount: Int {
337
+ guard let dIdx = focusedDisplayIndex else { return windows.count }
338
+ return windows.filter { $0.displayIndex == dIdx }.count
339
+ }
340
+
333
341
  /// Window count for a layer, scoped to the focused display
334
342
  func effectiveWindowCount(for layer: Int) -> Int {
335
343
  guard let dIdx = focusedDisplayIndex else {
@@ -338,6 +346,27 @@ final class ScreenMapEditorState: ObservableObject {
338
346
  return windows.filter { $0.layer == layer && $0.displayIndex == dIdx }.count
339
347
  }
340
348
 
349
+ /// Windows currently rendered in the main canvas.
350
+ var renderedCanvasWindows: [ScreenMapWindowEntry] {
351
+ focusedDisplayIndex != nil ? focusedVisibleWindows : visibleWindows
352
+ }
353
+
354
+ var namedEffectiveLayers: [Int] {
355
+ effectiveLayers.filter { layerNames[$0] != nil }
356
+ }
357
+
358
+ var unnamedEffectiveLayers: [Int] {
359
+ effectiveLayers.filter { layerNames[$0] == nil }
360
+ }
361
+
362
+ func layerTreeWindows(for layer: Int) -> [ScreenMapWindowEntry] {
363
+ var scoped = windows.filter { $0.layer == layer }
364
+ if let dIdx = focusedDisplayIndex {
365
+ scoped = scoped.filter { $0.displayIndex == dIdx }
366
+ }
367
+ return scoped.sorted { $0.zIndex < $1.zIndex }
368
+ }
369
+
341
370
  /// Visible window count per display index
342
371
  func visibleWindowCount(for displayIndex: Int) -> Int {
343
372
  visibleWindows.filter { $0.displayIndex == displayIndex }.count
@@ -510,6 +539,12 @@ final class ScreenMapEditorState: ObservableObject {
510
539
  return regions
511
540
  }
512
541
 
542
+ func displayRegion(for displayIndex: Int) -> ScreenMapCanvasRegion? {
543
+ canvasExplorerRegions.first {
544
+ $0.kind == .display && $0.displayIndex == displayIndex
545
+ }
546
+ }
547
+
513
548
  private func regionRect(for windows: [ScreenMapWindowEntry], fallback: CGRect, padding: CGFloat) -> CGRect {
514
549
  guard !windows.isEmpty else { return fallback.insetBy(dx: -padding, dy: -padding) }
515
550
 
@@ -1350,8 +1385,12 @@ final class ScreenMapActionLog {
1350
1385
  // MARK: - Screen Map Controller
1351
1386
 
1352
1387
  final class ScreenMapController: ObservableObject {
1353
- @Published var editor: ScreenMapEditorState?
1354
- @Published var selectedWindowIds: Set<UInt32> = []
1388
+ @Published var editor: ScreenMapEditorState? {
1389
+ didSet { bindEditor() }
1390
+ }
1391
+ @Published var selectedWindowIds: Set<UInt32> = [] {
1392
+ didSet { syncSharedSelection() }
1393
+ }
1355
1394
  @Published var windowSets: [ScreenMapWindowSet] = []
1356
1395
  @Published var activeWindowSetID: UUID? = nil
1357
1396
  @Published var flashMessage: String? = nil
@@ -1364,6 +1403,7 @@ final class ScreenMapController: ObservableObject {
1364
1403
  case left, right, none
1365
1404
  }
1366
1405
  @Published var displayTransition: DisplayTransitionDirection = .none
1406
+ private var editorObserver: AnyCancellable?
1367
1407
 
1368
1408
  var previewWindow: NSWindow? = nil
1369
1409
  private var previewGlobalMonitor: Any? = nil
@@ -1371,6 +1411,52 @@ final class ScreenMapController: ObservableObject {
1371
1411
 
1372
1412
  var onDismiss: (() -> Void)?
1373
1413
 
1414
+ enum DisplayFocusDirection {
1415
+ case previous
1416
+ case next
1417
+ }
1418
+
1419
+ private func bindEditor() {
1420
+ editorObserver = editor?.objectWillChange.sink { [weak self] _ in
1421
+ self?.objectWillChange.send()
1422
+ }
1423
+ }
1424
+
1425
+ private func finalizeDisplayFocusChange(flashLabel: Bool) {
1426
+ guard let ed = editor else { return }
1427
+ focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
1428
+ if flashLabel {
1429
+ flash(ed.focusedDisplay?.label ?? "All displays")
1430
+ }
1431
+ objectWillChange.send()
1432
+ }
1433
+
1434
+ func setDisplayFocus(_ index: Int?, flashLabel: Bool = false) {
1435
+ guard let ed = editor else { return }
1436
+ ed.focusDisplay(index)
1437
+ finalizeDisplayFocusChange(flashLabel: flashLabel)
1438
+ }
1439
+
1440
+ func stepDisplayFocus(_ direction: DisplayFocusDirection, flashLabel: Bool = true) {
1441
+ guard let ed = editor, ed.displays.count > 1 else { return }
1442
+ switch direction {
1443
+ case .previous:
1444
+ ed.cyclePreviousDisplay()
1445
+ case .next:
1446
+ ed.cycleNextDisplay()
1447
+ }
1448
+ finalizeDisplayFocusChange(flashLabel: flashLabel)
1449
+ }
1450
+
1451
+ func adjustZoom(by delta: CGFloat) {
1452
+ guard let ed = editor else { return }
1453
+ let newZoom = max(ScreenMapEditorState.minZoom, min(ScreenMapEditorState.maxZoom, ed.zoomLevel + delta))
1454
+ guard newZoom != ed.zoomLevel else { return }
1455
+ ed.activeViewportPreset = nil
1456
+ ed.zoomLevel = newZoom
1457
+ objectWillChange.send()
1458
+ }
1459
+
1374
1460
  // MARK: - Selection
1375
1461
 
1376
1462
  func isSelected(_ id: UInt32) -> Bool { selectedWindowIds.contains(id) }
@@ -1400,6 +1486,28 @@ final class ScreenMapController: ObservableObject {
1400
1486
  activeWindowSetID = nil
1401
1487
  }
1402
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
+
1403
1511
  func selectNextWindow() {
1404
1512
  guard let ed = editor else { return }
1405
1513
  let wins = ed.focusedVisibleWindows.sorted(by: { $0.zIndex < $1.zIndex })
@@ -1616,7 +1724,7 @@ final class ScreenMapController: ObservableObject {
1616
1724
  guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) else { continue }
1617
1725
  guard rect.width >= 100 && rect.height >= 50 else { continue }
1618
1726
  let app = info[kCGWindowOwnerName as String] as? String ?? ""
1619
- if app == "Lattices" || app == "lattices" || app == "Lattices" { continue }
1727
+ if app == "Lattices" || app == "lattices" || app == "AutoFill" { continue }
1620
1728
  let pid = info[kCGWindowOwnerPID as String] as? Int32 ?? 0
1621
1729
  let title = info[kCGWindowName as String] as? String ?? ""
1622
1730
  let dIdx = displayIndex(for: rect)
@@ -2052,23 +2160,11 @@ final class ScreenMapController: ObservableObject {
2052
2160
  // MARK: Right hand — Navigation
2053
2161
 
2054
2162
  case 4: // h → previous display
2055
- if let ed = editor, ed.displays.count > 1 {
2056
- ed.cyclePreviousDisplay()
2057
- focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
2058
- let label = ed.focusedDisplay?.label ?? "All displays"
2059
- flash(label)
2060
- objectWillChange.send()
2061
- }
2163
+ stepDisplayFocus(.previous)
2062
2164
  return true
2063
2165
 
2064
2166
  case 37: // l → next display
2065
- if let ed = editor, ed.displays.count > 1 {
2066
- ed.cycleNextDisplay()
2067
- focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
2068
- let label = ed.focusedDisplay?.label ?? "All displays"
2069
- flash(label)
2070
- objectWillChange.send()
2071
- }
2167
+ stepDisplayFocus(.next)
2072
2168
  return true
2073
2169
 
2074
2170
  case 38: // j → next layer
@@ -2213,23 +2309,11 @@ final class ScreenMapController: ObservableObject {
2213
2309
  return true
2214
2310
 
2215
2311
  case 123: // ← previous display (secondary)
2216
- if let ed = editor, ed.displays.count > 1 {
2217
- ed.cyclePreviousDisplay()
2218
- focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
2219
- let label = ed.focusedDisplay?.label ?? "All displays"
2220
- flash(label)
2221
- objectWillChange.send()
2222
- }
2312
+ stepDisplayFocus(.previous)
2223
2313
  return true
2224
2314
 
2225
2315
  case 124: // → next display (secondary)
2226
- if let ed = editor, ed.displays.count > 1 {
2227
- ed.cycleNextDisplay()
2228
- focusViewportPreset(ed.activeViewportPreset ?? .main, flashView: false)
2229
- let label = ed.focusedDisplay?.label ?? "All displays"
2230
- flash(label)
2231
- objectWillChange.send()
2232
- }
2316
+ stepDisplayFocus(.next)
2233
2317
  return true
2234
2318
 
2235
2319
  case 44: // / → open window search