@lattices/cli 0.4.14 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,467 +0,0 @@
1
- import AppKit
2
- import CryptoKit
3
- import Vision
4
-
5
- // MARK: - Data Types
6
-
7
- enum TextSource: String {
8
- case accessibility
9
- case ocr
10
- }
11
-
12
- struct OcrTextBlock {
13
- let text: String
14
- let confidence: Float // 0.0–1.0
15
- let boundingBox: CGRect // normalized coordinates within window
16
- }
17
-
18
- struct OcrWindowResult {
19
- let wid: UInt32
20
- let app: String
21
- let title: String
22
- let frame: WindowFrame
23
- let texts: [OcrTextBlock]
24
- let fullText: String
25
- let timestamp: Date
26
- let source: TextSource
27
- }
28
-
29
- // MARK: - OCR Scanner
30
-
31
- final class OcrModel: ObservableObject {
32
- static let shared = OcrModel()
33
-
34
- @Published private(set) var results: [UInt32: OcrWindowResult] = [:]
35
- @Published private(set) var isScanning: Bool = false
36
- @Published var interval: TimeInterval = 60
37
- @Published var enabled: Bool = true
38
-
39
- private var timer: Timer?
40
- private var deepTimer: Timer?
41
- private let queue = DispatchQueue(label: "com.arach.lattices.ocr", qos: .background)
42
- private let axExtractor = AccessibilityTextExtractor()
43
- private var imageHashes: [UInt32: Data] = [:]
44
- private var lastAXHashes: [UInt32: Data] = [:]
45
- private var lastOCRTextHashes: [UInt32: Data] = [:]
46
- private var lastScanned: [UInt32: Date] = [:]
47
- private var scanGeneration: Int = 0
48
-
49
- private let myPid = ProcessInfo.processInfo.processIdentifier
50
-
51
- private var prefs: Preferences { Preferences.shared }
52
-
53
- func start(interval: TimeInterval? = nil) {
54
- guard timer == nil else { return }
55
- if let interval { self.interval = interval }
56
- self.interval = prefs.ocrQuickInterval
57
- self.enabled = prefs.ocrEnabled
58
- guard enabled else {
59
- DiagnosticLog.shared.info("OcrModel: disabled by user preference")
60
- return
61
- }
62
- let deepInterval = prefs.ocrDeepInterval
63
- DiagnosticLog.shared.info("OcrModel: starting (quick=\(self.interval)s/\(prefs.ocrQuickLimit)win, deep=\(deepInterval)s/\(prefs.ocrDeepLimit)win)")
64
- // Run initial scan immediately so search works right away
65
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 2) { [weak self] in
66
- self?.quickScan()
67
- }
68
- timer = Timer.scheduledTimer(withTimeInterval: self.interval, repeats: true) { [weak self] _ in
69
- guard let self, self.enabled else { return }
70
- self.quickScan()
71
- }
72
- // Deep scan on a slower cadence
73
- deepTimer = Timer.scheduledTimer(withTimeInterval: deepInterval, repeats: true) { [weak self] _ in
74
- guard let self, self.enabled else { return }
75
- self.scan()
76
- }
77
- }
78
-
79
- func stop() {
80
- timer?.invalidate()
81
- timer = nil
82
- deepTimer?.invalidate()
83
- deepTimer = nil
84
- }
85
-
86
- func setEnabled(_ on: Bool) {
87
- enabled = on
88
- prefs.ocrEnabled = on
89
- if on {
90
- // User intentionally turning on OCR clears any prior snooze and is
91
- // the moment we ask macOS for Screen Recording.
92
- prefs.clearDismissal(Capability.screenSearch.rawValue)
93
- if timer == nil {
94
- PermissionChecker.shared.requestScreenRecording()
95
- start()
96
- }
97
- } else {
98
- stop()
99
- }
100
- }
101
-
102
- // MARK: - Single Window Scan
103
-
104
- /// Scan a single window by wid (AX extraction, instant).
105
- func scanSingle(wid: UInt32) {
106
- guard let entry = DesktopModel.shared.windows[wid] else { return }
107
- queue.async { [weak self] in
108
- guard let self else { return }
109
- if let axResult = self.axExtractor.extract(pid: entry.pid, wid: wid) {
110
- let blocks = axResult.texts.map { text in
111
- OcrTextBlock(text: text, confidence: 1.0, boundingBox: .zero)
112
- }
113
- let result = OcrWindowResult(
114
- wid: wid,
115
- app: entry.app,
116
- title: entry.title,
117
- frame: entry.frame,
118
- texts: blocks,
119
- fullText: axResult.fullText,
120
- timestamp: Date(),
121
- source: .accessibility
122
- )
123
- OcrStore.shared.insert(results: [result])
124
- DispatchQueue.main.async {
125
- self.results[wid] = result
126
- DiagnosticLog.shared.info("OcrModel: single scan wid=\(wid) → \(axResult.texts.count) blocks")
127
- }
128
- }
129
- }
130
- }
131
-
132
- // MARK: - Scan
133
-
134
- /// Quick scan: AX-only text extraction for topmost windows (called every 60s).
135
- /// No screenshots, no Vision OCR — nearly free.
136
- func quickScan() {
137
- guard !isScanning else { return }
138
- DispatchQueue.main.async { self.isScanning = true }
139
-
140
- queue.async { [weak self] in
141
- guard let self else { return }
142
- let windows = Array(self.enumerateWindows().prefix(self.prefs.ocrQuickLimit))
143
- let previousResults = self.results
144
- var fresh = previousResults // carry forward all existing results
145
- var changed = 0
146
-
147
- for win in windows {
148
- if let axResult = self.axExtractor.extract(pid: win.pid, wid: win.wid) {
149
- let textHash = SHA256.hash(data: Data(axResult.fullText.utf8))
150
- let hashData = Data(textHash)
151
-
152
- if hashData == self.lastAXHashes[win.wid], previousResults[win.wid] != nil {
153
- // Unchanged — carry forward cached result
154
- continue
155
- }
156
-
157
- // Changed — build new result
158
- self.lastAXHashes[win.wid] = hashData
159
- changed += 1
160
-
161
- let blocks = axResult.texts.map { text in
162
- OcrTextBlock(text: text, confidence: 1.0, boundingBox: .zero)
163
- }
164
- let result = OcrWindowResult(
165
- wid: win.wid,
166
- app: win.app,
167
- title: win.title,
168
- frame: win.frame,
169
- texts: blocks,
170
- fullText: axResult.fullText,
171
- timestamp: Date(),
172
- source: .accessibility
173
- )
174
- fresh[win.wid] = result
175
- OcrStore.shared.insert(results: [result])
176
- }
177
- }
178
-
179
- DiagnosticLog.shared.info("OcrModel: quick scan (AX) \(windows.count)/\(self.enumerateWindows().count) windows, \(changed) changed")
180
-
181
- DispatchQueue.main.async {
182
- self.results = fresh
183
- self.isScanning = false
184
- }
185
-
186
- EventBus.shared.post(.ocrScanComplete(
187
- windowCount: fresh.count,
188
- totalBlocks: fresh.values.reduce(0) { $0 + $1.texts.count }
189
- ))
190
- }
191
- }
192
-
193
- /// Deep scan: all visible windows (called every 2h, or manually via ocr.scan)
194
- /// Uses a budget to limit how many windows get OCR'd per tick.
195
- func scan() {
196
- guard !isScanning else { return }
197
- DispatchQueue.main.async { self.isScanning = true }
198
- scanGeneration += 1
199
- let generation = scanGeneration
200
-
201
- queue.async { [weak self] in
202
- guard let self else { return }
203
- var windows = self.enumerateWindows()
204
- let limit = self.prefs.ocrDeepLimit
205
- if windows.count > limit {
206
- windows = Array(windows.prefix(limit))
207
- }
208
-
209
- let previousResults = self.results
210
- var newHashes: [UInt32: Data] = [:]
211
- var changedWindows: [WindowEntry] = []
212
- var unchangedWindows: [WindowEntry] = []
213
-
214
- // Phase 1: capture + hash all windows (cheap)
215
- for win in windows {
216
- if let cgImage = WindowCapture.image(
217
- listOption: .optionIncludingWindow,
218
- windowID: CGWindowID(win.wid),
219
- imageOption: [.boundsIgnoreFraming, .bestResolution]
220
- ) {
221
- let hash = self.imageHash(cgImage)
222
- newHashes[win.wid] = hash
223
-
224
- if hash == self.imageHashes[win.wid], previousResults[win.wid] != nil {
225
- unchangedWindows.append(win)
226
- } else {
227
- changedWindows.append(win)
228
- }
229
- }
230
- }
231
-
232
- // Phase 2: budget which windows actually get OCR'd
233
- let budget = self.prefs.ocrDeepBudget
234
- let changedBudgeted = Array(changedWindows.prefix(budget))
235
- let remaining = max(0, budget - changedBudgeted.count)
236
-
237
- // Sort unchanged by lastScanned ascending (nil = stalest = highest priority)
238
- let stalestUnchanged = unchangedWindows.sorted { a, b in
239
- let aDate = self.lastScanned[a.wid] ?? .distantPast
240
- let bDate = self.lastScanned[b.wid] ?? .distantPast
241
- return aDate < bDate
242
- }
243
- let unchangedBudgeted = Array(stalestUnchanged.prefix(remaining))
244
- let toScan = changedBudgeted + unchangedBudgeted
245
- let toScanWids = Set(toScan.map(\.wid))
246
-
247
- // Carry forward cached results for non-budgeted windows
248
- var fresh: [UInt32: OcrWindowResult] = [:]
249
- var totalBlocks = 0
250
- for win in windows {
251
- if !toScanWids.contains(win.wid), let prev = previousResults[win.wid] {
252
- fresh[win.wid] = prev
253
- totalBlocks += prev.texts.count
254
- }
255
- }
256
-
257
- self.imageHashes = newHashes
258
-
259
- DiagnosticLog.shared.info("OcrModel: deep scan budget=\(budget), changed=\(changedWindows.count), scanning=\(toScan.count)/\(windows.count)")
260
-
261
- // Phase 3: OCR only the budgeted windows
262
- self.processNextWindow(
263
- windows: toScan,
264
- index: 0,
265
- generation: generation,
266
- previousResults: previousResults,
267
- fresh: fresh,
268
- newHashes: newHashes,
269
- totalBlocks: totalBlocks,
270
- changedResults: [],
271
- updateLastScanned: true
272
- )
273
- }
274
- }
275
-
276
- /// Process one window at a time, yielding back to the queue between each.
277
- /// This lets GCD schedule higher-priority work between windows.
278
- private func processNextWindow(
279
- windows: [WindowEntry],
280
- index: Int,
281
- generation: Int,
282
- previousResults: [UInt32: OcrWindowResult],
283
- fresh: [UInt32: OcrWindowResult],
284
- newHashes: [UInt32: Data],
285
- totalBlocks: Int,
286
- changedResults: [OcrWindowResult],
287
- updateLastScanned: Bool = false
288
- ) {
289
- // Stale scan — a newer one started, abandon this one
290
- guard generation == scanGeneration else {
291
- DispatchQueue.main.async { self.isScanning = false }
292
- return
293
- }
294
-
295
- // All windows processed — publish results & persist diffs
296
- guard index < windows.count else {
297
- self.imageHashes = newHashes
298
-
299
- if !changedResults.isEmpty {
300
- OcrStore.shared.insert(results: changedResults)
301
- }
302
-
303
- DispatchQueue.main.async {
304
- self.results = fresh
305
- self.isScanning = false
306
- }
307
-
308
- EventBus.shared.post(.ocrScanComplete(
309
- windowCount: fresh.count,
310
- totalBlocks: totalBlocks
311
- ))
312
- return
313
- }
314
-
315
- var fresh = fresh
316
- var newHashes = newHashes
317
- var totalBlocks = totalBlocks
318
- var changedResults = changedResults
319
-
320
- let win = windows[index]
321
-
322
- if let cgImage = WindowCapture.image(
323
- listOption: .optionIncludingWindow,
324
- windowID: CGWindowID(win.wid),
325
- imageOption: [.boundsIgnoreFraming, .bestResolution]
326
- ) {
327
- let hash = imageHash(cgImage)
328
- newHashes[win.wid] = hash
329
-
330
- if hash == imageHashes[win.wid], let prev = previousResults[win.wid] {
331
- // Unchanged — reuse cached result
332
- fresh[win.wid] = prev
333
- totalBlocks += prev.texts.count
334
- } else {
335
- // Changed — run OCR
336
- let blocks = recognizeText(in: cgImage)
337
- let fullText = blocks.map(\.text).joined(separator: "\n")
338
- totalBlocks += blocks.count
339
-
340
- let result = OcrWindowResult(
341
- wid: win.wid,
342
- app: win.app,
343
- title: win.title,
344
- frame: win.frame,
345
- texts: blocks,
346
- fullText: fullText,
347
- timestamp: Date(),
348
- source: .ocr
349
- )
350
- fresh[win.wid] = result
351
-
352
- // Text-level dedup: if OCR text is identical to previous, skip store insert
353
- let textHash = Data(SHA256.hash(data: Data(fullText.utf8)))
354
- if textHash != lastOCRTextHashes[win.wid] {
355
- changedResults.append(result)
356
- }
357
- lastOCRTextHashes[win.wid] = textHash
358
- }
359
-
360
- if updateLastScanned {
361
- self.lastScanned[win.wid] = Date()
362
- }
363
- }
364
-
365
- // Throttle: 100ms delay between windows to reduce CPU bursts
366
- queue.asyncAfter(deadline: .now() + 0.1) { [weak self] in
367
- self?.processNextWindow(
368
- windows: windows,
369
- index: index + 1,
370
- generation: generation,
371
- previousResults: previousResults,
372
- fresh: fresh,
373
- newHashes: newHashes,
374
- totalBlocks: totalBlocks,
375
- changedResults: changedResults,
376
- updateLastScanned: updateLastScanned
377
- )
378
- }
379
- }
380
-
381
- // MARK: - Window Enumeration
382
-
383
- private func enumerateWindows() -> [WindowEntry] {
384
- guard let list = CGWindowListCopyWindowInfo(
385
- [.optionOnScreenOnly, .excludeDesktopElements],
386
- kCGNullWindowID
387
- ) as? [[String: Any]] else { return [] }
388
-
389
- var entries: [WindowEntry] = []
390
-
391
- for info in list {
392
- guard let wid = info[kCGWindowNumber as String] as? UInt32,
393
- let ownerName = info[kCGWindowOwnerName as String] as? String,
394
- let pid = info[kCGWindowOwnerPID as String] as? Int32,
395
- let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
396
- else { continue }
397
-
398
- // Skip own windows
399
- guard pid != myPid else { continue }
400
-
401
- var rect = CGRect.zero
402
- guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect),
403
- rect.width >= 50, rect.height >= 50 else { continue }
404
-
405
- let title = info[kCGWindowName as String] as? String ?? ""
406
- let layer = info[kCGWindowLayer as String] as? Int ?? 0
407
- guard layer == 0 else { continue }
408
-
409
- let frame = WindowFrame(
410
- x: Double(rect.origin.x),
411
- y: Double(rect.origin.y),
412
- w: Double(rect.width),
413
- h: Double(rect.height)
414
- )
415
-
416
- entries.append(WindowEntry(
417
- wid: wid,
418
- app: ownerName,
419
- pid: pid,
420
- title: title,
421
- frame: frame,
422
- spaceIds: [],
423
- isOnScreen: true,
424
- latticesSession: nil
425
- ))
426
- }
427
-
428
- return entries
429
- }
430
-
431
- // MARK: - Image Hashing
432
-
433
- private func imageHash(_ image: CGImage) -> Data {
434
- guard let dataProvider = image.dataProvider,
435
- let data = dataProvider.data as Data? else {
436
- return Data()
437
- }
438
- let digest = SHA256.hash(data: data as Data)
439
- return Data(digest)
440
- }
441
-
442
- // MARK: - Vision OCR
443
-
444
- private func recognizeText(in image: CGImage) -> [OcrTextBlock] {
445
- let handler = VNImageRequestHandler(cgImage: image, options: [:])
446
- let request = VNRecognizeTextRequest()
447
- request.recognitionLevel = prefs.ocrAccuracy == "fast" ? .fast : .accurate
448
- request.usesLanguageCorrection = true
449
-
450
- do {
451
- try handler.perform([request])
452
- } catch {
453
- return []
454
- }
455
-
456
- guard let observations = request.results else { return [] }
457
-
458
- return observations.compactMap { obs in
459
- guard let candidate = obs.topCandidates(1).first else { return nil }
460
- return OcrTextBlock(
461
- text: candidate.string,
462
- confidence: candidate.confidence,
463
- boundingBox: obs.boundingBox
464
- )
465
- }
466
- }
467
- }