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