@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.
- package/README.md +5 -7
- package/apps/mac/Info.plist +2 -2
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +191 -63
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +4 -5
- package/docs/voice.md +11 -27
- package/package.json +9 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2271
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- 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
|
-
}
|