@lattices/cli 0.3.0 → 0.4.1

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 (111) hide show
  1. package/README.md +85 -9
  2. package/app/Info.plist +30 -0
  3. package/app/Lattices.app/Contents/Info.plist +8 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  6. package/app/Lattices.app/Contents/Resources/tap.wav +0 -0
  7. package/app/Lattices.app/Contents/_CodeSignature/CodeResources +139 -0
  8. package/app/Lattices.entitlements +15 -0
  9. package/app/Package.swift +8 -1
  10. package/app/Resources/tap.wav +0 -0
  11. package/app/Sources/AdvisorLearningStore.swift +90 -0
  12. package/app/Sources/AgentSession.swift +377 -0
  13. package/app/Sources/AppDelegate.swift +45 -12
  14. package/app/Sources/AppShellView.swift +81 -8
  15. package/app/Sources/AudioProvider.swift +386 -0
  16. package/app/Sources/CheatSheetHUD.swift +261 -19
  17. package/app/Sources/DaemonProtocol.swift +13 -0
  18. package/app/Sources/DaemonServer.swift +8 -0
  19. package/app/Sources/DesktopModel.swift +189 -6
  20. package/app/Sources/DesktopModelTypes.swift +2 -0
  21. package/app/Sources/DiagnosticLog.swift +104 -2
  22. package/app/Sources/EventBus.swift +1 -0
  23. package/app/Sources/HUDBottomBar.swift +279 -0
  24. package/app/Sources/HUDController.swift +1158 -0
  25. package/app/Sources/HUDLeftBar.swift +849 -0
  26. package/app/Sources/HUDMinimap.swift +179 -0
  27. package/app/Sources/HUDRightBar.swift +774 -0
  28. package/app/Sources/HUDState.swift +367 -0
  29. package/app/Sources/HUDTopBar.swift +243 -0
  30. package/app/Sources/HandsOffSession.swift +802 -0
  31. package/app/Sources/HomeDashboardView.swift +125 -0
  32. package/app/Sources/HotkeyManager.swift +2 -0
  33. package/app/Sources/HotkeyStore.swift +49 -9
  34. package/app/Sources/IntentEngine.swift +962 -0
  35. package/app/Sources/Intents/CreateLayerIntent.swift +54 -0
  36. package/app/Sources/Intents/DistributeIntent.swift +56 -0
  37. package/app/Sources/Intents/FocusIntent.swift +69 -0
  38. package/app/Sources/Intents/HelpIntent.swift +41 -0
  39. package/app/Sources/Intents/KillIntent.swift +47 -0
  40. package/app/Sources/Intents/LatticeIntent.swift +78 -0
  41. package/app/Sources/Intents/LaunchIntent.swift +67 -0
  42. package/app/Sources/Intents/ListSessionsIntent.swift +32 -0
  43. package/app/Sources/Intents/ListWindowsIntent.swift +30 -0
  44. package/app/Sources/Intents/ScanIntent.swift +52 -0
  45. package/app/Sources/Intents/SearchIntent.swift +190 -0
  46. package/app/Sources/Intents/SwitchLayerIntent.swift +50 -0
  47. package/app/Sources/Intents/TileIntent.swift +61 -0
  48. package/app/Sources/LatticesApi.swift +1275 -30
  49. package/app/Sources/LauncherHUD.swift +348 -0
  50. package/app/Sources/MainView.swift +147 -44
  51. package/app/Sources/MouseFinder.swift +222 -0
  52. package/app/Sources/OcrModel.swift +34 -1
  53. package/app/Sources/OmniSearchState.swift +99 -102
  54. package/app/Sources/OnboardingView.swift +457 -0
  55. package/app/Sources/PermissionChecker.swift +2 -12
  56. package/app/Sources/PiChatDock.swift +454 -0
  57. package/app/Sources/PiChatSession.swift +815 -0
  58. package/app/Sources/PiWorkspaceView.swift +364 -0
  59. package/app/Sources/PlacementSpec.swift +195 -0
  60. package/app/Sources/Preferences.swift +59 -0
  61. package/app/Sources/ProjectScanner.swift +58 -45
  62. package/app/Sources/ScreenMapState.swift +701 -55
  63. package/app/Sources/ScreenMapView.swift +843 -103
  64. package/app/Sources/ScreenMapWindowController.swift +22 -0
  65. package/app/Sources/SessionLayerStore.swift +285 -0
  66. package/app/Sources/SessionManager.swift +4 -1
  67. package/app/Sources/SettingsView.swift +186 -3
  68. package/app/Sources/Theme.swift +9 -8
  69. package/app/Sources/TmuxModel.swift +7 -0
  70. package/app/Sources/TmuxQuery.swift +27 -3
  71. package/app/Sources/VoiceChatView.swift +192 -0
  72. package/app/Sources/VoiceCommandWindow.swift +1594 -0
  73. package/app/Sources/VoiceIntentResolver.swift +671 -0
  74. package/app/Sources/VoxClient.swift +454 -0
  75. package/app/Sources/WindowTiler.swift +348 -87
  76. package/app/Sources/WorkspaceManager.swift +127 -18
  77. package/app/Tests/StageDragTests.swift +333 -0
  78. package/app/Tests/StageJoinTests.swift +313 -0
  79. package/app/Tests/StageManagerTests.swift +280 -0
  80. package/app/Tests/StageTileTests.swift +353 -0
  81. package/assets/AppIcon.icns +0 -0
  82. package/bin/client.ts +16 -0
  83. package/bin/{daemon-client.js → daemon-client.ts} +49 -30
  84. package/bin/handsoff-infer.ts +280 -0
  85. package/bin/handsoff-worker.ts +740 -0
  86. package/bin/lattices-app.ts +338 -0
  87. package/bin/lattices-dev +208 -0
  88. package/bin/{lattices.js → lattices.ts} +777 -140
  89. package/bin/project-twin.ts +645 -0
  90. package/docs/agent-execution-plan.md +562 -0
  91. package/docs/agent-layer-guide.md +207 -0
  92. package/docs/agents.md +142 -0
  93. package/docs/api.md +153 -34
  94. package/docs/app.md +29 -1
  95. package/docs/config.md +5 -1
  96. package/docs/handsoff-test-scenarios.md +84 -0
  97. package/docs/layers.md +20 -20
  98. package/docs/ocr.md +14 -5
  99. package/docs/overview.md +5 -1
  100. package/docs/presentation-execution-review.md +491 -0
  101. package/docs/prompts/hands-off-system.md +374 -0
  102. package/docs/prompts/hands-off-turn.md +30 -0
  103. package/docs/prompts/voice-advisor.md +31 -0
  104. package/docs/prompts/voice-fallback.md +23 -0
  105. package/docs/tiling-reference.md +167 -0
  106. package/docs/twins.md +138 -0
  107. package/docs/voice-command-protocol.md +278 -0
  108. package/docs/voice.md +219 -0
  109. package/package.json +29 -11
  110. package/bin/client.js +0 -4
  111. package/bin/lattices-app.js +0 -221
@@ -0,0 +1,386 @@
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
+
47
+ private init() {
48
+ let vox = VoxAudioProvider()
49
+ provider = vox
50
+ providerName = "vox"
51
+ // Connection is managed by VoiceCommandWindow — not here.
52
+ // Connecting here would race with (and destroy) the existing WebSocket.
53
+ }
54
+
55
+ /// Start a voice command capture. Transcription is piped to the intent engine.
56
+ func startVoiceCommand() {
57
+ guard !isListening else { return }
58
+
59
+ // Clear previous state
60
+ lastTranscript = nil
61
+ matchedIntent = nil
62
+ matchedSlots = [:]
63
+ matchConfidence = 0
64
+ executionResult = nil
65
+ didExecuteIntent = false
66
+
67
+ guard let provider = provider else {
68
+ executionResult = "No voice provider — install Vox"
69
+ return
70
+ }
71
+
72
+ isListening = true
73
+
74
+ provider.startListening { [weak self] transcription in
75
+ DispatchQueue.main.async {
76
+ guard let self = self else { return }
77
+
78
+ if transcription.isPartial {
79
+ self.lastTranscript = transcription.text
80
+ return
81
+ }
82
+
83
+ // Final transcript (e.g. from streaming providers)
84
+ self.lastTranscript = transcription.text
85
+ self.isListening = false
86
+
87
+ // Empty transcript = transcription failed, don't try to execute
88
+ guard !transcription.text.isEmpty else {
89
+ if self.executionResult == nil || self.executionResult == "Transcribing..." {
90
+ self.executionResult = "No speech detected"
91
+ }
92
+ return
93
+ }
94
+
95
+ EventBus.shared.post(.voiceCommand(text: transcription.text, confidence: transcription.confidence))
96
+ self.executeVoiceIntent(transcription)
97
+ }
98
+ }
99
+ }
100
+
101
+ /// Track whether we already executed for this recording session.
102
+ private var didExecuteIntent = false
103
+
104
+ func stopVoiceCommand() {
105
+ guard let provider = provider, isListening else { return }
106
+
107
+ isListening = false
108
+ executionResult = "Transcribing..."
109
+
110
+ provider.stopListening { [weak self] transcription in
111
+ DispatchQueue.main.async {
112
+ guard let self = self else { return }
113
+ if let t = transcription {
114
+ self.lastTranscript = t.text
115
+ // Skip if the streaming callback already executed the intent
116
+ guard !self.didExecuteIntent else { return }
117
+ EventBus.shared.post(.voiceCommand(text: t.text, confidence: t.confidence))
118
+ self.executeVoiceIntent(t)
119
+ } else if !self.didExecuteIntent {
120
+ self.executionResult = "No speech detected"
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ private func executeVoiceIntent(_ transcription: Transcription) {
127
+ didExecuteIntent = true
128
+ let matcher = PhraseMatcher.shared
129
+
130
+ // Clear previous agent response
131
+ agentResponse = nil
132
+
133
+ if let match = matcher.match(text: transcription.text) {
134
+ matchedIntent = match.intentName
135
+ matchConfidence = match.confidence
136
+ matchedSlots = match.slots.reduce(into: [:]) { dict, pair in
137
+ dict[pair.key] = pair.value.stringValue ?? "\(pair.value)"
138
+ }
139
+ DiagnosticLog.shared.info("AudioLayer: matched '\(match.intentName)' via '\(match.matchedPhrase)' slots=\(matchedSlots)")
140
+
141
+ do {
142
+ let result = try matcher.execute(match)
143
+ executionResult = "ok"
144
+ executionData = result
145
+ DiagnosticLog.shared.info("AudioLayer: executed '\(match.intentName)' → ok")
146
+ } catch {
147
+ DiagnosticLog.shared.info("AudioLayer: intent error — \(error.localizedDescription), falling back to Claude")
148
+ executionResult = "thinking..."
149
+ executionData = nil
150
+ claudeFallback(transcription: transcription)
151
+ }
152
+
153
+ // Fire parallel Haiku advisor for 5+ word utterances
154
+ fireAdvisor(transcript: transcription.text, matched: "\(match.intentName)(\(matchedSlots))")
155
+
156
+ } else {
157
+ // No local match — Claude fallback
158
+ DiagnosticLog.shared.info("AudioLayer: no phrase match for '\(transcription.text)', falling back to Claude")
159
+ matchedIntent = nil
160
+ matchedSlots = [:]
161
+ executionResult = "thinking..."
162
+ executionData = nil
163
+ claudeFallback(transcription: transcription)
164
+ }
165
+ }
166
+
167
+ /// Fire the Haiku advisor in parallel — non-blocking, result arrives later.
168
+ private func fireAdvisor(transcript: String, matched: String) {
169
+ let haiku = AgentPool.shared.haiku
170
+ guard haiku.isReady else {
171
+ DiagnosticLog.shared.info("AudioLayer: advisor skipped (haiku not ready)")
172
+ return
173
+ }
174
+
175
+ let message = "Transcript: \"\(transcript)\"\nMatched: \(matched)"
176
+ DiagnosticLog.shared.info("AudioLayer: firing haiku advisor")
177
+
178
+ haiku.send(message: message) { [weak self] response in
179
+ guard let self = self, let response = response else { return }
180
+ self.agentResponse = response
181
+ if let commentary = response.commentary {
182
+ DiagnosticLog.shared.info("AudioLayer: haiku says — \(commentary)")
183
+ }
184
+ if let suggestion = response.suggestion {
185
+ DiagnosticLog.shared.info("AudioLayer: haiku suggests — \(suggestion.label) → \(suggestion.intent)")
186
+ }
187
+ }
188
+ }
189
+
190
+ private func claudeFallback(transcription: Transcription) {
191
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
192
+ guard let self else { return }
193
+
194
+ let result = ClaudeFallback.resolve(
195
+ transcript: transcription.text,
196
+ windows: DesktopModel.shared.windows.values.map { $0 },
197
+ intentCatalog: PhraseMatcher.shared.catalog()
198
+ )
199
+
200
+ DispatchQueue.main.async {
201
+ guard let resolved = result else {
202
+ self.executionResult = "Claude couldn't resolve intent"
203
+ DiagnosticLog.shared.info("AudioLayer: Claude fallback returned nil")
204
+ return
205
+ }
206
+
207
+ DiagnosticLog.shared.info("AudioLayer: Claude resolved → \(resolved.intent) \(resolved.slots)")
208
+ self.matchedIntent = resolved.intent
209
+ self.matchedSlots = resolved.slots.reduce(into: [:]) { dict, pair in
210
+ dict[pair.key] = pair.value.stringValue ?? "\(pair.value)"
211
+ }
212
+
213
+ let intentMatch = IntentMatch(
214
+ intentName: resolved.intent,
215
+ slots: resolved.slots,
216
+ confidence: 0.8,
217
+ matchedPhrase: "claude-fallback"
218
+ )
219
+
220
+ do {
221
+ let execResult = try PhraseMatcher.shared.execute(intentMatch)
222
+ self.executionResult = "ok"
223
+ self.executionData = execResult
224
+ DiagnosticLog.shared.info("AudioLayer: Claude-resolved executed → \(execResult)")
225
+ } catch {
226
+ self.executionResult = error.localizedDescription
227
+ self.executionData = nil
228
+ DiagnosticLog.shared.info("AudioLayer: Claude-resolved execution error — \(error.localizedDescription)")
229
+ }
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ // Old IntentExtractor removed — PhraseMatcher handles all intent matching now.
236
+ // See app/Sources/Intents/LatticeIntent.swift
237
+
238
+
239
+ // MARK: - Vox Audio Provider (WebSocket JSON-RPC via VoxClient)
240
+ //
241
+ // Delegates recording and transcription entirely to the Vox daemon (voxd).
242
+ // Lattices never touches the mic — Vox owns the mic, recording, and
243
+ // transcription. We call transcribe.startSession to begin recording
244
+ // and transcribe.stopSession to stop and get the transcript.
245
+ //
246
+ // Session events flow on the startSession call ID:
247
+ // session.state: {state, sessionId, previous}
248
+ // session.final: {sessionId, text, words[], elapsedMs, metrics}
249
+
250
+ final class VoxAudioProvider: AudioProvider {
251
+ private var onTranscript: ((Transcription) -> Void)?
252
+ private var stopCompletion: ((Transcription?) -> Void)?
253
+ private var _isListening = false
254
+ private var startTime: Date?
255
+
256
+ var isAvailable: Bool {
257
+ VoxClient.shared.connectionState == .connected
258
+ }
259
+
260
+ var isListening: Bool { _isListening }
261
+
262
+ func checkHealth(completion: @escaping (Bool) -> Void) {
263
+ let client = VoxClient.shared
264
+ if client.connectionState == .connected {
265
+ client.call(method: "health") { result in
266
+ switch result {
267
+ case .success: DispatchQueue.main.async { completion(true) }
268
+ case .failure: DispatchQueue.main.async { completion(false) }
269
+ }
270
+ }
271
+ } else {
272
+ completion(false)
273
+ }
274
+ }
275
+
276
+ func startListening(onTranscript: @escaping (Transcription) -> Void) {
277
+ let client = VoxClient.shared
278
+ guard client.connectionState == .connected else {
279
+ DiagnosticLog.shared.warn("VoxAudioProvider: not connected to Vox")
280
+ onTranscript(Transcription(text: "", confidence: 0, source: "vox", isPartial: false, durationMs: nil))
281
+ return
282
+ }
283
+
284
+ self.onTranscript = onTranscript
285
+ _isListening = true
286
+ startTime = Date()
287
+
288
+ DiagnosticLog.shared.info("VoxAudioProvider: starting session via Vox")
289
+
290
+ // transcribe.startSession — Vox records from mic, emits session events on this call ID
291
+ client.startSession(
292
+ onProgress: { [weak self] event, data in
293
+ guard let self else { return }
294
+ DispatchQueue.main.async {
295
+ switch event {
296
+ case "session.state":
297
+ let state = data["state"] as? String ?? ""
298
+ DiagnosticLog.shared.info("VoxAudioProvider: session → \(state)")
299
+
300
+ case "session.final":
301
+ // Final transcript arrived — deliver it
302
+ if let text = data["text"] as? String, !text.isEmpty {
303
+ let elapsed = data["elapsedMs"] as? Int
304
+ let t = Transcription(
305
+ text: text, confidence: 0.95, source: "vox",
306
+ isPartial: false, durationMs: elapsed
307
+ )
308
+ DiagnosticLog.shared.info("VoxAudioProvider: transcribed → '\(text)' (\(elapsed ?? 0)ms)")
309
+ self.onTranscript?(t)
310
+ self.stopCompletion?(t)
311
+ self.stopCompletion = nil
312
+ }
313
+
314
+ default:
315
+ break
316
+ }
317
+ }
318
+ },
319
+ completion: { [weak self] result in
320
+ guard let self else { return }
321
+ DispatchQueue.main.async {
322
+ self._isListening = false
323
+
324
+ switch result {
325
+ case .success(let data):
326
+ // Final result also comes here (same data as session.final)
327
+ if let text = data["text"] as? String, !text.isEmpty,
328
+ self.stopCompletion != nil {
329
+ // Only deliver if session.final didn't already
330
+ let elapsed = data["elapsedMs"] as? Int
331
+ let t = Transcription(
332
+ text: text, confidence: 0.95, source: "vox",
333
+ isPartial: false, durationMs: elapsed
334
+ )
335
+ self.onTranscript?(t)
336
+ self.stopCompletion?(t)
337
+ self.stopCompletion = nil
338
+ } else if self.stopCompletion != nil {
339
+ self.stopCompletion?(nil)
340
+ self.stopCompletion = nil
341
+ }
342
+
343
+ case .failure(let error):
344
+ DiagnosticLog.shared.warn("VoxAudioProvider: session error — \(error.localizedDescription)")
345
+ if case .sessionBusy = error {
346
+ AudioLayer.shared.executionResult = "Session already active"
347
+ } else {
348
+ AudioLayer.shared.executionResult = "Transcription failed"
349
+ }
350
+ self.onTranscript?(Transcription(
351
+ text: "", confidence: 0, source: "vox",
352
+ isPartial: false, durationMs: nil
353
+ ))
354
+ self.stopCompletion?(nil)
355
+ self.stopCompletion = nil
356
+ }
357
+ }
358
+ }
359
+ )
360
+ }
361
+
362
+ func stopListening(completion: @escaping (Transcription?) -> Void) {
363
+ _isListening = false
364
+
365
+ let client = VoxClient.shared
366
+ guard client.connectionState == .connected else {
367
+ completion(nil)
368
+ return
369
+ }
370
+
371
+ DiagnosticLog.shared.info("VoxAudioProvider: stopping session")
372
+
373
+ // Store completion — the startSession's session.final event delivers the transcript
374
+ self.stopCompletion = completion
375
+
376
+ client.stopSession { result in
377
+ if case .failure(let error) = result {
378
+ DiagnosticLog.shared.warn("VoxAudioProvider: stopSession error — \(error.localizedDescription)")
379
+ DispatchQueue.main.async {
380
+ self.stopCompletion?(nil)
381
+ self.stopCompletion = nil
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }