@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,139 @@
1
+ import AppKit
2
+ import ApplicationServices
3
+ import CoreGraphics
4
+
5
+ struct LocatedWindow {
6
+ let wid: UInt32
7
+ let pid: pid_t
8
+ }
9
+
10
+ enum SessionWindowLocator {
11
+ static func tag(for session: String) -> String {
12
+ Terminal.windowTag(for: session)
13
+ }
14
+
15
+ static func extractSessionName(from title: String) -> String? {
16
+ guard let range = title.range(of: #"\[lattices:([^\]]+)\]"#, options: .regularExpression) else {
17
+ return nil
18
+ }
19
+ let match = String(title[range])
20
+ return String(match.dropFirst(10).dropLast(1))
21
+ }
22
+
23
+ static func matches(session: String, title: String, extractedSessionName: String? = nil) -> Bool {
24
+ if extractedSessionName == session {
25
+ return true
26
+ }
27
+ return title.contains(tag(for: session))
28
+ }
29
+
30
+ static func cachedWindow(forSession session: String, in windows: [UInt32: WindowEntry]) -> WindowEntry? {
31
+ windows.values.first { entry in
32
+ matches(session: session, title: entry.title, extractedSessionName: entry.latticesSession)
33
+ }
34
+ }
35
+
36
+ static func cachedWindow(forSession session: String, desktopModel: DesktopModel = .shared) -> WindowEntry? {
37
+ cachedWindow(forSession: session, in: desktopModel.windows)
38
+ }
39
+
40
+ static func findCGWindow(tag: String) -> LocatedWindow? {
41
+ guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
42
+ return nil
43
+ }
44
+
45
+ for info in windowList {
46
+ if let name = info[kCGWindowName as String] as? String,
47
+ name.contains(tag),
48
+ let wid = info[kCGWindowNumber as String] as? UInt32,
49
+ let pid = info[kCGWindowOwnerPID as String] as? pid_t {
50
+ return LocatedWindow(wid: wid, pid: pid)
51
+ }
52
+ }
53
+ return nil
54
+ }
55
+
56
+ static func findWindow(session: String, terminal: Terminal) -> LocatedWindow? {
57
+ findWindow(tag: tag(for: session), terminal: terminal)
58
+ }
59
+
60
+ static func findWindow(tag: String, terminal: Terminal) -> LocatedWindow? {
61
+ if let match = findCGWindow(tag: tag) {
62
+ return match
63
+ }
64
+
65
+ if let ax = findAXWindow(terminal: terminal, tag: tag),
66
+ let wid = matchCGWindow(pid: ax.pid, axWindow: ax.window) {
67
+ return LocatedWindow(wid: wid, pid: ax.pid)
68
+ }
69
+
70
+ return nil
71
+ }
72
+
73
+ static func findAXWindow(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
74
+ let diag = DiagnosticLog.shared
75
+ guard let app = NSWorkspace.shared.runningApplications.first(where: {
76
+ $0.bundleIdentifier == terminal.bundleId
77
+ }) else {
78
+ diag.error("SessionWindowLocator.findAXWindow: \(terminal.rawValue) (\(terminal.bundleId)) not running")
79
+ return nil
80
+ }
81
+
82
+ let pid = app.processIdentifier
83
+ let appRef = AXUIElementCreateApplication(pid)
84
+ var windowsRef: CFTypeRef?
85
+ let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
86
+ guard err == .success, let windows = windowsRef as? [AXUIElement] else {
87
+ diag.error("SessionWindowLocator.findAXWindow: AX error \(err.rawValue) — Accessibility not granted?")
88
+ return nil
89
+ }
90
+
91
+ diag.info("SessionWindowLocator.findAXWindow: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
92
+ for win in windows {
93
+ var titleRef: CFTypeRef?
94
+ AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
95
+ let title = titleRef as? String ?? "<no title>"
96
+ if title.contains(tag) {
97
+ diag.success("SessionWindowLocator.findAXWindow: matched \"\(title)\"")
98
+ return (pid, win)
99
+ } else {
100
+ diag.info(" skip: \"\(title)\"")
101
+ }
102
+ }
103
+
104
+ diag.warn("SessionWindowLocator.findAXWindow: no window matched tag \(tag)")
105
+ return nil
106
+ }
107
+
108
+ static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
109
+ var posRef: CFTypeRef?
110
+ var sizeRef: CFTypeRef?
111
+ AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
112
+ AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
113
+ guard let pv = posRef, let sv = sizeRef else { return nil }
114
+
115
+ var pos = CGPoint.zero
116
+ var size = CGSize.zero
117
+ AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
118
+ AXValueGetValue(sv as! AXValue, .cgSize, &size)
119
+
120
+ guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
121
+ return nil
122
+ }
123
+
124
+ for info in windowList {
125
+ guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
126
+ wPid == pid,
127
+ let wid = info[kCGWindowNumber as String] as? UInt32,
128
+ let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
129
+ var rect = CGRect.zero
130
+ if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
131
+ if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
132
+ abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
133
+ return wid
134
+ }
135
+ }
136
+ }
137
+ return nil
138
+ }
139
+ }
@@ -0,0 +1,100 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ struct WindowPreviewCardStyle {
5
+ var containerCornerRadius: CGFloat = 10
6
+ var imageCornerRadius: CGFloat = 8
7
+ var imagePadding: CGFloat = 8
8
+ var background: Color = Palette.surface.opacity(0.8)
9
+ var border: Color = Palette.border
10
+ }
11
+
12
+ struct WindowPreviewCard<Overlay: View>: View {
13
+ let image: NSImage?
14
+ let isLoading: Bool
15
+ let appName: String
16
+ var loadingTitle: String = "Capturing preview"
17
+ var unavailableTitle: String = "Preview unavailable"
18
+ var style: WindowPreviewCardStyle = WindowPreviewCardStyle()
19
+ var holdingPreviousPreview: Bool = false
20
+ @ViewBuilder let overlay: () -> Overlay
21
+
22
+ var body: some View {
23
+ ZStack {
24
+ RoundedRectangle(cornerRadius: style.containerCornerRadius)
25
+ .fill(style.background)
26
+ .overlay(
27
+ RoundedRectangle(cornerRadius: style.containerCornerRadius)
28
+ .strokeBorder(style.border, lineWidth: 0.5)
29
+ )
30
+
31
+ if let image {
32
+ Image(nsImage: image)
33
+ .resizable()
34
+ .aspectRatio(contentMode: .fit)
35
+ .clipShape(RoundedRectangle(cornerRadius: style.imageCornerRadius))
36
+ .padding(style.imagePadding)
37
+ .opacity(holdingPreviousPreview ? 0.88 : 1)
38
+ } else if isLoading {
39
+ WindowPreviewPlaceholder(
40
+ icon: "photo",
41
+ title: loadingTitle,
42
+ subtitle: appName
43
+ )
44
+ } else {
45
+ WindowPreviewPlaceholder(
46
+ icon: "eye.slash",
47
+ title: unavailableTitle,
48
+ subtitle: appName
49
+ )
50
+ }
51
+
52
+ overlay()
53
+ }
54
+ }
55
+ }
56
+
57
+ extension WindowPreviewCard where Overlay == EmptyView {
58
+ init(
59
+ image: NSImage?,
60
+ isLoading: Bool,
61
+ appName: String,
62
+ loadingTitle: String = "Capturing preview",
63
+ unavailableTitle: String = "Preview unavailable",
64
+ style: WindowPreviewCardStyle = WindowPreviewCardStyle(),
65
+ holdingPreviousPreview: Bool = false
66
+ ) {
67
+ self.init(
68
+ image: image,
69
+ isLoading: isLoading,
70
+ appName: appName,
71
+ loadingTitle: loadingTitle,
72
+ unavailableTitle: unavailableTitle,
73
+ style: style,
74
+ holdingPreviousPreview: holdingPreviousPreview,
75
+ overlay: { EmptyView() }
76
+ )
77
+ }
78
+ }
79
+
80
+ private struct WindowPreviewPlaceholder: View {
81
+ let icon: String
82
+ let title: String
83
+ let subtitle: String
84
+
85
+ var body: some View {
86
+ VStack(spacing: 8) {
87
+ Image(systemName: icon)
88
+ .font(.system(size: 18, weight: .medium))
89
+ .foregroundColor(Palette.textMuted.opacity(0.7))
90
+ Text(title)
91
+ .font(Typo.monoBold(10))
92
+ .foregroundColor(Palette.textMuted)
93
+ Text(subtitle)
94
+ .font(Typo.mono(9))
95
+ .foregroundColor(Palette.textDim)
96
+ .lineLimit(1)
97
+ }
98
+ .padding(16)
99
+ }
100
+ }
@@ -0,0 +1,113 @@
1
+ import AppKit
2
+ import SwiftUI
3
+
4
+ final class WindowPreviewStore: ObservableObject {
5
+ static let shared = WindowPreviewStore()
6
+
7
+ @Published private var images: [UInt32: NSImage] = [:]
8
+ @Published private var loading: Set<UInt32> = []
9
+
10
+ private var lastAttemptAt: [UInt32: Date] = [:]
11
+ private var accessOrder: [UInt32] = []
12
+ private let maxCached = 15
13
+ private let queue = DispatchQueue(label: "com.arach.lattices.window-preview", qos: .userInitiated)
14
+ private let previewMaxSize = NSSize(width: 360, height: 190)
15
+
16
+ func image(for wid: UInt32) -> NSImage? {
17
+ if images[wid] != nil {
18
+ touchLRU(wid)
19
+ }
20
+ return images[wid]
21
+ }
22
+
23
+ func hasSettled(_ wid: UInt32) -> Bool {
24
+ images[wid] != nil || (lastAttemptAt[wid] != nil && !loading.contains(wid))
25
+ }
26
+
27
+ func isLoading(_ wid: UInt32) -> Bool {
28
+ loading.contains(wid)
29
+ }
30
+
31
+ func prewarm(windows: [WindowEntry], limit: Int = 4) {
32
+ for window in windows.prefix(limit) {
33
+ load(window: window)
34
+ }
35
+ }
36
+
37
+ func load(window: WindowEntry) {
38
+ if images[window.wid] != nil || loading.contains(window.wid) {
39
+ return
40
+ }
41
+
42
+ let now = Date()
43
+ if let lastAttemptAt = lastAttemptAt[window.wid], now.timeIntervalSince(lastAttemptAt) < 1.0 {
44
+ return
45
+ }
46
+ lastAttemptAt[window.wid] = now
47
+
48
+ loading.insert(window.wid)
49
+ let wid = window.wid
50
+ let frame = window.frame
51
+ let startedAt = Date()
52
+
53
+ queue.async { [weak self] in
54
+ guard let self else { return }
55
+
56
+ let cgImage = CGWindowListCreateImage(
57
+ .null,
58
+ .optionIncludingWindow,
59
+ CGWindowID(wid),
60
+ [.boundsIgnoreFraming, .nominalResolution]
61
+ )
62
+
63
+ let image = cgImage.map {
64
+ NSImage(
65
+ cgImage: $0,
66
+ size: self.previewSize(for: frame)
67
+ )
68
+ }
69
+
70
+ DispatchQueue.main.async {
71
+ self.loading.remove(wid)
72
+ let elapsedMs = Int(Date().timeIntervalSince(startedAt) * 1000)
73
+ if let image {
74
+ self.images[wid] = image
75
+ self.touchLRU(wid)
76
+ self.evictIfNeeded()
77
+ if elapsedMs >= 80 {
78
+ DiagnosticLog.shared.info("HUDPreview: captured wid=\(wid) in \(elapsedMs)ms")
79
+ }
80
+ } else {
81
+ DiagnosticLog.shared.info("HUDPreview: capture unavailable wid=\(wid) after \(elapsedMs)ms")
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ private func touchLRU(_ wid: UInt32) {
88
+ accessOrder.removeAll { $0 == wid }
89
+ accessOrder.append(wid)
90
+ }
91
+
92
+ private func evictIfNeeded() {
93
+ while images.count > maxCached, let oldest = accessOrder.first {
94
+ accessOrder.removeFirst()
95
+ images.removeValue(forKey: oldest)
96
+ lastAttemptAt.removeValue(forKey: oldest)
97
+ }
98
+ }
99
+
100
+ private func previewSize(for frame: WindowFrame) -> NSSize {
101
+ let width = max(CGFloat(frame.w), CGFloat(1))
102
+ let height = max(CGFloat(frame.h), CGFloat(1))
103
+ let scale = min(
104
+ previewMaxSize.width / width,
105
+ previewMaxSize.height / height,
106
+ CGFloat(1)
107
+ )
108
+ return NSSize(
109
+ width: max(CGFloat(1), width * scale),
110
+ height: max(CGFloat(1), height * scale)
111
+ )
112
+ }
113
+ }
@@ -0,0 +1,76 @@
1
+ import Combine
2
+ import Foundation
3
+
4
+ struct SelectedWindowSummary: Identifiable, Equatable {
5
+ let wid: UInt32
6
+ let app: String
7
+ let title: String
8
+ let latticesSession: String?
9
+
10
+ var id: UInt32 { wid }
11
+
12
+ var displayTitle: String {
13
+ if !title.isEmpty { return title }
14
+ if let latticesSession, !latticesSession.isEmpty { return latticesSession }
15
+ return app
16
+ }
17
+ }
18
+
19
+ final class WindowSelectionStore: ObservableObject {
20
+ static let shared = WindowSelectionStore()
21
+
22
+ @Published private(set) var windows: [SelectedWindowSummary] = []
23
+ @Published private(set) var source: String?
24
+ @Published private(set) var updatedAt: Date?
25
+
26
+ private init() {}
27
+
28
+ var isActive: Bool { !windows.isEmpty }
29
+ var windowIds: [UInt32] { windows.map(\.wid) }
30
+ var count: Int { windows.count }
31
+ var sourceLabel: String? {
32
+ switch source {
33
+ case "desktop-inventory": return "window selector"
34
+ case "screen-map": return "screen map"
35
+ case let value?: return value
36
+ case nil: return nil
37
+ }
38
+ }
39
+
40
+ func setSelection(_ windows: [SelectedWindowSummary], source: String) {
41
+ let unique = Array(
42
+ Dictionary(uniqueKeysWithValues: windows.map { ($0.wid, $0) }).values
43
+ ).sorted { lhs, rhs in
44
+ if lhs.app == rhs.app { return lhs.wid < rhs.wid }
45
+ return lhs.app.localizedCaseInsensitiveCompare(rhs.app) == .orderedAscending
46
+ }
47
+
48
+ DispatchQueue.main.async {
49
+ self.windows = unique
50
+ self.source = source
51
+ self.updatedAt = Date()
52
+ }
53
+ }
54
+
55
+ func clear(source: String? = nil) {
56
+ DispatchQueue.main.async {
57
+ if let source, let current = self.source, current != source {
58
+ return
59
+ }
60
+ self.windows = []
61
+ self.source = source ?? self.source
62
+ self.updatedAt = Date()
63
+ }
64
+ }
65
+
66
+ func summary(maxItems: Int = 3) -> String {
67
+ guard !windows.isEmpty else { return "No selection" }
68
+ let titles = windows.prefix(maxItems).map { item in
69
+ item.app == item.displayTitle ? item.app : "\(item.app): \(item.displayTitle)"
70
+ }
71
+ if windows.count > maxItems {
72
+ return titles.joined(separator: " • ") + " +\(windows.count - maxItems)"
73
+ }
74
+ return titles.joined(separator: " • ")
75
+ }
76
+ }
@@ -671,18 +671,7 @@ enum WindowTiler {
671
671
 
672
672
  /// Find a window by its title tag and return its CGWindowID and owner PID
673
673
  static func findWindow(tag: String) -> (wid: UInt32, pid: pid_t)? {
674
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
675
- return nil
676
- }
677
- for info in windowList {
678
- if let name = info[kCGWindowName as String] as? String,
679
- name.contains(tag),
680
- let wid = info[kCGWindowNumber as String] as? UInt32,
681
- let pid = info[kCGWindowOwnerPID as String] as? pid_t {
682
- return (wid, pid)
683
- }
684
- }
685
- return nil
674
+ SessionWindowLocator.findCGWindow(tag: tag).map { ($0.wid, $0.pid) }
686
675
  }
687
676
 
688
677
  /// Get the space ID(s) a window is on
@@ -760,13 +749,9 @@ enum WindowTiler {
760
749
 
761
750
  // Find the window — CG first, then AX→CG fallback
762
751
  let wid: UInt32
763
- if let (w, _) = findWindow(tag: tag) {
764
- wid = w
765
- diag.info("moveWindowToSpace: found via CG wid=\(w)")
766
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
767
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
768
- wid = w
769
- diag.info("moveWindowToSpace: found via AX→CG wid=\(w)")
752
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
753
+ wid = match.wid
754
+ diag.info("moveWindowToSpace: located wid=\(match.wid) pid=\(match.pid)")
770
755
  } else {
771
756
  diag.warn("moveWindowToSpace: window not found for tag \(tag) — switching view only")
772
757
  switchToSpace(spaceId: spaceId)
@@ -834,39 +819,31 @@ enum WindowTiler {
834
819
  let tag = Terminal.windowTag(for: session)
835
820
 
836
821
  // Path 1: CG window lookup (needs Screen Recording permission for window names)
837
- if let (wid, pid) = findWindow(tag: tag) {
838
- diag.success("Path 1 (CG): found wid=\(wid) pid=\(pid)")
839
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
822
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
823
+ diag.success("Path 1/2 (locator): found wid=\(match.wid) pid=\(match.pid)")
824
+ navigateToKnownWindow(wid: match.wid, pid: match.pid, tag: tag, session: session, terminal: terminal)
840
825
  diag.finish(t)
841
826
  return
842
827
  }
843
- diag.warn("Path 1 (CG): findWindow failed — no Screen Recording?")
828
+ diag.warn("SessionWindowLocator failed — trying direct AX fallback")
844
829
 
845
- // Path 2: AX API fallback (needs Accessibility permission)
846
- if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag) {
847
- diag.success("Path 2 (AX): found window for \(terminal.rawValue) pid=\(pid)")
848
- // Try to match AX window → CG window for space switching
849
- if let wid = matchCGWindow(pid: pid, axWindow: axWindow) {
850
- diag.success("Path 2 (AX→CG): matched CG wid=\(wid)")
851
- navigateToKnownWindow(wid: wid, pid: pid, tag: tag, session: session, terminal: terminal)
830
+ if let (pid, axWindow) = SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag) {
831
+ diag.success("Direct AX fallback: found window for \(terminal.rawValue) pid=\(pid)")
832
+ AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
833
+ AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
834
+ if let app = NSRunningApplication(processIdentifier: pid) {
835
+ app.activate()
836
+ }
837
+ if let frame = axWindowFrame(axWindow) {
838
+ diag.info("Highlighting via AX frame: \(frame)")
839
+ DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
852
840
  } else {
853
- diag.warn("Path 2 (AX): no CG match raising without space switch")
854
- AXUIElementPerformAction(axWindow, kAXRaiseAction as CFString)
855
- AXUIElementSetAttributeValue(axWindow, kAXMainAttribute as CFString, kCFBooleanTrue)
856
- if let app = NSRunningApplication(processIdentifier: pid) {
857
- app.activate()
858
- }
859
- if let frame = axWindowFrame(axWindow) {
860
- diag.info("Highlighting via AX frame: \(frame)")
861
- DispatchQueue.main.async { WindowHighlight.shared.flash(frame: frame) }
862
- } else {
863
- diag.error("axWindowFrame returned nil — no highlight")
864
- }
841
+ diag.error("axWindowFrame returned nilno highlight")
865
842
  }
866
843
  diag.finish(t)
867
844
  return
868
845
  }
869
- diag.warn("Path 2 (AX): findWindowViaAX failed — no Accessibility?")
846
+ diag.warn("Direct AX fallback failed — no Accessibility?")
870
847
 
871
848
  // Path 3: AppleScript / bare activate fallback
872
849
  diag.warn("Path 3: falling back to AppleScript/activate")
@@ -894,70 +871,12 @@ enum WindowTiler {
894
871
  }
895
872
  }
896
873
 
897
- /// Find a terminal window by title tag using AX API (requires Accessibility permission)
898
874
  private static func findWindowViaAX(terminal: Terminal, tag: String) -> (pid: pid_t, window: AXUIElement)? {
899
- let diag = DiagnosticLog.shared
900
- guard let app = NSWorkspace.shared.runningApplications.first(where: {
901
- $0.bundleIdentifier == terminal.bundleId
902
- }) else {
903
- diag.error("findWindowViaAX: \(terminal.rawValue) (\(terminal.bundleId)) not running")
904
- return nil
905
- }
906
-
907
- let pid = app.processIdentifier
908
- let appRef = AXUIElementCreateApplication(pid)
909
- var windowsRef: CFTypeRef?
910
- let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
911
- guard err == .success, let windows = windowsRef as? [AXUIElement] else {
912
- diag.error("findWindowViaAX: AX error \(err.rawValue) — Accessibility not granted?")
913
- return nil
914
- }
915
-
916
- diag.info("findWindowViaAX: \(windows.count) windows for \(terminal.rawValue), searching for \(tag)")
917
- for win in windows {
918
- var titleRef: CFTypeRef?
919
- AXUIElementCopyAttributeValue(win, kAXTitleAttribute as CFString, &titleRef)
920
- let title = titleRef as? String ?? "<no title>"
921
- if title.contains(tag) {
922
- diag.success("findWindowViaAX: matched \"\(title)\"")
923
- return (pid, win)
924
- } else {
925
- diag.info(" skip: \"\(title)\"")
926
- }
927
- }
928
- diag.warn("findWindowViaAX: no window matched tag \(tag)")
929
- return nil
875
+ SessionWindowLocator.findAXWindow(terminal: terminal, tag: tag)
930
876
  }
931
877
 
932
- /// Match an AX window to its CG window ID using PID + bounds comparison
933
878
  private static func matchCGWindow(pid: pid_t, axWindow: AXUIElement) -> UInt32? {
934
- var posRef: CFTypeRef?
935
- var sizeRef: CFTypeRef?
936
- AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &posRef)
937
- AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
938
- guard let pv = posRef, let sv = sizeRef else { return nil }
939
-
940
- var pos = CGPoint.zero
941
- var size = CGSize.zero
942
- AXValueGetValue(pv as! AXValue, .cgPoint, &pos)
943
- AXValueGetValue(sv as! AXValue, .cgSize, &size)
944
-
945
- guard let windowList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else { return nil }
946
-
947
- for info in windowList {
948
- guard let wPid = info[kCGWindowOwnerPID as String] as? pid_t,
949
- wPid == pid,
950
- let wid = info[kCGWindowNumber as String] as? UInt32,
951
- let boundsDict = info[kCGWindowBounds as String] as? NSDictionary else { continue }
952
- var rect = CGRect.zero
953
- if CGRectMakeWithDictionaryRepresentation(boundsDict, &rect) {
954
- if abs(rect.origin.x - pos.x) < 2 && abs(rect.origin.y - pos.y) < 2 &&
955
- abs(rect.width - size.width) < 2 && abs(rect.height - size.height) < 2 {
956
- return wid
957
- }
958
- }
959
- }
960
- return nil
879
+ SessionWindowLocator.matchCGWindow(pid: pid, axWindow: axWindow)
961
880
  }
962
881
 
963
882
  /// Get NSRect from an AX window element (AX uses top-left origin, convert to NS bottom-left)
@@ -1107,11 +1026,8 @@ enum WindowTiler {
1107
1026
 
1108
1027
  // Find the window
1109
1028
  let wid: UInt32
1110
- if let (w, _) = findWindow(tag: tag) {
1111
- wid = w
1112
- } else if let (pid, axWindow) = findWindowViaAX(terminal: terminal, tag: tag),
1113
- let w = matchCGWindow(pid: pid, axWindow: axWindow) {
1114
- wid = w
1029
+ if let match = SessionWindowLocator.findWindow(tag: tag, terminal: terminal) {
1030
+ wid = match.wid
1115
1031
  } else {
1116
1032
  return nil
1117
1033
  }
@@ -2202,7 +2118,6 @@ enum WindowTiler {
2202
2118
  }
2203
2119
  return nil
2204
2120
  }
2205
-
2206
2121
  private static func displaySpaces(containing cgPoint: CGPoint) -> DisplaySpaces? {
2207
2122
  guard let screenIndex = screenIndex(for: cgPoint) else { return nil }
2208
2123
  return getDisplaySpaces().first(where: { $0.displayIndex == screenIndex })
@@ -2218,7 +2133,6 @@ enum WindowTiler {
2218
2133
  private static func formatCGPoint(_ point: CGPoint) -> String {
2219
2134
  "\(Int(point.x)),\(Int(point.y))"
2220
2135
  }
2221
-
2222
2136
  private static func screenIndex(for cgPoint: CGPoint) -> Int? {
2223
2137
  let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
2224
2138
  let nsPoint = NSPoint(x: cgPoint.x, y: primaryHeight - cgPoint.y)