@lattices/cli 0.4.14 → 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 +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/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 -2583
  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,1463 +0,0 @@
1
- import AppKit
2
- import DeckKit
3
- import Foundation
4
-
5
- enum LatticesDeckHostError: LocalizedError {
6
- case unsupportedAction(String)
7
- case missingPayload(String)
8
- case invalidSwitcherItem(String)
9
- case noFrontmostWindow
10
- case invalidResizeDimension(String)
11
- case invalidResizeDirection(String)
12
- case noVisibleTargets(String)
13
-
14
- var errorDescription: String? {
15
- switch self {
16
- case .unsupportedAction(let actionID):
17
- return "Unsupported deck action: \(actionID)"
18
- case .missingPayload(let name):
19
- return "Missing deck payload field: \(name)"
20
- case .invalidSwitcherItem(let itemID):
21
- return "Unknown switcher item: \(itemID)"
22
- case .noFrontmostWindow:
23
- return "There is no frontmost desktop window to control."
24
- case .invalidResizeDimension(let value):
25
- return "Unsupported resize dimension: \(value)"
26
- case .invalidResizeDirection(let value):
27
- return "Unsupported resize direction: \(value)"
28
- case .noVisibleTargets(let label):
29
- return "There are no visible \(label) to switch to right now."
30
- }
31
- }
32
- }
33
-
34
- final class LatticesDeckHost: DeckHost, @unchecked Sendable {
35
- static let shared = LatticesDeckHost()
36
-
37
- private let security: DeckSecurityConfiguration
38
- private let replayLock = NSLock()
39
- private var lastReplayMessage: String?
40
- private var lastReplayAt: Date?
41
- private var lastReplayUndoActionID: String?
42
-
43
- init(security: DeckSecurityConfiguration = .standaloneBonjour()) {
44
- self.security = security
45
- }
46
-
47
- var securityConfiguration: DeckSecurityConfiguration {
48
- security
49
- }
50
-
51
- func manifest() async throws -> DeckManifest {
52
- try manifestSync()
53
- }
54
-
55
- func runtimeSnapshot() async throws -> DeckRuntimeSnapshot {
56
- try runtimeSnapshotSync()
57
- }
58
-
59
- func perform(_ request: DeckActionRequest) async throws -> DeckActionResult {
60
- try performSync(request)
61
- }
62
-
63
- func manifestSync() throws -> DeckManifest {
64
- var capabilities: [DeckCapability] = [
65
- .trackpadProxy,
66
- .voiceAgent,
67
- .layoutControl,
68
- .appSwitching,
69
- .taskSwitching,
70
- .historyFeed,
71
- .systemTelemetry,
72
- .spaces,
73
- .keyboardForwarding,
74
- .activityLog,
75
- .cockpitModes,
76
- .transcriptStream,
77
- ]
78
- if security.mode == .embedded {
79
- capabilities.append(.embeddedSecurityDelegation)
80
- }
81
-
82
- return DeckManifest(
83
- product: DeckProductIdentity(
84
- id: "com.arach.lattices.companion",
85
- displayName: "Lattices Companion",
86
- owner: "lattices"
87
- ),
88
- security: security,
89
- capabilities: capabilities,
90
- pages: [
91
- DeckPage(
92
- id: "command",
93
- title: "Command",
94
- iconSystemName: "circle.grid.2x2.fill",
95
- kind: .cockpit,
96
- accentToken: "lattices-cockpit",
97
- deckID: "command"
98
- ),
99
- DeckPage(
100
- id: "dev",
101
- title: "Dev",
102
- iconSystemName: "terminal.fill",
103
- kind: .switch,
104
- accentToken: "lattices-dev",
105
- deckID: "dev"
106
- ),
107
- DeckPage(
108
- id: "media",
109
- title: "Media",
110
- iconSystemName: "play.rectangle.fill",
111
- kind: .custom,
112
- accentToken: "lattices-media",
113
- deckID: "media"
114
- ),
115
- DeckPage(
116
- id: "windows",
117
- title: "Windows",
118
- iconSystemName: "rectangle.3.group.fill",
119
- kind: .layout,
120
- accentToken: "lattices-layout",
121
- deckID: "windows"
122
- ),
123
- DeckPage(
124
- id: "voice",
125
- title: "Voice",
126
- iconSystemName: "waveform.badge.mic",
127
- kind: .voice,
128
- accentToken: "lattices-voice",
129
- deckID: "voice"
130
- ),
131
- ]
132
- )
133
- }
134
-
135
- func runtimeSnapshotSync() throws -> DeckRuntimeSnapshot {
136
- try MainActorSync.run { self.snapshotOnMainActor() }
137
- }
138
-
139
- func performSync(_ request: DeckActionRequest) throws -> DeckActionResult {
140
- let outcome = try handle(request)
141
- recordAction(request: request, outcome: outcome)
142
- flushMainQueue()
143
- let snapshot = try runtimeSnapshotSync()
144
-
145
- return DeckActionResult(
146
- ok: true,
147
- summary: outcome.summary,
148
- detail: outcome.detail,
149
- runtimeSnapshot: snapshot,
150
- suggestedActions: outcome.suggestedActions
151
- )
152
- }
153
- }
154
-
155
- private extension LatticesDeckHost {
156
- enum ResizeDimension: String {
157
- case width
158
- case height
159
- case both
160
-
161
- init(requestValue: String) throws {
162
- switch requestValue.lowercased() {
163
- case "width":
164
- self = .width
165
- case "height":
166
- self = .height
167
- case "both", "size":
168
- self = .both
169
- default:
170
- throw LatticesDeckHostError.invalidResizeDimension(requestValue)
171
- }
172
- }
173
- }
174
-
175
- enum ResizeDirection: String {
176
- case grow
177
- case shrink
178
-
179
- init(requestValue: String) throws {
180
- switch requestValue.lowercased() {
181
- case "grow", "increase", "expand":
182
- self = .grow
183
- case "shrink", "decrease", "reduce":
184
- self = .shrink
185
- default:
186
- throw LatticesDeckHostError.invalidResizeDirection(requestValue)
187
- }
188
- }
189
- }
190
-
191
- struct ActionOutcome {
192
- let summary: String
193
- let detail: String?
194
- let suggestedActions: [DeckSuggestedAction]
195
- }
196
-
197
- func handle(_ request: DeckActionRequest) throws -> ActionOutcome {
198
- switch request.actionID {
199
- case "voice.toggle":
200
- try MainActorSync.run {
201
- HandsOffSession.shared.toggle()
202
- }
203
- return try voiceOutcome()
204
-
205
- case "voice.cancel":
206
- try MainActorSync.run {
207
- HandsOffSession.shared.cancel()
208
- }
209
- return ActionOutcome(
210
- summary: "Stopped voice control",
211
- detail: "Cancelled the active hands-off voice turn.",
212
- suggestedActions: voiceSuggestions(for: .idle)
213
- )
214
-
215
- // Single-shot voice command path (transcribe → match intent → execute).
216
- // Distinct from voice.toggle / voice.cancel which drive the chat-style
217
- // HandsOffSession. Lives here so the iPad companion can fire dictation
218
- // on the active Mac via the existing /deck/perform bridge.
219
- case "voice.command.start":
220
- try MainActorSync.run {
221
- AudioLayer.shared.startVoiceCommand()
222
- }
223
- return try voiceOutcome()
224
-
225
- case "voice.command.stop":
226
- try MainActorSync.run {
227
- AudioLayer.shared.stopVoiceCommand()
228
- }
229
- return try voiceOutcome()
230
-
231
- case "voice.command.toggle":
232
- try MainActorSync.run {
233
- if AudioLayer.shared.isListening {
234
- AudioLayer.shared.stopVoiceCommand()
235
- } else {
236
- AudioLayer.shared.startVoiceCommand()
237
- }
238
- }
239
- return try voiceOutcome()
240
-
241
- case "switch.cycleApplication":
242
- return try cycleApplication(direction: request.payload["direction"]?.stringValue ?? "next")
243
-
244
- case "switch.cycleWindow":
245
- return try cycleWindow(direction: request.payload["direction"]?.stringValue ?? "next")
246
-
247
- case "layout.activateLayer":
248
- var params: [String: JSON] = [:]
249
- if let index = request.payload["index"]?.intValue {
250
- params["index"] = .int(index)
251
- }
252
- if let name = request.payload["name"]?.stringValue {
253
- params["name"] = .string(name)
254
- }
255
- params["mode"] = .string(request.payload["mode"]?.stringValue ?? "launch")
256
- let result = try callAPI("layer.activate", params: params)
257
- let label = result["label"]?.stringValue ?? params["name"]?.stringValue ?? "layer"
258
- return ActionOutcome(
259
- summary: "Activated \(label)",
260
- detail: "Focused the requested workspace layer.",
261
- suggestedActions: [
262
- DeckSuggestedAction(
263
- id: "layout.optimize",
264
- title: "Retile Visible Windows",
265
- iconSystemName: "rectangle.3.group"
266
- )
267
- ]
268
- )
269
-
270
- case "layout.optimize":
271
- try MainActorSync.run {
272
- let wids = DesktopModel.shared.allWindows()
273
- .filter { $0.isOnScreen && $0.app != "Lattices" }
274
- .map(\.wid)
275
- HandsOffSession.shared.snapshotFrames(wids: wids)
276
- }
277
- var params: [String: JSON] = [:]
278
- if let scope = request.payload["scope"]?.stringValue {
279
- params["scope"] = .string(scope)
280
- }
281
- if let strategy = request.payload["strategy"]?.stringValue {
282
- params["strategy"] = .string(strategy)
283
- }
284
- if let region = request.payload["region"]?.stringValue {
285
- params["region"] = .string(region)
286
- }
287
- if let app = request.payload["app"]?.stringValue {
288
- params["app"] = .string(app)
289
- }
290
- if let type = request.payload["type"]?.stringValue {
291
- params["type"] = .string(type)
292
- }
293
- let result = try callAPI("space.optimize", params: params)
294
- let count = result["windowCount"]?.intValue ?? 0
295
- return ActionOutcome(
296
- summary: count > 0 ? "Optimized \(count) windows" : "Nothing needed rearranging",
297
- detail: "Applied the current layout strategy to the visible workspace.",
298
- suggestedActions: []
299
- )
300
-
301
- case "layout.placeFrontmost":
302
- guard let placement = request.payload["placement"]?.stringValue else {
303
- throw LatticesDeckHostError.missingPayload("placement")
304
- }
305
- try MainActorSync.run {
306
- if let frontmost = self.currentFrontmostWindow(
307
- from: DesktopModel.shared.allWindows().filter(\.isOnScreen)
308
- ) {
309
- HandsOffSession.shared.snapshotFrames(wids: [frontmost.wid])
310
- }
311
- }
312
- _ = try callAPI("window.place", params: [
313
- "placement": .string(placement)
314
- ])
315
- return ActionOutcome(
316
- summary: "Placed the frontmost window",
317
- detail: "Applied the \(placement) placement to the current frontmost target.",
318
- suggestedActions: []
319
- )
320
-
321
- case "layout.resizeFrontmost":
322
- let dimension = try ResizeDimension(requestValue: request.payload["dimension"]?.stringValue ?? "both")
323
- let direction = try ResizeDirection(requestValue: request.payload["direction"]?.stringValue ?? "grow")
324
- return try resizeFrontmostWindow(dimension: dimension, direction: direction)
325
-
326
- case "switch.focusItem":
327
- guard let itemID = request.payload["itemID"]?.stringValue else {
328
- throw LatticesDeckHostError.missingPayload("itemID")
329
- }
330
- return try focusSwitcherItem(itemID)
331
-
332
- case "history.undoLast":
333
- _ = try callAPI("intents.execute", params: [
334
- "intent": .string("undo")
335
- ])
336
- return ActionOutcome(
337
- summary: "Undid the last window move",
338
- detail: "Restored the most recent saved window frames.",
339
- suggestedActions: []
340
- )
341
-
342
- case "keys.send", "key.send":
343
- guard let key = request.payload["key"]?.stringValue else {
344
- throw LatticesDeckHostError.missingPayload("key")
345
- }
346
- let modifiers = request.payload["modifiers"]?.arrayValue?.compactMap(\.stringValue) ?? []
347
- DiagnosticLog.shared.info("DeckHost keys.send: key=\(key) modifiers=\(modifiers)")
348
-
349
- // Ctrl+Left / Ctrl+Right: macOS filters synthesized Mission Control hot keys at
350
- // .cghidEventTap, so route directly to the SkyLight space switcher instead.
351
- if let direction = spaceSwitchDirection(key: key, modifiers: modifiers) {
352
- DiagnosticLog.shared.info("DeckHost keys.send → spaces.switchActive direction=\(direction)")
353
- return try MainActorSync.run { self.switchActiveSpace(direction: direction) }
354
- }
355
-
356
- let sent = try CompanionKeyboardController.shared.send(key: key, modifiers: modifiers)
357
- return ActionOutcome(
358
- summary: "Sent \(sent)",
359
- detail: "Forwarded the key chord to the active macOS application.",
360
- suggestedActions: []
361
- )
362
-
363
- case "spaces.focusRelative":
364
- let direction = request.payload["direction"]?.intValue ?? 1
365
- return try MainActorSync.run { self.switchActiveSpace(direction: direction > 0 ? 1 : -1) }
366
-
367
- case "mouse.find":
368
- let result = try callAPI("mouse.find")
369
- let x = result["x"]?.intValue ?? 0
370
- let y = result["y"]?.intValue ?? 0
371
- return ActionOutcome(
372
- summary: "Located the mouse",
373
- detail: "Pulsed the cursor near \(x), \(y).",
374
- suggestedActions: []
375
- )
376
-
377
- case "mouse.summon":
378
- let result = try callAPI("mouse.summon")
379
- let x = result["x"]?.intValue ?? 0
380
- let y = result["y"]?.intValue ?? 0
381
- return ActionOutcome(
382
- summary: "Summoned the mouse",
383
- detail: "Moved the cursor toward \(x), \(y).",
384
- suggestedActions: []
385
- )
386
-
387
- default:
388
- throw LatticesDeckHostError.unsupportedAction(request.actionID)
389
- }
390
- }
391
-
392
- func voiceOutcome() throws -> ActionOutcome {
393
- let phase = try MainActorSync.run { self.currentVoicePhase() }
394
- let summary: String
395
- let detail: String
396
-
397
- switch phase {
398
- case .listening:
399
- summary = "Voice control is listening"
400
- detail = "The hands-off voice session is capturing your next instruction."
401
- case .reasoning:
402
- summary = "Voice control is working"
403
- detail = "The hands-off voice session is resolving your last request."
404
- default:
405
- summary = "Voice control is idle"
406
- detail = "The hands-off voice session is ready for the next command."
407
- }
408
-
409
- return ActionOutcome(
410
- summary: summary,
411
- detail: detail,
412
- suggestedActions: voiceSuggestions(for: phase)
413
- )
414
- }
415
-
416
- func focusSwitcherItem(_ itemID: String) throws -> ActionOutcome {
417
- if let raw = itemID.stripPrefix("window:"), let wid = UInt32(raw) {
418
- let entry = try MainActorSync.run {
419
- guard let window = DesktopModel.shared.windows[wid] else {
420
- throw LatticesDeckHostError.invalidSwitcherItem(itemID)
421
- }
422
- return window
423
- }
424
- _ = try callAPI("window.focus", params: ["wid": .int(Int(wid))])
425
- return ActionOutcome(
426
- summary: "Focused \(entry.app)",
427
- detail: entry.title.isEmpty ? "Brought the selected window to the front." : entry.title,
428
- suggestedActions: []
429
- )
430
- }
431
-
432
- if let session = itemID.stripPrefix("session:") {
433
- _ = try callAPI("window.focus", params: ["session": .string(session)])
434
- return ActionOutcome(
435
- summary: "Focused \(session)",
436
- detail: "Raised the tmux session window.",
437
- suggestedActions: []
438
- )
439
- }
440
-
441
- if let appName = itemID.stripPrefix("app:") {
442
- let entry = try MainActorSync.run {
443
- guard let window = DesktopModel.shared.windowForApp(app: appName, title: nil) else {
444
- throw LatticesDeckHostError.invalidSwitcherItem(itemID)
445
- }
446
- return window
447
- }
448
- _ = try callAPI("window.focus", params: ["wid": .int(Int(entry.wid))])
449
- return ActionOutcome(
450
- summary: "Focused \(entry.app)",
451
- detail: entry.title.isEmpty ? "Brought the app's active window forward." : entry.title,
452
- suggestedActions: []
453
- )
454
- }
455
-
456
- if let raw = itemID.stripPrefix("workspace-layer:"), let index = Int(raw) {
457
- let result = try callAPI("layer.activate", params: [
458
- "index": .int(index),
459
- "mode": .string("focus")
460
- ])
461
- let label = result["label"]?.stringValue ?? "layer"
462
- return ActionOutcome(
463
- summary: "Switched to \(label)",
464
- detail: "Focused the workspace layer's windows.",
465
- suggestedActions: []
466
- )
467
- }
468
-
469
- if let layerID = itemID.stripPrefix("session-layer:") {
470
- let layerName = try MainActorSync.run {
471
- guard let layer = SessionLayerStore.shared.layerById(layerID) else {
472
- throw LatticesDeckHostError.invalidSwitcherItem(itemID)
473
- }
474
- return layer.name
475
- }
476
- _ = try callAPI("session.layers.switch", params: [
477
- "name": .string(layerName)
478
- ])
479
- return ActionOutcome(
480
- summary: "Switched to \(layerName)",
481
- detail: "Raised the tagged windows for that session layer.",
482
- suggestedActions: []
483
- )
484
- }
485
-
486
- throw LatticesDeckHostError.invalidSwitcherItem(itemID)
487
- }
488
-
489
- func callAPI(_ method: String, params: [String: JSON] = [:]) throws -> JSON {
490
- try LatticesApi.shared.dispatch(
491
- method: method,
492
- params: params.isEmpty ? nil : .object(params)
493
- )
494
- }
495
-
496
- func resizeFrontmostWindow(
497
- dimension: ResizeDimension,
498
- direction: ResizeDirection
499
- ) throws -> ActionOutcome {
500
- let resolved = try MainActorSync.run {
501
- try self.resizeFrontmostWindowOnMainActor(
502
- dimension: dimension,
503
- direction: direction
504
- )
505
- }
506
-
507
- try MainActorSync.run {
508
- HandsOffSession.shared.snapshotFrames(wids: [resolved.entry.wid])
509
- }
510
-
511
- _ = try callAPI("window.present", params: [
512
- "wid": .int(Int(resolved.entry.wid)),
513
- "x": .int(Int(resolved.frame.origin.x.rounded())),
514
- "y": .int(Int(resolved.frame.origin.y.rounded())),
515
- "w": .int(Int(resolved.frame.width.rounded())),
516
- "h": .int(Int(resolved.frame.height.rounded())),
517
- ])
518
-
519
- let summary: String
520
- switch (direction, dimension) {
521
- case (.grow, .width):
522
- summary = "Made \(resolved.entry.app) wider"
523
- case (.grow, .height):
524
- summary = "Made \(resolved.entry.app) taller"
525
- case (.grow, .both):
526
- summary = "Grew \(resolved.entry.app)"
527
- case (.shrink, .width):
528
- summary = "Made \(resolved.entry.app) narrower"
529
- case (.shrink, .height):
530
- summary = "Made \(resolved.entry.app) shorter"
531
- case (.shrink, .both):
532
- summary = "Shrank \(resolved.entry.app)"
533
- }
534
-
535
- let title = resolved.entry.title.isEmpty ? resolved.entry.app : resolved.entry.title
536
- let size = "\(Int(resolved.frame.width.rounded()))×\(Int(resolved.frame.height.rounded()))"
537
-
538
- return ActionOutcome(
539
- summary: summary,
540
- detail: "\(title) is now \(size).",
541
- suggestedActions: []
542
- )
543
- }
544
-
545
- @MainActor
546
- func snapshotOnMainActor() -> DeckRuntimeSnapshot {
547
- let handsOff = HandsOffSession.shared
548
- let audio = AudioLayer.shared
549
- let windows = DesktopModel.shared.allWindows()
550
- let visibleWindows = windows.filter(\.isOnScreen)
551
- let sessions = TmuxModel.shared.sessions
552
- let spacesState = buildSpacesState()
553
- let currentSpaceIndex = spacesState.currentSpaceIndex
554
- let currentSpaceName = spacesState.currentSpaceName
555
- let voice = DeckVoiceState(
556
- phase: currentVoicePhase(),
557
- transcript: handsOff.lastTranscript ?? audio.lastTranscript,
558
- transcriptLines: buildTranscriptLines(handsOff: handsOff, audio: audio),
559
- responseSummary: handsOff.lastResponse ?? audio.executionResult,
560
- provider: audio.providerName == "none" ? "vox" : audio.providerName
561
- )
562
- let desktop = DeckDesktopSummary(
563
- activeLayerName: activeLayerName(),
564
- activeAppName: visibleWindows.first?.app ?? NSWorkspace.shared.frontmostApplication?.localizedName,
565
- screenCount: NSScreen.screens.count,
566
- visibleWindowCount: visibleWindows.count,
567
- sessionCount: sessions.count,
568
- currentSpaceIndex: currentSpaceIndex,
569
- currentSpaceName: currentSpaceName
570
- )
571
- let layoutState = buildLayoutState(windows: visibleWindows)
572
- let switcherState = DeckSwitcherState(items: buildSwitcherItems(
573
- windows: visibleWindows,
574
- sessions: sessions
575
- ))
576
- let telemetry = SystemTelemetryMonitor.shared.snapshot(
577
- windowCount: visibleWindows.count,
578
- sessionCount: sessions.count
579
- )
580
- let cockpitMode = buildCockpitModeState(handsOff: handsOff)
581
- let activityLog = CompanionActivityLog.shared.snapshot()
582
-
583
- return DeckRuntimeSnapshot(
584
- updatedAt: Date(),
585
- cockpit: buildCockpitState(
586
- voice: voice,
587
- desktop: desktop,
588
- layoutState: layoutState
589
- ),
590
- trackpad: LatticesCompanionTrackpadController.shared.state(
591
- isEnabled: Preferences.shared.companionTrackpadEnabled
592
- ),
593
- voice: voice,
594
- desktop: desktop,
595
- layout: layoutState,
596
- switcher: switcherState,
597
- telemetry: telemetry,
598
- spaces: spacesState,
599
- cockpitMode: cockpitMode,
600
- activityLog: activityLog,
601
- history: buildHistoryEntries(handsOff: handsOff),
602
- questions: []
603
- )
604
- }
605
-
606
- @MainActor
607
- func buildCockpitState(
608
- voice: DeckVoiceState,
609
- desktop: DeckDesktopSummary,
610
- layoutState: DeckLayoutState?
611
- ) -> DeckCockpitState {
612
- LatticesCompanionCockpitCatalog.renderedState(
613
- layout: Preferences.shared.companionCockpitLayout,
614
- voice: voice,
615
- desktop: desktop,
616
- layoutState: layoutState
617
- )
618
- }
619
-
620
- @MainActor
621
- func buildTranscriptLines(handsOff: HandsOffSession, audio: AudioLayer) -> [DeckTranscriptLine]? {
622
- var lines = handsOff.chatLog.suffix(10).compactMap { entry -> DeckTranscriptLine? in
623
- guard entry.role == .user else { return nil }
624
- return DeckTranscriptLine(
625
- id: "voice-\(entry.id.uuidString)",
626
- createdAt: entry.timestamp,
627
- text: entry.text,
628
- isFinal: true,
629
- confidence: nil,
630
- source: "hands-off"
631
- )
632
- }
633
-
634
- if let transcript = audio.lastTranscript ?? handsOff.lastTranscript,
635
- !transcript.isEmpty,
636
- !lines.contains(where: { $0.text == transcript }) {
637
- lines.append(DeckTranscriptLine(
638
- id: "voice-current",
639
- createdAt: Date(),
640
- text: transcript,
641
- isFinal: currentVoicePhase() == .idle,
642
- confidence: audio.matchConfidence > 0 ? audio.matchConfidence : nil,
643
- source: audio.providerName == "none" ? "vox" : audio.providerName
644
- ))
645
- }
646
-
647
- return lines.isEmpty ? nil : Array(lines.suffix(8).reversed())
648
- }
649
-
650
- @MainActor
651
- func buildSpacesState() -> DeckSpacesState {
652
- let displays = WindowTiler.getDisplaySpaces()
653
- let deckDisplays = displays.map { display -> DeckSpaceDisplay in
654
- let spaces = display.spaces.map { space in
655
- DeckSpace(
656
- id: space.id,
657
- index: space.index,
658
- name: spaceName(for: space.index),
659
- isCurrent: space.isCurrent
660
- )
661
- }
662
- let current = spaces.first(where: \.isCurrent)
663
- return DeckSpaceDisplay(
664
- id: display.displayId.isEmpty ? "display-\(display.displayIndex)" : display.displayId,
665
- displayIndex: display.displayIndex,
666
- currentSpaceID: display.currentSpaceId == 0 ? nil : display.currentSpaceId,
667
- currentSpaceIndex: current?.index,
668
- currentSpaceName: current?.name,
669
- spaces: spaces
670
- )
671
- }
672
-
673
- let primaryCurrent = deckDisplays.first?.spaces.first(where: \.isCurrent)
674
- ?? deckDisplays.flatMap(\.spaces).first(where: \.isCurrent)
675
-
676
- return DeckSpacesState(
677
- currentSpaceIndex: primaryCurrent?.index,
678
- currentSpaceName: primaryCurrent?.name,
679
- displays: deckDisplays
680
- )
681
- }
682
-
683
- @MainActor
684
- func buildCockpitModeState(handsOff: HandsOffSession) -> DeckCockpitModeState {
685
- let now = Date()
686
-
687
- switch handsOff.state {
688
- case .connecting, .listening:
689
- return DeckCockpitModeState(
690
- mode: .rec,
691
- startedAt: handsOff.stateChangedAt,
692
- elapsedSeconds: now.timeIntervalSince(handsOff.stateChangedAt)
693
- )
694
- case .thinking:
695
- return DeckCockpitModeState(
696
- mode: .agent,
697
- startedAt: handsOff.stateChangedAt,
698
- elapsedSeconds: now.timeIntervalSince(handsOff.stateChangedAt),
699
- agentProgress: 0.45,
700
- agentRows: buildAgentRows(handsOff: handsOff)
701
- )
702
- case .idle:
703
- break
704
- }
705
-
706
- if let replay = currentReplay(now: now) {
707
- return DeckCockpitModeState(
708
- mode: .replay,
709
- startedAt: replay.createdAt,
710
- elapsedSeconds: now.timeIntervalSince(replay.createdAt),
711
- replayMessage: replay.message,
712
- replayUndoExpiresAt: replay.createdAt.addingTimeInterval(5),
713
- replayUndoActionID: replay.undoActionID
714
- )
715
- }
716
-
717
- if let historyDate = handsOff.frameHistoryUpdatedAt,
718
- !handsOff.frameHistory.isEmpty,
719
- now.timeIntervalSince(historyDate) <= 5 {
720
- return DeckCockpitModeState(
721
- mode: .replay,
722
- startedAt: historyDate,
723
- elapsedSeconds: now.timeIntervalSince(historyDate),
724
- replayMessage: replayMessageFromRecentAction(handsOff: handsOff),
725
- replayUndoExpiresAt: historyDate.addingTimeInterval(5),
726
- replayUndoActionID: "history.undoLast"
727
- )
728
- }
729
-
730
- return DeckCockpitModeState(mode: .idle)
731
- }
732
-
733
- @MainActor
734
- func resizeFrontmostWindowOnMainActor(
735
- dimension: ResizeDimension,
736
- direction: ResizeDirection
737
- ) throws -> (entry: WindowEntry, frame: CGRect) {
738
- let windows = DesktopModel.shared.allWindows().filter(\.isOnScreen)
739
- guard let entry = currentFrontmostWindow(from: windows) else {
740
- throw LatticesDeckHostError.noFrontmostWindow
741
- }
742
-
743
- let screen = WindowTiler.screenForWindowFrame(entry.frame)
744
- let visibleFrame = cgVisibleFrame(for: screen)
745
- let currentFrame = CGRect(
746
- x: entry.frame.x,
747
- y: entry.frame.y,
748
- width: entry.frame.w,
749
- height: entry.frame.h
750
- )
751
-
752
- return (
753
- entry,
754
- adjustedFrame(
755
- currentFrame: currentFrame,
756
- visibleFrame: visibleFrame,
757
- dimension: dimension,
758
- direction: direction
759
- )
760
- )
761
- }
762
-
763
- @MainActor
764
- func currentVoicePhase() -> DeckVoicePhase {
765
- let handsOff = HandsOffSession.shared
766
- switch handsOff.state {
767
- case .idle:
768
- break
769
- case .connecting, .listening:
770
- return .listening
771
- case .thinking:
772
- return .reasoning
773
- }
774
-
775
- let audio = AudioLayer.shared
776
- if audio.isListening {
777
- return .listening
778
- }
779
- if audio.executionResult == "Transcribing..." {
780
- return .transcribing
781
- }
782
- if audio.executionResult == "thinking..." {
783
- return .reasoning
784
- }
785
- return .idle
786
- }
787
-
788
- @MainActor
789
- func activeLayerName() -> String? {
790
- let workspace = WorkspaceManager.shared
791
- if let label = workspace.activeLayer?.label, !label.isEmpty {
792
- return label
793
- }
794
-
795
- let sessionLayers = SessionLayerStore.shared
796
- guard sessionLayers.activeIndex >= 0,
797
- sessionLayers.activeIndex < sessionLayers.layers.count else {
798
- return nil
799
- }
800
- return sessionLayers.layers[sessionLayers.activeIndex].name
801
- }
802
-
803
- @MainActor
804
- func buildLayoutState(windows: [WindowEntry]) -> DeckLayoutState? {
805
- let deckWindows = windows.filter { $0.app != "Lattices" }
806
- guard let frontmost = currentFrontmostWindow(from: deckWindows) else {
807
- return nil
808
- }
809
-
810
- let frontmostScreen = WindowTiler.screenForWindowFrame(frontmost.frame)
811
- let frontmostVisible = cgVisibleFrame(for: frontmostScreen)
812
- let frontmostRect = normalizedRect(for: frontmost.frame, within: frontmostVisible)
813
- let placement = WindowTiler.inferTilePosition(frame: frontmost.frame, screen: frontmostScreen)?.rawValue
814
- let aspectRatio = frontmostVisible.height > 0 ? frontmostVisible.width / frontmostVisible.height : 1.0
815
-
816
- // Distribute windows per NSScreen — same pattern as HUDMinimap.windowsOnScreen.
817
- // Each window's normalizedFrame is relative to its own display's visible frame so
818
- // the iPad mini-displays render correctly per-monitor.
819
- let screens = NSScreen.screens
820
- var previewWindows: [DeckLayoutPreviewWindow] = []
821
- for (idx, screen) in screens.enumerated() {
822
- let visible = cgVisibleFrame(for: screen)
823
- let screenID = ObjectIdentifier(screen)
824
- let onScreen = deckWindows
825
- .filter { ObjectIdentifier(WindowTiler.screenForWindowFrame($0.frame)) == screenID }
826
- .sorted { $0.zIndex > $1.zIndex }
827
- for window in onScreen {
828
- guard let rect = normalizedRect(for: window.frame, within: visible) else { continue }
829
- previewWindows.append(DeckLayoutPreviewWindow(
830
- id: "window:\(window.wid)",
831
- itemID: "window:\(window.wid)",
832
- title: window.title.isEmpty ? window.app : window.title,
833
- subtitle: window.title.isEmpty ? nil : window.app,
834
- normalizedFrame: rect,
835
- appCategory: appCategory(for: window.app),
836
- appCategoryTint: appCategoryTint(for: window.app),
837
- isFrontmost: window.wid == frontmost.wid,
838
- displayIndex: idx
839
- ))
840
- }
841
- }
842
-
843
- return DeckLayoutState(
844
- screenName: frontmostScreen.localizedName,
845
- frontmostWindow: DeckLayoutFocusWindow(
846
- id: "window:\(frontmost.wid)",
847
- itemID: "window:\(frontmost.wid)",
848
- appName: frontmost.app,
849
- title: frontmost.title.isEmpty ? nil : frontmost.title,
850
- frame: deckRect(for: frontmost.frame),
851
- normalizedFrame: frontmostRect,
852
- placement: placement
853
- ),
854
- preview: DeckLayoutPreview(
855
- aspectRatio: aspectRatio,
856
- windows: previewWindows,
857
- displayCount: screens.count
858
- )
859
- )
860
- }
861
-
862
- @MainActor
863
- func buildSwitcherItems(
864
- windows: [WindowEntry],
865
- sessions: [TmuxSession]
866
- ) -> [DeckSwitcherItem] {
867
- var items: [DeckSwitcherItem] = []
868
-
869
- if let layers = WorkspaceManager.shared.config?.layers {
870
- for (index, layer) in layers.enumerated() {
871
- items.append(DeckSwitcherItem(
872
- id: "workspace-layer:\(index)",
873
- title: layer.label,
874
- subtitle: "\(layer.projects.count) project target(s)",
875
- iconToken: "workspace-layer",
876
- kind: .task,
877
- isFrontmost: WorkspaceManager.shared.activeLayerIndex == index
878
- ))
879
- }
880
- }
881
-
882
- let sessionLayerStore = SessionLayerStore.shared
883
- for (index, layer) in sessionLayerStore.layers.enumerated() {
884
- items.append(DeckSwitcherItem(
885
- id: "session-layer:\(layer.id)",
886
- title: layer.name,
887
- subtitle: "\(layer.windows.count) tagged window(s)",
888
- iconToken: "session-layer",
889
- kind: .task,
890
- isFrontmost: sessionLayerStore.activeIndex == index
891
- ))
892
- }
893
-
894
- var seenApps = Set<String>()
895
- for window in windows where seenApps.insert(window.app).inserted {
896
- items.append(DeckSwitcherItem(
897
- id: "app:\(window.app)",
898
- title: window.app,
899
- subtitle: window.title.isEmpty ? "Application" : window.title,
900
- iconToken: window.app.lowercased(),
901
- kind: .application,
902
- isFrontmost: window.zIndex == 0
903
- ))
904
- }
905
-
906
- for window in windows.prefix(8) {
907
- items.append(DeckSwitcherItem(
908
- id: "window:\(window.wid)",
909
- title: window.title.isEmpty ? window.app : window.title,
910
- subtitle: window.app,
911
- iconToken: "window",
912
- kind: .window,
913
- isFrontmost: window.zIndex == 0
914
- ))
915
- }
916
-
917
- for session in sessions.prefix(8) {
918
- let paneSummary = session.panes
919
- .map(\.currentCommand)
920
- .filter { !["zsh", "bash", "fish", "sh"].contains($0) }
921
- .prefix(2)
922
- .joined(separator: " · ")
923
-
924
- items.append(DeckSwitcherItem(
925
- id: "session:\(session.name)",
926
- title: session.name,
927
- subtitle: paneSummary.isEmpty ? "tmux session" : paneSummary,
928
- iconToken: "terminal",
929
- kind: .session,
930
- isFrontmost: false
931
- ))
932
- }
933
-
934
- return items
935
- }
936
-
937
- @MainActor
938
- func buildHistoryEntries(handsOff: HandsOffSession) -> [DeckHistoryEntry] {
939
- var entries: [DeckHistoryEntry] = []
940
-
941
- if !handsOff.frameHistory.isEmpty {
942
- entries.append(DeckHistoryEntry(
943
- id: "undo-last-move",
944
- createdAt: Date(),
945
- title: "Last window move can be undone",
946
- detail: "Use the history action to roll back the most recent layout change.",
947
- kind: .layout,
948
- undoActionID: "history.undoLast"
949
- ))
950
- }
951
-
952
- for (index, action) in handsOff.recentActions.enumerated() {
953
- let summary = actionSummary(for: action)
954
- entries.append(DeckHistoryEntry(
955
- id: "recent-action-\(index)",
956
- createdAt: Date(),
957
- title: summary.title,
958
- detail: summary.detail,
959
- kind: summary.kind,
960
- undoActionID: summary.kind == .layout && !handsOff.frameHistory.isEmpty
961
- ? "history.undoLast"
962
- : nil
963
- ))
964
- }
965
-
966
- for entry in handsOff.chatLog.suffix(8).reversed() {
967
- let kind: DeckHistoryKind
968
- switch entry.role {
969
- case .user, .assistant:
970
- kind = .voice
971
- case .system:
972
- kind = .automation
973
- }
974
-
975
- entries.append(DeckHistoryEntry(
976
- id: "chat-\(entry.id.uuidString)",
977
- createdAt: entry.timestamp,
978
- title: historyTitle(for: entry),
979
- detail: entry.detail,
980
- kind: kind
981
- ))
982
- }
983
-
984
- return Array(entries.prefix(12))
985
- }
986
-
987
- @MainActor
988
- func currentFrontmostWindow(from windows: [WindowEntry]) -> WindowEntry? {
989
- if let target = frontmostWindowTarget(),
990
- let entry = DesktopModel.shared.windows[target.wid],
991
- entry.isOnScreen,
992
- entry.app != "Lattices" {
993
- return entry
994
- }
995
-
996
- return windows
997
- .filter { $0.app != "Lattices" }
998
- .min { lhs, rhs in
999
- lhs.zIndex < rhs.zIndex
1000
- }
1001
- }
1002
-
1003
- func spaceSwitchDirection(key: String, modifiers: [String]) -> Int? {
1004
- let normalized = key.lowercased()
1005
- .replacingOccurrences(of: "←", with: "left")
1006
- .replacingOccurrences(of: "→", with: "right")
1007
- guard normalized == "left" || normalized == "right" else { return nil }
1008
- let hasControl = modifiers.contains { mod in
1009
- let m = mod.lowercased()
1010
- return m == "control" || m == "ctrl" || m == "⌃"
1011
- }
1012
- guard hasControl else { return nil }
1013
- return normalized == "right" ? 1 : -1
1014
- }
1015
-
1016
- @MainActor
1017
- func switchActiveSpace(direction: Int) -> ActionOutcome {
1018
- let displays = WindowTiler.getDisplaySpaces()
1019
- guard !displays.isEmpty else {
1020
- return ActionOutcome(summary: "No displays available", detail: nil, suggestedActions: [])
1021
- }
1022
-
1023
- let activeScreen: NSScreen? = {
1024
- if let frontmost = frontmostWindowTarget(),
1025
- let entry = DesktopModel.shared.windows[frontmost.wid] {
1026
- return WindowTiler.screenForWindowFrame(entry.frame)
1027
- }
1028
- let mouse = NSEvent.mouseLocation
1029
- return NSScreen.screens.first(where: { $0.frame.contains(mouse) }) ?? NSScreen.main
1030
- }()
1031
-
1032
- let preferredDisplayIndex = NSScreen.screens.firstIndex { activeScreen === $0 } ?? 0
1033
- let display = displays.first(where: { $0.displayIndex == preferredDisplayIndex }) ?? displays[0]
1034
-
1035
- guard let currentIdx = display.spaces.firstIndex(where: { $0.isCurrent }) else {
1036
- return ActionOutcome(
1037
- summary: "No current space",
1038
- detail: "Could not determine the active space on display \(display.displayIndex + 1).",
1039
- suggestedActions: []
1040
- )
1041
- }
1042
-
1043
- let targetIdx = currentIdx + direction
1044
- guard targetIdx >= 0, targetIdx < display.spaces.count else {
1045
- return ActionOutcome(
1046
- summary: direction > 0 ? "Already on last space" : "Already on first space",
1047
- detail: nil,
1048
- suggestedActions: []
1049
- )
1050
- }
1051
-
1052
- let target = display.spaces[targetIdx]
1053
- WindowTiler.switchToSpace(spaceId: target.id)
1054
- return ActionOutcome(
1055
- summary: direction > 0 ? "Next space" : "Previous space",
1056
- detail: "Switched display \(display.displayIndex + 1) to space \(target.index).",
1057
- suggestedActions: []
1058
- )
1059
- }
1060
-
1061
- @MainActor
1062
- func buildAgentRows(handsOff: HandsOffSession) -> [DeckAgentPlanRow] {
1063
- let transcript = handsOff.lastTranscript ?? "voice request"
1064
- var rows: [DeckAgentPlanRow] = [
1065
- DeckAgentPlanRow(id: "capture", state: .done, text: "captured: \(transcript)"),
1066
- DeckAgentPlanRow(id: "resolve", state: .live, text: "resolve workspace intent"),
1067
- DeckAgentPlanRow(id: "apply", state: .next, text: "apply actions on Mac"),
1068
- ]
1069
-
1070
- for (index, action) in handsOff.recentActions.prefix(3).enumerated() {
1071
- let summary = actionSummary(for: action)
1072
- rows.append(DeckAgentPlanRow(
1073
- id: "recent-\(index)",
1074
- state: .next,
1075
- text: summary.title.lowercased()
1076
- ))
1077
- }
1078
-
1079
- return rows
1080
- }
1081
-
1082
- @MainActor
1083
- func replayMessageFromRecentAction(handsOff: HandsOffSession) -> String {
1084
- if let action = handsOff.recentActions.first {
1085
- return actionSummary(for: action).title
1086
- }
1087
- if let transcript = handsOff.lastTranscript, !transcript.isEmpty {
1088
- return transcript
1089
- }
1090
- return "Last window move"
1091
- }
1092
-
1093
- func cycleApplication(direction: String) throws -> ActionOutcome {
1094
- let target = try MainActorSync.run {
1095
- try self.nextApplicationTargetOnMainActor(direction: direction)
1096
- }
1097
- _ = try callAPI("window.focus", params: ["wid": .int(Int(target.wid))])
1098
- let title = target.title.isEmpty ? target.app : target.title
1099
- return ActionOutcome(
1100
- summary: "Focused \(target.app)",
1101
- detail: title,
1102
- suggestedActions: []
1103
- )
1104
- }
1105
-
1106
- func cycleWindow(direction: String) throws -> ActionOutcome {
1107
- let target = try MainActorSync.run {
1108
- try self.nextWindowTargetOnMainActor(direction: direction)
1109
- }
1110
- _ = try callAPI("window.focus", params: ["wid": .int(Int(target.wid))])
1111
- return ActionOutcome(
1112
- summary: "Focused \(target.app)",
1113
- detail: target.title.isEmpty ? "Moved to the next visible window." : target.title,
1114
- suggestedActions: []
1115
- )
1116
- }
1117
-
1118
- @MainActor
1119
- func nextApplicationTargetOnMainActor(direction: String) throws -> WindowEntry {
1120
- let windows = DesktopModel.shared.allWindows()
1121
- .filter { $0.isOnScreen && $0.app != "Lattices" }
1122
- .sorted { lhs, rhs in
1123
- lhs.zIndex < rhs.zIndex
1124
- }
1125
-
1126
- var orderedApps: [String] = []
1127
- for window in windows where !orderedApps.contains(window.app) {
1128
- orderedApps.append(window.app)
1129
- }
1130
-
1131
- guard !orderedApps.isEmpty else {
1132
- throw LatticesDeckHostError.noVisibleTargets("applications")
1133
- }
1134
-
1135
- let currentApp = currentFrontmostWindow(from: windows)?.app ?? orderedApps.first!
1136
- let currentIndex = orderedApps.firstIndex(of: currentApp) ?? 0
1137
- let targetIndex = wrappedIndex(
1138
- currentIndex,
1139
- count: orderedApps.count,
1140
- direction: direction
1141
- )
1142
- let targetApp = orderedApps[targetIndex]
1143
-
1144
- guard let target = windows.first(where: { $0.app == targetApp }) else {
1145
- throw LatticesDeckHostError.noVisibleTargets("applications")
1146
- }
1147
-
1148
- return target
1149
- }
1150
-
1151
- @MainActor
1152
- func nextWindowTargetOnMainActor(direction: String) throws -> WindowEntry {
1153
- let windows = DesktopModel.shared.allWindows()
1154
- .filter { $0.isOnScreen && $0.app != "Lattices" }
1155
- .sorted { lhs, rhs in
1156
- lhs.zIndex < rhs.zIndex
1157
- }
1158
-
1159
- guard !windows.isEmpty else {
1160
- throw LatticesDeckHostError.noVisibleTargets("windows")
1161
- }
1162
-
1163
- let currentWID = currentFrontmostWindow(from: windows)?.wid ?? windows[0].wid
1164
- let currentIndex = windows.firstIndex(where: { $0.wid == currentWID }) ?? 0
1165
- let targetIndex = wrappedIndex(
1166
- currentIndex,
1167
- count: windows.count,
1168
- direction: direction
1169
- )
1170
- return windows[targetIndex]
1171
- }
1172
-
1173
- func wrappedIndex(_ currentIndex: Int, count: Int, direction: String) -> Int {
1174
- guard count > 0 else { return 0 }
1175
- if direction.lowercased().hasPrefix("prev") {
1176
- return (currentIndex - 1 + count) % count
1177
- }
1178
- return (currentIndex + 1) % count
1179
- }
1180
-
1181
- @MainActor
1182
- func frontmostWindowTarget() -> (wid: UInt32, pid: Int32)? {
1183
- guard let app = NSWorkspace.shared.frontmostApplication,
1184
- app.bundleIdentifier != "com.arach.lattices" else {
1185
- return nil
1186
- }
1187
-
1188
- let appRef = AXUIElementCreateApplication(app.processIdentifier)
1189
- var focusedRef: CFTypeRef?
1190
- guard AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &focusedRef) == .success,
1191
- let focusedWindow = focusedRef else {
1192
- return nil
1193
- }
1194
-
1195
- var wid: CGWindowID = 0
1196
- guard _AXUIElementGetWindow(focusedWindow as! AXUIElement, &wid) == .success else {
1197
- return nil
1198
- }
1199
-
1200
- return (UInt32(wid), app.processIdentifier)
1201
- }
1202
-
1203
- @MainActor
1204
- func cgVisibleFrame(for screen: NSScreen) -> CGRect {
1205
- let visible = screen.visibleFrame
1206
- let primaryHeight = NSScreen.screens.first?.frame.height ?? screen.frame.height
1207
- return CGRect(
1208
- x: visible.minX,
1209
- y: primaryHeight - visible.maxY,
1210
- width: visible.width,
1211
- height: visible.height
1212
- )
1213
- }
1214
-
1215
- func deckRect(for frame: WindowFrame) -> DeckRect {
1216
- DeckRect(x: frame.x, y: frame.y, w: frame.w, h: frame.h)
1217
- }
1218
-
1219
- func deckRect(for frame: CGRect) -> DeckRect {
1220
- DeckRect(
1221
- x: frame.origin.x,
1222
- y: frame.origin.y,
1223
- w: frame.width,
1224
- h: frame.height
1225
- )
1226
- }
1227
-
1228
- func normalizedRect(for frame: WindowFrame, within visibleFrame: CGRect) -> DeckRect? {
1229
- guard visibleFrame.width > 0, visibleFrame.height > 0 else { return nil }
1230
-
1231
- let x1 = max(0, min(1, (frame.x - visibleFrame.minX) / visibleFrame.width))
1232
- let y1 = max(0, min(1, (frame.y - visibleFrame.minY) / visibleFrame.height))
1233
- let x2 = max(0, min(1, ((frame.x + frame.w) - visibleFrame.minX) / visibleFrame.width))
1234
- let y2 = max(0, min(1, ((frame.y + frame.h) - visibleFrame.minY) / visibleFrame.height))
1235
-
1236
- guard x2 > x1, y2 > y1 else { return nil }
1237
- return DeckRect(x: x1, y: y1, w: x2 - x1, h: y2 - y1)
1238
- }
1239
-
1240
- func adjustedFrame(
1241
- currentFrame: CGRect,
1242
- visibleFrame: CGRect,
1243
- dimension: ResizeDimension,
1244
- direction: ResizeDirection
1245
- ) -> CGRect {
1246
- var next = currentFrame
1247
-
1248
- let widthStep = max(88.0, visibleFrame.width * 0.08)
1249
- let heightStep = max(72.0, visibleFrame.height * 0.08)
1250
- let widthDelta = direction == .grow ? widthStep : -widthStep
1251
- let heightDelta = direction == .grow ? heightStep : -heightStep
1252
- let minWidth = min(max(320.0, visibleFrame.width * 0.24), visibleFrame.width)
1253
- let minHeight = min(max(220.0, visibleFrame.height * 0.24), visibleFrame.height)
1254
- let maxWidth = visibleFrame.width
1255
- let maxHeight = visibleFrame.height
1256
-
1257
- let centerX = currentFrame.midX
1258
- let centerY = currentFrame.midY
1259
-
1260
- switch dimension {
1261
- case .width:
1262
- next.size.width = max(minWidth, min(maxWidth, currentFrame.width + widthDelta))
1263
- case .height:
1264
- next.size.height = max(minHeight, min(maxHeight, currentFrame.height + heightDelta))
1265
- case .both:
1266
- next.size.width = max(minWidth, min(maxWidth, currentFrame.width + widthDelta))
1267
- next.size.height = max(minHeight, min(maxHeight, currentFrame.height + heightDelta))
1268
- }
1269
-
1270
- next.origin.x = centerX - next.width / 2
1271
- next.origin.y = centerY - next.height / 2
1272
-
1273
- next.origin.x = max(visibleFrame.minX, min(next.origin.x, visibleFrame.maxX - next.width))
1274
- next.origin.y = max(visibleFrame.minY, min(next.origin.y, visibleFrame.maxY - next.height))
1275
- return next.integral
1276
- }
1277
-
1278
- func actionSummary(for action: [String: Any]) -> (title: String, detail: String?, kind: DeckHistoryKind) {
1279
- let intent = action["intent"] as? String ?? "action"
1280
- let slots = action["slots"] as? [String: Any] ?? [:]
1281
- let title = intent
1282
- .split(separator: "_")
1283
- .map { $0.capitalized }
1284
- .joined(separator: " ")
1285
-
1286
- let detail = slots.keys.sorted().compactMap { key -> String? in
1287
- guard let value = slots[key] else { return nil }
1288
- return "\(key)=\(value)"
1289
- }
1290
- .joined(separator: ", ")
1291
-
1292
- let kind: DeckHistoryKind
1293
- if ["tile_window", "swap", "distribute", "move_to_display"].contains(intent) {
1294
- kind = .layout
1295
- } else if intent.contains("focus") || intent.contains("switch") || intent.contains("launch") {
1296
- kind = .switcher
1297
- } else {
1298
- kind = .automation
1299
- }
1300
-
1301
- return (title, detail.isEmpty ? nil : detail, kind)
1302
- }
1303
-
1304
- @MainActor
1305
- func spaceName(for index: Int) -> String {
1306
- if let layers = WorkspaceManager.shared.config?.layers,
1307
- layers.indices.contains(index - 1) {
1308
- return layers[index - 1].label
1309
- }
1310
-
1311
- let defaults = ["main", "code", "chat", "review", "media", "notes", "ops", "admin", "scratch"]
1312
- if defaults.indices.contains(index - 1) {
1313
- return defaults[index - 1]
1314
- }
1315
- return "space \(index)"
1316
- }
1317
-
1318
- func appCategory(for appName: String) -> String {
1319
- AppTypeClassifier.classify(appName).rawValue
1320
- }
1321
-
1322
- func appCategoryTint(for appName: String) -> String {
1323
- switch AppTypeClassifier.classify(appName) {
1324
- case .terminal, .editor:
1325
- return "green"
1326
- case .browser:
1327
- return "blue"
1328
- case .chat:
1329
- return "teal"
1330
- case .media:
1331
- return "pink"
1332
- case .design:
1333
- return "violet"
1334
- case .system:
1335
- return "amber"
1336
- case .other:
1337
- return "amber"
1338
- }
1339
- }
1340
-
1341
- func recordAction(request: DeckActionRequest, outcome: ActionOutcome) {
1342
- CompanionActivityLog.shared.record(
1343
- tag: "DECK",
1344
- tint: actionTint(for: request.actionID),
1345
- text: outcome.summary
1346
- )
1347
-
1348
- let hasUndo = ((try? MainActorSync.run {
1349
- !HandsOffSession.shared.frameHistory.isEmpty
1350
- }) ?? false)
1351
-
1352
- replayLock.lock()
1353
- lastReplayMessage = outcome.summary
1354
- lastReplayAt = Date()
1355
- lastReplayUndoActionID = hasUndo ? "history.undoLast" : nil
1356
- replayLock.unlock()
1357
- }
1358
-
1359
- func currentReplay(now: Date) -> (message: String, createdAt: Date, undoActionID: String?)? {
1360
- replayLock.lock()
1361
- let message = lastReplayMessage
1362
- let createdAt = lastReplayAt
1363
- let undoActionID = lastReplayUndoActionID
1364
- replayLock.unlock()
1365
-
1366
- guard let message, let createdAt, now.timeIntervalSince(createdAt) <= 5 else {
1367
- return nil
1368
- }
1369
- return (message, createdAt, undoActionID)
1370
- }
1371
-
1372
- func actionTint(for actionID: String) -> String {
1373
- if actionID.hasPrefix("voice") { return "red" }
1374
- if actionID.hasPrefix("layout") { return "blue" }
1375
- if actionID.hasPrefix("switch") { return "violet" }
1376
- if actionID.hasPrefix("mouse") { return "teal" }
1377
- if actionID.hasPrefix("key") || actionID.hasPrefix("keys") { return "amber" }
1378
- if actionID.hasPrefix("history") { return "green" }
1379
- return "amber"
1380
- }
1381
-
1382
- func historyTitle(for entry: VoiceChatEntry) -> String {
1383
- switch entry.role {
1384
- case .user:
1385
- return "You: \(entry.text)"
1386
- case .assistant:
1387
- return "Lattices: \(entry.text)"
1388
- case .system:
1389
- return "System: \(entry.text)"
1390
- }
1391
- }
1392
-
1393
- func voiceSuggestions(for phase: DeckVoicePhase) -> [DeckSuggestedAction] {
1394
- switch phase {
1395
- case .idle:
1396
- return [
1397
- DeckSuggestedAction(
1398
- id: "voice.toggle",
1399
- title: "Start Voice",
1400
- iconSystemName: "mic.fill"
1401
- )
1402
- ]
1403
- case .listening:
1404
- return [
1405
- DeckSuggestedAction(
1406
- id: "voice.toggle",
1407
- title: "Stop Listening",
1408
- iconSystemName: "stop.fill"
1409
- ),
1410
- DeckSuggestedAction(
1411
- id: "voice.cancel",
1412
- title: "Cancel",
1413
- iconSystemName: "xmark"
1414
- )
1415
- ]
1416
- case .transcribing, .reasoning, .speaking:
1417
- return [
1418
- DeckSuggestedAction(
1419
- id: "voice.cancel",
1420
- title: "Cancel",
1421
- iconSystemName: "xmark"
1422
- )
1423
- ]
1424
- }
1425
- }
1426
-
1427
- func flushMainQueue() {
1428
- guard !Thread.isMainThread else { return }
1429
- let semaphore = DispatchSemaphore(value: 0)
1430
- DispatchQueue.main.async {
1431
- semaphore.signal()
1432
- }
1433
- semaphore.wait()
1434
- }
1435
- }
1436
-
1437
- private enum MainActorSync {
1438
- static func run<T>(_ body: @escaping @MainActor () throws -> T) throws -> T {
1439
- if Thread.isMainThread {
1440
- return try MainActor.assumeIsolated(body)
1441
- }
1442
-
1443
- let semaphore = DispatchSemaphore(value: 0)
1444
- var result: Result<T, Error>!
1445
-
1446
- Task { @MainActor in
1447
- result = Result {
1448
- try body()
1449
- }
1450
- semaphore.signal()
1451
- }
1452
-
1453
- semaphore.wait()
1454
- return try result.get()
1455
- }
1456
- }
1457
-
1458
- private extension String {
1459
- func stripPrefix(_ prefix: String) -> String? {
1460
- guard hasPrefix(prefix) else { return nil }
1461
- return String(dropFirst(prefix.count))
1462
- }
1463
- }