@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,555 +0,0 @@
1
- import AppKit
2
-
3
- // MARK: - Audio Provider Protocol
4
-
5
- /// A provider that can capture audio and return transcriptions.
6
- /// Lattices doesn't do transcription itself — it delegates to an external
7
- /// service (Vox, Whisper, etc.) and maps the result to intents.
8
- protocol AudioProvider: AnyObject {
9
- var isAvailable: Bool { get }
10
- var isListening: Bool { get }
11
-
12
- /// Start listening. Transcription arrives via the callback.
13
- func startListening(onTranscript: @escaping (Transcription) -> Void)
14
-
15
- /// Stop listening and return the final transcription.
16
- func stopListening(completion: @escaping (Transcription?) -> Void)
17
-
18
- /// Check if the provider service is reachable.
19
- func checkHealth(completion: @escaping (Bool) -> Void)
20
- }
21
-
22
- struct Transcription {
23
- let text: String
24
- let confidence: Double
25
- let source: String // "vox", "whisper", etc.
26
- let isPartial: Bool // true for streaming partial results
27
- let durationMs: Int?
28
- }
29
-
30
- // MARK: - Audio Layer (coordinates provider + intent engine)
31
-
32
- final class AudioLayer: ObservableObject {
33
- static let shared = AudioLayer()
34
-
35
- @Published var isListening = false
36
- @Published var lastTranscript: String?
37
- @Published var matchedIntent: String?
38
- @Published var matchedSlots: [String: String] = [:]
39
- @Published var matchConfidence: Double = 0
40
- @Published var executionResult: String? // "ok" or error message
41
- @Published var executionData: JSON? // Full result data from intent execution
42
- @Published var provider: (any AudioProvider)?
43
- @Published var providerName: String = "none"
44
- @Published var agentResponse: AgentResponse?
45
-
46
- private var pendingVoiceStart = false
47
- private var voiceConnectionRetry: DispatchWorkItem?
48
-
49
- private init() {
50
- let vox = VoxAudioProvider()
51
- provider = vox
52
- providerName = "vox"
53
- // Voice entry points can arrive from the desktop UI, daemon, or iOS
54
- // bridge, so connection setup is handled lazily when capture starts.
55
- }
56
-
57
- /// Start a voice command capture. Transcription is piped to the intent engine.
58
- func startVoiceCommand() {
59
- guard !isListening else { return }
60
-
61
- // Clear previous state
62
- lastTranscript = nil
63
- matchedIntent = nil
64
- matchedSlots = [:]
65
- matchConfidence = 0
66
- executionResult = nil
67
- didExecuteIntent = false
68
-
69
- guard let provider = provider else {
70
- executionResult = "No voice provider — install Vox"
71
- return
72
- }
73
-
74
- pendingVoiceStart = true
75
- voiceConnectionRetry?.cancel()
76
- startVoiceCommandWhenReady(provider: provider, attempt: 0)
77
- }
78
-
79
- private func startVoiceCommandWhenReady(provider: any AudioProvider, attempt: Int) {
80
- guard pendingVoiceStart, !isListening else { return }
81
-
82
- if provider.isAvailable {
83
- pendingVoiceStart = false
84
- beginVoiceCommand(provider: provider)
85
- return
86
- }
87
-
88
- let client = VoxClient.shared
89
- if attempt == 0 {
90
- let launched = launchVoxIfNeeded()
91
- executionResult = launched ? "Starting Vox..." : "Connecting to Vox..."
92
- client.connect()
93
- } else if case .disconnected = client.connectionState {
94
- client.connect()
95
- } else if case .unavailable = client.connectionState {
96
- client.connect()
97
- }
98
-
99
- guard attempt < 40 else {
100
- pendingVoiceStart = false
101
- executionResult = "Vox unavailable — open Vox and try again"
102
- DiagnosticLog.shared.warn("AudioLayer: Vox connection failed before voice start")
103
- return
104
- }
105
-
106
- let retry = DispatchWorkItem { [weak self] in
107
- guard let self else { return }
108
- self.startVoiceCommandWhenReady(provider: provider, attempt: attempt + 1)
109
- }
110
- voiceConnectionRetry = retry
111
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: retry)
112
- }
113
-
114
- private func beginVoiceCommand(provider: any AudioProvider) {
115
- isListening = true
116
-
117
- provider.startListening { [weak self] transcription in
118
- DispatchQueue.main.async {
119
- guard let self = self else { return }
120
-
121
- if transcription.isPartial {
122
- self.lastTranscript = transcription.text
123
- return
124
- }
125
-
126
- // Final transcript (e.g. from streaming providers)
127
- self.lastTranscript = transcription.text
128
- self.isListening = false
129
-
130
- // Empty transcript = transcription failed, don't try to execute
131
- guard !transcription.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
132
- if self.executionResult == nil || self.executionResult == "Transcribing..." {
133
- self.executionResult = "No speech detected"
134
- }
135
- return
136
- }
137
-
138
- EventBus.shared.post(.voiceCommand(text: transcription.text, confidence: transcription.confidence))
139
- self.executeVoiceIntent(transcription)
140
- }
141
- }
142
- }
143
-
144
- private func launchVoxIfNeeded() -> Bool {
145
- guard VoxClient.shared.discoverDaemon() == nil else { return false }
146
-
147
- let candidates = [
148
- "/Applications/Vox.app",
149
- NSHomeDirectory() + "/Applications/Vox.app",
150
- ]
151
-
152
- guard let path = candidates.first(where: { FileManager.default.fileExists(atPath: $0) }) else {
153
- DiagnosticLog.shared.warn("AudioLayer: Vox daemon unavailable and Vox.app was not found")
154
- return false
155
- }
156
-
157
- let configuration = NSWorkspace.OpenConfiguration()
158
- configuration.activates = false
159
- NSWorkspace.shared.openApplication(at: URL(fileURLWithPath: path), configuration: configuration) { _, error in
160
- if let error {
161
- DiagnosticLog.shared.warn("AudioLayer: failed to open Vox — \(error.localizedDescription)")
162
- } else {
163
- DiagnosticLog.shared.info("AudioLayer: opened Vox for voice command")
164
- }
165
- }
166
- return true
167
- }
168
-
169
- /// Track whether we already executed for this recording session.
170
- private var didExecuteIntent = false
171
-
172
- func stopVoiceCommand() {
173
- if pendingVoiceStart, !isListening {
174
- pendingVoiceStart = false
175
- voiceConnectionRetry?.cancel()
176
- voiceConnectionRetry = nil
177
- executionResult = "Voice cancelled"
178
- return
179
- }
180
-
181
- guard let provider = provider, isListening else { return }
182
-
183
- pendingVoiceStart = false
184
- isListening = false
185
- executionResult = "Transcribing..."
186
-
187
- provider.stopListening { [weak self] transcription in
188
- DispatchQueue.main.async {
189
- guard let self = self else { return }
190
- if let t = transcription,
191
- !t.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
192
- self.lastTranscript = t.text
193
- // Skip if the streaming callback already executed the intent
194
- guard !self.didExecuteIntent else { return }
195
- EventBus.shared.post(.voiceCommand(text: t.text, confidence: t.confidence))
196
- self.executeVoiceIntent(t)
197
- } else if !self.didExecuteIntent {
198
- self.executionResult = "No speech detected"
199
- }
200
- }
201
- }
202
- }
203
-
204
- private func executeVoiceIntent(_ transcription: Transcription) {
205
- didExecuteIntent = true
206
- let matcher = PhraseMatcher.shared
207
-
208
- // Clear previous agent response
209
- agentResponse = nil
210
-
211
- if shouldAnswerWithAssistant(transcription.text) {
212
- DiagnosticLog.shared.info("AudioLayer: question-like voice request, asking Assistant provider")
213
- matchedIntent = nil
214
- matchedSlots = [:]
215
- executionResult = "thinking..."
216
- executionData = nil
217
- assistantQuestion(transcription: transcription)
218
- return
219
- }
220
-
221
- if let match = matcher.match(text: transcription.text) {
222
- matchedIntent = match.intentName
223
- matchConfidence = match.confidence
224
- matchedSlots = match.slots.reduce(into: [:]) { dict, pair in
225
- dict[pair.key] = pair.value.stringValue ?? "\(pair.value)"
226
- }
227
- DiagnosticLog.shared.info("AudioLayer: matched '\(match.intentName)' via '\(match.matchedPhrase)' slots=\(matchedSlots)")
228
-
229
- do {
230
- let result = try matcher.execute(match)
231
- executionResult = voiceResultSummary(for: match, result: result)
232
- executionData = result
233
- DiagnosticLog.shared.info("AudioLayer: executed '\(match.intentName)' → \(executionResult ?? "ok")")
234
- } catch {
235
- DiagnosticLog.shared.info("AudioLayer: intent error — \(error.localizedDescription), asking Assistant provider")
236
- executionResult = "thinking..."
237
- executionData = nil
238
- assistantFallback(transcription: transcription)
239
- }
240
-
241
- // Fire parallel provider-backed advisor for 5+ word utterances.
242
- fireAdvisor(transcript: transcription.text, matched: "\(match.intentName)(\(matchedSlots))")
243
-
244
- } else {
245
- // No local match — ask the selected Assistant provider.
246
- DiagnosticLog.shared.info("AudioLayer: no phrase match for '\(transcription.text)', asking Assistant provider")
247
- matchedIntent = nil
248
- matchedSlots = [:]
249
- executionResult = "thinking..."
250
- executionData = nil
251
- assistantFallback(transcription: transcription)
252
- }
253
- }
254
-
255
- /// Fire the selected Assistant provider in parallel — non-blocking, result arrives later.
256
- private func fireAdvisor(transcript: String, matched: String) {
257
- let assistant = PiChatSession.shared
258
- guard assistant.isProviderInferenceReady else {
259
- DiagnosticLog.shared.info("AudioLayer: advisor skipped (Assistant provider not ready)")
260
- return
261
- }
262
-
263
- DiagnosticLog.shared.info("AudioLayer: firing Assistant advisor via \(assistant.currentProvider.name)")
264
-
265
- assistant.askVoiceAdvisor(transcript: transcript, matched: matched) { [weak self] response in
266
- guard let self = self, let response = response else { return }
267
- self.agentResponse = response
268
- if let commentary = response.commentary {
269
- DiagnosticLog.shared.info("AudioLayer: Assistant advisor says — \(commentary)")
270
- }
271
- if let suggestion = response.suggestion {
272
- DiagnosticLog.shared.info("AudioLayer: Assistant advisor suggests — \(suggestion.label) → \(suggestion.intent)")
273
- }
274
- }
275
- }
276
-
277
- private func assistantFallback(transcription: Transcription) {
278
- let assistant = PiChatSession.shared
279
- guard assistant.isProviderInferenceReady else {
280
- executionResult = "Connect an Assistant provider in Settings"
281
- DiagnosticLog.shared.info("AudioLayer: Assistant provider not ready")
282
- return
283
- }
284
-
285
- assistant.resolveVoiceIntent(transcript: transcription.text) { [weak self] resolved in
286
- guard let self else { return }
287
- guard let resolved else {
288
- self.executionResult = "Assistant couldn't resolve intent"
289
- DiagnosticLog.shared.info("AudioLayer: Assistant provider returned no intent")
290
- return
291
- }
292
-
293
- DiagnosticLog.shared.info("AudioLayer: Assistant resolved → \(resolved.intent) \(resolved.slots)")
294
- self.matchedIntent = resolved.intent
295
- self.matchedSlots = resolved.slots.reduce(into: [:]) { dict, pair in
296
- dict[pair.key] = pair.value.stringValue ?? "\(pair.value)"
297
- }
298
-
299
- let intentMatch = IntentMatch(
300
- intentName: resolved.intent,
301
- slots: resolved.slots,
302
- confidence: 0.8,
303
- matchedPhrase: "assistant-provider"
304
- )
305
-
306
- do {
307
- let execResult = try PhraseMatcher.shared.execute(intentMatch)
308
- self.executionResult = self.voiceResultSummary(for: intentMatch, result: execResult)
309
- self.executionData = execResult
310
- DiagnosticLog.shared.info("AudioLayer: Assistant-resolved executed → \(self.executionResult ?? "\(execResult)")")
311
- } catch {
312
- self.executionResult = error.localizedDescription
313
- self.executionData = nil
314
- DiagnosticLog.shared.info("AudioLayer: Assistant-resolved execution error — \(error.localizedDescription)")
315
- }
316
- }
317
- }
318
-
319
- private func assistantQuestion(transcription: Transcription) {
320
- let assistant = PiChatSession.shared
321
- guard assistant.isProviderInferenceReady else {
322
- executionResult = "Connect an Assistant provider in Settings"
323
- DiagnosticLog.shared.info("AudioLayer: Assistant provider not ready for question")
324
- return
325
- }
326
-
327
- assistant.answerVoiceQuestion(transcription.text) { [weak self] response in
328
- guard let self else { return }
329
- guard let response else {
330
- self.executionResult = "Assistant couldn't answer"
331
- DiagnosticLog.shared.info("AudioLayer: Assistant provider returned no answer")
332
- return
333
- }
334
- self.agentResponse = response
335
- self.executionResult = "ok"
336
- self.executionData = nil
337
- if let commentary = response.commentary {
338
- DiagnosticLog.shared.info("AudioLayer: Assistant answered — \(commentary.prefix(160))")
339
- }
340
- }
341
- }
342
-
343
- private func shouldAnswerWithAssistant(_ text: String) -> Bool {
344
- let lower = text.lowercased()
345
- let questionStarters = [
346
- "what", "how", "why", "where", "when", "who",
347
- "can you tell", "tell me about", "explain", "summarize", "describe"
348
- ]
349
- let asksQuestion = text.contains("?") || questionStarters.contains(where: lower.hasPrefix)
350
- guard asksQuestion else { return false }
351
-
352
- let assistantTopics = [
353
- "setting", "settings", "configured", "enabled", "disabled",
354
- "mouse", "shortcut", "shortcuts", "gesture", "gestures",
355
- "ocr", "terminal", "scan root", "assistant", "provider",
356
- "lattices", "workspace"
357
- ]
358
- return assistantTopics.contains(where: lower.contains)
359
- }
360
-
361
- private func voiceResultSummary(for match: IntentMatch, result: JSON) -> String {
362
- if let summary = result["summary"]?.stringValue, !summary.isEmpty {
363
- return summary
364
- }
365
- if let message = result["message"]?.stringValue, !message.isEmpty {
366
- return message
367
- }
368
- if result["ok"]?.boolValue == false {
369
- return result["reason"]?.stringValue ?? "Voice command did not complete"
370
- }
371
-
372
- switch match.intentName {
373
- case "tile_window":
374
- let position = match.slots["position"]?.stringValue
375
- ?? result["position"]?.stringValue
376
- ?? "requested position"
377
- return "Moved window to \(position)"
378
-
379
- case "focus":
380
- let target = result["focused"]?.stringValue
381
- ?? match.slots["app"]?.stringValue
382
- ?? "target"
383
- return "Focused \(target)"
384
-
385
- case "launch":
386
- if let launched = result["launched"]?.stringValue {
387
- return "Launched \(launched)"
388
- }
389
- let target = match.slots["project"]?.stringValue ?? "requested target"
390
- return "Opened \(target)"
391
-
392
- case "kill":
393
- let target = match.slots["session"]?.stringValue
394
- ?? match.slots["app"]?.stringValue
395
- ?? "target"
396
- return "Closed \(target)"
397
-
398
- default:
399
- return "ok"
400
- }
401
- }
402
- }
403
-
404
- // Old IntentExtractor removed — PhraseMatcher handles all intent matching now.
405
- // See apps/mac/Sources/Intents/LatticeIntent.swift
406
-
407
-
408
- // MARK: - Vox Audio Provider (WebSocket JSON-RPC via VoxClient)
409
- //
410
- // Delegates recording and transcription entirely to the Vox daemon (voxd).
411
- // Lattices never touches the mic — Vox owns the mic, recording, and
412
- // transcription. We call transcribe.startSession to begin recording
413
- // and transcribe.stopSession to stop and get the transcript.
414
- //
415
- // Session events flow on the startSession call ID:
416
- // session.state: {state, sessionId, previous}
417
- // session.final: {sessionId, text, words[], elapsedMs, metrics}
418
-
419
- final class VoxAudioProvider: AudioProvider {
420
- private var onTranscript: ((Transcription) -> Void)?
421
- private var stopCompletion: ((Transcription?) -> Void)?
422
- private var _isListening = false
423
- private var startTime: Date?
424
-
425
- var isAvailable: Bool {
426
- VoxClient.shared.connectionState == .connected
427
- }
428
-
429
- var isListening: Bool { _isListening }
430
-
431
- func checkHealth(completion: @escaping (Bool) -> Void) {
432
- let client = VoxClient.shared
433
- if client.connectionState == .connected {
434
- client.call(method: "health") { result in
435
- switch result {
436
- case .success: DispatchQueue.main.async { completion(true) }
437
- case .failure: DispatchQueue.main.async { completion(false) }
438
- }
439
- }
440
- } else {
441
- completion(false)
442
- }
443
- }
444
-
445
- func startListening(onTranscript: @escaping (Transcription) -> Void) {
446
- let client = VoxClient.shared
447
- guard client.connectionState == .connected else {
448
- DiagnosticLog.shared.warn("VoxAudioProvider: not connected to Vox")
449
- onTranscript(Transcription(text: "", confidence: 0, source: "vox", isPartial: false, durationMs: nil))
450
- return
451
- }
452
-
453
- self.onTranscript = onTranscript
454
- _isListening = true
455
- startTime = Date()
456
-
457
- DiagnosticLog.shared.info("VoxAudioProvider: starting session via Vox")
458
-
459
- // transcribe.startSession — Vox records from mic, emits session events on this call ID
460
- client.startSession(
461
- onProgress: { [weak self] event, data in
462
- guard let self else { return }
463
- DispatchQueue.main.async {
464
- switch event {
465
- case "session.state":
466
- let state = data["state"] as? String ?? ""
467
- DiagnosticLog.shared.info("VoxAudioProvider: session → \(state)")
468
-
469
- case "session.final":
470
- // Final transcript arrived — deliver it
471
- if let text = data["text"] as? String, !text.isEmpty {
472
- let elapsed = data["elapsedMs"] as? Int
473
- let t = Transcription(
474
- text: text, confidence: 0.95, source: "vox",
475
- isPartial: false, durationMs: elapsed
476
- )
477
- DiagnosticLog.shared.info("VoxAudioProvider: transcribed → '\(text)' (\(elapsed ?? 0)ms)")
478
- self.onTranscript?(t)
479
- self.stopCompletion?(t)
480
- self.stopCompletion = nil
481
- }
482
-
483
- default:
484
- break
485
- }
486
- }
487
- },
488
- completion: { [weak self] result in
489
- guard let self else { return }
490
- DispatchQueue.main.async {
491
- self._isListening = false
492
-
493
- switch result {
494
- case .success(let data):
495
- // Final result also comes here (same data as session.final)
496
- if let text = data["text"] as? String, !text.isEmpty,
497
- self.stopCompletion != nil {
498
- // Only deliver if session.final didn't already
499
- let elapsed = data["elapsedMs"] as? Int
500
- let t = Transcription(
501
- text: text, confidence: 0.95, source: "vox",
502
- isPartial: false, durationMs: elapsed
503
- )
504
- self.onTranscript?(t)
505
- self.stopCompletion?(t)
506
- self.stopCompletion = nil
507
- } else if self.stopCompletion != nil {
508
- self.stopCompletion?(nil)
509
- self.stopCompletion = nil
510
- }
511
-
512
- case .failure(let error):
513
- DiagnosticLog.shared.warn("VoxAudioProvider: session error — \(error.localizedDescription)")
514
- if case .sessionBusy = error {
515
- AudioLayer.shared.executionResult = "Session already active"
516
- } else {
517
- AudioLayer.shared.executionResult = "Transcription failed"
518
- }
519
- self.onTranscript?(Transcription(
520
- text: "", confidence: 0, source: "vox",
521
- isPartial: false, durationMs: nil
522
- ))
523
- self.stopCompletion?(nil)
524
- self.stopCompletion = nil
525
- }
526
- }
527
- }
528
- )
529
- }
530
-
531
- func stopListening(completion: @escaping (Transcription?) -> Void) {
532
- _isListening = false
533
-
534
- let client = VoxClient.shared
535
- guard client.connectionState == .connected else {
536
- completion(nil)
537
- return
538
- }
539
-
540
- DiagnosticLog.shared.info("VoxAudioProvider: stopping session")
541
-
542
- // Store completion — the startSession's session.final event delivers the transcript
543
- self.stopCompletion = completion
544
-
545
- client.stopSession { result in
546
- if case .failure(let error) = result {
547
- DiagnosticLog.shared.warn("VoxAudioProvider: stopSession error — \(error.localizedDescription)")
548
- DispatchQueue.main.async {
549
- self.stopCompletion?(nil)
550
- self.stopCompletion = nil
551
- }
552
- }
553
- }
554
- }
555
- }