@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,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
- }