@lattices/cli 0.4.13 → 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 +191 -63
  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 -2271
  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,624 +0,0 @@
1
- import Foundation
2
- import CoreGraphics
3
-
4
- // MARK: - Path Point
5
-
6
- /// A single point in the captured mouse path with timestamp
7
- struct GesturePathPoint: Codable {
8
- let x: CGFloat
9
- let y: CGFloat
10
- let timestamp: TimeInterval
11
-
12
- var cgPoint: CGPoint {
13
- CGPoint(x: x, y: y)
14
- }
15
-
16
- init(x: CGFloat, y: CGFloat, timestamp: TimeInterval) {
17
- self.x = x
18
- self.y = y
19
- self.timestamp = timestamp
20
- }
21
-
22
- init(point: CGPoint, timestamp: TimeInterval) {
23
- self.x = point.x
24
- self.y = point.y
25
- self.timestamp = timestamp
26
- }
27
- }
28
-
29
- // MARK: - Direction Segment
30
-
31
- /// A detected direction segment in the path
32
- struct DirectionSegment {
33
- let direction: MouseGestureDirection
34
- let startIndex: Int
35
- let endIndex: Int
36
- let length: CGFloat
37
-
38
- var label: String {
39
- direction.rawValue
40
- }
41
- }
42
-
43
- // MARK: - Shape Label
44
-
45
- /// Recognized shape label from the gesture
46
- enum GestureShapeLabel: String, Codable, CaseIterable {
47
- // Single segment
48
- case up = "up"
49
- case down = "down"
50
- case left = "left"
51
- case right = "right"
52
-
53
- // Two-segment shapes
54
- case lShapeDownRight = "l-shape-down-right"
55
- case lShapeDownLeft = "l-shape-down-left"
56
- case lShapeUpRight = "l-shape-up-right"
57
- case lShapeUpLeft = "l-shape-up-left"
58
- case reverseLShapeRightDown = "reverse-l-right-down"
59
- case reverseLShapeLeftDown = "reverse-l-left-down"
60
- case vShape = "v-shape"
61
- case reverseV = "reverse-v"
62
- case zShape = "z-shape"
63
- case reverseZ = "reverse-z"
64
- case sShape = "s-shape"
65
-
66
- // Three-segment shapes
67
- case uShape = "u-shape"
68
- case uShapeFlipped = "u-shape-flipped"
69
- case nShape = "n-shape"
70
- case mShape = "m-shape"
71
-
72
- var displayName: String {
73
- switch self {
74
- case .up: return "Up"
75
- case .down: return "Down"
76
- case .left: return "Left"
77
- case .right: return "Right"
78
- case .lShapeDownRight: return "L (↓ then →)"
79
- case .lShapeDownLeft: return "L (↓ then ←)"
80
- case .lShapeUpRight: return "L (↑ then →)"
81
- case .lShapeUpLeft: return "L (↑ then ←)"
82
- case .reverseLShapeRightDown: return "Reverse L (→ then ↓)"
83
- case .reverseLShapeLeftDown: return "Reverse L (← then ↓)"
84
- case .vShape: return "V (↓ then ↑)"
85
- case .reverseV: return "Reverse V (↑ then ↓)"
86
- case .zShape: return "Z (→ then ↓ then →)"
87
- case .reverseZ: return "Reverse Z (← then ↓ then ←)"
88
- case .sShape: return "S (→ then ↑ then →)"
89
- case .uShape: return "U (↓ then → then ↑)"
90
- case .uShapeFlipped: return "U Flipped (↑ then → then ↓)"
91
- case .nShape: return "N (↓ then ← then ↑)"
92
- case .mShape: return "M (↑ then ← then ↓)"
93
- }
94
- }
95
-
96
- var segmentCount: Int {
97
- switch self {
98
- case .up, .down, .left, .right:
99
- return 1
100
- case .lShapeDownRight, .lShapeDownLeft, .lShapeUpRight, .lShapeUpLeft,
101
- .reverseLShapeRightDown, .reverseLShapeLeftDown, .vShape, .reverseV:
102
- return 2
103
- case .zShape, .reverseZ, .sShape:
104
- return 3
105
- case .uShape, .uShapeFlipped, .nShape, .mShape:
106
- return 3
107
- }
108
- }
109
-
110
- static func from(segments: [DirectionSegment]) -> GestureShapeLabel? {
111
- guard !segments.isEmpty else { return nil }
112
-
113
- let directions = segments.map { $0.direction }
114
-
115
- // Single direction
116
- if segments.count == 1 {
117
- switch directions[0] {
118
- case .up: return .up
119
- case .down: return .down
120
- case .left: return .left
121
- case .right: return .right
122
- }
123
- }
124
-
125
- // Two directions - L-shapes, V-shapes, etc.
126
- if segments.count == 2 {
127
- let first = directions[0]
128
- let second = directions[1]
129
-
130
- // L shapes
131
- if first == .down && second == .right {
132
- return .lShapeDownRight
133
- }
134
- if first == .down && second == .left {
135
- return .lShapeDownLeft
136
- }
137
- if first == .up && second == .right {
138
- return .lShapeUpRight
139
- }
140
- if first == .up && second == .left {
141
- return .lShapeUpLeft
142
- }
143
-
144
- // Reverse L shapes (horizontal first)
145
- if first == .right && second == .down {
146
- return .reverseLShapeRightDown
147
- }
148
- if first == .left && second == .down {
149
- return .reverseLShapeLeftDown
150
- }
151
-
152
- // V shapes (opposite vertical directions)
153
- if first == .down && second == .up {
154
- return .vShape
155
- }
156
- if first == .up && second == .down {
157
- return .reverseV
158
- }
159
- }
160
-
161
- // Three directions - Z-shapes, U-shapes, etc.
162
- if segments.count == 3 {
163
- let first = directions[0]
164
- let second = directions[1]
165
- let third = directions[2]
166
-
167
- // Z shapes (horizontal, vertical, horizontal)
168
- if first == .right && second == .down && third == .right {
169
- return .zShape
170
- }
171
- if first == .left && second == .down && third == .left {
172
- return .reverseZ
173
- }
174
- if first == .right && second == .up && third == .right {
175
- return .sShape
176
- }
177
-
178
- // U shapes (down, right, up or similar)
179
- if first == .down && second == .right && third == .up {
180
- return .uShape
181
- }
182
- if first == .up && second == .right && third == .down {
183
- return .uShapeFlipped
184
- }
185
- if first == .down && second == .left && third == .up {
186
- return .nShape
187
- }
188
- if first == .up && second == .left && third == .down {
189
- return .mShape
190
- }
191
- }
192
-
193
- return nil
194
- }
195
- }
196
-
197
- // MARK: - Recognition Result
198
-
199
- struct ShapeRecognitionResult {
200
- let shape: GestureShapeLabel?
201
- let segments: [DirectionSegment]
202
- let confidence: CGFloat
203
- let pathLength: CGFloat
204
-
205
- var displayLabel: String {
206
- if let shape {
207
- return shape.displayName
208
- }
209
- if let first = segments.first {
210
- return "Unknown (\(first.label))"
211
- }
212
- return "Unknown"
213
- }
214
-
215
- var shapeToken: String? {
216
- shape?.rawValue
217
- }
218
- }
219
-
220
- // MARK: - Shape Recognizer
221
-
222
- final class ShapeRecognizer {
223
- // Configuration
224
- private let minSegmentLength: CGFloat
225
- private let angularThreshold: CGFloat // radians, default ~45 degrees
226
- private let minTotalPathLength: CGFloat
227
- private let cornerSmoothRadius: Int // points to consider around corners
228
-
229
- init(
230
- minSegmentLength: CGFloat = 40,
231
- angularThreshold: CGFloat = .pi / 4, // 45 degrees
232
- minTotalPathLength: CGFloat = 80,
233
- cornerSmoothRadius: Int = 3
234
- ) {
235
- self.minSegmentLength = minSegmentLength
236
- self.angularThreshold = angularThreshold
237
- self.minTotalPathLength = minTotalPathLength
238
- self.cornerSmoothRadius = cornerSmoothRadius
239
- }
240
-
241
- // MARK: - Main Entry Point
242
-
243
- func recognize(points: [GesturePathPoint]) -> ShapeRecognitionResult {
244
- guard points.count >= 3 else {
245
- return ShapeRecognitionResult(shape: nil, segments: [], confidence: 0, pathLength: 0)
246
- }
247
-
248
- // Calculate total path length
249
- let totalLength = calculatePathLength(points)
250
- guard totalLength >= minTotalPathLength else {
251
- return ShapeRecognitionResult(shape: nil, segments: [], confidence: 0, pathLength: totalLength)
252
- }
253
-
254
- // Direction runs work better for mouse gestures than strict corner
255
- // detection because real paths rarely contain one crisp corner sample.
256
- let runSegments = buildDirectionRunSegments(points: points)
257
- let corners = detectCorners(points: points)
258
- let segments = runSegments.isEmpty ? buildSegments(points: points, corners: corners) : runSegments
259
-
260
- // Classify shape
261
- let shape = GestureShapeLabel.from(segments: segments)
262
-
263
- // Calculate confidence based on segment clarity
264
- let confidence = calculateConfidence(segments: segments, corners: corners.count, totalLength: totalLength)
265
-
266
- return ShapeRecognitionResult(
267
- shape: shape,
268
- segments: segments,
269
- confidence: confidence,
270
- pathLength: totalLength
271
- )
272
- }
273
-
274
- // MARK: - Corner Detection
275
-
276
- private func detectCorners(points: [GesturePathPoint]) -> [Int] {
277
- guard points.count > cornerSmoothRadius * 2 else { return [] }
278
-
279
- var corners: [Int] = []
280
- var lastSignificantDirection: MouseGestureDirection?
281
-
282
- for i in cornerSmoothRadius..<(points.count - cornerSmoothRadius) {
283
- let before = averageVector(in: points, from: i - cornerSmoothRadius, to: i)
284
- let after = averageVector(in: points, from: i, to: i + cornerSmoothRadius)
285
-
286
- // Skip if either vector is too short (near-zero movement)
287
- guard vectorLength(before) > minSegmentLength / 4 else { continue }
288
- guard vectorLength(after) > minSegmentLength / 4 else { continue }
289
-
290
- let directionBefore = vectorToDirection(before)
291
- let directionAfter = vectorToDirection(after)
292
-
293
- guard let dirBefore = directionBefore, let dirAfter = directionAfter else { continue }
294
-
295
- // Check if this is a meaningful direction change
296
- if dirBefore != dirAfter {
297
- // Verify it's not just noise - the change should be significant
298
- let angle = angleBetween(before, after)
299
- if angle >= angularThreshold {
300
- // Only add if it's a new direction (not rapid back-and-forth)
301
- if lastSignificantDirection != dirAfter {
302
- corners.append(i)
303
- lastSignificantDirection = dirAfter
304
- }
305
- }
306
- }
307
- }
308
-
309
- return corners
310
- }
311
-
312
- private func averageVector(in points: [GesturePathPoint], from start: Int, to end: Int) -> CGPoint {
313
- guard end > start else { return .zero }
314
- var sum = CGPoint.zero
315
- var count = 0
316
-
317
- for i in start..<min(end, points.count) {
318
- if i > start {
319
- let prev = points[i - 1]
320
- let curr = points[i]
321
- sum.x += curr.x - prev.x
322
- sum.y += curr.y - prev.y
323
- count += 1
324
- }
325
- }
326
-
327
- return count > 0 ? CGPoint(x: sum.x / CGFloat(count), y: sum.y / CGFloat(count)) : .zero
328
- }
329
-
330
- private func vectorLength(_ v: CGPoint) -> CGFloat {
331
- sqrt(v.x * v.x + v.y * v.y)
332
- }
333
-
334
- private func vectorToDirection(_ v: CGPoint) -> MouseGestureDirection? {
335
- let length = vectorLength(v)
336
- guard length > 5 else { return nil } // minimum threshold
337
-
338
- // Determine primary direction based on dominant axis
339
- let absX = abs(v.x)
340
- let absY = abs(v.y)
341
-
342
- if absX > absY * 1.2 { // axis bias
343
- return v.x >= 0 ? .right : .left
344
- } else if absY > absX * 1.2 {
345
- return v.y >= 0 ? .down : .up
346
- }
347
-
348
- // If diagonal, use the dominant component
349
- if absX >= absY {
350
- return v.x >= 0 ? .right : .left
351
- } else {
352
- return v.y >= 0 ? .down : .up
353
- }
354
- }
355
-
356
- private func angleBetween(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
357
- let dot = a.x * b.x + a.y * b.y
358
- let lenA = vectorLength(a)
359
- let lenB = vectorLength(b)
360
- guard lenA > 0 && lenB > 0 else { return 0 }
361
-
362
- let cosAngle = max(-1, min(1, dot / (lenA * lenB)))
363
- return acos(cosAngle)
364
- }
365
-
366
- // MARK: - Segment Building
367
-
368
- private func makeDirectionSegment(
369
- direction: MouseGestureDirection?,
370
- start: Int,
371
- end: Int,
372
- length: CGFloat
373
- ) -> DirectionSegment? {
374
- guard let direction, length > 0 else { return nil }
375
- return DirectionSegment(
376
- direction: direction,
377
- startIndex: start,
378
- endIndex: end,
379
- length: length
380
- )
381
- }
382
-
383
- private func buildDirectionRunSegments(points: [GesturePathPoint]) -> [DirectionSegment] {
384
- guard points.count >= 2 else { return [] }
385
-
386
- var rawSegments: [DirectionSegment] = []
387
- var currentDirection: MouseGestureDirection?
388
- var currentStart = 0
389
- var currentEnd = 0
390
- var currentLength: CGFloat = 0
391
-
392
- for index in 1..<points.count {
393
- let previous = points[index - 1]
394
- let current = points[index]
395
- let delta = CGPoint(x: current.x - previous.x, y: current.y - previous.y)
396
- let length = vectorLength(delta)
397
- guard length >= 2, let direction = vectorToDirection(delta) else { continue }
398
-
399
- if currentDirection == nil {
400
- currentDirection = direction
401
- currentStart = index - 1
402
- currentEnd = index
403
- currentLength = length
404
- continue
405
- }
406
-
407
- if direction == currentDirection {
408
- currentEnd = index
409
- currentLength += length
410
- } else {
411
- if let segment = makeDirectionSegment(
412
- direction: currentDirection,
413
- start: currentStart,
414
- end: currentEnd,
415
- length: currentLength
416
- ) {
417
- rawSegments.append(segment)
418
- }
419
- currentDirection = direction
420
- currentStart = index - 1
421
- currentEnd = index
422
- currentLength = length
423
- }
424
- }
425
- if let segment = makeDirectionSegment(
426
- direction: currentDirection,
427
- start: currentStart,
428
- end: currentEnd,
429
- length: currentLength
430
- ) {
431
- rawSegments.append(segment)
432
- }
433
-
434
- let merged = mergeShortDirectionRuns(rawSegments)
435
- let filtered = merged.filter { $0.length >= minSegmentLength }
436
- return mergeAdjacentSegments(filtered)
437
- }
438
-
439
- private func mergeShortDirectionRuns(_ segments: [DirectionSegment]) -> [DirectionSegment] {
440
- guard !segments.isEmpty else { return [] }
441
-
442
- var result: [DirectionSegment] = []
443
- for segment in segments {
444
- guard segment.length < minSegmentLength / 2 else {
445
- result.append(segment)
446
- continue
447
- }
448
-
449
- if let last = result.last {
450
- result[result.count - 1] = DirectionSegment(
451
- direction: last.direction,
452
- startIndex: last.startIndex,
453
- endIndex: segment.endIndex,
454
- length: last.length + segment.length
455
- )
456
- }
457
- }
458
-
459
- return result
460
- }
461
-
462
- private func mergeAdjacentSegments(_ segments: [DirectionSegment]) -> [DirectionSegment] {
463
- guard !segments.isEmpty else { return [] }
464
-
465
- var result: [DirectionSegment] = []
466
- for segment in segments {
467
- if let last = result.last, last.direction == segment.direction {
468
- result[result.count - 1] = DirectionSegment(
469
- direction: last.direction,
470
- startIndex: last.startIndex,
471
- endIndex: segment.endIndex,
472
- length: last.length + segment.length
473
- )
474
- } else {
475
- result.append(segment)
476
- }
477
- }
478
-
479
- return result
480
- }
481
-
482
- private func buildSegments(points: [GesturePathPoint], corners: [Int]) -> [DirectionSegment] {
483
- guard !points.isEmpty else { return [] }
484
-
485
- // If no corners, use entire path as one segment
486
- if corners.isEmpty {
487
- let direction = overallDirection(points: points)
488
- if let dir = direction {
489
- let length = calculatePathLength(points)
490
- return [DirectionSegment(direction: dir, startIndex: 0, endIndex: points.count - 1, length: length)]
491
- }
492
- return []
493
- }
494
-
495
- var segments: [DirectionSegment] = []
496
-
497
- // First segment
498
- let firstCorner = corners[0]
499
- let firstDir = segmentDirection(points: points, from: 0, to: firstCorner)
500
- if let dir = firstDir {
501
- let length = segmentLength(points: points, from: 0, to: firstCorner)
502
- if length >= minSegmentLength {
503
- segments.append(DirectionSegment(direction: dir, startIndex: 0, endIndex: firstCorner, length: length))
504
- }
505
- }
506
-
507
- // Middle segments (between corners)
508
- for i in 0..<(corners.count - 1) {
509
- let startIdx = corners[i]
510
- let endIdx = corners[i + 1]
511
- let dir = segmentDirection(points: points, from: startIdx, to: endIdx)
512
- if let dir = dir {
513
- let length = segmentLength(points: points, from: startIdx, to: endIdx)
514
- if length >= minSegmentLength {
515
- segments.append(DirectionSegment(direction: dir, startIndex: startIdx, endIndex: endIdx, length: length))
516
- }
517
- }
518
- }
519
-
520
- // Last segment
521
- let lastCorner = corners[corners.count - 1]
522
- let lastDir = segmentDirection(points: points, from: lastCorner, to: points.count - 1)
523
- if let dir = lastDir {
524
- let length = segmentLength(points: points, from: lastCorner, to: points.count - 1)
525
- if length >= minSegmentLength {
526
- segments.append(DirectionSegment(direction: dir, startIndex: lastCorner, endIndex: points.count - 1, length: length))
527
- }
528
- }
529
-
530
- return segments
531
- }
532
-
533
- private func overallDirection(points: [GesturePathPoint]) -> MouseGestureDirection? {
534
- guard let first = points.first, let last = points.last else { return nil }
535
- let delta = CGPoint(x: last.x - first.x, y: last.y - first.y)
536
- return vectorToDirection(delta)
537
- }
538
-
539
- private func segmentDirection(points: [GesturePathPoint], from startIdx: Int, to endIdx: Int) -> MouseGestureDirection? {
540
- guard startIdx < endIdx, endIdx < points.count else { return nil }
541
- let start = points[startIdx]
542
- let end = points[endIdx]
543
- let delta = CGPoint(x: end.x - start.x, y: end.y - start.y)
544
- return vectorToDirection(delta)
545
- }
546
-
547
- private func segmentLength(points: [GesturePathPoint], from startIdx: Int, to endIdx: Int) -> CGFloat {
548
- guard startIdx < endIdx else { return 0 }
549
- var length: CGFloat = 0
550
- for i in (startIdx + 1)...endIdx {
551
- if i < points.count {
552
- let dx = points[i].x - points[i - 1].x
553
- let dy = points[i].y - points[i - 1].y
554
- length += sqrt(dx * dx + dy * dy)
555
- }
556
- }
557
- return length
558
- }
559
-
560
- private func calculatePathLength(_ points: [GesturePathPoint]) -> CGFloat {
561
- guard points.count >= 2 else { return 0 }
562
- var length: CGFloat = 0
563
- for i in 1..<points.count {
564
- let dx = points[i].x - points[i - 1].x
565
- let dy = points[i].y - points[i - 1].y
566
- length += sqrt(dx * dx + dy * dy)
567
- }
568
- return length
569
- }
570
-
571
- // MARK: - Confidence Calculation
572
-
573
- private func calculateConfidence(segments: [DirectionSegment], corners: Int, totalLength: CGFloat) -> CGFloat {
574
- guard !segments.isEmpty else { return 0 }
575
-
576
- var confidence: CGFloat = 1.0
577
-
578
- // Penalize for many corners (noisy path)
579
- if corners > 2 {
580
- confidence -= CGFloat(corners - 2) * 0.1
581
- }
582
-
583
- // Penalize if segment lengths are very uneven (might be accidental)
584
- if segments.count >= 2 {
585
- let lengths = segments.map { $0.length }
586
- let avgLength = lengths.reduce(0, +) / CGFloat(lengths.count)
587
- let variance = lengths.map { abs($0 - avgLength) / avgLength }.reduce(0, +) / CGFloat(lengths.count)
588
- confidence -= min(0.3, CGFloat(variance) * 0.5)
589
- }
590
-
591
- // Boost if path is long and smooth
592
- if totalLength > 200 && corners == 0 {
593
- confidence = min(1.0, confidence + 0.1)
594
- }
595
-
596
- return max(0, min(1, confidence))
597
- }
598
- }
599
-
600
- // MARK: - Convenience Extensions
601
-
602
- extension ShapeRecognizer {
603
- /// Recognize from raw CGPoints
604
- func recognize(points: [CGPoint], timestamps: [TimeInterval]? = nil) -> ShapeRecognitionResult {
605
- let gesturePoints: [GesturePathPoint]
606
- if let ts = timestamps {
607
- gesturePoints = zip(points, ts).map { GesturePathPoint(x: $0.0.x, y: $0.0.y, timestamp: $0.1) }
608
- } else {
609
- let now = Date().timeIntervalSinceReferenceDate
610
- gesturePoints = points.enumerated().map { GesturePathPoint(x: $0.1.x, y: $0.1.y, timestamp: now + Double($0.0) * 0.01) }
611
- }
612
- return recognize(points: gesturePoints)
613
- }
614
-
615
- /// Quick check if path contains a corner
616
- func hasCorner(at point: CGPoint, in points: [GesturePathPoint]) -> Bool {
617
- // Simple check - find nearest point index and check context
618
- guard let nearestIndex = points.firstIndex(where: { abs($0.x - point.x) < 5 && abs($0.y - point.y) < 5 }) else {
619
- return false
620
- }
621
- let corners = detectCorners(points: points)
622
- return corners.contains(nearestIndex)
623
- }
624
- }
@@ -1,56 +0,0 @@
1
- import Foundation
2
-
3
- /// Measures CGEventTap callback duration and logs throttled warnings when
4
- /// callbacks exceed the budget. Thread-safe — designed to be invoked from
5
- /// the event-tap thread on every event.
6
- ///
7
- /// Logging is throttled to at most one warning per second per meter, with
8
- /// the peak value observed in that window. This keeps the log readable
9
- /// when something is misbehaving without losing the signal.
10
- ///
11
- /// Why this exists: the whole point of the EventTapThread off-main move is
12
- /// "tap callbacks are now fast." This is the measurement that confirms it
13
- /// stays true and surfaces regressions before they cause `tapDisabledByTimeout`.
14
- final class TapBudgetMeter {
15
- private let label: String
16
- private let warnThresholdMs: Double
17
- private let throttleSec: TimeInterval
18
-
19
- private let lock = NSLock()
20
- private var maxMsInWindow: Double = 0
21
- private var samplesInWindow: Int = 0
22
- private var lastLog: Date = .distantPast
23
-
24
- init(label: String, warnThresholdMs: Double = 5.0, throttleSec: TimeInterval = 1.0) {
25
- self.label = label
26
- self.warnThresholdMs = warnThresholdMs
27
- self.throttleSec = throttleSec
28
- }
29
-
30
- /// Records one callback's wall-clock duration. No-op when below the
31
- /// threshold. Logs at most once per `throttleSec` window.
32
- func record(durationMs: Double) {
33
- guard durationMs > warnThresholdMs else { return }
34
-
35
- lock.lock()
36
- if durationMs > maxMsInWindow { maxMsInWindow = durationMs }
37
- samplesInWindow += 1
38
-
39
- let now = Date()
40
- guard now.timeIntervalSince(lastLog) > throttleSec else {
41
- lock.unlock()
42
- return
43
- }
44
-
45
- let peak = maxMsInWindow
46
- let count = samplesInWindow
47
- maxMsInWindow = 0
48
- samplesInWindow = 0
49
- lastLog = now
50
- lock.unlock()
51
-
52
- DiagnosticLog.shared.warn(
53
- "\(label): tap callback peak \(Int(peak))ms (× \(count) over threshold \(Int(warnThresholdMs))ms in last \(Int(throttleSec))s)"
54
- )
55
- }
56
- }