@lattices/cli 0.4.2 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/app/Info.plist +2 -2
- package/app/Lattices.app/Contents/Info.plist +2 -2
- package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/app/Package.swift +6 -0
- package/app/Sources/AppShell/App.swift +20 -0
- package/app/Sources/{AppDelegate.swift → AppShell/AppDelegate.swift} +94 -34
- package/app/Sources/{AppShellView.swift → AppShell/AppShellView.swift} +12 -1
- package/app/Sources/AppShell/AppUpdater.swift +92 -0
- package/app/Sources/AppShell/CliActionLauncher.swift +50 -0
- package/app/Sources/{HomeDashboardView.swift → AppShell/HomeDashboardView.swift} +18 -10
- package/app/Sources/AppShell/LatticesRuntime.swift +61 -0
- package/app/Sources/{MainView.swift → AppShell/MainView.swift} +351 -191
- package/app/Sources/{OnboardingView.swift → AppShell/OnboardingView.swift} +30 -16
- package/app/Sources/{Preferences.swift → AppShell/Preferences.swift} +78 -0
- package/app/Sources/{SettingsView.swift → AppShell/SettingsView.swift} +869 -152
- package/app/Sources/{HotkeyStore.swift → Core/Actions/HotkeyStore.swift} +9 -5
- package/app/Sources/{IntentEngine.swift → Core/Actions/IntentEngine.swift} +51 -27
- package/app/Sources/Core/Actions/IntentSchema.swift +94 -0
- package/app/Sources/{Intents → Core/Actions/Intents}/LatticeIntent.swift +0 -25
- package/app/Sources/{PaletteCommand.swift → Core/Actions/PaletteCommand.swift} +26 -6
- package/app/Sources/{VoiceIntentResolver.swift → Core/Actions/VoiceIntentResolver.swift} +46 -4
- package/app/Sources/Core/Companion/CompanionActivityLog.swift +70 -0
- package/app/Sources/Core/Companion/CompanionKeyboardController.swift +141 -0
- package/app/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +438 -0
- package/app/Sources/Core/Companion/LatticesCompanionCockpit.swift +555 -0
- package/app/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +594 -0
- package/app/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +204 -0
- package/app/Sources/Core/Companion/LatticesDeckHost.swift +1463 -0
- package/app/Sources/{LatticesApi.swift → Core/Daemon/LatticesApi.swift} +125 -4
- package/app/Sources/{AppTypeClassifier.swift → Core/Desktop/AppTypeClassifier.swift} +36 -0
- package/app/Sources/{DesktopModel.swift → Core/Desktop/DesktopModel.swift} +6 -8
- package/app/Sources/Core/Desktop/MouseFinder.swift +527 -0
- package/app/Sources/Core/Desktop/SessionWindowLocator.swift +139 -0
- package/app/Sources/Core/Desktop/WindowDragSnapController.swift +628 -0
- package/app/Sources/Core/Desktop/WindowPreviewCard.swift +100 -0
- package/app/Sources/Core/Desktop/WindowPreviewStore.swift +113 -0
- package/app/Sources/Core/Desktop/WindowSelectionStore.swift +76 -0
- package/app/Sources/{WindowTiler.swift → Core/Desktop/WindowTiler.swift} +351 -172
- package/app/Sources/Core/Input/MouseGestureConfig.swift +364 -0
- package/app/Sources/Core/Input/MouseGestureController.swift +1203 -0
- package/app/Sources/Core/Input/MouseInputDeviceStore.swift +98 -0
- package/app/Sources/Core/Input/MouseInputEventViewer.swift +272 -0
- package/app/Sources/Core/Input/MouseShortcutStore.swift +107 -0
- package/app/Sources/{CommandModeState.swift → Core/Overlays/CommandMode/CommandModeState.swift} +127 -24
- package/app/Sources/{CommandModeView.swift → Core/Overlays/CommandMode/CommandModeView.swift} +492 -79
- package/app/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +67 -0
- package/app/Sources/{CheatSheetHUD.swift → Core/Overlays/HUD/CheatSheetHUD.swift} +1 -0
- package/app/Sources/{HUDRightBar.swift → Core/Overlays/HUD/HUDRightBar.swift} +23 -201
- package/app/Sources/{LauncherHUD.swift → Core/Overlays/HUD/LauncherHUD.swift} +12 -26
- package/app/Sources/{OmniSearchView.swift → Core/Overlays/OmniSearch/OmniSearchView.swift} +136 -2
- package/app/Sources/{OmniSearchWindow.swift → Core/Overlays/OmniSearch/OmniSearchWindow.swift} +21 -32
- package/app/Sources/Core/Overlays/OverlayPanelShell.swift +241 -0
- package/app/Sources/{ScreenMapState.swift → Core/Overlays/ScreenMap/ScreenMapState.swift} +116 -32
- package/app/Sources/{ScreenMapView.swift → Core/Overlays/ScreenMap/ScreenMapView.swift} +510 -524
- package/app/Sources/{ScreenMapWindowController.swift → Core/Overlays/ScreenMap/ScreenMapWindowController.swift} +12 -4
- package/app/Sources/{VoiceCommandWindow.swift → Core/Overlays/Voice/VoiceCommandWindow.swift} +46 -53
- package/app/Sources/Core/Pi/PiAuthNextStepCard.swift +148 -0
- package/app/Sources/Core/Pi/PiAuthPromptCard.swift +90 -0
- package/app/Sources/{PiChatDock.swift → Core/Pi/PiChatDock.swift} +137 -74
- package/app/Sources/{PiChatSession.swift → Core/Pi/PiChatSession.swift} +608 -108
- package/app/Sources/Core/Pi/PiInstallCallout.swift +86 -0
- package/app/Sources/Core/Pi/PiProviderSetupCallout.swift +99 -0
- package/app/Sources/{PiWorkspaceView.swift → Core/Pi/PiWorkspaceView.swift} +174 -77
- package/app/Sources/{PermissionChecker.swift → Core/System/PermissionChecker.swift} +76 -2
- package/app/Sources/Core/System/SystemTelemetryMonitor.swift +273 -0
- package/app/Sources/{HandsOffSession.swift → Core/Voice/HandsOffSession.swift} +15 -4
- package/app/Sources/{WorkspaceManager.swift → Core/Workspace/WorkspaceManager.swift} +288 -0
- package/bin/assistant-intelligence.ts +874 -0
- package/bin/handsoff-infer.ts +16 -209
- package/bin/handsoff-worker.ts +45 -258
- package/bin/lattices-app.ts +62 -0
- package/bin/lattices-dev +4 -0
- package/bin/lattices.ts +125 -14
- package/docs/agents.md +14 -0
- package/docs/api.md +55 -0
- package/docs/app.md +3 -0
- package/docs/companion-deck.md +180 -0
- package/docs/component-extraction-roadmap.md +392 -0
- package/docs/config.md +25 -0
- package/docs/tiling-reference.md +55 -0
- package/docs/voice-error-model.md +73 -0
- package/package.json +4 -1
- package/app/Sources/App.swift +0 -10
- package/app/Sources/CommandPaletteWindow.swift +0 -134
- package/app/Sources/MouseFinder.swift +0 -222
- /package/app/Sources/{KeyRecorderView.swift → AppShell/KeyRecorderView.swift} +0 -0
- /package/app/Sources/{MainWindow.swift → AppShell/MainWindow.swift} +0 -0
- /package/app/Sources/{SettingsWindow.swift → AppShell/SettingsWindow.swift} +0 -0
- /package/app/Sources/{HotkeyManager.swift → Core/Actions/HotkeyManager.swift} +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/CreateLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/DistributeIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/FocusIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/HelpIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/KillIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/LaunchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListSessionsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ListWindowsIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/ScanIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SearchIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/SwitchLayerIntent.swift +0 -0
- /package/app/Sources/{Intents → Core/Actions/Intents}/TileIntent.swift +0 -0
- /package/app/Sources/{DaemonProtocol.swift → Core/Daemon/DaemonProtocol.swift} +0 -0
- /package/app/Sources/{DaemonServer.swift → Core/Daemon/DaemonServer.swift} +0 -0
- /package/app/Sources/{AccessibilityTextExtractor.swift → Core/Desktop/AccessibilityTextExtractor.swift} +0 -0
- /package/app/Sources/{DesktopModelTypes.swift → Core/Desktop/DesktopModelTypes.swift} +0 -0
- /package/app/Sources/{InventoryManager.swift → Core/Desktop/InventoryManager.swift} +0 -0
- /package/app/Sources/{InventoryPath.swift → Core/Desktop/InventoryPath.swift} +0 -0
- /package/app/Sources/{OcrModel.swift → Core/Desktop/OcrModel.swift} +0 -0
- /package/app/Sources/{OcrStore.swift → Core/Desktop/OcrStore.swift} +0 -0
- /package/app/Sources/{PlacementSpec.swift → Core/Desktop/PlacementSpec.swift} +0 -0
- /package/app/Sources/{TilePickerView.swift → Core/Desktop/TilePickerView.swift} +0 -0
- /package/app/Sources/{AppWindowShell.swift → Core/Overlays/AppWindowShell.swift} +0 -0
- /package/app/Sources/{CommandModeWindow.swift → Core/Overlays/CommandMode/CommandModeWindow.swift} +0 -0
- /package/app/Sources/{CommandPaletteView.swift → Core/Overlays/CommandPalette/CommandPaletteView.swift} +0 -0
- /package/app/Sources/{HUDBottomBar.swift → Core/Overlays/HUD/HUDBottomBar.swift} +0 -0
- /package/app/Sources/{HUDController.swift → Core/Overlays/HUD/HUDController.swift} +0 -0
- /package/app/Sources/{HUDLeftBar.swift → Core/Overlays/HUD/HUDLeftBar.swift} +0 -0
- /package/app/Sources/{HUDMinimap.swift → Core/Overlays/HUD/HUDMinimap.swift} +0 -0
- /package/app/Sources/{HUDState.swift → Core/Overlays/HUD/HUDState.swift} +0 -0
- /package/app/Sources/{HUDTopBar.swift → Core/Overlays/HUD/HUDTopBar.swift} +0 -0
- /package/app/Sources/{LayerBezel.swift → Core/Overlays/HUD/LayerBezel.swift} +0 -0
- /package/app/Sources/{OmniSearchState.swift → Core/Overlays/OmniSearch/OmniSearchState.swift} +0 -0
- /package/app/Sources/{DiagnosticLog.swift → Core/System/DiagnosticLog.swift} +0 -0
- /package/app/Sources/{EventBus.swift → Core/System/EventBus.swift} +0 -0
- /package/app/Sources/{ProcessModel.swift → Core/System/ProcessModel.swift} +0 -0
- /package/app/Sources/{ProcessQuery.swift → Core/System/ProcessQuery.swift} +0 -0
- /package/app/Sources/{AdvisorLearningStore.swift → Core/Voice/AdvisorLearningStore.swift} +0 -0
- /package/app/Sources/{AgentSession.swift → Core/Voice/AgentSession.swift} +0 -0
- /package/app/Sources/{AudioProvider.swift → Core/Voice/AudioProvider.swift} +0 -0
- /package/app/Sources/{VoiceChatView.swift → Core/Voice/VoiceChatView.swift} +0 -0
- /package/app/Sources/{VoxClient.swift → Core/Voice/VoxClient.swift} +0 -0
- /package/app/Sources/{Project.swift → Core/Workspace/Project.swift} +0 -0
- /package/app/Sources/{ProjectScanner.swift → Core/Workspace/ProjectScanner.swift} +0 -0
- /package/app/Sources/{SessionLayerStore.swift → Core/Workspace/SessionLayerStore.swift} +0 -0
- /package/app/Sources/{SessionManager.swift → Core/Workspace/SessionManager.swift} +0 -0
- /package/app/Sources/{Terminal.swift → Core/Workspace/Terminal/Terminal.swift} +0 -0
- /package/app/Sources/{TerminalQuery.swift → Core/Workspace/Terminal/TerminalQuery.swift} +0 -0
- /package/app/Sources/{TerminalSynthesizer.swift → Core/Workspace/Terminal/TerminalSynthesizer.swift} +0 -0
- /package/app/Sources/{TmuxModel.swift → Core/Workspace/Tmux/TmuxModel.swift} +0 -0
- /package/app/Sources/{TmuxQuery.swift → Core/Workspace/Tmux/TmuxQuery.swift} +0 -0
- /package/app/Sources/{ActionRow.swift → UI/ActionRow.swift} +0 -0
- /package/app/Sources/{OrphanRow.swift → UI/OrphanRow.swift} +0 -0
- /package/app/Sources/{ProjectRow.swift → UI/ProjectRow.swift} +0 -0
- /package/app/Sources/{TabGroupRow.swift → UI/TabGroupRow.swift} +0 -0
- /package/app/Sources/{Theme.swift → UI/Theme.swift} +0 -0
|
@@ -0,0 +1,1463 @@
|
|
|
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
|
+
}
|