@lattices/cli 0.4.13 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +191 -63
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2271
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,192 +0,0 @@
1
- import SwiftUI
2
-
3
- /// A scrolling chat log for voice mode. Shows the running conversation between
4
- /// user (transcripts), assistant (spoken responses), and system (silent info like
5
- /// executed actions or search results).
6
- ///
7
- /// Embeddable in both the standalone voice bar and the full HUD.
8
- struct VoiceChatView: View {
9
- @ObservedObject var session: HandsOffSession
10
-
11
- var body: some View {
12
- ScrollViewReader { proxy in
13
- ScrollView(.vertical, showsIndicators: false) {
14
- LazyVStack(alignment: .leading, spacing: 6) {
15
- ForEach(session.chatLog) { entry in
16
- chatBubble(entry)
17
- .id(entry.id)
18
- }
19
-
20
- // Live state indicator
21
- if session.state == .listening {
22
- listeningIndicator
23
- } else if session.state == .thinking {
24
- thinkingIndicator
25
- }
26
- }
27
- .padding(.horizontal, 12)
28
- .padding(.vertical, 8)
29
- }
30
- .onChange(of: session.chatLog.count) { _ in
31
- // Auto-scroll to bottom
32
- if let last = session.chatLog.last {
33
- withAnimation(.easeOut(duration: 0.15)) {
34
- proxy.scrollTo(last.id, anchor: .bottom)
35
- }
36
- }
37
- }
38
- }
39
- }
40
-
41
- // MARK: - Chat bubble
42
-
43
- @ViewBuilder
44
- private func chatBubble(_ entry: VoiceChatEntry) -> some View {
45
- switch entry.role {
46
- case .user:
47
- HStack(alignment: .top, spacing: 6) {
48
- Text("you")
49
- .font(Typo.monoBold(9))
50
- .foregroundColor(Palette.text)
51
- .frame(width: 28, alignment: .trailing)
52
- Text(entry.text)
53
- .font(Typo.mono(11))
54
- .foregroundColor(Palette.text)
55
- .textSelection(.enabled)
56
- Spacer(minLength: 0)
57
- }
58
-
59
- case .assistant:
60
- HStack(alignment: .top, spacing: 6) {
61
- Text("lat")
62
- .font(Typo.monoBold(9))
63
- .foregroundColor(Palette.running)
64
- .frame(width: 28, alignment: .trailing)
65
- Text(entry.text)
66
- .font(Typo.mono(11))
67
- .foregroundColor(Palette.textMuted)
68
- .textSelection(.enabled)
69
- Spacer(minLength: 0)
70
- }
71
-
72
- case .system:
73
- HStack(alignment: .top, spacing: 6) {
74
- Image(systemName: "bolt.fill")
75
- .font(.system(size: 7))
76
- .foregroundColor(Palette.running.opacity(0.6))
77
- .frame(width: 28, alignment: .trailing)
78
- Text(entry.text)
79
- .font(Typo.mono(9))
80
- .foregroundColor(Palette.textDim)
81
- .textSelection(.enabled)
82
- if let detail = entry.detail {
83
- Text(detail)
84
- .font(Typo.mono(9))
85
- .foregroundColor(Palette.textDim.opacity(0.6))
86
- }
87
- Spacer(minLength: 0)
88
- }
89
- .opacity(0.7)
90
- }
91
- }
92
-
93
- // MARK: - Live indicators
94
-
95
- private var listeningIndicator: some View {
96
- HStack(spacing: 6) {
97
- Text("you")
98
- .font(Typo.monoBold(9))
99
- .foregroundColor(Palette.text)
100
- .frame(width: 28, alignment: .trailing)
101
-
102
- if let partial = session.lastTranscript, session.state == .listening {
103
- Text(partial)
104
- .font(Typo.mono(11))
105
- .foregroundColor(Palette.text.opacity(0.5))
106
- } else {
107
- HStack(spacing: 3) {
108
- ForEach(0..<3, id: \.self) { i in
109
- Circle()
110
- .fill(Palette.text)
111
- .frame(width: 4, height: 4)
112
- .opacity(0.4)
113
- .animation(
114
- .easeInOut(duration: 0.5)
115
- .repeatForever()
116
- .delay(Double(i) * 0.15),
117
- value: session.state
118
- )
119
- }
120
- }
121
- }
122
- Spacer(minLength: 0)
123
- }
124
- }
125
-
126
- private var thinkingIndicator: some View {
127
- HStack(spacing: 6) {
128
- Text("lat")
129
- .font(Typo.monoBold(9))
130
- .foregroundColor(Palette.running)
131
- .frame(width: 28, alignment: .trailing)
132
- HStack(spacing: 3) {
133
- ForEach(0..<3, id: \.self) { i in
134
- Circle()
135
- .fill(Palette.running)
136
- .frame(width: 4, height: 4)
137
- .opacity(0.4)
138
- .animation(
139
- .easeInOut(duration: 0.5)
140
- .repeatForever()
141
- .delay(Double(i) * 0.15),
142
- value: session.state
143
- )
144
- }
145
- }
146
- Spacer(minLength: 0)
147
- }
148
- }
149
- }
150
-
151
- // MARK: - Compact variant (for top/bottom bar embedding)
152
-
153
- struct VoiceChatCompact: View {
154
- @ObservedObject var session: HandsOffSession
155
- /// How many recent entries to show
156
- var maxEntries: Int = 3
157
-
158
- var body: some View {
159
- VStack(alignment: .leading, spacing: 3) {
160
- ForEach(Array(session.chatLog.suffix(maxEntries))) { entry in
161
- HStack(spacing: 4) {
162
- switch entry.role {
163
- case .user:
164
- Text("you")
165
- .font(Typo.monoBold(8))
166
- .foregroundColor(Palette.text)
167
- Text(entry.text)
168
- .font(Typo.mono(9))
169
- .foregroundColor(Palette.text)
170
- .lineLimit(1)
171
- case .assistant:
172
- Text("→")
173
- .font(Typo.mono(9))
174
- .foregroundColor(Palette.running)
175
- Text(entry.text)
176
- .font(Typo.mono(9))
177
- .foregroundColor(Palette.textMuted)
178
- .lineLimit(1)
179
- case .system:
180
- Text("⚡")
181
- .font(.system(size: 7))
182
- Text(entry.text)
183
- .font(Typo.mono(8))
184
- .foregroundColor(Palette.textDim)
185
- .lineLimit(1)
186
- }
187
- Spacer(minLength: 0)
188
- }
189
- }
190
- }
191
- }
192
- }
@@ -1,454 +0,0 @@
1
- import AppKit
2
-
3
- /// WebSocket JSON-RPC client for the Vox transcription runtime.
4
- ///
5
- /// Vox is a local-first transcription daemon (voxd) that runs on a configurable port
6
- /// (default 42137). Service discovery is file-based via ~/.vox/runtime.json.
7
- ///
8
- /// Key differences from the old Talkie integration:
9
- /// - Discovery: ~/.vox/runtime.json (not ~/.talkie/services.json)
10
- /// - Port: 42137 (not 19823)
11
- /// - No distributed notifications — poll runtime.json or check on demand
12
- /// - API: transcribe.startSession/stopSession (not startDictation/stopDictation)
13
- /// - No register call — pass clientId per request
14
- /// - All session events flow on the startSession call ID
15
- final class VoxClient: ObservableObject {
16
- static let shared = VoxClient()
17
-
18
- enum ConnectionState: Equatable {
19
- case disconnected
20
- case connecting
21
- case connected
22
- case unavailable(reason: String)
23
- }
24
-
25
- @Published var connectionState: ConnectionState = .disconnected
26
-
27
- static let clientId = "lattices"
28
-
29
- private var pendingCalls: [String: PendingCall] = [:]
30
- private var eventHandler: ((String, [String: Any]) -> Void)?
31
- private var reconnectDelay: TimeInterval = 0.5
32
- private var reconnectTimer: DispatchSourceTimer?
33
- private var heartbeatTimer: DispatchSourceTimer?
34
- private var intentionalDisconnect = false
35
- private let queue = DispatchQueue(label: "com.lattices.vox-client")
36
-
37
- private struct PendingCall {
38
- let completion: (Result<[String: Any], VoxError>) -> Void
39
- let onProgress: ((String, [String: Any]) -> Void)?
40
- let timer: DispatchSourceTimer?
41
- }
42
-
43
- enum VoxError: LocalizedError {
44
- case notConnected
45
- case callFailed(String)
46
- case timeout(String)
47
- case sessionBusy
48
- case connectionDropped
49
- case daemonNotRunning
50
-
51
- var errorDescription: String? {
52
- switch self {
53
- case .notConnected: return "Not connected to Vox"
54
- case .callFailed(let msg): return msg
55
- case .timeout(let method): return "Call to '\(method)' timed out"
56
- case .sessionBusy: return "A live session is already active"
57
- case .connectionDropped: return "Connection to Vox dropped"
58
- case .daemonNotRunning: return "Vox daemon not running — start with 'vox daemon start'"
59
- }
60
- }
61
- }
62
-
63
- // MARK: - Service Discovery (file-based via ~/.vox/runtime.json)
64
-
65
- private static let defaultPort: UInt16 = 42137
66
- private static let runtimePath = NSHomeDirectory() + "/.vox/runtime.json"
67
-
68
- struct RuntimeInfo {
69
- let port: UInt16
70
- let pid: Int
71
- let version: String
72
- }
73
-
74
- /// Read ~/.vox/runtime.json and check if the daemon is alive.
75
- func discoverDaemon() -> RuntimeInfo? {
76
- guard let data = FileManager.default.contents(atPath: Self.runtimePath),
77
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
78
- let port = json["port"] as? Int,
79
- let pid = json["pid"] as? Int else {
80
- return nil
81
- }
82
-
83
- // Verify the PID is still alive
84
- let alive = kill(Int32(pid), 0) == 0
85
- guard alive else {
86
- DiagnosticLog.shared.warn("VoxClient: stale runtime.json — pid \(pid) not running")
87
- return nil
88
- }
89
-
90
- let version = json["version"] as? String ?? "unknown"
91
- return RuntimeInfo(port: UInt16(port), pid: pid, version: version)
92
- }
93
-
94
- // MARK: - Connection
95
-
96
- func connect() {
97
- if connectionState == .connected || connectionState == .connecting { return }
98
-
99
- intentionalDisconnect = false
100
-
101
- guard let runtime = discoverDaemon() else {
102
- DiagnosticLog.shared.warn("VoxClient: daemon not found — check ~/.vox/runtime.json")
103
- DispatchQueue.main.async {
104
- self.connectionState = .unavailable(reason: "Vox daemon not running")
105
- }
106
- return
107
- }
108
-
109
- DiagnosticLog.shared.info("VoxClient: discovered daemon v\(runtime.version) on port \(runtime.port) (pid \(runtime.pid))")
110
- connectToPort(runtime.port)
111
- }
112
-
113
- func disconnect() {
114
- intentionalDisconnect = true
115
- reconnectTimer?.cancel()
116
- reconnectTimer = nil
117
- heartbeatTimer?.cancel()
118
- heartbeatTimer = nil
119
- wsTask?.cancel(with: .goingAway, reason: nil)
120
- wsTask = nil
121
- wsSession?.invalidateAndCancel()
122
- wsSession = nil
123
- pendingCalls.removeAll()
124
- DispatchQueue.main.async {
125
- self.connectionState = .disconnected
126
- }
127
- }
128
-
129
- /// Force a full disconnect + reconnect cycle.
130
- func reconnect() {
131
- DiagnosticLog.shared.info("VoxClient: forced reconnect requested")
132
- disconnect()
133
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
134
- self?.connect()
135
- }
136
- }
137
-
138
- private var wsTask: URLSessionWebSocketTask?
139
- private var wsSession: URLSession?
140
-
141
- private func connectToPort(_ port: UInt16) {
142
- DispatchQueue.main.async {
143
- self.connectionState = .connecting
144
- }
145
-
146
- let connectStart = Date()
147
- DiagnosticLog.shared.info("VoxClient: connecting to ws://127.0.0.1:\(port)")
148
-
149
- let url = URL(string: "ws://127.0.0.1:\(port)")!
150
- let config = URLSessionConfiguration.default
151
- config.connectionProxyDictionary = [:]
152
- let session = URLSession(configuration: config)
153
- let task = session.webSocketTask(with: url)
154
-
155
- self.wsSession = session
156
- self.wsTask = task
157
- task.resume()
158
-
159
- // Verify with a health check instead of raw ping
160
- task.sendPing { [weak self] error in
161
- guard let self else { return }
162
- let ms = Int(Date().timeIntervalSince(connectStart) * 1000)
163
- if let error {
164
- DiagnosticLog.shared.warn("VoxClient: WebSocket ping failed (\(ms)ms) — \(error)")
165
- self.handleDisconnect()
166
- } else {
167
- self.reconnectDelay = 0.5
168
- DiagnosticLog.shared.info("VoxClient: connected on port \(port) (\(ms)ms)")
169
- self.receiveLoop()
170
- self.startHeartbeat()
171
- DispatchQueue.main.async {
172
- self.connectionState = .connected
173
- }
174
- // Verify with a health RPC
175
- self.call(method: "health") { result in
176
- switch result {
177
- case .success(let data):
178
- let svc = data["serviceName"] as? String ?? "?"
179
- let ver = data["version"] as? String ?? "?"
180
- DiagnosticLog.shared.info("VoxClient: health OK — \(svc) v\(ver)")
181
- case .failure(let error):
182
- DiagnosticLog.shared.warn("VoxClient: health check failed — \(error.localizedDescription)")
183
- }
184
- }
185
- }
186
- }
187
- }
188
-
189
- /// Periodic WebSocket ping every 30s to detect dead connections early.
190
- private func startHeartbeat() {
191
- heartbeatTimer?.cancel()
192
- let timer = DispatchSource.makeTimerSource(queue: queue)
193
- timer.schedule(deadline: .now() + 30, repeating: 30)
194
- timer.setEventHandler { [weak self] in
195
- guard let self, let task = self.wsTask else { return }
196
- task.sendPing { error in
197
- if let error {
198
- DiagnosticLog.shared.warn("VoxClient: heartbeat failed — \(error)")
199
- self.handleDisconnect()
200
- }
201
- }
202
- }
203
- timer.resume()
204
- heartbeatTimer = timer
205
- }
206
-
207
- private func handleDisconnect() {
208
- heartbeatTimer?.cancel()
209
- heartbeatTimer = nil
210
- wsTask?.cancel(with: .goingAway, reason: nil)
211
- wsTask = nil
212
-
213
- for (_, pending) in pendingCalls {
214
- pending.timer?.cancel()
215
- pending.completion(.failure(.connectionDropped))
216
- }
217
- pendingCalls.removeAll()
218
-
219
- DispatchQueue.main.async {
220
- self.connectionState = .disconnected
221
- }
222
-
223
- guard !intentionalDisconnect else { return }
224
-
225
- let delay = reconnectDelay
226
- reconnectDelay = min(reconnectDelay * 2, 10)
227
- DiagnosticLog.shared.info("VoxClient: reconnecting in \(delay)s")
228
-
229
- let timer = DispatchSource.makeTimerSource(queue: queue)
230
- timer.schedule(deadline: .now() + delay)
231
- timer.setEventHandler { [weak self] in
232
- self?.connect()
233
- }
234
- timer.resume()
235
- reconnectTimer = timer
236
- }
237
-
238
- // MARK: - WebSocket I/O
239
-
240
- private func receiveLoop() {
241
- guard let task = wsTask else { return }
242
-
243
- task.receive { [weak self] result in
244
- guard let self else { return }
245
- switch result {
246
- case .success(let message):
247
- switch message {
248
- case .string(let text): self.handleMessage(text)
249
- case .data(let data):
250
- if let text = String(data: data, encoding: .utf8) { self.handleMessage(text) }
251
- @unknown default: break
252
- }
253
- self.receiveLoop()
254
- case .failure(let error):
255
- DiagnosticLog.shared.warn("VoxClient: receive error — \(error)")
256
- self.handleDisconnect()
257
- }
258
- }
259
- }
260
-
261
- private func handleMessage(_ text: String) {
262
- guard let data = text.data(using: .utf8),
263
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
264
-
265
- // Match by request ID
266
- if let id = json["id"] as? String, let pending = pendingCalls.removeValue(forKey: id) {
267
- pending.timer?.cancel()
268
-
269
- // Streaming event — has "event" key alongside "id"
270
- if let event = json["event"] as? String {
271
- let eventData = json["data"] as? [String: Any] ?? [:]
272
- pending.onProgress?(event, eventData)
273
- // Re-add — still pending until final result/error
274
- pendingCalls[id] = pending
275
- return
276
- }
277
-
278
- // Final result or error
279
- if let errorStr = json["error"] as? String {
280
- if errorStr == "live_session_busy" {
281
- pending.completion(.failure(.sessionBusy))
282
- } else {
283
- pending.completion(.failure(.callFailed(errorStr)))
284
- }
285
- } else {
286
- let result = json["result"] as? [String: Any] ?? [:]
287
- pending.completion(.success(result))
288
- }
289
- return
290
- }
291
-
292
- // Push event (no matching ID)
293
- if let event = json["event"] as? String {
294
- let eventData = json["data"] as? [String: Any] ?? [:]
295
- DispatchQueue.main.async {
296
- self.eventHandler?(event, eventData)
297
- }
298
- }
299
- }
300
-
301
- private func sendJSON(_ dict: [String: Any]) {
302
- guard let task = wsTask,
303
- let data = try? JSONSerialization.data(withJSONObject: dict),
304
- let text = String(data: data, encoding: .utf8) else { return }
305
-
306
- task.send(.string(text)) { error in
307
- if let error {
308
- DiagnosticLog.shared.warn("VoxClient: send error — \(error)")
309
- }
310
- }
311
- }
312
-
313
- // MARK: - RPC (fire-and-forget and request-response)
314
-
315
- func call(method: String, params: [String: Any]? = nil, timeout: TimeInterval = 30,
316
- completion: @escaping (Result<[String: Any], VoxError>) -> Void) {
317
- guard wsTask != nil, connectionState == .connected else {
318
- completion(.failure(.notConnected))
319
- return
320
- }
321
-
322
- let id = UUID().uuidString
323
- var payload: [String: Any] = ["id": id, "method": method]
324
- if var p = params {
325
- // Inject clientId into all calls
326
- if p["clientId"] == nil { p["clientId"] = Self.clientId }
327
- payload["params"] = p
328
- } else {
329
- payload["params"] = ["clientId": Self.clientId]
330
- }
331
-
332
- let timer = DispatchSource.makeTimerSource(queue: queue)
333
- timer.schedule(deadline: .now() + timeout)
334
- timer.setEventHandler { [weak self] in
335
- if let pending = self?.pendingCalls.removeValue(forKey: id) {
336
- pending.completion(.failure(.timeout(method)))
337
- }
338
- }
339
- timer.resume()
340
-
341
- pendingCalls[id] = PendingCall(completion: completion, onProgress: nil, timer: timer)
342
- sendJSON(payload)
343
- }
344
-
345
- /// Streaming RPC — receives progress events before the final result.
346
- /// Used for transcribe.startSession where events flow on the start call ID.
347
- func callStreaming(method: String, params: [String: Any]? = nil, timeout: TimeInterval = 120,
348
- onProgress: @escaping (String, [String: Any]) -> Void,
349
- completion: @escaping (Result<[String: Any], VoxError>) -> Void) {
350
- guard wsTask != nil, connectionState == .connected else {
351
- completion(.failure(.notConnected))
352
- return
353
- }
354
-
355
- let id = UUID().uuidString
356
- var payload: [String: Any] = ["id": id, "method": method]
357
- if var p = params {
358
- if p["clientId"] == nil { p["clientId"] = Self.clientId }
359
- payload["params"] = p
360
- } else {
361
- payload["params"] = ["clientId": Self.clientId]
362
- }
363
-
364
- let timer = DispatchSource.makeTimerSource(queue: queue)
365
- timer.schedule(deadline: .now() + timeout)
366
- timer.setEventHandler { [weak self] in
367
- if let pending = self?.pendingCalls.removeValue(forKey: id) {
368
- pending.completion(.failure(.timeout(method)))
369
- }
370
- }
371
- timer.resume()
372
-
373
- pendingCalls[id] = PendingCall(completion: completion, onProgress: { event, data in
374
- timer.schedule(deadline: .now() + timeout) // Reset timeout on activity
375
- onProgress(event, data)
376
- }, timer: timer)
377
-
378
- sendJSON(payload)
379
- }
380
-
381
- func onServiceEvent(_ handler: @escaping (String, [String: Any]) -> Void) {
382
- eventHandler = handler
383
- }
384
-
385
- // MARK: - High-level session helpers
386
-
387
- /// Current active session ID, if any.
388
- @Published var activeSessionId: String?
389
-
390
- /// Start a live transcription session. Vox records from the mic and transcribes on stop.
391
- ///
392
- /// Events arrive on this call's ID:
393
- /// - session.state: {state: "starting"|"recording"|"processing"|"done", sessionId, previous}
394
- /// - session.final: {sessionId, text, words[], elapsedMs, metrics}
395
- func startSession(
396
- modelId: String = "parakeet:v3",
397
- onProgress: @escaping (String, [String: Any]) -> Void,
398
- completion: @escaping (Result<[String: Any], VoxError>) -> Void
399
- ) {
400
- callStreaming(
401
- method: "transcribe.startSession",
402
- params: ["modelId": modelId],
403
- onProgress: { [weak self] event, data in
404
- if event == "session.state", let sid = data["sessionId"] as? String {
405
- DispatchQueue.main.async { self?.activeSessionId = sid }
406
- }
407
- onProgress(event, data)
408
- },
409
- completion: { [weak self] result in
410
- DispatchQueue.main.async { self?.activeSessionId = nil }
411
- completion(result)
412
- }
413
- )
414
- }
415
-
416
- /// Stop the current live session. The final transcript arrives via the startSession callback.
417
- func stopSession(completion: ((Result<[String: Any], VoxError>) -> Void)? = nil) {
418
- guard let sessionId = activeSessionId else {
419
- completion?(.failure(.callFailed("No active session")))
420
- return
421
- }
422
- call(method: "transcribe.stopSession", params: ["sessionId": sessionId]) { result in
423
- completion?(result)
424
- }
425
- }
426
-
427
- /// Cancel the current session without waiting for transcription.
428
- func cancelSession(completion: ((Result<[String: Any], VoxError>) -> Void)? = nil) {
429
- guard let sessionId = activeSessionId else {
430
- completion?(.failure(.callFailed("No active session")))
431
- return
432
- }
433
- call(method: "transcribe.cancelSession", params: ["sessionId": sessionId]) { result in
434
- completion?(result)
435
- }
436
- }
437
-
438
- /// Request model warm-up so first transcription is fast.
439
- func warmup(modelId: String = "parakeet:v3") {
440
- call(method: "warmup.start", params: ["modelId": modelId]) { result in
441
- switch result {
442
- case .success: DiagnosticLog.shared.info("VoxClient: warmup started")
443
- case .failure(let e): DiagnosticLog.shared.warn("VoxClient: warmup failed — \(e.localizedDescription)")
444
- }
445
- }
446
- }
447
-
448
- // MARK: - Init
449
-
450
- private init() {
451
- // No distributed notifications for Vox — discovery is file-based.
452
- // We connect on demand when voice mode activates.
453
- }
454
- }
@@ -1,28 +0,0 @@
1
- import CryptoKit
2
- import Foundation
3
-
4
- struct Project: Identifiable {
5
- let id: String
6
- let path: String
7
- let name: String
8
- let devCommand: String?
9
- let packageManager: String?
10
- let hasConfig: Bool
11
- let paneCount: Int
12
- let paneNames: [String]
13
- let paneSummary: String
14
- var isRunning: Bool
15
-
16
- /// Unique session name: basename-{6-char SHA256 hash of full path}
17
- /// Must match the JS `toSessionName()` in lattices.js exactly
18
- var sessionName: String {
19
- let base = name.replacingOccurrences(
20
- of: "[^a-zA-Z0-9_-]",
21
- with: "-",
22
- options: .regularExpression
23
- )
24
- let hash = SHA256.hash(data: Data(path.utf8))
25
- let short = hash.prefix(3).map { String(format: "%02x", $0) }.joined()
26
- return "\(base)-\(short)"
27
- }
28
- }