@lattices/cli 0.4.9 → 0.4.11
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/LICENSE +21 -0
- package/README.md +13 -13
- package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
- package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/{app → apps/mac}/Package.swift +2 -1
- package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
- package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
- package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
- package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
- package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
- package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
- package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
- package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
- package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
- package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
- package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
- package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
- package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
- package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
- package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
- package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
- package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
- package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
- package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
- package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
- package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
- package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
- package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
- package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
- package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
- package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
- package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
- package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
- package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
- package/apps/mac/Sources/Core/System/Capability.swift +79 -0
- package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
- package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
- package/bin/handsoff-infer.ts +14 -5
- package/bin/handsoff-worker.ts +11 -7
- package/bin/infer.ts +406 -0
- package/bin/lattices-app.ts +57 -7
- package/bin/lattices-dev +40 -1
- package/bin/lattices.ts +1 -1
- package/docs/agent-execution-plan.md +9 -9
- package/docs/api.md +119 -0
- package/docs/app.md +1 -0
- package/docs/companion-deck.md +1 -1
- package/docs/gesture-customization-proposal.md +520 -0
- package/docs/mouse-gestures.md +79 -0
- package/docs/overview.md +2 -2
- package/docs/presentation-execution-review.md +9 -9
- package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
- package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
- package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
- package/docs/reference/dewey.config.ts +74 -0
- package/docs/reference/install-agent.md +79 -0
- package/docs/repo-structure.md +100 -0
- package/docs/voice-error-model.md +7 -7
- package/docs/voice.md +18 -0
- package/package.json +23 -13
- package/swift/Package.swift +20 -0
- package/swift/Sources/DeckKit/DeckAction.swift +51 -0
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
- package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
- package/swift/Sources/DeckKit/DeckHost.swift +7 -0
- package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
- package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
- package/swift/Sources/DeckKit/DeckValue.swift +93 -0
- package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
- package/app/Sources/AppShell/AppDelegate.swift +0 -408
- package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
- package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
- package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
- package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
- /package/{app → apps/mac}/Info.plist +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
- /package/{app → apps/mac}/Lattices.entitlements +0 -0
- /package/{app → apps/mac}/Resources/tap.wav +0 -0
- /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
- /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
- /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
- /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
- /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
- /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
|
@@ -43,13 +43,15 @@ final class AudioLayer: ObservableObject {
|
|
|
43
43
|
@Published var providerName: String = "none"
|
|
44
44
|
@Published var agentResponse: AgentResponse?
|
|
45
45
|
|
|
46
|
+
private var pendingVoiceStart = false
|
|
47
|
+
private var voiceConnectionRetry: DispatchWorkItem?
|
|
46
48
|
|
|
47
49
|
private init() {
|
|
48
50
|
let vox = VoxAudioProvider()
|
|
49
51
|
provider = vox
|
|
50
52
|
providerName = "vox"
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
+
// Voice entry points can arrive from the desktop UI, daemon, or iOS
|
|
54
|
+
// bridge, so connection setup is handled lazily when capture starts.
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/// Start a voice command capture. Transcription is piped to the intent engine.
|
|
@@ -69,6 +71,47 @@ final class AudioLayer: ObservableObject {
|
|
|
69
71
|
return
|
|
70
72
|
}
|
|
71
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) {
|
|
72
115
|
isListening = true
|
|
73
116
|
|
|
74
117
|
provider.startListening { [weak self] transcription in
|
|
@@ -85,7 +128,7 @@ final class AudioLayer: ObservableObject {
|
|
|
85
128
|
self.isListening = false
|
|
86
129
|
|
|
87
130
|
// Empty transcript = transcription failed, don't try to execute
|
|
88
|
-
guard !transcription.text.isEmpty else {
|
|
131
|
+
guard !transcription.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
|
89
132
|
if self.executionResult == nil || self.executionResult == "Transcribing..." {
|
|
90
133
|
self.executionResult = "No speech detected"
|
|
91
134
|
}
|
|
@@ -98,19 +141,54 @@ final class AudioLayer: ObservableObject {
|
|
|
98
141
|
}
|
|
99
142
|
}
|
|
100
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
|
+
|
|
101
169
|
/// Track whether we already executed for this recording session.
|
|
102
170
|
private var didExecuteIntent = false
|
|
103
171
|
|
|
104
172
|
func stopVoiceCommand() {
|
|
173
|
+
if pendingVoiceStart, !isListening {
|
|
174
|
+
pendingVoiceStart = false
|
|
175
|
+
voiceConnectionRetry?.cancel()
|
|
176
|
+
voiceConnectionRetry = nil
|
|
177
|
+
executionResult = "Voice cancelled"
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
105
181
|
guard let provider = provider, isListening else { return }
|
|
106
182
|
|
|
183
|
+
pendingVoiceStart = false
|
|
107
184
|
isListening = false
|
|
108
185
|
executionResult = "Transcribing..."
|
|
109
186
|
|
|
110
187
|
provider.stopListening { [weak self] transcription in
|
|
111
188
|
DispatchQueue.main.async {
|
|
112
189
|
guard let self = self else { return }
|
|
113
|
-
if let t = transcription
|
|
190
|
+
if let t = transcription,
|
|
191
|
+
!t.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
114
192
|
self.lastTranscript = t.text
|
|
115
193
|
// Skip if the streaming callback already executed the intent
|
|
116
194
|
guard !self.didExecuteIntent else { return }
|
|
@@ -130,6 +208,16 @@ final class AudioLayer: ObservableObject {
|
|
|
130
208
|
// Clear previous agent response
|
|
131
209
|
agentResponse = nil
|
|
132
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
|
+
|
|
133
221
|
if let match = matcher.match(text: transcription.text) {
|
|
134
222
|
matchedIntent = match.intentName
|
|
135
223
|
matchConfidence = match.confidence
|
|
@@ -140,100 +228,181 @@ final class AudioLayer: ObservableObject {
|
|
|
140
228
|
|
|
141
229
|
do {
|
|
142
230
|
let result = try matcher.execute(match)
|
|
143
|
-
executionResult =
|
|
231
|
+
executionResult = voiceResultSummary(for: match, result: result)
|
|
144
232
|
executionData = result
|
|
145
|
-
DiagnosticLog.shared.info("AudioLayer: executed '\(match.intentName)' → ok")
|
|
233
|
+
DiagnosticLog.shared.info("AudioLayer: executed '\(match.intentName)' → \(executionResult ?? "ok")")
|
|
146
234
|
} catch {
|
|
147
|
-
DiagnosticLog.shared.info("AudioLayer: intent error — \(error.localizedDescription),
|
|
235
|
+
DiagnosticLog.shared.info("AudioLayer: intent error — \(error.localizedDescription), asking Assistant provider")
|
|
148
236
|
executionResult = "thinking..."
|
|
149
237
|
executionData = nil
|
|
150
|
-
|
|
238
|
+
assistantFallback(transcription: transcription)
|
|
151
239
|
}
|
|
152
240
|
|
|
153
|
-
// Fire parallel
|
|
241
|
+
// Fire parallel provider-backed advisor for 5+ word utterances.
|
|
154
242
|
fireAdvisor(transcript: transcription.text, matched: "\(match.intentName)(\(matchedSlots))")
|
|
155
243
|
|
|
156
244
|
} else {
|
|
157
|
-
// No local match —
|
|
158
|
-
DiagnosticLog.shared.info("AudioLayer: no phrase match for '\(transcription.text)',
|
|
245
|
+
// No local match — ask the selected Assistant provider.
|
|
246
|
+
DiagnosticLog.shared.info("AudioLayer: no phrase match for '\(transcription.text)', asking Assistant provider")
|
|
159
247
|
matchedIntent = nil
|
|
160
248
|
matchedSlots = [:]
|
|
161
249
|
executionResult = "thinking..."
|
|
162
250
|
executionData = nil
|
|
163
|
-
|
|
251
|
+
assistantFallback(transcription: transcription)
|
|
164
252
|
}
|
|
165
253
|
}
|
|
166
254
|
|
|
167
|
-
/// Fire the
|
|
255
|
+
/// Fire the selected Assistant provider in parallel — non-blocking, result arrives later.
|
|
168
256
|
private func fireAdvisor(transcript: String, matched: String) {
|
|
169
|
-
let
|
|
170
|
-
guard
|
|
171
|
-
DiagnosticLog.shared.info("AudioLayer: advisor skipped (
|
|
257
|
+
let assistant = PiChatSession.shared
|
|
258
|
+
guard assistant.isProviderInferenceReady else {
|
|
259
|
+
DiagnosticLog.shared.info("AudioLayer: advisor skipped (Assistant provider not ready)")
|
|
172
260
|
return
|
|
173
261
|
}
|
|
174
262
|
|
|
175
|
-
|
|
176
|
-
DiagnosticLog.shared.info("AudioLayer: firing haiku advisor")
|
|
263
|
+
DiagnosticLog.shared.info("AudioLayer: firing Assistant advisor via \(assistant.currentProvider.name)")
|
|
177
264
|
|
|
178
|
-
|
|
265
|
+
assistant.askVoiceAdvisor(transcript: transcript, matched: matched) { [weak self] response in
|
|
179
266
|
guard let self = self, let response = response else { return }
|
|
180
267
|
self.agentResponse = response
|
|
181
268
|
if let commentary = response.commentary {
|
|
182
|
-
DiagnosticLog.shared.info("AudioLayer:
|
|
269
|
+
DiagnosticLog.shared.info("AudioLayer: Assistant advisor says — \(commentary)")
|
|
183
270
|
}
|
|
184
271
|
if let suggestion = response.suggestion {
|
|
185
|
-
DiagnosticLog.shared.info("AudioLayer:
|
|
272
|
+
DiagnosticLog.shared.info("AudioLayer: Assistant advisor suggests — \(suggestion.label) → \(suggestion.intent)")
|
|
186
273
|
}
|
|
187
274
|
}
|
|
188
275
|
}
|
|
189
276
|
|
|
190
|
-
private func
|
|
191
|
-
|
|
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
|
|
192
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
|
+
}
|
|
193
292
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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"
|
|
198
304
|
)
|
|
199
305
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
}
|
|
206
318
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
+
}
|
|
212
326
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
} catch {
|
|
226
|
-
self.executionResult = error.localizedDescription
|
|
227
|
-
self.executionData = nil
|
|
228
|
-
DiagnosticLog.shared.info("AudioLayer: Claude-resolved execution error — \(error.localizedDescription)")
|
|
229
|
-
}
|
|
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))")
|
|
230
339
|
}
|
|
231
340
|
}
|
|
232
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
|
+
}
|
|
233
402
|
}
|
|
234
403
|
|
|
235
404
|
// Old IntentExtractor removed — PhraseMatcher handles all intent matching now.
|
|
236
|
-
// See
|
|
405
|
+
// See apps/mac/Sources/Intents/LatticeIntent.swift
|
|
237
406
|
|
|
238
407
|
|
|
239
408
|
// MARK: - Vox Audio Provider (WebSocket JSON-RPC via VoxClient)
|
package/bin/handsoff-infer.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Usage: echo '{"transcript":"tile chrome left","snapshot":{...}}' | bun run bin/handsoff-infer.ts
|
|
6
6
|
*
|
|
7
|
-
* Reads JSON from stdin, calls
|
|
7
|
+
* Reads JSON from stdin, calls the configured voice inference provider, prints JSON result to stdout.
|
|
8
8
|
* All logging goes to stderr so it doesn't pollute the JSON output.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
normalizeAssistantPlan,
|
|
15
15
|
tryLocalAssistantPlan,
|
|
16
16
|
} from "./assistant-intelligence.ts";
|
|
17
|
-
import { inferJSON } from "
|
|
17
|
+
import { inferJSON, resolveVoiceInferenceOptions } from "./infer.ts";
|
|
18
|
+
|
|
19
|
+
const INFER_TIMEOUT_MS = 15_000;
|
|
18
20
|
|
|
19
21
|
// ── Read input from stdin ──────────────────────────────────────────
|
|
20
22
|
|
|
@@ -36,6 +38,7 @@ const req = JSON.parse(input) as {
|
|
|
36
38
|
const transcript = req.transcript ?? "";
|
|
37
39
|
const systemPrompt = buildAssistantSystemPrompt();
|
|
38
40
|
const userMessage = buildAssistantContextMessage(transcript, req.snapshot ?? {});
|
|
41
|
+
const voiceInference = resolveVoiceInferenceOptions();
|
|
39
42
|
|
|
40
43
|
const localPlan = tryLocalAssistantPlan(transcript, req.snapshot ?? {});
|
|
41
44
|
if (localPlan) {
|
|
@@ -50,14 +53,18 @@ const messages = (req.history ?? []).map((h) => ({
|
|
|
50
53
|
content: h.content,
|
|
51
54
|
}));
|
|
52
55
|
|
|
56
|
+
const controller = new AbortController();
|
|
57
|
+
const timer = setTimeout(() => controller.abort(), INFER_TIMEOUT_MS);
|
|
58
|
+
|
|
53
59
|
try {
|
|
54
60
|
const { data, raw } = await inferJSON(userMessage, {
|
|
55
|
-
provider:
|
|
56
|
-
model:
|
|
61
|
+
provider: voiceInference.provider,
|
|
62
|
+
model: voiceInference.model,
|
|
57
63
|
system: systemPrompt,
|
|
58
64
|
messages,
|
|
59
65
|
temperature: 0.2,
|
|
60
66
|
maxTokens: 512,
|
|
67
|
+
abortSignal: controller.signal,
|
|
61
68
|
tag: "hands-off",
|
|
62
69
|
});
|
|
63
70
|
|
|
@@ -83,5 +90,7 @@ try {
|
|
|
83
90
|
_meta: { error: err.message },
|
|
84
91
|
})
|
|
85
92
|
);
|
|
86
|
-
process.
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
} finally {
|
|
95
|
+
clearTimeout(timer);
|
|
87
96
|
}
|
package/bin/handsoff-worker.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Hands-off worker — long-running process that handles both inference and TTS.
|
|
4
4
|
*
|
|
5
5
|
* Reads newline-delimited JSON commands from stdin, writes JSON responses to stdout.
|
|
6
|
-
* Keeps
|
|
6
|
+
* Keeps TTS and inference warm — no cold starts.
|
|
7
7
|
*
|
|
8
8
|
* Commands:
|
|
9
9
|
* {"cmd":"infer","transcript":"...","snapshot":{...},"history":[...]}
|
|
@@ -23,9 +23,10 @@ import {
|
|
|
23
23
|
normalizeAssistantPlan,
|
|
24
24
|
tryLocalAssistantPlan,
|
|
25
25
|
} from "./assistant-intelligence.ts";
|
|
26
|
-
import { infer } from "
|
|
26
|
+
import { infer, resolveVoiceInferenceOptions } from "./infer.ts";
|
|
27
27
|
|
|
28
28
|
const INFER_TIMEOUT_MS = 15_000;
|
|
29
|
+
const voiceInference = resolveVoiceInferenceOptions();
|
|
29
30
|
|
|
30
31
|
/** Call infer and parse JSON if possible, otherwise treat as spoken-only response */
|
|
31
32
|
async function inferSmart(prompt: string, options: any): Promise<{ data: any; raw: any }> {
|
|
@@ -291,12 +292,15 @@ log("worker started, streaming TTS ready");
|
|
|
291
292
|
|
|
292
293
|
const systemPrompt = buildAssistantSystemPrompt();
|
|
293
294
|
log("system prompt loaded");
|
|
295
|
+
log(`voice inference: ${voiceInference.provider}/${voiceInference.model}`);
|
|
294
296
|
|
|
295
297
|
// ── Auto-restart on file changes ───────────────────────────────────
|
|
296
298
|
|
|
297
299
|
const watchFiles = [
|
|
298
300
|
assistantPromptPath,
|
|
299
301
|
join(import.meta.dir, "assistant-intelligence.ts"),
|
|
302
|
+
join(import.meta.dir, "..", ".env"),
|
|
303
|
+
join(import.meta.dir, "..", ".env.local"),
|
|
300
304
|
import.meta.path, // this script itself
|
|
301
305
|
];
|
|
302
306
|
|
|
@@ -376,8 +380,8 @@ async function processLine(line: string) {
|
|
|
376
380
|
}));
|
|
377
381
|
|
|
378
382
|
const { data, raw } = await inferSmart(userMessage, {
|
|
379
|
-
provider:
|
|
380
|
-
model:
|
|
383
|
+
provider: voiceInference.provider,
|
|
384
|
+
model: voiceInference.model,
|
|
381
385
|
system: systemPrompt,
|
|
382
386
|
messages,
|
|
383
387
|
temperature: 0.2,
|
|
@@ -416,7 +420,7 @@ async function processLine(line: string) {
|
|
|
416
420
|
//
|
|
417
421
|
// Timeline:
|
|
418
422
|
// t=0 ──┬── ack TTS (fire & forget)
|
|
419
|
-
// └──
|
|
423
|
+
// └── model inference
|
|
420
424
|
// t=~600ms ─┬── narrate TTS (what we're doing)
|
|
421
425
|
// └── execute actions (in parallel with narrate)
|
|
422
426
|
// t=done ── respond with results
|
|
@@ -445,8 +449,8 @@ async function processLine(line: string) {
|
|
|
445
449
|
const userMessage = buildAssistantContextMessage(transcript, snap);
|
|
446
450
|
try {
|
|
447
451
|
const { data, raw } = await inferSmart(userMessage, {
|
|
448
|
-
provider:
|
|
449
|
-
model:
|
|
452
|
+
provider: voiceInference.provider,
|
|
453
|
+
model: voiceInference.model,
|
|
450
454
|
system: systemPrompt,
|
|
451
455
|
messages,
|
|
452
456
|
temperature: 0.2,
|