@lattices/cli 0.4.14 → 0.6.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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  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/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. 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
- }