@lattices/cli 0.4.14 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,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
- }