@lattices/cli 0.4.14 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -7
- package/apps/mac/Info.plist +4 -4
- package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
- package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
- package/bin/lattices-app.ts +110 -17
- package/bin/lattices-build +125 -0
- package/bin/lattices-dev +89 -16
- package/bin/lattices.ts +977 -16
- package/docs/agents.md +81 -4
- package/docs/ai-chat-ux-review.md +416 -0
- package/docs/api.md +135 -3
- package/docs/app.md +30 -8
- package/docs/config.md +4 -0
- package/docs/mouse-gestures.md +60 -1
- package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
- package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
- package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
- package/docs/proposals/LAT-007-unified-app-shell.md +128 -0
- package/docs/reference/dewey.config.ts +2 -2
- package/docs/release.md +171 -0
- package/docs/repo-structure.md +5 -5
- package/docs/voice.md +11 -27
- package/package.json +11 -10
- package/apps/mac/Package.swift +0 -27
- package/apps/mac/Sources/AppShell/App.swift +0 -26
- package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
- package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
- package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
- package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
- package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
- package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
- package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
- package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
- package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
- package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
- package/apps/mac/Sources/AppShell/MainView.swift +0 -847
- package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
- package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
- package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
- package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
- package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
- package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
- package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
- package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
- package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
- package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
- package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
- package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
- package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
- package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
- package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
- package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
- package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
- package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
- package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
- package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
- package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
- package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
- package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
- package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
- package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
- package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
- package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
- package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
- package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
- package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
- package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
- package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
- package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
- package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
- package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
- package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
- package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
- package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
- package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
- package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
- package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
- package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
- package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
- package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
- package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
- package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
- package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
- package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
- package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
- package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
- package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
- package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
- package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
- package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
- package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
- package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
- package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
- package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
- package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
- package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
- package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
- package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
- package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
- package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
- package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
- package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
- package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
- package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
- package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
- package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
- package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
- package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
- package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
- package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
- package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
- package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
- package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
- package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
- package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
- package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
- package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
- package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
- package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
- package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
- package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
- package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
- package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
- package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
- package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
- package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
- package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
- package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
- package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
- package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
- package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
- package/apps/mac/Sources/Core/System/Capability.swift +0 -79
- package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
- package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
- package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
- package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
- package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
- package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
- package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
- package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
- package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
- package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
- package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
- package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
- package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
- package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
- package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
- package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
- package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
- package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
- package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
- package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
- package/apps/mac/Sources/UI/ActionRow.swift +0 -78
- package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
- package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
- package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
- package/apps/mac/Sources/UI/Theme.swift +0 -164
- package/apps/mac/Tests/StageDragTests.swift +0 -333
- package/apps/mac/Tests/StageJoinTests.swift +0 -313
- package/apps/mac/Tests/StageManagerTests.swift +0 -280
- package/apps/mac/Tests/StageTileTests.swift +0 -353
- package/swift/Package.swift +0 -20
- package/swift/Sources/DeckKit/DeckAction.swift +0 -51
- package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
- package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
- package/swift/Sources/DeckKit/DeckHost.swift +0 -7
- package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
- package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
- package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
- package/swift/Sources/DeckKit/DeckValue.swift +0 -93
- package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
- package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
|
@@ -1,2965 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import ApplicationServices
|
|
3
|
-
import DeckKit
|
|
4
|
-
import Foundation
|
|
5
|
-
|
|
6
|
-
// MARK: - Registry Types
|
|
7
|
-
|
|
8
|
-
enum Access: String, Codable {
|
|
9
|
-
case read, mutate
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
struct Param {
|
|
13
|
-
let name: String
|
|
14
|
-
let type: String // "string", "int", "uint32", "bool"
|
|
15
|
-
let required: Bool
|
|
16
|
-
let description: String
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
enum ReturnShape {
|
|
20
|
-
case array(model: String)
|
|
21
|
-
case object(model: String)
|
|
22
|
-
case ok
|
|
23
|
-
case custom(String)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
struct Endpoint {
|
|
27
|
-
let method: String
|
|
28
|
-
let description: String
|
|
29
|
-
let access: Access
|
|
30
|
-
let params: [Param]
|
|
31
|
-
let returns: ReturnShape
|
|
32
|
-
let handler: (JSON?) throws -> JSON
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
struct Field {
|
|
36
|
-
let name: String
|
|
37
|
-
let type: String // "string", "int", "double", "bool", "[Model]", "Model?"
|
|
38
|
-
let required: Bool
|
|
39
|
-
let description: String
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
struct ApiModel {
|
|
43
|
-
let name: String
|
|
44
|
-
let fields: [Field]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// MARK: - Central Registry
|
|
48
|
-
|
|
49
|
-
final class LatticesApi {
|
|
50
|
-
static let shared = LatticesApi()
|
|
51
|
-
|
|
52
|
-
private(set) var endpoints: [String: Endpoint] = [:]
|
|
53
|
-
private(set) var models: [String: ApiModel] = [:]
|
|
54
|
-
private var endpointOrder: [String] = []
|
|
55
|
-
private var modelOrder: [String] = []
|
|
56
|
-
|
|
57
|
-
private let startTime = Date()
|
|
58
|
-
|
|
59
|
-
func register(_ endpoint: Endpoint) {
|
|
60
|
-
endpoints[endpoint.method] = endpoint
|
|
61
|
-
if !endpointOrder.contains(endpoint.method) {
|
|
62
|
-
endpointOrder.append(endpoint.method)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
func model(_ model: ApiModel) {
|
|
67
|
-
models[model.name] = model
|
|
68
|
-
if !modelOrder.contains(model.name) {
|
|
69
|
-
modelOrder.append(model.name)
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
func dispatch(method: String, params: JSON?) throws -> JSON {
|
|
74
|
-
guard let endpoint = endpoints[method] else {
|
|
75
|
-
throw RouterError.unknownMethod(method)
|
|
76
|
-
}
|
|
77
|
-
return try endpoint.handler(params)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
func handle(_ request: DaemonRequest) -> DaemonResponse {
|
|
81
|
-
do {
|
|
82
|
-
let result = try dispatch(method: request.method, params: request.params)
|
|
83
|
-
return DaemonResponse(id: request.id, result: result, error: nil)
|
|
84
|
-
} catch {
|
|
85
|
-
return DaemonResponse(id: request.id, result: nil, error: error.localizedDescription)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func schema() -> JSON {
|
|
90
|
-
let modelsList: [JSON] = modelOrder.compactMap { name in
|
|
91
|
-
guard let m = models[name] else { return nil }
|
|
92
|
-
return .object([
|
|
93
|
-
"name": .string(m.name),
|
|
94
|
-
"fields": .array(m.fields.map { f in
|
|
95
|
-
.object([
|
|
96
|
-
"name": .string(f.name),
|
|
97
|
-
"type": .string(f.type),
|
|
98
|
-
"required": .bool(f.required),
|
|
99
|
-
"description": .string(f.description)
|
|
100
|
-
])
|
|
101
|
-
})
|
|
102
|
-
])
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let methodsList: [JSON] = endpointOrder.compactMap { name in
|
|
106
|
-
guard let ep = endpoints[name] else { return nil }
|
|
107
|
-
|
|
108
|
-
let returnsJson: JSON
|
|
109
|
-
switch ep.returns {
|
|
110
|
-
case .array(let model):
|
|
111
|
-
returnsJson = .object(["type": .string("array"), "model": .string(model)])
|
|
112
|
-
case .object(let model):
|
|
113
|
-
returnsJson = .object(["type": .string("object"), "model": .string(model)])
|
|
114
|
-
case .ok:
|
|
115
|
-
returnsJson = .object(["type": .string("ok")])
|
|
116
|
-
case .custom(let desc):
|
|
117
|
-
returnsJson = .object(["type": .string("custom"), "description": .string(desc)])
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return .object([
|
|
121
|
-
"method": .string(ep.method),
|
|
122
|
-
"description": .string(ep.description),
|
|
123
|
-
"access": .string(ep.access.rawValue),
|
|
124
|
-
"params": .array(ep.params.map { p in
|
|
125
|
-
.object([
|
|
126
|
-
"name": .string(p.name),
|
|
127
|
-
"type": .string(p.type),
|
|
128
|
-
"required": .bool(p.required),
|
|
129
|
-
"description": .string(p.description)
|
|
130
|
-
])
|
|
131
|
-
}),
|
|
132
|
-
"returns": returnsJson
|
|
133
|
-
])
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return .object([
|
|
137
|
-
"version": .string("1.0"),
|
|
138
|
-
"models": .array(modelsList),
|
|
139
|
-
"methods": .array(methodsList)
|
|
140
|
-
])
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// MARK: - Setup
|
|
144
|
-
|
|
145
|
-
static func setup() {
|
|
146
|
-
let api = LatticesApi.shared
|
|
147
|
-
|
|
148
|
-
// ── Models ──────────────────────────────────────────────
|
|
149
|
-
|
|
150
|
-
api.model(ApiModel(name: "Window", fields: [
|
|
151
|
-
Field(name: "wid", type: "int", required: true, description: "CGWindowID"),
|
|
152
|
-
Field(name: "app", type: "string", required: true, description: "Application name"),
|
|
153
|
-
Field(name: "pid", type: "int", required: true, description: "Process ID"),
|
|
154
|
-
Field(name: "title", type: "string", required: true, description: "Window title"),
|
|
155
|
-
Field(name: "frame", type: "Frame", required: true, description: "Window frame {x, y, w, h}"),
|
|
156
|
-
Field(name: "spaceIds", type: "[int]", required: true, description: "Space IDs the window is on"),
|
|
157
|
-
Field(name: "isOnScreen", type: "bool", required: true, description: "Whether window is currently visible"),
|
|
158
|
-
Field(name: "latticesSession", type: "string", required: false, description: "Associated lattices session name"),
|
|
159
|
-
]))
|
|
160
|
-
|
|
161
|
-
api.model(ApiModel(name: "TmuxSession", fields: [
|
|
162
|
-
Field(name: "name", type: "string", required: true, description: "Session name"),
|
|
163
|
-
Field(name: "windowCount", type: "int", required: true, description: "Number of tmux windows"),
|
|
164
|
-
Field(name: "attached", type: "bool", required: true, description: "Whether a client is attached"),
|
|
165
|
-
Field(name: "panes", type: "[TmuxPane]", required: true, description: "Panes in this session"),
|
|
166
|
-
]))
|
|
167
|
-
|
|
168
|
-
api.model(ApiModel(name: "TmuxPane", fields: [
|
|
169
|
-
Field(name: "id", type: "string", required: true, description: "Pane ID (e.g. %0)"),
|
|
170
|
-
Field(name: "windowIndex", type: "int", required: true, description: "Tmux window index"),
|
|
171
|
-
Field(name: "windowName", type: "string", required: true, description: "Tmux window name"),
|
|
172
|
-
Field(name: "title", type: "string", required: true, description: "Pane title"),
|
|
173
|
-
Field(name: "currentCommand", type: "string", required: true, description: "Currently running command"),
|
|
174
|
-
Field(name: "pid", type: "int", required: true, description: "Process ID of the pane"),
|
|
175
|
-
Field(name: "isActive", type: "bool", required: true, description: "Whether this pane is active"),
|
|
176
|
-
Field(name: "children", type: "[PaneChild]", required: false, description: "Interesting child processes in this pane"),
|
|
177
|
-
]))
|
|
178
|
-
|
|
179
|
-
api.model(ApiModel(name: "Project", fields: [
|
|
180
|
-
Field(name: "path", type: "string", required: true, description: "Absolute path to project"),
|
|
181
|
-
Field(name: "name", type: "string", required: true, description: "Project display name"),
|
|
182
|
-
Field(name: "sessionName", type: "string", required: true, description: "Tmux session name"),
|
|
183
|
-
Field(name: "isRunning", type: "bool", required: true, description: "Whether the session is active"),
|
|
184
|
-
Field(name: "hasConfig", type: "bool", required: true, description: "Whether .lattices.json exists"),
|
|
185
|
-
Field(name: "paneCount", type: "int", required: true, description: "Number of configured panes"),
|
|
186
|
-
Field(name: "paneNames", type: "[string]", required: true, description: "Names of configured panes"),
|
|
187
|
-
Field(name: "devCommand", type: "string", required: false, description: "Dev command if detected"),
|
|
188
|
-
Field(name: "packageManager", type: "string", required: false, description: "Detected package manager"),
|
|
189
|
-
]))
|
|
190
|
-
|
|
191
|
-
api.model(ApiModel(name: "Display", fields: [
|
|
192
|
-
Field(name: "displayIndex", type: "int", required: true, description: "Display index"),
|
|
193
|
-
Field(name: "displayId", type: "string", required: true, description: "Display identifier"),
|
|
194
|
-
Field(name: "currentSpaceId", type: "int", required: true, description: "Currently active space ID"),
|
|
195
|
-
Field(name: "spaces", type: "[Space]", required: true, description: "Spaces on this display"),
|
|
196
|
-
]))
|
|
197
|
-
|
|
198
|
-
api.model(ApiModel(name: "Space", fields: [
|
|
199
|
-
Field(name: "id", type: "int", required: true, description: "Space ID"),
|
|
200
|
-
Field(name: "index", type: "int", required: true, description: "Space index"),
|
|
201
|
-
Field(name: "name", type: "string", required: true, description: "Lattices display name for the space"),
|
|
202
|
-
Field(name: "display", type: "int", required: true, description: "Display index"),
|
|
203
|
-
Field(name: "isCurrent", type: "bool", required: true, description: "Whether this is the active space"),
|
|
204
|
-
]))
|
|
205
|
-
|
|
206
|
-
api.model(ApiModel(name: "Layer", fields: [
|
|
207
|
-
Field(name: "id", type: "string", required: true, description: "Layer identifier"),
|
|
208
|
-
Field(name: "label", type: "string", required: true, description: "Layer display label"),
|
|
209
|
-
Field(name: "index", type: "int", required: true, description: "Layer index"),
|
|
210
|
-
Field(name: "projectCount", type: "int", required: true, description: "Number of projects in layer"),
|
|
211
|
-
]))
|
|
212
|
-
|
|
213
|
-
api.model(ApiModel(name: "Process", fields: [
|
|
214
|
-
Field(name: "pid", type: "int", required: true, description: "Process ID"),
|
|
215
|
-
Field(name: "ppid", type: "int", required: true, description: "Parent process ID"),
|
|
216
|
-
Field(name: "command", type: "string", required: true, description: "Command basename (e.g. node, claude)"),
|
|
217
|
-
Field(name: "args", type: "string", required: true, description: "Full command line"),
|
|
218
|
-
Field(name: "cwd", type: "string", required: false, description: "Working directory"),
|
|
219
|
-
Field(name: "tty", type: "string", required: true, description: "Controlling TTY"),
|
|
220
|
-
Field(name: "tmuxSession", type: "string", required: false, description: "Linked tmux session name"),
|
|
221
|
-
Field(name: "tmuxPaneId", type: "string", required: false, description: "Linked tmux pane ID"),
|
|
222
|
-
Field(name: "windowId", type: "int", required: false, description: "Linked macOS window ID"),
|
|
223
|
-
]))
|
|
224
|
-
|
|
225
|
-
api.model(ApiModel(name: "PaneChild", fields: [
|
|
226
|
-
Field(name: "pid", type: "int", required: true, description: "Process ID"),
|
|
227
|
-
Field(name: "command", type: "string", required: true, description: "Command basename"),
|
|
228
|
-
Field(name: "args", type: "string", required: true, description: "Full command line"),
|
|
229
|
-
Field(name: "cwd", type: "string", required: false, description: "Working directory"),
|
|
230
|
-
]))
|
|
231
|
-
|
|
232
|
-
api.model(ApiModel(name: "TerminalInstance", fields: [
|
|
233
|
-
Field(name: "tty", type: "string", required: true, description: "Controlling TTY (universal join key)"),
|
|
234
|
-
Field(name: "app", type: "string", required: false, description: "Terminal emulator name (iTerm2, Terminal, etc.)"),
|
|
235
|
-
Field(name: "windowIndex", type: "int", required: false, description: "Terminal window index"),
|
|
236
|
-
Field(name: "tabIndex", type: "int", required: false, description: "Tab index within the window"),
|
|
237
|
-
Field(name: "isActiveTab", type: "bool", required: true, description: "Whether this is the selected tab"),
|
|
238
|
-
Field(name: "tabTitle", type: "string", required: false, description: "Tab title from the terminal emulator"),
|
|
239
|
-
Field(name: "terminalSessionId", type: "string", required: false, description: "Terminal-specific session ID (iTerm2 unique ID)"),
|
|
240
|
-
Field(name: "processes", type: "[Process]", required: true, description: "Interesting processes on this TTY"),
|
|
241
|
-
Field(name: "shellPid", type: "int", required: false, description: "Root shell PID for this TTY"),
|
|
242
|
-
Field(name: "cwd", type: "string", required: false, description: "Working directory (from deepest interesting process)"),
|
|
243
|
-
Field(name: "tmuxSession", type: "string", required: false, description: "Linked tmux session name"),
|
|
244
|
-
Field(name: "tmuxPaneId", type: "string", required: false, description: "Linked tmux pane ID"),
|
|
245
|
-
Field(name: "windowId", type: "int", required: false, description: "Linked macOS window ID (CGWindowID)"),
|
|
246
|
-
Field(name: "windowTitle", type: "string", required: false, description: "macOS window title"),
|
|
247
|
-
Field(name: "hasClaude", type: "bool", required: true, description: "Whether a claude process is running on this TTY"),
|
|
248
|
-
Field(name: "displayName", type: "string", required: true, description: "Best display name (session > tab title > tty)"),
|
|
249
|
-
]))
|
|
250
|
-
|
|
251
|
-
api.model(ApiModel(name: "OcrResult", fields: [
|
|
252
|
-
Field(name: "wid", type: "int", required: true, description: "Window ID"),
|
|
253
|
-
Field(name: "app", type: "string", required: true, description: "Application name"),
|
|
254
|
-
Field(name: "title", type: "string", required: true, description: "Window title"),
|
|
255
|
-
Field(name: "frame", type: "Frame", required: true, description: "Window frame"),
|
|
256
|
-
Field(name: "fullText", type: "string", required: true, description: "All recognized text"),
|
|
257
|
-
Field(name: "blocks", type: "[OcrBlock]", required: true, description: "Individual text blocks with position/confidence"),
|
|
258
|
-
Field(name: "timestamp", type: "double", required: true, description: "Scan timestamp (Unix)"),
|
|
259
|
-
]))
|
|
260
|
-
|
|
261
|
-
api.model(ApiModel(name: "OcrBlock", fields: [
|
|
262
|
-
Field(name: "text", type: "string", required: true, description: "Recognized text"),
|
|
263
|
-
Field(name: "confidence", type: "double", required: true, description: "Recognition confidence 0-1"),
|
|
264
|
-
Field(name: "x", type: "double", required: true, description: "Normalized bounding box x"),
|
|
265
|
-
Field(name: "y", type: "double", required: true, description: "Normalized bounding box y"),
|
|
266
|
-
Field(name: "w", type: "double", required: true, description: "Normalized bounding box width"),
|
|
267
|
-
Field(name: "h", type: "double", required: true, description: "Normalized bounding box height"),
|
|
268
|
-
]))
|
|
269
|
-
|
|
270
|
-
api.model(ApiModel(name: "OcrSearchResult", fields: [
|
|
271
|
-
Field(name: "id", type: "int", required: true, description: "Database row ID"),
|
|
272
|
-
Field(name: "wid", type: "int", required: true, description: "Window ID"),
|
|
273
|
-
Field(name: "app", type: "string", required: true, description: "Application name"),
|
|
274
|
-
Field(name: "title", type: "string", required: true, description: "Window title"),
|
|
275
|
-
Field(name: "frame", type: "Frame", required: true, description: "Window frame at scan time"),
|
|
276
|
-
Field(name: "fullText", type: "string", required: true, description: "Full recognized text"),
|
|
277
|
-
Field(name: "snippet", type: "string", required: true, description: "Highlighted snippet (FTS5)"),
|
|
278
|
-
Field(name: "timestamp", type: "double", required: true, description: "Scan timestamp (Unix)"),
|
|
279
|
-
Field(name: "source", type: "string", required: true, description: "Text source: 'accessibility' or 'ocr'"),
|
|
280
|
-
]))
|
|
281
|
-
|
|
282
|
-
api.model(ApiModel(name: "DaemonStatus", fields: [
|
|
283
|
-
Field(name: "uptime", type: "double", required: true, description: "Seconds since daemon started"),
|
|
284
|
-
Field(name: "clientCount", type: "int", required: true, description: "Connected WebSocket clients"),
|
|
285
|
-
Field(name: "version", type: "string", required: true, description: "Daemon version"),
|
|
286
|
-
Field(name: "windowCount", type: "int", required: true, description: "Tracked window count"),
|
|
287
|
-
Field(name: "tmuxSessionCount", type: "int", required: true, description: "Active tmux session count"),
|
|
288
|
-
]))
|
|
289
|
-
|
|
290
|
-
api.model(ApiModel(name: "OverlayLayer", fields: [
|
|
291
|
-
Field(name: "id", type: "string", required: true, description: "Overlay layer identifier"),
|
|
292
|
-
Field(name: "kind", type: "string", required: true, description: "Overlay kind: toast, label, highlight, pet"),
|
|
293
|
-
Field(name: "owner", type: "string", required: true, description: "Layer owner namespace"),
|
|
294
|
-
Field(name: "expiresAt", type: "double", required: false, description: "Expiration timestamp (Unix seconds)"),
|
|
295
|
-
]))
|
|
296
|
-
|
|
297
|
-
// ── Endpoints: Read ─────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
api.register(Endpoint(
|
|
300
|
-
method: "windows.list",
|
|
301
|
-
description: "List all windows known to the system",
|
|
302
|
-
access: .read,
|
|
303
|
-
params: [],
|
|
304
|
-
returns: .array(model: "Window"),
|
|
305
|
-
handler: { _ in
|
|
306
|
-
let entries = DesktopModel.shared.allWindows()
|
|
307
|
-
return .array(entries.map { Encoders.window($0) })
|
|
308
|
-
}
|
|
309
|
-
))
|
|
310
|
-
|
|
311
|
-
api.register(Endpoint(
|
|
312
|
-
method: "windows.get",
|
|
313
|
-
description: "Get a single window by ID",
|
|
314
|
-
access: .read,
|
|
315
|
-
params: [Param(name: "wid", type: "uint32", required: true, description: "Window ID")],
|
|
316
|
-
returns: .object(model: "Window"),
|
|
317
|
-
handler: { params in
|
|
318
|
-
guard let wid = params?["wid"]?.uint32Value else {
|
|
319
|
-
throw RouterError.missingParam("wid")
|
|
320
|
-
}
|
|
321
|
-
guard let entry = DesktopModel.shared.windows[wid] else {
|
|
322
|
-
throw RouterError.notFound("window \(wid)")
|
|
323
|
-
}
|
|
324
|
-
return Encoders.window(entry)
|
|
325
|
-
}
|
|
326
|
-
))
|
|
327
|
-
|
|
328
|
-
api.register(Endpoint(
|
|
329
|
-
method: "windows.search",
|
|
330
|
-
description: "Search windows by title, app, and OCR content",
|
|
331
|
-
access: .read,
|
|
332
|
-
params: [
|
|
333
|
-
Param(name: "query", type: "string", required: true, description: "Search text"),
|
|
334
|
-
Param(name: "ocr", type: "bool", required: false, description: "Include OCR content (default true)"),
|
|
335
|
-
Param(name: "limit", type: "int", required: false, description: "Max results (default 50)"),
|
|
336
|
-
],
|
|
337
|
-
returns: .array(model: "Window"),
|
|
338
|
-
handler: { params in
|
|
339
|
-
guard let query = params?["query"]?.stringValue?.lowercased(), !query.isEmpty else {
|
|
340
|
-
throw RouterError.missingParam("query")
|
|
341
|
-
}
|
|
342
|
-
let includeOcr = params?["ocr"]?.boolValue ?? true
|
|
343
|
-
let limit = params?["limit"]?.intValue ?? 50
|
|
344
|
-
let ocrResults = OcrModel.shared.results
|
|
345
|
-
|
|
346
|
-
var matches: [JSON] = []
|
|
347
|
-
for entry in DesktopModel.shared.allWindows() {
|
|
348
|
-
let matchesApp = entry.app.lowercased().contains(query)
|
|
349
|
-
let matchesTitle = entry.title.lowercased().contains(query)
|
|
350
|
-
let matchesSession = entry.latticesSession?.lowercased().contains(query) ?? false
|
|
351
|
-
let ocrText = includeOcr ? ocrResults[entry.wid]?.fullText : nil
|
|
352
|
-
let matchesOcrContent = ocrText?.lowercased().contains(query) ?? false
|
|
353
|
-
|
|
354
|
-
if matchesApp || matchesTitle || matchesSession || matchesOcrContent {
|
|
355
|
-
var obj = Encoders.window(entry)
|
|
356
|
-
if matchesOcrContent, let text = ocrText,
|
|
357
|
-
let range = text.lowercased().range(of: query) {
|
|
358
|
-
// Extract snippet around match
|
|
359
|
-
let half = max(0, (80 - text.distance(from: range.lowerBound, to: range.upperBound)) / 2)
|
|
360
|
-
let start = text.index(range.lowerBound, offsetBy: -half, limitedBy: text.startIndex) ?? text.startIndex
|
|
361
|
-
let end = text.index(range.upperBound, offsetBy: half, limitedBy: text.endIndex) ?? text.endIndex
|
|
362
|
-
var snippet = String(text[start..<end])
|
|
363
|
-
.replacingOccurrences(of: "\n", with: " ")
|
|
364
|
-
.trimmingCharacters(in: .whitespaces)
|
|
365
|
-
if start > text.startIndex { snippet = "…" + snippet }
|
|
366
|
-
if end < text.endIndex { snippet += "…" }
|
|
367
|
-
if case .object(var dict) = obj {
|
|
368
|
-
dict["ocrSnippet"] = .string(snippet)
|
|
369
|
-
dict["matchSource"] = .string("ocr")
|
|
370
|
-
obj = .object(dict)
|
|
371
|
-
}
|
|
372
|
-
} else if case .object(var dict) = obj {
|
|
373
|
-
let source = matchesTitle ? "title" : matchesApp ? "app" : "session"
|
|
374
|
-
dict["matchSource"] = .string(source)
|
|
375
|
-
obj = .object(dict)
|
|
376
|
-
}
|
|
377
|
-
matches.append(obj)
|
|
378
|
-
if matches.count >= limit { break }
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return .array(matches)
|
|
382
|
-
}
|
|
383
|
-
))
|
|
384
|
-
|
|
385
|
-
// MARK: - Unified Search
|
|
386
|
-
|
|
387
|
-
api.register(Endpoint(
|
|
388
|
-
method: "lattices.search",
|
|
389
|
-
description: "Unified search across windows, terminals, and OCR. Single entry point for all search surfaces.",
|
|
390
|
-
access: .read,
|
|
391
|
-
params: [
|
|
392
|
-
Param(name: "query", type: "string", required: true, description: "Search text"),
|
|
393
|
-
Param(name: "sources", type: "array<string>", required: false, description: "Data sources to include: titles, apps, sessions, cwd, tabs, tmux, ocr, processes. Omit for smart default (everything except ocr). Use ['all'] for everything."),
|
|
394
|
-
Param(name: "after", type: "string", required: false, description: "ISO8601 timestamp — only windows interacted with after this time"),
|
|
395
|
-
Param(name: "before", type: "string", required: false, description: "ISO8601 timestamp — only windows interacted with before this time"),
|
|
396
|
-
Param(name: "recency", type: "bool", required: false, description: "Boost score for recently-focused windows (default true)"),
|
|
397
|
-
Param(name: "limit", type: "int", required: false, description: "Max results (default 20)"),
|
|
398
|
-
// Legacy compat
|
|
399
|
-
Param(name: "mode", type: "string", required: false, description: "Legacy: 'quick', 'complete', 'terminal'. Mapped to sources internally."),
|
|
400
|
-
],
|
|
401
|
-
returns: .array(model: "SearchResult"),
|
|
402
|
-
handler: { params in
|
|
403
|
-
guard let query = params?["query"]?.stringValue?.lowercased(), !query.isEmpty else {
|
|
404
|
-
throw RouterError.missingParam("query")
|
|
405
|
-
}
|
|
406
|
-
let limit = params?["limit"]?.intValue ?? 20
|
|
407
|
-
let useRecency = params?["recency"]?.boolValue ?? true
|
|
408
|
-
|
|
409
|
-
// ── Resolve sources ──
|
|
410
|
-
|
|
411
|
-
// All available source names
|
|
412
|
-
let allSources: Set<String> = ["titles", "apps", "sessions", "cwd", "tabs", "tmux", "ocr", "processes"]
|
|
413
|
-
// Smart default: everything except OCR (fast)
|
|
414
|
-
let defaultSources: Set<String> = ["titles", "apps", "sessions", "cwd", "tabs", "tmux"]
|
|
415
|
-
|
|
416
|
-
var sources: Set<String>
|
|
417
|
-
if let arr = params?["sources"]?.arrayValue {
|
|
418
|
-
let names = arr.compactMap(\.stringValue)
|
|
419
|
-
if names.contains("all") {
|
|
420
|
-
sources = allSources
|
|
421
|
-
} else if names.contains("terminals") {
|
|
422
|
-
// Shorthand expansion
|
|
423
|
-
sources = Set(names).subtracting(["terminals"]).union(["cwd", "tabs", "tmux", "processes"])
|
|
424
|
-
} else {
|
|
425
|
-
sources = Set(names).intersection(allSources)
|
|
426
|
-
if sources.isEmpty { sources = defaultSources }
|
|
427
|
-
}
|
|
428
|
-
} else if let mode = params?["mode"]?.stringValue {
|
|
429
|
-
// Legacy mode param → sources mapping
|
|
430
|
-
switch mode {
|
|
431
|
-
case "quick": sources = ["titles", "apps", "sessions"]
|
|
432
|
-
case "terminal": sources = ["cwd", "tabs", "tmux", "processes"]
|
|
433
|
-
default: sources = allSources // "complete"
|
|
434
|
-
}
|
|
435
|
-
} else {
|
|
436
|
-
sources = defaultSources
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
let includeWindowIndex = !sources.isDisjoint(with: ["titles", "apps", "sessions"])
|
|
440
|
-
let includeOcr = sources.contains("ocr")
|
|
441
|
-
let includeTerminals = !sources.isDisjoint(with: ["cwd", "tabs", "tmux", "processes"])
|
|
442
|
-
|
|
443
|
-
// ── Resolve time filters ──
|
|
444
|
-
|
|
445
|
-
let isoFormatter = ISO8601DateFormatter()
|
|
446
|
-
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
447
|
-
let isoFormatterNoFrac = ISO8601DateFormatter()
|
|
448
|
-
|
|
449
|
-
func parseDate(_ str: String?) -> Date? {
|
|
450
|
-
guard let str else { return nil }
|
|
451
|
-
return isoFormatter.date(from: str) ?? isoFormatterNoFrac.date(from: str)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
let afterDate = parseDate(params?["after"]?.stringValue)
|
|
455
|
-
let beforeDate = parseDate(params?["before"]?.stringValue)
|
|
456
|
-
|
|
457
|
-
// Default time window: 2 days (only applies when no explicit time filters given)
|
|
458
|
-
let defaultCutoff = (afterDate == nil && beforeDate == nil)
|
|
459
|
-
? Date().addingTimeInterval(-2 * 24 * 3600)
|
|
460
|
-
: nil
|
|
461
|
-
|
|
462
|
-
let now = Date()
|
|
463
|
-
let desktop = DesktopModel.shared
|
|
464
|
-
|
|
465
|
-
/// Check if a window passes the time filter.
|
|
466
|
-
/// Windows with no interaction date are included if they're currently on screen (live windows).
|
|
467
|
-
func passesTimeFilter(wid: UInt32, entry: WindowEntry) -> Bool {
|
|
468
|
-
if let interacted = desktop.lastInteractionDate(for: wid) {
|
|
469
|
-
if let after = afterDate, interacted < after { return false }
|
|
470
|
-
if let before = beforeDate, interacted > before { return false }
|
|
471
|
-
if let cutoff = defaultCutoff, interacted < cutoff { return false }
|
|
472
|
-
return true
|
|
473
|
-
}
|
|
474
|
-
// No interaction date — include if currently visible (it's a live window we just haven't tracked yet)
|
|
475
|
-
return entry.isOnScreen
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/// Recency boost: windows focused recently score higher.
|
|
479
|
-
/// Frontmost (zIndex 0) gets +4, last 5 min +3, last hour +2, last day +1.
|
|
480
|
-
func recencyBoost(wid: UInt32, zIndex: Int) -> Int {
|
|
481
|
-
guard useRecency else { return 0 }
|
|
482
|
-
if zIndex == 0 { return 4 }
|
|
483
|
-
guard let last = desktop.lastInteractionDate(for: wid) else { return 0 }
|
|
484
|
-
let ago = now.timeIntervalSince(last)
|
|
485
|
-
if ago < 300 { return 3 } // 5 min
|
|
486
|
-
if ago < 3600 { return 2 } // 1 hour
|
|
487
|
-
if ago < 86400 { return 1 } // 1 day
|
|
488
|
-
return 0
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// ── Accumulator ──
|
|
492
|
-
|
|
493
|
-
struct Accum {
|
|
494
|
-
let entry: WindowEntry
|
|
495
|
-
var score: Int
|
|
496
|
-
var sources: [String]
|
|
497
|
-
var ocrSnippet: String?
|
|
498
|
-
var tabs: [JSON]
|
|
499
|
-
var lastInteraction: Date?
|
|
500
|
-
}
|
|
501
|
-
var byWid: [UInt32: Accum] = [:]
|
|
502
|
-
|
|
503
|
-
// ── Tier 1: Window index (title, app, session) ──
|
|
504
|
-
|
|
505
|
-
if includeWindowIndex {
|
|
506
|
-
let ocrResults = includeOcr ? OcrModel.shared.results : [:]
|
|
507
|
-
let checkTitles = sources.contains("titles")
|
|
508
|
-
let checkApps = sources.contains("apps")
|
|
509
|
-
let checkSessions = sources.contains("sessions")
|
|
510
|
-
|
|
511
|
-
for entry in desktop.allWindows() {
|
|
512
|
-
guard passesTimeFilter(wid: entry.wid, entry: entry) else { continue }
|
|
513
|
-
|
|
514
|
-
var score = 0
|
|
515
|
-
var matchSources: [String] = []
|
|
516
|
-
var ocrSnippet: String? = nil
|
|
517
|
-
|
|
518
|
-
if checkTitles && entry.title.lowercased().contains(query) { score += 3; matchSources.append("title") }
|
|
519
|
-
if checkApps && entry.app.lowercased().contains(query) { score += 2; matchSources.append("app") }
|
|
520
|
-
if checkSessions && entry.latticesSession?.lowercased().contains(query) == true { score += 3; matchSources.append("session") }
|
|
521
|
-
|
|
522
|
-
if includeOcr, let ocrResult = ocrResults[entry.wid] {
|
|
523
|
-
let text = ocrResult.fullText
|
|
524
|
-
if text.lowercased().contains(query) {
|
|
525
|
-
score += 1; matchSources.append("ocr")
|
|
526
|
-
if let range = text.lowercased().range(of: query) {
|
|
527
|
-
let half = max(0, (80 - text.distance(from: range.lowerBound, to: range.upperBound)) / 2)
|
|
528
|
-
let start = text.index(range.lowerBound, offsetBy: -half, limitedBy: text.startIndex) ?? text.startIndex
|
|
529
|
-
let end = text.index(range.upperBound, offsetBy: half, limitedBy: text.endIndex) ?? text.endIndex
|
|
530
|
-
var snippet = String(text[start..<end])
|
|
531
|
-
.replacingOccurrences(of: "\n", with: " ")
|
|
532
|
-
.trimmingCharacters(in: .whitespaces)
|
|
533
|
-
if start > text.startIndex { snippet = "…" + snippet }
|
|
534
|
-
if end < text.endIndex { snippet += "…" }
|
|
535
|
-
ocrSnippet = snippet
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
if score > 0 {
|
|
541
|
-
score += recencyBoost(wid: entry.wid, zIndex: entry.zIndex)
|
|
542
|
-
byWid[entry.wid] = Accum(
|
|
543
|
-
entry: entry, score: score, sources: matchSources,
|
|
544
|
-
ocrSnippet: ocrSnippet, tabs: [],
|
|
545
|
-
lastInteraction: desktop.lastInteractionDate(for: entry.wid)
|
|
546
|
-
)
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ── Tier 2: Terminal inspection (cwd, tab titles, tmux sessions, processes) ──
|
|
552
|
-
|
|
553
|
-
if includeTerminals {
|
|
554
|
-
let checkCwd = sources.contains("cwd")
|
|
555
|
-
let checkTabs = sources.contains("tabs")
|
|
556
|
-
let checkTmux = sources.contains("tmux")
|
|
557
|
-
let checkProcesses = sources.contains("processes")
|
|
558
|
-
|
|
559
|
-
let instances = ProcessModel.shared.synthesizeTerminals()
|
|
560
|
-
for inst in instances {
|
|
561
|
-
let cwdMatch = checkCwd && (inst.cwd?.lowercased().contains(query) ?? false)
|
|
562
|
-
let tabMatch = checkTabs && (inst.tabTitle?.lowercased().contains(query) ?? false)
|
|
563
|
-
let tmuxMatch = checkTmux && (inst.tmuxSession?.lowercased().contains(query) ?? false)
|
|
564
|
-
let processMatch = checkProcesses && inst.processes.contains {
|
|
565
|
-
$0.comm.lowercased().contains(query) || $0.args.lowercased().contains(query)
|
|
566
|
-
}
|
|
567
|
-
guard cwdMatch || tabMatch || tmuxMatch || processMatch else { continue }
|
|
568
|
-
|
|
569
|
-
var tab: [String: JSON] = [:]
|
|
570
|
-
if let idx = inst.tabIndex { tab["tabIndex"] = .int(idx) }
|
|
571
|
-
if let cwd = inst.cwd { tab["cwd"] = .string(cwd) }
|
|
572
|
-
if let title = inst.tabTitle { tab["tabTitle"] = .string(title) }
|
|
573
|
-
tab["hasClaude"] = .bool(inst.hasClaude)
|
|
574
|
-
if let session = inst.tmuxSession { tab["tmuxSession"] = .string(session) }
|
|
575
|
-
if processMatch {
|
|
576
|
-
let matched = inst.processes.filter {
|
|
577
|
-
$0.comm.lowercased().contains(query) || $0.args.lowercased().contains(query)
|
|
578
|
-
}
|
|
579
|
-
tab["matchedProcesses"] = .array(matched.map { .string($0.comm) })
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
let tabJson = JSON.object(tab)
|
|
583
|
-
var tabScore = 0
|
|
584
|
-
if cwdMatch { tabScore += 3 }
|
|
585
|
-
if tabMatch { tabScore += 2 }
|
|
586
|
-
if tmuxMatch { tabScore += 3 }
|
|
587
|
-
if processMatch { tabScore += 2 }
|
|
588
|
-
|
|
589
|
-
if let wid = inst.windowId {
|
|
590
|
-
if var acc = byWid[wid] {
|
|
591
|
-
acc.score += tabScore
|
|
592
|
-
if cwdMatch && !acc.sources.contains("cwd") { acc.sources.append("cwd") }
|
|
593
|
-
if tabMatch && !acc.sources.contains("tab") { acc.sources.append("tab") }
|
|
594
|
-
if tmuxMatch && !acc.sources.contains("tmux") { acc.sources.append("tmux") }
|
|
595
|
-
if processMatch && !acc.sources.contains("process") { acc.sources.append("process") }
|
|
596
|
-
acc.tabs.append(tabJson)
|
|
597
|
-
byWid[wid] = acc
|
|
598
|
-
} else if let entry = desktop.windows[wid] {
|
|
599
|
-
guard passesTimeFilter(wid: wid, entry: entry) else { continue }
|
|
600
|
-
var matchSources: [String] = []
|
|
601
|
-
if cwdMatch { matchSources.append("cwd") }
|
|
602
|
-
if tabMatch { matchSources.append("tab") }
|
|
603
|
-
if tmuxMatch { matchSources.append("tmux") }
|
|
604
|
-
if processMatch { matchSources.append("process") }
|
|
605
|
-
let score = tabScore + recencyBoost(wid: wid, zIndex: entry.zIndex)
|
|
606
|
-
byWid[wid] = Accum(
|
|
607
|
-
entry: entry, score: score, sources: matchSources,
|
|
608
|
-
ocrSnippet: nil, tabs: [tabJson],
|
|
609
|
-
lastInteraction: desktop.lastInteractionDate(for: wid)
|
|
610
|
-
)
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// ── Build results ──
|
|
617
|
-
|
|
618
|
-
let sorted = byWid.values.sorted { $0.score > $1.score }
|
|
619
|
-
return .array(Array(sorted.prefix(limit)).map { acc in
|
|
620
|
-
var obj = Encoders.window(acc.entry)
|
|
621
|
-
if case .object(var dict) = obj {
|
|
622
|
-
dict["score"] = .int(acc.score)
|
|
623
|
-
dict["matchSources"] = .array(acc.sources.map { .string($0) })
|
|
624
|
-
if let snippet = acc.ocrSnippet { dict["ocrSnippet"] = .string(snippet) }
|
|
625
|
-
if !acc.tabs.isEmpty { dict["terminalTabs"] = .array(acc.tabs) }
|
|
626
|
-
if let last = acc.lastInteraction {
|
|
627
|
-
dict["lastInteraction"] = .string(ISO8601DateFormatter().string(from: last))
|
|
628
|
-
}
|
|
629
|
-
obj = .object(dict)
|
|
630
|
-
}
|
|
631
|
-
return obj
|
|
632
|
-
})
|
|
633
|
-
}
|
|
634
|
-
))
|
|
635
|
-
|
|
636
|
-
// MARK: - Window Layer Tags
|
|
637
|
-
|
|
638
|
-
api.register(Endpoint(
|
|
639
|
-
method: "window.assignLayer",
|
|
640
|
-
description: "Tag a window with a layer id (in-memory only)",
|
|
641
|
-
access: .mutate,
|
|
642
|
-
params: [
|
|
643
|
-
Param(name: "wid", type: "uint32", required: true, description: "Window ID"),
|
|
644
|
-
Param(name: "layer", type: "string", required: true, description: "Layer id (e.g. 'lattices', 'vox')")
|
|
645
|
-
],
|
|
646
|
-
returns: .ok,
|
|
647
|
-
handler: { params in
|
|
648
|
-
guard let wid = params?["wid"]?.uint32Value else {
|
|
649
|
-
throw RouterError.missingParam("wid")
|
|
650
|
-
}
|
|
651
|
-
guard let layerId = params?["layer"]?.stringValue, !layerId.isEmpty else {
|
|
652
|
-
throw RouterError.missingParam("layer")
|
|
653
|
-
}
|
|
654
|
-
DesktopModel.shared.assignLayer(wid: wid, layerId: layerId)
|
|
655
|
-
return .object(["ok": .bool(true), "wid": .int(Int(wid)), "layer": .string(layerId)])
|
|
656
|
-
}
|
|
657
|
-
))
|
|
658
|
-
|
|
659
|
-
api.register(Endpoint(
|
|
660
|
-
method: "window.removeLayer",
|
|
661
|
-
description: "Remove layer tag from a window",
|
|
662
|
-
access: .mutate,
|
|
663
|
-
params: [
|
|
664
|
-
Param(name: "wid", type: "uint32", required: true, description: "Window ID")
|
|
665
|
-
],
|
|
666
|
-
returns: .ok,
|
|
667
|
-
handler: { params in
|
|
668
|
-
guard let wid = params?["wid"]?.uint32Value else {
|
|
669
|
-
throw RouterError.missingParam("wid")
|
|
670
|
-
}
|
|
671
|
-
DesktopModel.shared.removeLayerTag(wid: wid)
|
|
672
|
-
return .object(["ok": .bool(true)])
|
|
673
|
-
}
|
|
674
|
-
))
|
|
675
|
-
|
|
676
|
-
api.register(Endpoint(
|
|
677
|
-
method: "window.layerMap",
|
|
678
|
-
description: "Get all window-to-layer assignments",
|
|
679
|
-
access: .read,
|
|
680
|
-
params: [],
|
|
681
|
-
returns: .custom("{ [wid]: layerId }"),
|
|
682
|
-
handler: { _ in
|
|
683
|
-
let tags = DesktopModel.shared.windowLayerTags
|
|
684
|
-
var obj: [String: JSON] = [:]
|
|
685
|
-
for (wid, layerId) in tags {
|
|
686
|
-
obj[String(wid)] = .string(layerId)
|
|
687
|
-
}
|
|
688
|
-
return .object(obj)
|
|
689
|
-
}
|
|
690
|
-
))
|
|
691
|
-
|
|
692
|
-
api.register(Endpoint(
|
|
693
|
-
method: "tmux.sessions",
|
|
694
|
-
description: "List all tmux sessions with child process enrichment",
|
|
695
|
-
access: .read,
|
|
696
|
-
params: [],
|
|
697
|
-
returns: .array(model: "TmuxSession"),
|
|
698
|
-
handler: { _ in
|
|
699
|
-
let sessions = TmuxModel.shared.sessions
|
|
700
|
-
return .array(sessions.map { Encoders.enrichedSession($0) })
|
|
701
|
-
}
|
|
702
|
-
))
|
|
703
|
-
|
|
704
|
-
api.register(Endpoint(
|
|
705
|
-
method: "tmux.inventory",
|
|
706
|
-
description: "Get full tmux inventory including orphaned sessions",
|
|
707
|
-
access: .read,
|
|
708
|
-
params: [],
|
|
709
|
-
returns: .custom("Object with 'all' and 'orphans' arrays of TmuxSession"),
|
|
710
|
-
handler: { _ in
|
|
711
|
-
let inv = InventoryManager.shared
|
|
712
|
-
return .object([
|
|
713
|
-
"all": .array(inv.allSessions.map { Encoders.session($0) }),
|
|
714
|
-
"orphans": .array(inv.orphans.map { Encoders.session($0) })
|
|
715
|
-
])
|
|
716
|
-
}
|
|
717
|
-
))
|
|
718
|
-
|
|
719
|
-
api.register(Endpoint(
|
|
720
|
-
method: "projects.list",
|
|
721
|
-
description: "List all discovered projects",
|
|
722
|
-
access: .read,
|
|
723
|
-
params: [],
|
|
724
|
-
returns: .array(model: "Project"),
|
|
725
|
-
handler: { _ in
|
|
726
|
-
let projects = ProjectScanner.shared.projects
|
|
727
|
-
return .array(projects.map { Encoders.project($0) })
|
|
728
|
-
}
|
|
729
|
-
))
|
|
730
|
-
|
|
731
|
-
api.register(Endpoint(
|
|
732
|
-
method: "spaces.list",
|
|
733
|
-
description: "List all displays and their spaces",
|
|
734
|
-
access: .read,
|
|
735
|
-
params: [],
|
|
736
|
-
returns: .array(model: "Display"),
|
|
737
|
-
handler: { _ in
|
|
738
|
-
let displays = WindowTiler.getDisplaySpaces()
|
|
739
|
-
return .array(displays.map { display in
|
|
740
|
-
.object([
|
|
741
|
-
"displayIndex": .int(display.displayIndex),
|
|
742
|
-
"displayId": .string(display.displayId),
|
|
743
|
-
"currentSpaceId": .int(display.currentSpaceId),
|
|
744
|
-
"spaces": .array(display.spaces.map { space in
|
|
745
|
-
.object([
|
|
746
|
-
"id": .int(space.id),
|
|
747
|
-
"index": .int(space.index),
|
|
748
|
-
"name": .string(Self.defaultSpaceName(for: space.index)),
|
|
749
|
-
"display": .int(space.display),
|
|
750
|
-
"isCurrent": .bool(space.isCurrent)
|
|
751
|
-
])
|
|
752
|
-
})
|
|
753
|
-
])
|
|
754
|
-
})
|
|
755
|
-
}
|
|
756
|
-
))
|
|
757
|
-
|
|
758
|
-
api.register(Endpoint(
|
|
759
|
-
method: "layers.list",
|
|
760
|
-
description: "List all workspace layers and the active index",
|
|
761
|
-
access: .read,
|
|
762
|
-
params: [],
|
|
763
|
-
returns: .custom("Object with 'layers' array of Layer and 'active' index"),
|
|
764
|
-
handler: { _ in
|
|
765
|
-
let wm = WorkspaceManager.shared
|
|
766
|
-
guard let config = wm.config, let layers = config.layers else {
|
|
767
|
-
return .object([
|
|
768
|
-
"layers": .array([]),
|
|
769
|
-
"active": .int(0)
|
|
770
|
-
])
|
|
771
|
-
}
|
|
772
|
-
return .object([
|
|
773
|
-
"layers": .array(layers.enumerated().map { i, layer in
|
|
774
|
-
.object([
|
|
775
|
-
"id": .string(layer.id),
|
|
776
|
-
"label": .string(layer.label),
|
|
777
|
-
"index": .int(i),
|
|
778
|
-
"projectCount": .int(layer.projects.count)
|
|
779
|
-
])
|
|
780
|
-
}),
|
|
781
|
-
"active": .int(wm.activeLayerIndex)
|
|
782
|
-
])
|
|
783
|
-
}
|
|
784
|
-
))
|
|
785
|
-
|
|
786
|
-
api.register(Endpoint(
|
|
787
|
-
method: "deck.manifest",
|
|
788
|
-
description: "Get the shared companion deck manifest exposed by the macOS app",
|
|
789
|
-
access: .read,
|
|
790
|
-
params: [],
|
|
791
|
-
returns: .custom("DeckKit manifest for the Lattices companion surface"),
|
|
792
|
-
handler: { _ in
|
|
793
|
-
try Self.encodeDeckValue(LatticesDeckHost.shared.manifestSync())
|
|
794
|
-
}
|
|
795
|
-
))
|
|
796
|
-
|
|
797
|
-
api.register(Endpoint(
|
|
798
|
-
method: "deck.snapshot",
|
|
799
|
-
description: "Get the current companion deck runtime snapshot",
|
|
800
|
-
access: .read,
|
|
801
|
-
params: [],
|
|
802
|
-
returns: .custom("DeckKit runtime snapshot with voice, layout, switcher, and history state"),
|
|
803
|
-
handler: { _ in
|
|
804
|
-
try Self.encodeDeckValue(LatticesDeckHost.shared.runtimeSnapshotSync())
|
|
805
|
-
}
|
|
806
|
-
))
|
|
807
|
-
|
|
808
|
-
api.register(Endpoint(
|
|
809
|
-
method: "deck.perform",
|
|
810
|
-
description: "Perform a companion deck action and return the updated runtime snapshot",
|
|
811
|
-
access: .mutate,
|
|
812
|
-
params: [
|
|
813
|
-
Param(name: "pageID", type: "string", required: false, description: "Deck page ID"),
|
|
814
|
-
Param(name: "actionID", type: "string", required: true, description: "Deck action identifier"),
|
|
815
|
-
Param(name: "payload", type: "object", required: false, description: "Deck action payload"),
|
|
816
|
-
],
|
|
817
|
-
returns: .custom("DeckKit action result"),
|
|
818
|
-
handler: { params in
|
|
819
|
-
let request = try Self.decodeDeckActionRequest(from: params)
|
|
820
|
-
let result = try LatticesDeckHost.shared.performSync(request)
|
|
821
|
-
return try Self.encodeDeckValue(result)
|
|
822
|
-
}
|
|
823
|
-
))
|
|
824
|
-
|
|
825
|
-
api.register(Endpoint(
|
|
826
|
-
method: "overlay.publish",
|
|
827
|
-
description: "Publish a transient visual layer on the invisible screen overlay canvas",
|
|
828
|
-
access: .mutate,
|
|
829
|
-
params: [
|
|
830
|
-
Param(name: "kind", type: "string", required: true, description: "toast, label, highlight, or pet"),
|
|
831
|
-
Param(name: "id", type: "string", required: false, description: "Stable layer id; generated if omitted"),
|
|
832
|
-
Param(name: "text", type: "string", required: false, description: "Toast/label text"),
|
|
833
|
-
Param(name: "detail", type: "string", required: false, description: "Secondary toast/label text"),
|
|
834
|
-
Param(name: "message", type: "string", required: false, description: "Pet speech/message"),
|
|
835
|
-
Param(name: "glyph", type: "string", required: false, description: "Pet glyph, emoji, or short symbol"),
|
|
836
|
-
Param(name: "petId", type: "string", required: false, description: "Bundled pet id from Resources/Pets"),
|
|
837
|
-
Param(name: "state", type: "string", required: false, description: "Pet animation state"),
|
|
838
|
-
Param(name: "name", type: "string", required: false, description: "Pet name"),
|
|
839
|
-
Param(name: "x", type: "double", required: false, description: "Screen-local x coordinate"),
|
|
840
|
-
Param(name: "y", type: "double", required: false, description: "Screen-local y coordinate"),
|
|
841
|
-
Param(name: "w", type: "double", required: false, description: "Highlight width"),
|
|
842
|
-
Param(name: "h", type: "double", required: false, description: "Highlight height"),
|
|
843
|
-
Param(name: "placement", type: "string", required: false, description: "top, bottom, center, cursor, or point"),
|
|
844
|
-
Param(name: "style", type: "string", required: false, description: "info, success, warning, danger, or playful"),
|
|
845
|
-
Param(name: "display", type: "int", required: false, description: "Display index; omit for all displays"),
|
|
846
|
-
Param(name: "ttlMs", type: "int", required: false, description: "Time to live in milliseconds"),
|
|
847
|
-
Param(name: "opacity", type: "double", required: false, description: "Opacity 0-1"),
|
|
848
|
-
Param(name: "zIndex", type: "int", required: false, description: "Layer ordering"),
|
|
849
|
-
Param(name: "dismissible", type: "bool", required: false, description: "Whether click-away dismissal removes the layer"),
|
|
850
|
-
],
|
|
851
|
-
returns: .object(model: "OverlayLayer"),
|
|
852
|
-
handler: { params in
|
|
853
|
-
try Self.publishOverlay(params)
|
|
854
|
-
}
|
|
855
|
-
))
|
|
856
|
-
|
|
857
|
-
api.register(Endpoint(
|
|
858
|
-
method: "overlay.clear",
|
|
859
|
-
description: "Clear overlay layers published through the daemon API",
|
|
860
|
-
access: .mutate,
|
|
861
|
-
params: [
|
|
862
|
-
Param(name: "id", type: "string", required: false, description: "Specific layer id to clear"),
|
|
863
|
-
Param(name: "owner", type: "string", required: false, description: "Owner namespace to clear; defaults to agentApi"),
|
|
864
|
-
],
|
|
865
|
-
returns: .ok,
|
|
866
|
-
handler: { params in
|
|
867
|
-
try Self.clearOverlay(params)
|
|
868
|
-
}
|
|
869
|
-
))
|
|
870
|
-
|
|
871
|
-
api.register(Endpoint(
|
|
872
|
-
method: "overlay.actor.publish",
|
|
873
|
-
description: "Create or update a small generative overlay actor",
|
|
874
|
-
access: .mutate,
|
|
875
|
-
params: [
|
|
876
|
-
Param(name: "id", type: "string", required: false, description: "Stable actor id; generated if omitted"),
|
|
877
|
-
Param(name: "renderer", type: "string", required: false, description: "Renderer type; sprite is currently supported"),
|
|
878
|
-
Param(name: "asset", type: "string", required: false, description: "Bundled sprite asset id"),
|
|
879
|
-
Param(name: "state", type: "string", required: false, description: "Actor state or animation name"),
|
|
880
|
-
Param(name: "name", type: "string", required: false, description: "Actor display name"),
|
|
881
|
-
Param(name: "message", type: "string", required: false, description: "Attached message text"),
|
|
882
|
-
Param(name: "x", type: "double", required: false, description: "Screen-local x coordinate"),
|
|
883
|
-
Param(name: "y", type: "double", required: false, description: "Screen-local y coordinate"),
|
|
884
|
-
Param(name: "placement", type: "string", required: false, description: "top, bottom, center, cursor, or point"),
|
|
885
|
-
Param(name: "style", type: "string", required: false, description: "info, success, warning, danger, or playful"),
|
|
886
|
-
Param(name: "display", type: "int", required: false, description: "Display index; omit for all displays"),
|
|
887
|
-
Param(name: "ttlMs", type: "int", required: false, description: "Time to live in milliseconds; omit or pass 0 for persistent"),
|
|
888
|
-
Param(name: "opacity", type: "double", required: false, description: "Opacity 0-1"),
|
|
889
|
-
Param(name: "zIndex", type: "int", required: false, description: "Layer ordering"),
|
|
890
|
-
Param(name: "dismissible", type: "bool", required: false, description: "Whether click-away dismissal removes the actor; defaults false"),
|
|
891
|
-
],
|
|
892
|
-
returns: .object(model: "OverlayLayer"),
|
|
893
|
-
handler: { params in
|
|
894
|
-
try Self.publishOverlayActor(params)
|
|
895
|
-
}
|
|
896
|
-
))
|
|
897
|
-
|
|
898
|
-
api.register(Endpoint(
|
|
899
|
-
method: "overlay.actor.moveTo",
|
|
900
|
-
description: "Move an overlay actor with app-owned easing",
|
|
901
|
-
access: .mutate,
|
|
902
|
-
params: [
|
|
903
|
-
Param(name: "id", type: "string", required: true, description: "Actor id"),
|
|
904
|
-
Param(name: "x", type: "double", required: true, description: "Target screen-local x coordinate"),
|
|
905
|
-
Param(name: "y", type: "double", required: true, description: "Target screen-local y coordinate"),
|
|
906
|
-
Param(name: "durationMs", type: "int", required: false, description: "Animation duration in milliseconds"),
|
|
907
|
-
Param(name: "easing", type: "string", required: false, description: "linear, easeInOut, or spring"),
|
|
908
|
-
],
|
|
909
|
-
returns: .ok,
|
|
910
|
-
handler: { params in
|
|
911
|
-
try Self.moveOverlayActor(params)
|
|
912
|
-
}
|
|
913
|
-
))
|
|
914
|
-
|
|
915
|
-
api.register(Endpoint(
|
|
916
|
-
method: "daemon.status",
|
|
917
|
-
description: "Get daemon status including uptime and counts",
|
|
918
|
-
access: .read,
|
|
919
|
-
params: [],
|
|
920
|
-
returns: .object(model: "DaemonStatus"),
|
|
921
|
-
handler: { _ in
|
|
922
|
-
let uptime = Date().timeIntervalSince(api.startTime)
|
|
923
|
-
return .object([
|
|
924
|
-
"uptime": .double(uptime),
|
|
925
|
-
"clientCount": .int(DaemonServer.shared.clientCount),
|
|
926
|
-
"version": .string("1.0.0"),
|
|
927
|
-
"windowCount": .int(DesktopModel.shared.windows.count),
|
|
928
|
-
"tmuxSessionCount": .int(TmuxModel.shared.sessions.count)
|
|
929
|
-
])
|
|
930
|
-
}
|
|
931
|
-
))
|
|
932
|
-
|
|
933
|
-
api.register(Endpoint(
|
|
934
|
-
method: "diagnostics.list",
|
|
935
|
-
description: "Get recent diagnostic log entries",
|
|
936
|
-
access: .read,
|
|
937
|
-
params: [Param(name: "limit", type: "int", required: false, description: "Max entries to return (default 40)")],
|
|
938
|
-
returns: .custom("Array of log entries with time, level, message"),
|
|
939
|
-
handler: { params in
|
|
940
|
-
let limit = params?["limit"]?.intValue ?? 40
|
|
941
|
-
let entries = DiagnosticLog.shared.entries.suffix(limit)
|
|
942
|
-
let fmt = DateFormatter()
|
|
943
|
-
fmt.dateFormat = "HH:mm:ss.SSS"
|
|
944
|
-
return .object([
|
|
945
|
-
"entries": .array(entries.map { entry in
|
|
946
|
-
.object([
|
|
947
|
-
"time": .string(fmt.string(from: entry.time)),
|
|
948
|
-
"level": .string("\(entry.level)"),
|
|
949
|
-
"message": .string(entry.message)
|
|
950
|
-
])
|
|
951
|
-
})
|
|
952
|
-
])
|
|
953
|
-
}
|
|
954
|
-
))
|
|
955
|
-
|
|
956
|
-
api.register(Endpoint(
|
|
957
|
-
method: "processes.list",
|
|
958
|
-
description: "List interesting developer processes with tmux/window linkage",
|
|
959
|
-
access: .read,
|
|
960
|
-
params: [Param(name: "command", type: "string", required: false, description: "Filter by command name (e.g. claude)")],
|
|
961
|
-
returns: .array(model: "Process"),
|
|
962
|
-
handler: { params in
|
|
963
|
-
let pm = ProcessModel.shared
|
|
964
|
-
var enriched = pm.enrichedProcesses()
|
|
965
|
-
if let cmd = params?["command"]?.stringValue {
|
|
966
|
-
enriched = enriched.filter { $0.process.comm == cmd }
|
|
967
|
-
}
|
|
968
|
-
return .array(enriched.map { Encoders.process($0) })
|
|
969
|
-
}
|
|
970
|
-
))
|
|
971
|
-
|
|
972
|
-
api.register(Endpoint(
|
|
973
|
-
method: "processes.tree",
|
|
974
|
-
description: "Get all descendant processes of a given PID",
|
|
975
|
-
access: .read,
|
|
976
|
-
params: [Param(name: "pid", type: "int", required: true, description: "Parent process ID")],
|
|
977
|
-
returns: .array(model: "Process"),
|
|
978
|
-
handler: { params in
|
|
979
|
-
guard let pid = params?["pid"]?.intValue else {
|
|
980
|
-
throw RouterError.missingParam("pid")
|
|
981
|
-
}
|
|
982
|
-
let pm = ProcessModel.shared
|
|
983
|
-
let descendants = pm.descendants(of: pid)
|
|
984
|
-
return .array(descendants.map { entry in
|
|
985
|
-
let enrichment = pm.enrich(entry)
|
|
986
|
-
return Encoders.process(enrichment)
|
|
987
|
-
})
|
|
988
|
-
}
|
|
989
|
-
))
|
|
990
|
-
|
|
991
|
-
api.register(Endpoint(
|
|
992
|
-
method: "terminals.list",
|
|
993
|
-
description: "List all synthesized terminal instances (unified TTY view)",
|
|
994
|
-
access: .read,
|
|
995
|
-
params: [
|
|
996
|
-
Param(name: "refresh", type: "bool", required: false, description: "Force-refresh terminal tab cache before synthesizing"),
|
|
997
|
-
],
|
|
998
|
-
returns: .array(model: "TerminalInstance"),
|
|
999
|
-
handler: { params in
|
|
1000
|
-
let pm = ProcessModel.shared
|
|
1001
|
-
if params?["refresh"]?.boolValue == true {
|
|
1002
|
-
pm.refreshTerminalTabs()
|
|
1003
|
-
}
|
|
1004
|
-
let instances = pm.synthesizeTerminals()
|
|
1005
|
-
return .array(instances.map { Encoders.terminalInstance($0) })
|
|
1006
|
-
}
|
|
1007
|
-
))
|
|
1008
|
-
|
|
1009
|
-
api.register(Endpoint(
|
|
1010
|
-
method: "terminals.search",
|
|
1011
|
-
description: "Search terminal instances by command, cwd, app, session, or hasClaude",
|
|
1012
|
-
access: .read,
|
|
1013
|
-
params: [
|
|
1014
|
-
Param(name: "command", type: "string", required: false, description: "Filter by command name substring"),
|
|
1015
|
-
Param(name: "cwd", type: "string", required: false, description: "Filter by working directory substring"),
|
|
1016
|
-
Param(name: "app", type: "string", required: false, description: "Filter by terminal app name"),
|
|
1017
|
-
Param(name: "session", type: "string", required: false, description: "Filter by tmux session name"),
|
|
1018
|
-
Param(name: "hasClaude", type: "bool", required: false, description: "Filter to only Claude-running TTYs"),
|
|
1019
|
-
],
|
|
1020
|
-
returns: .array(model: "TerminalInstance"),
|
|
1021
|
-
handler: { params in
|
|
1022
|
-
var instances = ProcessModel.shared.synthesizeTerminals()
|
|
1023
|
-
|
|
1024
|
-
if let cmd = params?["command"]?.stringValue {
|
|
1025
|
-
instances = instances.filter { inst in
|
|
1026
|
-
inst.processes.contains { $0.comm.contains(cmd) || $0.args.contains(cmd) }
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if let cwd = params?["cwd"]?.stringValue {
|
|
1030
|
-
instances = instances.filter { inst in
|
|
1031
|
-
inst.cwd?.contains(cwd) == true
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if let app = params?["app"]?.stringValue {
|
|
1035
|
-
instances = instances.filter { $0.app?.rawValue == app }
|
|
1036
|
-
}
|
|
1037
|
-
if let session = params?["session"]?.stringValue {
|
|
1038
|
-
instances = instances.filter { $0.tmuxSession == session }
|
|
1039
|
-
}
|
|
1040
|
-
if let hasClaude = params?["hasClaude"]?.boolValue {
|
|
1041
|
-
instances = instances.filter { $0.hasClaude == hasClaude }
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
return .array(instances.map { Encoders.terminalInstance($0) })
|
|
1045
|
-
}
|
|
1046
|
-
))
|
|
1047
|
-
|
|
1048
|
-
// ── Endpoints: OCR ─────────────────────────────────────
|
|
1049
|
-
|
|
1050
|
-
api.register(Endpoint(
|
|
1051
|
-
method: "ocr.snapshot",
|
|
1052
|
-
description: "Get the latest OCR scan results for all on-screen windows",
|
|
1053
|
-
access: .read,
|
|
1054
|
-
params: [],
|
|
1055
|
-
returns: .array(model: "OcrResult"),
|
|
1056
|
-
handler: { _ in
|
|
1057
|
-
let results = OcrModel.shared.results
|
|
1058
|
-
return .array(results.values.map { Encoders.ocrResult($0) })
|
|
1059
|
-
}
|
|
1060
|
-
))
|
|
1061
|
-
|
|
1062
|
-
api.register(Endpoint(
|
|
1063
|
-
method: "ocr.search",
|
|
1064
|
-
description: "Search OCR text across all windows (queries persistent SQLite FTS5 index by default)",
|
|
1065
|
-
access: .read,
|
|
1066
|
-
params: [
|
|
1067
|
-
Param(name: "query", type: "string", required: true, description: "Search text (FTS5 query syntax)"),
|
|
1068
|
-
Param(name: "app", type: "string", required: false, description: "Filter by app name"),
|
|
1069
|
-
Param(name: "limit", type: "int", required: false, description: "Max results (default 50)"),
|
|
1070
|
-
Param(name: "live", type: "bool", required: false, description: "Search in-memory snapshot instead of history (default false)"),
|
|
1071
|
-
],
|
|
1072
|
-
returns: .array(model: "OcrSearchResult"),
|
|
1073
|
-
handler: { params in
|
|
1074
|
-
guard let query = params?["query"]?.stringValue else {
|
|
1075
|
-
throw RouterError.missingParam("query")
|
|
1076
|
-
}
|
|
1077
|
-
let app = params?["app"]?.stringValue
|
|
1078
|
-
let limit = params?["limit"]?.intValue ?? 50
|
|
1079
|
-
let live = params?["live"]?.boolValue ?? false
|
|
1080
|
-
|
|
1081
|
-
if live {
|
|
1082
|
-
// In-memory snapshot search (original behavior)
|
|
1083
|
-
var results = Array(OcrModel.shared.results.values)
|
|
1084
|
-
let q = query.lowercased()
|
|
1085
|
-
results = results.filter { $0.fullText.lowercased().contains(q) }
|
|
1086
|
-
if let app { results = results.filter { $0.app == app } }
|
|
1087
|
-
return .array(results.prefix(limit).map { Encoders.ocrResult($0) })
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Persistent FTS5 search
|
|
1091
|
-
let results = OcrStore.shared.search(query: query, app: app, limit: limit)
|
|
1092
|
-
return .array(results.map { Encoders.ocrSearchResult($0) })
|
|
1093
|
-
}
|
|
1094
|
-
))
|
|
1095
|
-
|
|
1096
|
-
api.register(Endpoint(
|
|
1097
|
-
method: "ocr.history",
|
|
1098
|
-
description: "Get OCR content timeline for a specific window",
|
|
1099
|
-
access: .read,
|
|
1100
|
-
params: [
|
|
1101
|
-
Param(name: "wid", type: "uint32", required: true, description: "Window ID"),
|
|
1102
|
-
Param(name: "limit", type: "int", required: false, description: "Max results (default 50)"),
|
|
1103
|
-
],
|
|
1104
|
-
returns: .array(model: "OcrSearchResult"),
|
|
1105
|
-
handler: { params in
|
|
1106
|
-
guard let wid = params?["wid"]?.uint32Value else {
|
|
1107
|
-
throw RouterError.missingParam("wid")
|
|
1108
|
-
}
|
|
1109
|
-
let limit = params?["limit"]?.intValue ?? 50
|
|
1110
|
-
let results = OcrStore.shared.history(wid: wid, limit: limit)
|
|
1111
|
-
return .array(results.map { Encoders.ocrSearchResult($0) })
|
|
1112
|
-
}
|
|
1113
|
-
))
|
|
1114
|
-
|
|
1115
|
-
api.register(Endpoint(
|
|
1116
|
-
method: "ocr.recent",
|
|
1117
|
-
description: "Get recent OCR entries across all windows (chronological, from persistent store)",
|
|
1118
|
-
access: .read,
|
|
1119
|
-
params: [
|
|
1120
|
-
Param(name: "limit", type: "int", required: false, description: "Max results (default 50)"),
|
|
1121
|
-
],
|
|
1122
|
-
returns: .array(model: "OcrSearchResult"),
|
|
1123
|
-
handler: { params in
|
|
1124
|
-
let limit = params?["limit"]?.intValue ?? 50
|
|
1125
|
-
let results = OcrStore.shared.recent(limit: limit)
|
|
1126
|
-
return .array(results.map { Encoders.ocrSearchResult($0) })
|
|
1127
|
-
}
|
|
1128
|
-
))
|
|
1129
|
-
|
|
1130
|
-
api.register(Endpoint(
|
|
1131
|
-
method: "ocr.scan",
|
|
1132
|
-
description: "Trigger an immediate OCR scan",
|
|
1133
|
-
access: .mutate,
|
|
1134
|
-
params: [],
|
|
1135
|
-
returns: .ok,
|
|
1136
|
-
handler: { _ in
|
|
1137
|
-
OcrModel.shared.scan()
|
|
1138
|
-
return .object(["ok": .bool(true)])
|
|
1139
|
-
}
|
|
1140
|
-
))
|
|
1141
|
-
|
|
1142
|
-
// ── Endpoints: Mutations ────────────────────────────────
|
|
1143
|
-
|
|
1144
|
-
api.register(Endpoint(
|
|
1145
|
-
method: "window.tile",
|
|
1146
|
-
description: "Tile a session's terminal window to a position",
|
|
1147
|
-
access: .mutate,
|
|
1148
|
-
params: [
|
|
1149
|
-
Param(name: "session", type: "string", required: true, description: "Tmux session name"),
|
|
1150
|
-
Param(name: "position", type: "string", required: true,
|
|
1151
|
-
description: "Placement shorthand or grid syntax"),
|
|
1152
|
-
],
|
|
1153
|
-
returns: .ok,
|
|
1154
|
-
handler: { params in
|
|
1155
|
-
guard case .object(var dict) = params else {
|
|
1156
|
-
throw RouterError.missingParam("session")
|
|
1157
|
-
}
|
|
1158
|
-
guard dict["session"]?.stringValue != nil else {
|
|
1159
|
-
throw RouterError.missingParam("session")
|
|
1160
|
-
}
|
|
1161
|
-
dict["placement"] = dict["placement"] ?? dict["position"]
|
|
1162
|
-
return try Self.executeWindowPlacement(params: .object(dict))
|
|
1163
|
-
}
|
|
1164
|
-
))
|
|
1165
|
-
|
|
1166
|
-
api.register(Endpoint(
|
|
1167
|
-
method: "window.focus",
|
|
1168
|
-
description: "Focus a window by wid or session name",
|
|
1169
|
-
access: .mutate,
|
|
1170
|
-
params: [
|
|
1171
|
-
Param(name: "wid", type: "uint32", required: false, description: "Window ID (takes priority)"),
|
|
1172
|
-
Param(name: "session", type: "string", required: false, description: "Tmux session name (fallback)"),
|
|
1173
|
-
],
|
|
1174
|
-
returns: .ok,
|
|
1175
|
-
handler: { params in
|
|
1176
|
-
if let wid = params?["wid"]?.uint32Value {
|
|
1177
|
-
guard let entry = DesktopModel.shared.windows[wid] else {
|
|
1178
|
-
throw RouterError.notFound("window \(wid)")
|
|
1179
|
-
}
|
|
1180
|
-
var raised = false
|
|
1181
|
-
if Thread.isMainThread {
|
|
1182
|
-
raised = WindowTiler.focusWindow(wid: wid, pid: entry.pid)
|
|
1183
|
-
} else {
|
|
1184
|
-
DispatchQueue.main.sync {
|
|
1185
|
-
raised = WindowTiler.focusWindow(wid: wid, pid: entry.pid)
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
return .object(["ok": .bool(raised), "wid": .int(Int(wid)), "app": .string(entry.app),
|
|
1189
|
-
"raised": .bool(raised)])
|
|
1190
|
-
}
|
|
1191
|
-
guard let session = params?["session"]?.stringValue else {
|
|
1192
|
-
throw RouterError.missingParam("session or wid")
|
|
1193
|
-
}
|
|
1194
|
-
let terminal = Preferences.shared.terminal
|
|
1195
|
-
DispatchQueue.main.async {
|
|
1196
|
-
WindowTiler.navigateToWindow(session: session, terminal: terminal)
|
|
1197
|
-
}
|
|
1198
|
-
return .object(["ok": .bool(true)])
|
|
1199
|
-
}
|
|
1200
|
-
))
|
|
1201
|
-
|
|
1202
|
-
api.register(Endpoint(
|
|
1203
|
-
method: "window.place",
|
|
1204
|
-
description: "Place a window or session using a typed placement spec",
|
|
1205
|
-
access: .mutate,
|
|
1206
|
-
params: [
|
|
1207
|
-
Param(name: "wid", type: "uint32", required: false, description: "Window ID"),
|
|
1208
|
-
Param(name: "session", type: "string", required: false, description: "Tmux session name"),
|
|
1209
|
-
Param(name: "app", type: "string", required: false, description: "Application name"),
|
|
1210
|
-
Param(name: "title", type: "string", required: false, description: "Optional title substring for app matching"),
|
|
1211
|
-
Param(name: "display", type: "int", required: false, description: "Target display index"),
|
|
1212
|
-
Param(name: "placement", type: "string|object", required: true, description: "Placement shorthand or typed placement object"),
|
|
1213
|
-
],
|
|
1214
|
-
returns: .custom("Execution receipt with target resolution, placement, and trace"),
|
|
1215
|
-
handler: { params in
|
|
1216
|
-
try Self.executeWindowPlacement(params: params)
|
|
1217
|
-
}
|
|
1218
|
-
))
|
|
1219
|
-
|
|
1220
|
-
// ── Present Window ────────────────────────────────────────────
|
|
1221
|
-
api.register(Endpoint(
|
|
1222
|
-
method: "window.present",
|
|
1223
|
-
description: "Present a window: move to current space, bring to front, optionally position it",
|
|
1224
|
-
access: .mutate,
|
|
1225
|
-
params: [
|
|
1226
|
-
Param(name: "wid", type: "uint32", required: true, description: "Window ID"),
|
|
1227
|
-
Param(name: "x", type: "double", required: false, description: "Target x position"),
|
|
1228
|
-
Param(name: "y", type: "double", required: false, description: "Target y position"),
|
|
1229
|
-
Param(name: "w", type: "double", required: false, description: "Target width"),
|
|
1230
|
-
Param(name: "h", type: "double", required: false, description: "Target height"),
|
|
1231
|
-
Param(name: "position", type: "string", required: false,
|
|
1232
|
-
description: "Tile position (e.g. center, left, right, bottom-right)"),
|
|
1233
|
-
],
|
|
1234
|
-
returns: .ok,
|
|
1235
|
-
handler: { params in
|
|
1236
|
-
guard let wid = params?["wid"]?.uint32Value else {
|
|
1237
|
-
throw RouterError.missingParam("wid")
|
|
1238
|
-
}
|
|
1239
|
-
guard let entry = DesktopModel.shared.windows[wid] else {
|
|
1240
|
-
throw RouterError.notFound("window \(wid)")
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// Resolve position to fractional rect
|
|
1244
|
-
var fractions: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
|
|
1245
|
-
if let placement = Self.parsePlacement(from: params?["placement"] ?? params?["position"]) {
|
|
1246
|
-
fractions = placement.fractions
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
var frame: CGRect? = nil
|
|
1250
|
-
if let fracs = fractions {
|
|
1251
|
-
let screen = Self.resolveTargetScreen(for: entry, displayIndex: params?["display"]?.intValue)
|
|
1252
|
-
// Compute pixel frame (needs main thread for NSScreen)
|
|
1253
|
-
if Thread.isMainThread {
|
|
1254
|
-
frame = WindowTiler.tileFrame(fractions: fracs, on: screen)
|
|
1255
|
-
} else {
|
|
1256
|
-
DispatchQueue.main.sync {
|
|
1257
|
-
frame = WindowTiler.tileFrame(fractions: fracs, on: screen)
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
} else if let x = params?["x"]?.intValue,
|
|
1261
|
-
let y = params?["y"]?.intValue,
|
|
1262
|
-
let w = params?["w"]?.intValue,
|
|
1263
|
-
let h = params?["h"]?.intValue {
|
|
1264
|
-
frame = CGRect(x: x, y: y, width: w, height: h)
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
var presented = false
|
|
1268
|
-
if Thread.isMainThread {
|
|
1269
|
-
presented = WindowTiler.present(wid: wid, pid: entry.pid, frame: frame)
|
|
1270
|
-
} else {
|
|
1271
|
-
DispatchQueue.main.sync {
|
|
1272
|
-
presented = WindowTiler.present(wid: wid, pid: entry.pid, frame: frame)
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
return .object(["ok": .bool(presented), "wid": .int(Int(wid)), "app": .string(entry.app)])
|
|
1276
|
-
}
|
|
1277
|
-
))
|
|
1278
|
-
|
|
1279
|
-
api.register(Endpoint(
|
|
1280
|
-
method: "window.move",
|
|
1281
|
-
description: "Move a session's window to a different space",
|
|
1282
|
-
access: .mutate,
|
|
1283
|
-
params: [
|
|
1284
|
-
Param(name: "session", type: "string", required: true, description: "Tmux session name"),
|
|
1285
|
-
Param(name: "spaceId", type: "int", required: true, description: "Target space ID"),
|
|
1286
|
-
],
|
|
1287
|
-
returns: .ok,
|
|
1288
|
-
handler: { params in
|
|
1289
|
-
guard let session = params?["session"]?.stringValue else {
|
|
1290
|
-
throw RouterError.missingParam("session")
|
|
1291
|
-
}
|
|
1292
|
-
guard let spaceId = params?["spaceId"]?.intValue else {
|
|
1293
|
-
throw RouterError.missingParam("spaceId")
|
|
1294
|
-
}
|
|
1295
|
-
let terminal = Preferences.shared.terminal
|
|
1296
|
-
DispatchQueue.main.async {
|
|
1297
|
-
_ = WindowTiler.moveWindowToSpace(session: session, terminal: terminal, spaceId: spaceId)
|
|
1298
|
-
}
|
|
1299
|
-
return .object(["ok": .bool(true)])
|
|
1300
|
-
}
|
|
1301
|
-
))
|
|
1302
|
-
|
|
1303
|
-
api.register(Endpoint(
|
|
1304
|
-
method: "session.launch",
|
|
1305
|
-
description: "Launch a project's tmux session",
|
|
1306
|
-
access: .mutate,
|
|
1307
|
-
params: [Param(name: "path", type: "string", required: true, description: "Absolute project path")],
|
|
1308
|
-
returns: .ok,
|
|
1309
|
-
handler: { params in
|
|
1310
|
-
guard let path = params?["path"]?.stringValue else {
|
|
1311
|
-
throw RouterError.missingParam("path")
|
|
1312
|
-
}
|
|
1313
|
-
guard let project = ProjectScanner.shared.projects.first(where: { $0.path == path }) else {
|
|
1314
|
-
throw RouterError.notFound("project at \(path)")
|
|
1315
|
-
}
|
|
1316
|
-
DispatchQueue.main.async {
|
|
1317
|
-
SessionManager.launch(project: project)
|
|
1318
|
-
}
|
|
1319
|
-
return .object(["ok": .bool(true)])
|
|
1320
|
-
}
|
|
1321
|
-
))
|
|
1322
|
-
|
|
1323
|
-
api.register(Endpoint(
|
|
1324
|
-
method: "session.kill",
|
|
1325
|
-
description: "Kill a tmux session by name",
|
|
1326
|
-
access: .mutate,
|
|
1327
|
-
params: [Param(name: "name", type: "string", required: true, description: "Session name")],
|
|
1328
|
-
returns: .ok,
|
|
1329
|
-
handler: { params in
|
|
1330
|
-
guard let name = params?["name"]?.stringValue else {
|
|
1331
|
-
throw RouterError.missingParam("name")
|
|
1332
|
-
}
|
|
1333
|
-
SessionManager.killByName(name)
|
|
1334
|
-
return .object(["ok": .bool(true)])
|
|
1335
|
-
}
|
|
1336
|
-
))
|
|
1337
|
-
|
|
1338
|
-
api.register(Endpoint(
|
|
1339
|
-
method: "session.detach",
|
|
1340
|
-
description: "Detach all clients from a tmux session",
|
|
1341
|
-
access: .mutate,
|
|
1342
|
-
params: [Param(name: "name", type: "string", required: true, description: "Session name")],
|
|
1343
|
-
returns: .ok,
|
|
1344
|
-
handler: { params in
|
|
1345
|
-
guard let name = params?["name"]?.stringValue else {
|
|
1346
|
-
throw RouterError.missingParam("name")
|
|
1347
|
-
}
|
|
1348
|
-
SessionManager.detachByName(name)
|
|
1349
|
-
return .object(["ok": .bool(true)])
|
|
1350
|
-
}
|
|
1351
|
-
))
|
|
1352
|
-
|
|
1353
|
-
api.register(Endpoint(
|
|
1354
|
-
method: "session.sync",
|
|
1355
|
-
description: "Sync a project's tmux session panes to match config",
|
|
1356
|
-
access: .mutate,
|
|
1357
|
-
params: [Param(name: "path", type: "string", required: true, description: "Absolute project path")],
|
|
1358
|
-
returns: .ok,
|
|
1359
|
-
handler: { params in
|
|
1360
|
-
guard let path = params?["path"]?.stringValue else {
|
|
1361
|
-
throw RouterError.missingParam("path")
|
|
1362
|
-
}
|
|
1363
|
-
guard let project = ProjectScanner.shared.projects.first(where: { $0.path == path }) else {
|
|
1364
|
-
throw RouterError.notFound("project at \(path)")
|
|
1365
|
-
}
|
|
1366
|
-
SessionManager.sync(project: project)
|
|
1367
|
-
return .object(["ok": .bool(true)])
|
|
1368
|
-
}
|
|
1369
|
-
))
|
|
1370
|
-
|
|
1371
|
-
api.register(Endpoint(
|
|
1372
|
-
method: "session.restart",
|
|
1373
|
-
description: "Restart a project session or specific pane",
|
|
1374
|
-
access: .mutate,
|
|
1375
|
-
params: [
|
|
1376
|
-
Param(name: "path", type: "string", required: true, description: "Absolute project path"),
|
|
1377
|
-
Param(name: "pane", type: "string", required: false, description: "Specific pane name to restart"),
|
|
1378
|
-
],
|
|
1379
|
-
returns: .ok,
|
|
1380
|
-
handler: { params in
|
|
1381
|
-
guard let path = params?["path"]?.stringValue else {
|
|
1382
|
-
throw RouterError.missingParam("path")
|
|
1383
|
-
}
|
|
1384
|
-
guard let project = ProjectScanner.shared.projects.first(where: { $0.path == path }) else {
|
|
1385
|
-
throw RouterError.notFound("project at \(path)")
|
|
1386
|
-
}
|
|
1387
|
-
let paneName = params?["pane"]?.stringValue
|
|
1388
|
-
SessionManager.restart(project: project, paneName: paneName)
|
|
1389
|
-
return .object(["ok": .bool(true)])
|
|
1390
|
-
}
|
|
1391
|
-
))
|
|
1392
|
-
|
|
1393
|
-
api.register(Endpoint(
|
|
1394
|
-
method: "layer.switch",
|
|
1395
|
-
description: "Switch to a workspace layer by index or name",
|
|
1396
|
-
access: .mutate,
|
|
1397
|
-
params: [
|
|
1398
|
-
Param(name: "index", type: "int", required: false, description: "Layer index"),
|
|
1399
|
-
Param(name: "name", type: "string", required: false, description: "Layer id or label (case-insensitive)")
|
|
1400
|
-
],
|
|
1401
|
-
returns: .ok,
|
|
1402
|
-
handler: { params in
|
|
1403
|
-
var dict: [String: JSON] = [:]
|
|
1404
|
-
if case .object(let obj) = params {
|
|
1405
|
-
dict = obj
|
|
1406
|
-
}
|
|
1407
|
-
dict["mode"] = dict["mode"] ?? .string("launch")
|
|
1408
|
-
return try Self.executeLayerActivation(params: .object(dict))
|
|
1409
|
-
}
|
|
1410
|
-
))
|
|
1411
|
-
|
|
1412
|
-
api.register(Endpoint(
|
|
1413
|
-
method: "layer.activate",
|
|
1414
|
-
description: "Activate a workspace layer using an explicit activation mode",
|
|
1415
|
-
access: .mutate,
|
|
1416
|
-
params: [
|
|
1417
|
-
Param(name: "index", type: "int", required: false, description: "Layer index"),
|
|
1418
|
-
Param(name: "name", type: "string", required: false, description: "Layer id or label (case-insensitive)"),
|
|
1419
|
-
Param(name: "mode", type: "string", required: false, description: "Activation mode: launch, focus, or retile"),
|
|
1420
|
-
],
|
|
1421
|
-
returns: .custom("Execution receipt with resolved layer, activation mode, and trace"),
|
|
1422
|
-
handler: { params in
|
|
1423
|
-
try Self.executeLayerActivation(params: params)
|
|
1424
|
-
}
|
|
1425
|
-
))
|
|
1426
|
-
|
|
1427
|
-
api.register(Endpoint(
|
|
1428
|
-
method: "group.launch",
|
|
1429
|
-
description: "Launch all sessions in a project group",
|
|
1430
|
-
access: .mutate,
|
|
1431
|
-
params: [Param(name: "id", type: "string", required: true, description: "Group identifier")],
|
|
1432
|
-
returns: .ok,
|
|
1433
|
-
handler: { params in
|
|
1434
|
-
guard let groupId = params?["id"]?.stringValue else {
|
|
1435
|
-
throw RouterError.missingParam("id")
|
|
1436
|
-
}
|
|
1437
|
-
guard let group = WorkspaceManager.shared.group(byId: groupId) else {
|
|
1438
|
-
throw RouterError.notFound("group \(groupId)")
|
|
1439
|
-
}
|
|
1440
|
-
DispatchQueue.main.async {
|
|
1441
|
-
WorkspaceManager.shared.launchGroup(group)
|
|
1442
|
-
}
|
|
1443
|
-
return .object(["ok": .bool(true)])
|
|
1444
|
-
}
|
|
1445
|
-
))
|
|
1446
|
-
|
|
1447
|
-
api.register(Endpoint(
|
|
1448
|
-
method: "group.kill",
|
|
1449
|
-
description: "Kill all sessions in a project group",
|
|
1450
|
-
access: .mutate,
|
|
1451
|
-
params: [Param(name: "id", type: "string", required: true, description: "Group identifier")],
|
|
1452
|
-
returns: .ok,
|
|
1453
|
-
handler: { params in
|
|
1454
|
-
guard let groupId = params?["id"]?.stringValue else {
|
|
1455
|
-
throw RouterError.missingParam("id")
|
|
1456
|
-
}
|
|
1457
|
-
guard let group = WorkspaceManager.shared.group(byId: groupId) else {
|
|
1458
|
-
throw RouterError.notFound("group \(groupId)")
|
|
1459
|
-
}
|
|
1460
|
-
WorkspaceManager.shared.killGroup(group)
|
|
1461
|
-
return .object(["ok": .bool(true)])
|
|
1462
|
-
}
|
|
1463
|
-
))
|
|
1464
|
-
|
|
1465
|
-
api.register(Endpoint(
|
|
1466
|
-
method: "projects.scan",
|
|
1467
|
-
description: "Trigger a rescan of project directories",
|
|
1468
|
-
access: .mutate,
|
|
1469
|
-
params: [],
|
|
1470
|
-
returns: .ok,
|
|
1471
|
-
handler: { _ in
|
|
1472
|
-
DispatchQueue.main.async {
|
|
1473
|
-
ProjectScanner.shared.scan()
|
|
1474
|
-
}
|
|
1475
|
-
return .object(["ok": .bool(true)])
|
|
1476
|
-
}
|
|
1477
|
-
))
|
|
1478
|
-
|
|
1479
|
-
api.register(Endpoint(
|
|
1480
|
-
method: "layout.distribute",
|
|
1481
|
-
description: "Distribute windows evenly in a grid, optionally filtered by app or type and constrained to a screen region",
|
|
1482
|
-
access: .mutate,
|
|
1483
|
-
params: [
|
|
1484
|
-
Param(name: "app", type: "string", required: false, description: "Filter to windows of this app (e.g. 'iTerm2')"),
|
|
1485
|
-
Param(name: "type", type: "string", required: false, description: "Filter to an app type (e.g. 'terminal', 'browser', 'editor')"),
|
|
1486
|
-
Param(name: "region", type: "string", required: false, description: "Constrain grid to a screen region (e.g. 'right', 'left', 'top-right'). Uses tile position names."),
|
|
1487
|
-
],
|
|
1488
|
-
returns: .ok,
|
|
1489
|
-
handler: { params in
|
|
1490
|
-
var dict: [String: JSON] = [:]
|
|
1491
|
-
if case .object(let obj) = params {
|
|
1492
|
-
dict = obj
|
|
1493
|
-
}
|
|
1494
|
-
// Explicit filters select the matching scope automatically.
|
|
1495
|
-
if dict["app"] != nil && dict["scope"] == nil {
|
|
1496
|
-
dict["scope"] = .string("app")
|
|
1497
|
-
} else if dict["type"] != nil && dict["scope"] == nil {
|
|
1498
|
-
dict["scope"] = .string("type")
|
|
1499
|
-
} else {
|
|
1500
|
-
dict["scope"] = dict["scope"] ?? .string("visible")
|
|
1501
|
-
}
|
|
1502
|
-
dict["strategy"] = dict["strategy"] ?? .string("balanced")
|
|
1503
|
-
return try Self.executeSpaceOptimization(params: .object(dict))
|
|
1504
|
-
}
|
|
1505
|
-
))
|
|
1506
|
-
|
|
1507
|
-
api.register(Endpoint(
|
|
1508
|
-
method: "space.optimize",
|
|
1509
|
-
description: "Optimize a set of windows using an explicit scope and strategy",
|
|
1510
|
-
access: .mutate,
|
|
1511
|
-
params: [
|
|
1512
|
-
Param(name: "scope", type: "string", required: false, description: "Optimization scope: visible, active-app, active-type, app, type, or selection"),
|
|
1513
|
-
Param(name: "strategy", type: "string", required: false, description: "Optimization strategy: balanced or mosaic"),
|
|
1514
|
-
Param(name: "app", type: "string", required: false, description: "App name for app-scoped optimization"),
|
|
1515
|
-
Param(name: "type", type: "string", required: false, description: "App type for type-scoped optimization"),
|
|
1516
|
-
Param(name: "title", type: "string", required: false, description: "Optional title substring for app-scoped optimization"),
|
|
1517
|
-
Param(name: "windowIds", type: "[uint32]", required: false, description: "Explicit window selection for selection scope"),
|
|
1518
|
-
],
|
|
1519
|
-
returns: .custom("Execution receipt with scope, strategy, resolved windows, and trace"),
|
|
1520
|
-
handler: { params in
|
|
1521
|
-
try Self.executeSpaceOptimization(params: params)
|
|
1522
|
-
}
|
|
1523
|
-
))
|
|
1524
|
-
|
|
1525
|
-
// ── Session Layers ────────────────────────────────────────
|
|
1526
|
-
|
|
1527
|
-
api.model(ApiModel(name: "WindowRef", fields: [
|
|
1528
|
-
Field(name: "id", type: "string", required: true, description: "Stable UUID for this ref"),
|
|
1529
|
-
Field(name: "app", type: "string", required: true, description: "Application name"),
|
|
1530
|
-
Field(name: "contentHint", type: "string", required: false, description: "Title substring hint for matching"),
|
|
1531
|
-
Field(name: "tile", type: "string", required: false, description: "Intended tile position"),
|
|
1532
|
-
Field(name: "display", type: "int", required: false, description: "Intended display index"),
|
|
1533
|
-
Field(name: "wid", type: "int", required: false, description: "Resolved CGWindowID"),
|
|
1534
|
-
Field(name: "pid", type: "int", required: false, description: "Resolved process ID"),
|
|
1535
|
-
Field(name: "title", type: "string", required: false, description: "Resolved window title"),
|
|
1536
|
-
Field(name: "frame", type: "Frame", required: false, description: "Resolved window frame"),
|
|
1537
|
-
]))
|
|
1538
|
-
|
|
1539
|
-
api.model(ApiModel(name: "SessionLayer", fields: [
|
|
1540
|
-
Field(name: "id", type: "string", required: true, description: "Layer UUID"),
|
|
1541
|
-
Field(name: "name", type: "string", required: true, description: "Layer display name"),
|
|
1542
|
-
Field(name: "windows", type: "[WindowRef]", required: true, description: "Window references in this layer"),
|
|
1543
|
-
]))
|
|
1544
|
-
|
|
1545
|
-
api.register(Endpoint(
|
|
1546
|
-
method: "session.layers.create",
|
|
1547
|
-
description: "Create a named session layer with optional window references",
|
|
1548
|
-
access: .mutate,
|
|
1549
|
-
params: [
|
|
1550
|
-
Param(name: "name", type: "string", required: true, description: "Layer name"),
|
|
1551
|
-
Param(name: "windowIds", type: "[uint32]", required: false, description: "Window IDs to include"),
|
|
1552
|
-
Param(name: "windows", type: "[object]", required: false, description: "Window refs as {app, contentHint}"),
|
|
1553
|
-
],
|
|
1554
|
-
returns: .object(model: "SessionLayer"),
|
|
1555
|
-
handler: { params in
|
|
1556
|
-
guard let name = params?["name"]?.stringValue, !name.isEmpty else {
|
|
1557
|
-
throw RouterError.missingParam("name")
|
|
1558
|
-
}
|
|
1559
|
-
var refs: [WindowRef] = []
|
|
1560
|
-
|
|
1561
|
-
// Build refs from windowIds
|
|
1562
|
-
if case .array(let ids) = params?["windowIds"] {
|
|
1563
|
-
for idJson in ids {
|
|
1564
|
-
if let wid = idJson.uint32Value, let entry = DesktopModel.shared.windows[wid] {
|
|
1565
|
-
refs.append(WindowRef(
|
|
1566
|
-
app: entry.app, contentHint: entry.title,
|
|
1567
|
-
wid: entry.wid, pid: entry.pid, title: entry.title, frame: entry.frame
|
|
1568
|
-
))
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// Build refs from windows array
|
|
1574
|
-
if case .array(let winSpecs) = params?["windows"] {
|
|
1575
|
-
for spec in winSpecs {
|
|
1576
|
-
guard let app = spec["app"]?.stringValue else { continue }
|
|
1577
|
-
let hint = spec["contentHint"]?.stringValue
|
|
1578
|
-
var ref = WindowRef(app: app, contentHint: hint)
|
|
1579
|
-
// Try to resolve immediately
|
|
1580
|
-
if let entry = DesktopModel.shared.windowForApp(app: app, title: hint) {
|
|
1581
|
-
ref.wid = entry.wid
|
|
1582
|
-
ref.pid = entry.pid
|
|
1583
|
-
ref.title = entry.title
|
|
1584
|
-
ref.frame = entry.frame
|
|
1585
|
-
}
|
|
1586
|
-
refs.append(ref)
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
let layer = SessionLayerStore.shared.create(name: name, windows: refs)
|
|
1591
|
-
// Update layer tags
|
|
1592
|
-
for ref in refs {
|
|
1593
|
-
if let wid = ref.wid {
|
|
1594
|
-
DesktopModel.shared.assignLayer(wid: wid, layerId: name)
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
return Encoders.sessionLayer(layer)
|
|
1598
|
-
}
|
|
1599
|
-
))
|
|
1600
|
-
|
|
1601
|
-
api.register(Endpoint(
|
|
1602
|
-
method: "session.layers.delete",
|
|
1603
|
-
description: "Delete a session layer by id or name",
|
|
1604
|
-
access: .mutate,
|
|
1605
|
-
params: [
|
|
1606
|
-
Param(name: "id", type: "string", required: false, description: "Layer UUID"),
|
|
1607
|
-
Param(name: "name", type: "string", required: false, description: "Layer name"),
|
|
1608
|
-
],
|
|
1609
|
-
returns: .ok,
|
|
1610
|
-
handler: { params in
|
|
1611
|
-
let store = SessionLayerStore.shared
|
|
1612
|
-
if let id = params?["id"]?.stringValue {
|
|
1613
|
-
store.delete(id: id)
|
|
1614
|
-
} else if let name = params?["name"]?.stringValue, let layer = store.layerByName(name) {
|
|
1615
|
-
store.delete(id: layer.id)
|
|
1616
|
-
} else {
|
|
1617
|
-
throw RouterError.missingParam("id or name")
|
|
1618
|
-
}
|
|
1619
|
-
return .object(["ok": .bool(true)])
|
|
1620
|
-
}
|
|
1621
|
-
))
|
|
1622
|
-
|
|
1623
|
-
api.register(Endpoint(
|
|
1624
|
-
method: "session.layers.list",
|
|
1625
|
-
description: "List all session layers with resolved window info",
|
|
1626
|
-
access: .read,
|
|
1627
|
-
params: [],
|
|
1628
|
-
returns: .custom("Object with 'layers' array and 'activeIndex'"),
|
|
1629
|
-
handler: { _ in
|
|
1630
|
-
let store = SessionLayerStore.shared
|
|
1631
|
-
return .object([
|
|
1632
|
-
"layers": .array(store.layers.map { Encoders.sessionLayer($0) }),
|
|
1633
|
-
"activeIndex": .int(store.activeIndex)
|
|
1634
|
-
])
|
|
1635
|
-
}
|
|
1636
|
-
))
|
|
1637
|
-
|
|
1638
|
-
api.register(Endpoint(
|
|
1639
|
-
method: "session.layers.assign",
|
|
1640
|
-
description: "Add window ref(s) to a session layer",
|
|
1641
|
-
access: .mutate,
|
|
1642
|
-
params: [
|
|
1643
|
-
Param(name: "layerId", type: "string", required: false, description: "Layer UUID"),
|
|
1644
|
-
Param(name: "layerName", type: "string", required: false, description: "Layer name"),
|
|
1645
|
-
Param(name: "wid", type: "uint32", required: false, description: "Single window ID to add"),
|
|
1646
|
-
Param(name: "windowIds", type: "[uint32]", required: false, description: "Multiple window IDs to add"),
|
|
1647
|
-
Param(name: "window", type: "object", required: false, description: "Window ref as {app, contentHint}"),
|
|
1648
|
-
],
|
|
1649
|
-
returns: .ok,
|
|
1650
|
-
handler: { params in
|
|
1651
|
-
let store = SessionLayerStore.shared
|
|
1652
|
-
let layerId: String
|
|
1653
|
-
if let id = params?["layerId"]?.stringValue {
|
|
1654
|
-
layerId = id
|
|
1655
|
-
} else if let name = params?["layerName"]?.stringValue, let layer = store.layerByName(name) {
|
|
1656
|
-
layerId = layer.id
|
|
1657
|
-
} else {
|
|
1658
|
-
throw RouterError.missingParam("layerId or layerName")
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
if let wid = params?["wid"]?.uint32Value {
|
|
1662
|
-
store.assignByWid(wid, toLayerId: layerId)
|
|
1663
|
-
}
|
|
1664
|
-
if case .array(let ids) = params?["windowIds"] {
|
|
1665
|
-
for idJson in ids {
|
|
1666
|
-
if let wid = idJson.uint32Value {
|
|
1667
|
-
store.assignByWid(wid, toLayerId: layerId)
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
if let spec = params?["window"] {
|
|
1672
|
-
if let app = spec["app"]?.stringValue {
|
|
1673
|
-
let hint = spec["contentHint"]?.stringValue
|
|
1674
|
-
var ref = WindowRef(app: app, contentHint: hint)
|
|
1675
|
-
if let entry = DesktopModel.shared.windowForApp(app: app, title: hint) {
|
|
1676
|
-
ref.wid = entry.wid
|
|
1677
|
-
ref.pid = entry.pid
|
|
1678
|
-
ref.title = entry.title
|
|
1679
|
-
ref.frame = entry.frame
|
|
1680
|
-
}
|
|
1681
|
-
store.assign(ref: ref, toLayerId: layerId)
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1684
|
-
return .object(["ok": .bool(true)])
|
|
1685
|
-
}
|
|
1686
|
-
))
|
|
1687
|
-
|
|
1688
|
-
api.register(Endpoint(
|
|
1689
|
-
method: "session.layers.remove",
|
|
1690
|
-
description: "Remove window ref(s) from a session layer",
|
|
1691
|
-
access: .mutate,
|
|
1692
|
-
params: [
|
|
1693
|
-
Param(name: "layerId", type: "string", required: false, description: "Layer UUID"),
|
|
1694
|
-
Param(name: "layerName", type: "string", required: false, description: "Layer name"),
|
|
1695
|
-
Param(name: "refId", type: "string", required: true, description: "WindowRef ID to remove"),
|
|
1696
|
-
],
|
|
1697
|
-
returns: .ok,
|
|
1698
|
-
handler: { params in
|
|
1699
|
-
let store = SessionLayerStore.shared
|
|
1700
|
-
let layerId: String
|
|
1701
|
-
if let id = params?["layerId"]?.stringValue {
|
|
1702
|
-
layerId = id
|
|
1703
|
-
} else if let name = params?["layerName"]?.stringValue, let layer = store.layerByName(name) {
|
|
1704
|
-
layerId = layer.id
|
|
1705
|
-
} else {
|
|
1706
|
-
throw RouterError.missingParam("layerId or layerName")
|
|
1707
|
-
}
|
|
1708
|
-
guard let refId = params?["refId"]?.stringValue else {
|
|
1709
|
-
throw RouterError.missingParam("refId")
|
|
1710
|
-
}
|
|
1711
|
-
store.remove(refId: refId, fromLayerId: layerId)
|
|
1712
|
-
return .object(["ok": .bool(true)])
|
|
1713
|
-
}
|
|
1714
|
-
))
|
|
1715
|
-
|
|
1716
|
-
api.register(Endpoint(
|
|
1717
|
-
method: "session.layers.switch",
|
|
1718
|
-
description: "Switch to a session layer by index or name",
|
|
1719
|
-
access: .mutate,
|
|
1720
|
-
params: [
|
|
1721
|
-
Param(name: "index", type: "int", required: false, description: "Layer index"),
|
|
1722
|
-
Param(name: "name", type: "string", required: false, description: "Layer name"),
|
|
1723
|
-
],
|
|
1724
|
-
returns: .ok,
|
|
1725
|
-
handler: { params in
|
|
1726
|
-
let store = SessionLayerStore.shared
|
|
1727
|
-
let index: Int
|
|
1728
|
-
if let i = params?["index"]?.intValue {
|
|
1729
|
-
index = i
|
|
1730
|
-
} else if let name = params?["name"]?.stringValue,
|
|
1731
|
-
let i = store.layers.firstIndex(where: { $0.name.localizedCaseInsensitiveCompare(name) == .orderedSame }) {
|
|
1732
|
-
index = i
|
|
1733
|
-
} else {
|
|
1734
|
-
throw RouterError.missingParam("index or name")
|
|
1735
|
-
}
|
|
1736
|
-
DispatchQueue.main.async {
|
|
1737
|
-
store.switchTo(index: index)
|
|
1738
|
-
}
|
|
1739
|
-
return .object(["ok": .bool(true)])
|
|
1740
|
-
}
|
|
1741
|
-
))
|
|
1742
|
-
|
|
1743
|
-
api.register(Endpoint(
|
|
1744
|
-
method: "session.layers.rename",
|
|
1745
|
-
description: "Rename a session layer",
|
|
1746
|
-
access: .mutate,
|
|
1747
|
-
params: [
|
|
1748
|
-
Param(name: "id", type: "string", required: false, description: "Layer UUID"),
|
|
1749
|
-
Param(name: "oldName", type: "string", required: false, description: "Current layer name"),
|
|
1750
|
-
Param(name: "name", type: "string", required: true, description: "New layer name"),
|
|
1751
|
-
],
|
|
1752
|
-
returns: .ok,
|
|
1753
|
-
handler: { params in
|
|
1754
|
-
let store = SessionLayerStore.shared
|
|
1755
|
-
guard let newName = params?["name"]?.stringValue, !newName.isEmpty else {
|
|
1756
|
-
throw RouterError.missingParam("name")
|
|
1757
|
-
}
|
|
1758
|
-
if let id = params?["id"]?.stringValue {
|
|
1759
|
-
store.rename(id: id, name: newName)
|
|
1760
|
-
} else if let oldName = params?["oldName"]?.stringValue, let layer = store.layerByName(oldName) {
|
|
1761
|
-
store.rename(id: layer.id, name: newName)
|
|
1762
|
-
} else {
|
|
1763
|
-
throw RouterError.missingParam("id or oldName")
|
|
1764
|
-
}
|
|
1765
|
-
return .object(["ok": .bool(true)])
|
|
1766
|
-
}
|
|
1767
|
-
))
|
|
1768
|
-
|
|
1769
|
-
api.register(Endpoint(
|
|
1770
|
-
method: "session.layers.clear",
|
|
1771
|
-
description: "Clear all session layers",
|
|
1772
|
-
access: .mutate,
|
|
1773
|
-
params: [],
|
|
1774
|
-
returns: .ok,
|
|
1775
|
-
handler: { _ in
|
|
1776
|
-
SessionLayerStore.shared.clear()
|
|
1777
|
-
return .object(["ok": .bool(true)])
|
|
1778
|
-
}
|
|
1779
|
-
))
|
|
1780
|
-
|
|
1781
|
-
// ── Intents ───────────────────────────────────────────────
|
|
1782
|
-
|
|
1783
|
-
api.model(ApiModel(name: "IntentSlot", fields: [
|
|
1784
|
-
Field(name: "name", type: "string", required: true, description: "Slot name"),
|
|
1785
|
-
Field(name: "type", type: "string", required: true, description: "Slot type (string, int, position, query, bool)"),
|
|
1786
|
-
Field(name: "required", type: "bool", required: true, description: "Whether the slot is required"),
|
|
1787
|
-
Field(name: "description", type: "string", required: true, description: "Slot description"),
|
|
1788
|
-
Field(name: "values", type: "[string]", required: false, description: "Allowed values for enum slots"),
|
|
1789
|
-
]))
|
|
1790
|
-
|
|
1791
|
-
api.model(ApiModel(name: "IntentDef", fields: [
|
|
1792
|
-
Field(name: "intent", type: "string", required: true, description: "Intent identifier"),
|
|
1793
|
-
Field(name: "description", type: "string", required: true, description: "What the intent does"),
|
|
1794
|
-
Field(name: "examples", type: "[string]", required: true, description: "Example phrases"),
|
|
1795
|
-
Field(name: "slots", type: "[IntentSlot]", required: true, description: "Named parameters"),
|
|
1796
|
-
]))
|
|
1797
|
-
|
|
1798
|
-
api.register(Endpoint(
|
|
1799
|
-
method: "intents.list",
|
|
1800
|
-
description: "List all available intents with their slots and example phrases",
|
|
1801
|
-
access: .read,
|
|
1802
|
-
params: [],
|
|
1803
|
-
returns: .array(model: "IntentDef"),
|
|
1804
|
-
handler: { _ in
|
|
1805
|
-
IntentEngine.shared.catalog()
|
|
1806
|
-
}
|
|
1807
|
-
))
|
|
1808
|
-
|
|
1809
|
-
api.register(Endpoint(
|
|
1810
|
-
method: "intents.execute",
|
|
1811
|
-
description: "Execute a structured intent (from voice, agent, or script)",
|
|
1812
|
-
access: .mutate,
|
|
1813
|
-
params: [
|
|
1814
|
-
Param(name: "intent", type: "string", required: true, description: "Intent name (e.g. 'tile_window', 'focus', 'launch')"),
|
|
1815
|
-
Param(name: "slots", type: "object", required: false, description: "Named parameters for the intent"),
|
|
1816
|
-
Param(name: "rawText", type: "string", required: false, description: "Original transcription text"),
|
|
1817
|
-
Param(name: "confidence", type: "double", required: false, description: "Transcription confidence (0-1)"),
|
|
1818
|
-
Param(name: "source", type: "string", required: false, description: "Source of the intent (e.g. 'vox', 'siri', 'cli')"),
|
|
1819
|
-
],
|
|
1820
|
-
returns: .custom("Intent-specific result"),
|
|
1821
|
-
handler: { params in
|
|
1822
|
-
guard let intentName = params?["intent"]?.stringValue else {
|
|
1823
|
-
throw RouterError.missingParam("intent")
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
// Extract slots
|
|
1827
|
-
var slots: [String: JSON] = [:]
|
|
1828
|
-
if case .object(let obj) = params?["slots"] {
|
|
1829
|
-
slots = obj
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
let request = IntentRequest(
|
|
1833
|
-
intent: intentName,
|
|
1834
|
-
slots: slots,
|
|
1835
|
-
rawText: params?["rawText"]?.stringValue,
|
|
1836
|
-
confidence: params?["confidence"]?.numericDouble,
|
|
1837
|
-
source: params?["source"]?.stringValue
|
|
1838
|
-
)
|
|
1839
|
-
|
|
1840
|
-
return try IntentEngine.shared.execute(request)
|
|
1841
|
-
}
|
|
1842
|
-
))
|
|
1843
|
-
|
|
1844
|
-
// ── Voice / Audio ─────────────────────────────────────────
|
|
1845
|
-
|
|
1846
|
-
api.register(Endpoint(
|
|
1847
|
-
method: "voice.status",
|
|
1848
|
-
description: "Check audio provider status (e.g. Vox availability)",
|
|
1849
|
-
access: .read,
|
|
1850
|
-
params: [],
|
|
1851
|
-
returns: .custom("Provider status with name and listening state"),
|
|
1852
|
-
handler: { _ in
|
|
1853
|
-
let audio = AudioLayer.shared
|
|
1854
|
-
return .object([
|
|
1855
|
-
"provider": .string(audio.providerName),
|
|
1856
|
-
"available": .bool(audio.provider?.isAvailable ?? false),
|
|
1857
|
-
"listening": .bool(audio.isListening),
|
|
1858
|
-
"lastTranscript": audio.lastTranscript.map { .string($0) } ?? .null
|
|
1859
|
-
])
|
|
1860
|
-
}
|
|
1861
|
-
))
|
|
1862
|
-
|
|
1863
|
-
api.register(Endpoint(
|
|
1864
|
-
method: "voice.listen",
|
|
1865
|
-
description: "Start voice capture via the audio provider (e.g. Vox)",
|
|
1866
|
-
access: .mutate,
|
|
1867
|
-
params: [],
|
|
1868
|
-
returns: .ok,
|
|
1869
|
-
handler: { _ in
|
|
1870
|
-
guard AudioLayer.shared.provider != nil else {
|
|
1871
|
-
throw RouterError.custom("No audio provider available. Is Vox running?")
|
|
1872
|
-
}
|
|
1873
|
-
DispatchQueue.main.async {
|
|
1874
|
-
AudioLayer.shared.startVoiceCommand()
|
|
1875
|
-
}
|
|
1876
|
-
return .object(["ok": .bool(true), "provider": .string(AudioLayer.shared.providerName)])
|
|
1877
|
-
}
|
|
1878
|
-
))
|
|
1879
|
-
|
|
1880
|
-
api.register(Endpoint(
|
|
1881
|
-
method: "voice.stop",
|
|
1882
|
-
description: "Stop voice capture and process the transcription",
|
|
1883
|
-
access: .mutate,
|
|
1884
|
-
params: [],
|
|
1885
|
-
returns: .ok,
|
|
1886
|
-
handler: { _ in
|
|
1887
|
-
DispatchQueue.main.async {
|
|
1888
|
-
AudioLayer.shared.stopVoiceCommand()
|
|
1889
|
-
}
|
|
1890
|
-
return .object(["ok": .bool(true)])
|
|
1891
|
-
}
|
|
1892
|
-
))
|
|
1893
|
-
|
|
1894
|
-
api.register(Endpoint(
|
|
1895
|
-
method: "voice.simulate",
|
|
1896
|
-
description: "Simulate a voice command: parse text into an intent and execute it",
|
|
1897
|
-
access: .mutate,
|
|
1898
|
-
params: [
|
|
1899
|
-
Param(name: "text", type: "string", required: true, description: "Voice command text (as if transcribed)"),
|
|
1900
|
-
Param(name: "execute", type: "bool", required: false, description: "Actually execute the intent (default true)"),
|
|
1901
|
-
],
|
|
1902
|
-
returns: .custom("Parsed intent with execution result"),
|
|
1903
|
-
handler: { params in
|
|
1904
|
-
guard let text = params?["text"]?.stringValue, !text.isEmpty else {
|
|
1905
|
-
throw RouterError.missingParam("text")
|
|
1906
|
-
}
|
|
1907
|
-
let shouldExecute = params?["execute"]?.boolValue ?? true
|
|
1908
|
-
|
|
1909
|
-
let matcher = PhraseMatcher.shared
|
|
1910
|
-
guard let matched = matcher.match(text: text) else {
|
|
1911
|
-
return .object([
|
|
1912
|
-
"parsed": .bool(false),
|
|
1913
|
-
"text": .string(text),
|
|
1914
|
-
"intent": .null,
|
|
1915
|
-
"message": .string("No intent matched")
|
|
1916
|
-
])
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
var response: [String: JSON] = [
|
|
1920
|
-
"parsed": .bool(true),
|
|
1921
|
-
"text": .string(text),
|
|
1922
|
-
"intent": .string(matched.intentName),
|
|
1923
|
-
"slots": .object(matched.slots),
|
|
1924
|
-
"confidence": .double(matched.confidence),
|
|
1925
|
-
]
|
|
1926
|
-
|
|
1927
|
-
if shouldExecute {
|
|
1928
|
-
do {
|
|
1929
|
-
let result = try matcher.execute(matched)
|
|
1930
|
-
response["executed"] = .bool(true)
|
|
1931
|
-
response["result"] = result
|
|
1932
|
-
} catch {
|
|
1933
|
-
response["executed"] = .bool(false)
|
|
1934
|
-
response["error"] = .string(error.localizedDescription)
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
return .object(response)
|
|
1939
|
-
}
|
|
1940
|
-
))
|
|
1941
|
-
|
|
1942
|
-
api.register(Endpoint(
|
|
1943
|
-
method: "voice.reconnect",
|
|
1944
|
-
description: "Force disconnect and reconnect the Vox WebSocket connection",
|
|
1945
|
-
access: .mutate,
|
|
1946
|
-
params: [],
|
|
1947
|
-
returns: .custom("Reconnection initiated with previous and new connection state"),
|
|
1948
|
-
handler: { _ in
|
|
1949
|
-
let client = VoxClient.shared
|
|
1950
|
-
let previousState = "\(client.connectionState)"
|
|
1951
|
-
DispatchQueue.main.async {
|
|
1952
|
-
client.reconnect()
|
|
1953
|
-
}
|
|
1954
|
-
return .object([
|
|
1955
|
-
"ok": .bool(true),
|
|
1956
|
-
"previousState": .string(previousState),
|
|
1957
|
-
"action": .string("reconnecting"),
|
|
1958
|
-
])
|
|
1959
|
-
}
|
|
1960
|
-
))
|
|
1961
|
-
|
|
1962
|
-
// ── Meta endpoint ───────────────────────────────────────
|
|
1963
|
-
|
|
1964
|
-
// ── Mouse Finder ────────────────────────────────────────
|
|
1965
|
-
|
|
1966
|
-
api.register(Endpoint(
|
|
1967
|
-
method: "mouse.find",
|
|
1968
|
-
description: "Show a sonar pulse at the current mouse cursor position",
|
|
1969
|
-
access: .read,
|
|
1970
|
-
params: [],
|
|
1971
|
-
returns: .ok,
|
|
1972
|
-
handler: { _ in
|
|
1973
|
-
DispatchQueue.main.async { MouseFinder.shared.find() }
|
|
1974
|
-
let pos = NSEvent.mouseLocation
|
|
1975
|
-
return .object(["ok": .bool(true), "x": .int(Int(pos.x)), "y": .int(Int(pos.y))])
|
|
1976
|
-
}
|
|
1977
|
-
))
|
|
1978
|
-
|
|
1979
|
-
api.register(Endpoint(
|
|
1980
|
-
method: "mouse.summon",
|
|
1981
|
-
description: "Warp the mouse cursor to screen center (or a given point) and show a sonar pulse",
|
|
1982
|
-
access: .mutate,
|
|
1983
|
-
params: [
|
|
1984
|
-
Param(name: "x", type: "int", required: false, description: "Target X coordinate (screen, bottom-left origin)"),
|
|
1985
|
-
Param(name: "y", type: "int", required: false, description: "Target Y coordinate (screen, bottom-left origin)"),
|
|
1986
|
-
],
|
|
1987
|
-
returns: .ok,
|
|
1988
|
-
handler: { params in
|
|
1989
|
-
let target: CGPoint?
|
|
1990
|
-
if let x = params?["x"]?.intValue, let y = params?["y"]?.intValue {
|
|
1991
|
-
target = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
|
1992
|
-
} else {
|
|
1993
|
-
target = nil
|
|
1994
|
-
}
|
|
1995
|
-
DispatchQueue.main.async { MouseFinder.shared.summon(to: target) }
|
|
1996
|
-
let pos = target ?? {
|
|
1997
|
-
let screen = NSScreen.main ?? NSScreen.screens[0]
|
|
1998
|
-
return CGPoint(x: screen.frame.midX, y: screen.frame.midY)
|
|
1999
|
-
}()
|
|
2000
|
-
return .object(["ok": .bool(true), "x": .int(Int(pos.x)), "y": .int(Int(pos.y))])
|
|
2001
|
-
}
|
|
2002
|
-
))
|
|
2003
|
-
|
|
2004
|
-
api.register(Endpoint(
|
|
2005
|
-
method: "api.schema",
|
|
2006
|
-
description: "Get the full API schema including all methods and models",
|
|
2007
|
-
access: .read,
|
|
2008
|
-
params: [],
|
|
2009
|
-
returns: .custom("Full API schema with version, models, and methods"),
|
|
2010
|
-
handler: { _ in
|
|
2011
|
-
api.schema()
|
|
2012
|
-
}
|
|
2013
|
-
))
|
|
2014
|
-
}
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
private extension LatticesApi {
|
|
2018
|
-
static func publishOverlay(_ params: JSON?) throws -> JSON {
|
|
2019
|
-
guard let params else {
|
|
2020
|
-
throw RouterError.missingParam("kind")
|
|
2021
|
-
}
|
|
2022
|
-
guard let kind = params["kind"]?.stringValue?.lowercased(), !kind.isEmpty else {
|
|
2023
|
-
throw RouterError.missingParam("kind")
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
let id = params["id"]?.stringValue ?? "agent-\(UUID().uuidString)"
|
|
2027
|
-
let style = try parseOverlayStyle(params["style"]?.stringValue)
|
|
2028
|
-
let placement = try parseOverlayPlacement(params["placement"]?.stringValue)
|
|
2029
|
-
let screen = try parseOverlayScreen(params["display"]?.intValue)
|
|
2030
|
-
let point = parseOverlayPoint(params)
|
|
2031
|
-
let opacity = CGFloat(max(0.05, min(params["opacity"]?.numericDouble ?? 1.0, 1.0)))
|
|
2032
|
-
let zIndex = params["zIndex"]?.intValue ?? 500
|
|
2033
|
-
let ttlMs = params["ttlMs"]?.intValue ?? defaultOverlayTTL(for: kind)
|
|
2034
|
-
let expiresAt = ttlMs > 0 ? Date().addingTimeInterval(Double(ttlMs) / 1000.0) : nil
|
|
2035
|
-
let payload: ScreenOverlayPayload
|
|
2036
|
-
|
|
2037
|
-
switch kind {
|
|
2038
|
-
case "toast":
|
|
2039
|
-
let text = try requiredString(params, "text")
|
|
2040
|
-
payload = .toast(ScreenOverlayTextPayload(
|
|
2041
|
-
text: text,
|
|
2042
|
-
detail: params["detail"]?.stringValue,
|
|
2043
|
-
point: point,
|
|
2044
|
-
placement: placement,
|
|
2045
|
-
style: style
|
|
2046
|
-
))
|
|
2047
|
-
case "label":
|
|
2048
|
-
let text = try requiredString(params, "text")
|
|
2049
|
-
payload = .label(ScreenOverlayTextPayload(
|
|
2050
|
-
text: text,
|
|
2051
|
-
detail: params["detail"]?.stringValue,
|
|
2052
|
-
point: point,
|
|
2053
|
-
placement: placement == .top ? .point : placement,
|
|
2054
|
-
style: style
|
|
2055
|
-
))
|
|
2056
|
-
case "highlight":
|
|
2057
|
-
guard let rect = parseOverlayRect(params) else {
|
|
2058
|
-
throw RouterError.custom("highlight requires x, y, w, and h")
|
|
2059
|
-
}
|
|
2060
|
-
payload = .highlight(ScreenOverlayHighlightPayload(
|
|
2061
|
-
rect: rect,
|
|
2062
|
-
label: params["text"]?.stringValue ?? params["label"]?.stringValue,
|
|
2063
|
-
style: style,
|
|
2064
|
-
cornerRadius: CGFloat(params["cornerRadius"]?.numericDouble ?? 10)
|
|
2065
|
-
))
|
|
2066
|
-
case "pet":
|
|
2067
|
-
let glyph = params["glyph"]?.stringValue ?? "✦"
|
|
2068
|
-
payload = .pet(ScreenOverlayPetPayload(
|
|
2069
|
-
glyph: String(glyph.prefix(4)),
|
|
2070
|
-
petID: params["petId"]?.stringValue,
|
|
2071
|
-
state: params["state"]?.stringValue,
|
|
2072
|
-
name: params["name"]?.stringValue,
|
|
2073
|
-
message: params["message"]?.stringValue ?? params["text"]?.stringValue,
|
|
2074
|
-
point: point,
|
|
2075
|
-
placement: placement,
|
|
2076
|
-
style: style,
|
|
2077
|
-
isDragging: false,
|
|
2078
|
-
dismissible: params["dismissible"]?.boolValue ?? true
|
|
2079
|
-
))
|
|
2080
|
-
default:
|
|
2081
|
-
throw RouterError.custom("Unsupported overlay kind: \(kind)")
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
let layer = ScreenOverlayLayerSnapshot(
|
|
2085
|
-
id: ScreenOverlayLayerID(id),
|
|
2086
|
-
owner: .agentApi,
|
|
2087
|
-
screen: screen,
|
|
2088
|
-
zIndex: zIndex,
|
|
2089
|
-
opacity: opacity,
|
|
2090
|
-
payload: payload,
|
|
2091
|
-
expiresAt: expiresAt
|
|
2092
|
-
)
|
|
2093
|
-
|
|
2094
|
-
runOnMain {
|
|
2095
|
-
ScreenOverlayCanvasController.shared.publishLayer(layer)
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
var result: [String: JSON] = [
|
|
2099
|
-
"id": .string(id),
|
|
2100
|
-
"kind": .string(kind),
|
|
2101
|
-
"owner": .string(ScreenOverlayOwner.agentApi.rawValue),
|
|
2102
|
-
]
|
|
2103
|
-
if let expiresAt {
|
|
2104
|
-
result["expiresAt"] = .double(expiresAt.timeIntervalSince1970)
|
|
2105
|
-
}
|
|
2106
|
-
return .object(result)
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
static func clearOverlay(_ params: JSON?) throws -> JSON {
|
|
2110
|
-
let owner = try parseOverlayOwner(params?["owner"]?.stringValue)
|
|
2111
|
-
if let id = params?["id"]?.stringValue, !id.isEmpty {
|
|
2112
|
-
runOnMain {
|
|
2113
|
-
ScreenOverlayCanvasController.shared.removeLayer(id: ScreenOverlayLayerID(id))
|
|
2114
|
-
}
|
|
2115
|
-
} else {
|
|
2116
|
-
runOnMain {
|
|
2117
|
-
ScreenOverlayCanvasController.shared.removeLayers(owner: owner)
|
|
2118
|
-
}
|
|
2119
|
-
}
|
|
2120
|
-
return .object(["ok": .bool(true)])
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
static func publishOverlayActor(_ params: JSON?) throws -> JSON {
|
|
2124
|
-
guard let params else {
|
|
2125
|
-
throw RouterError.missingParam("id")
|
|
2126
|
-
}
|
|
2127
|
-
let id = params["id"]?.stringValue ?? "actor-\(UUID().uuidString)"
|
|
2128
|
-
let renderer = params["renderer"]?.stringValue?.lowercased() ?? "sprite"
|
|
2129
|
-
guard renderer == "sprite" || renderer == "pet" else {
|
|
2130
|
-
throw RouterError.custom("Unsupported overlay actor renderer: \(renderer)")
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
let style = try parseOverlayStyle(params["style"]?.stringValue)
|
|
2134
|
-
let point = parseOverlayPoint(params)
|
|
2135
|
-
let placement = try parseOverlayPlacement(params["placement"]?.stringValue ?? (point == nil ? "bottom" : "point"))
|
|
2136
|
-
let screen = try parseOverlayScreen(params["display"]?.intValue)
|
|
2137
|
-
let opacity = CGFloat(max(0.05, min(params["opacity"]?.numericDouble ?? 1.0, 1.0)))
|
|
2138
|
-
let zIndex = params["zIndex"]?.intValue ?? 520
|
|
2139
|
-
let ttlMs = params["ttlMs"]?.intValue ?? 0
|
|
2140
|
-
let expiresAt = ttlMs > 0 ? Date().addingTimeInterval(Double(ttlMs) / 1000.0) : nil
|
|
2141
|
-
let asset = params["asset"]?.stringValue ?? params["petId"]?.stringValue
|
|
2142
|
-
let message = params["message"]?.stringValue ?? params["text"]?.stringValue
|
|
2143
|
-
|
|
2144
|
-
let layer = ScreenOverlayLayerSnapshot(
|
|
2145
|
-
id: ScreenOverlayLayerID(id),
|
|
2146
|
-
owner: .agentApi,
|
|
2147
|
-
screen: screen,
|
|
2148
|
-
zIndex: zIndex,
|
|
2149
|
-
opacity: opacity,
|
|
2150
|
-
payload: .pet(ScreenOverlayPetPayload(
|
|
2151
|
-
glyph: String((params["glyph"]?.stringValue ?? "✦").prefix(4)),
|
|
2152
|
-
petID: asset,
|
|
2153
|
-
state: params["state"]?.stringValue ?? "idle",
|
|
2154
|
-
name: params["name"]?.stringValue,
|
|
2155
|
-
message: message,
|
|
2156
|
-
point: point,
|
|
2157
|
-
placement: placement,
|
|
2158
|
-
style: style,
|
|
2159
|
-
isDragging: false,
|
|
2160
|
-
dismissible: params["dismissible"]?.boolValue ?? false
|
|
2161
|
-
)),
|
|
2162
|
-
expiresAt: expiresAt
|
|
2163
|
-
)
|
|
2164
|
-
|
|
2165
|
-
runOnMain {
|
|
2166
|
-
ScreenOverlayCanvasController.shared.publishLayer(layer)
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
var result: [String: JSON] = [
|
|
2170
|
-
"id": .string(id),
|
|
2171
|
-
"kind": .string("actor"),
|
|
2172
|
-
"owner": .string(ScreenOverlayOwner.agentApi.rawValue),
|
|
2173
|
-
"renderer": .string(renderer),
|
|
2174
|
-
]
|
|
2175
|
-
if let expiresAt {
|
|
2176
|
-
result["expiresAt"] = .double(expiresAt.timeIntervalSince1970)
|
|
2177
|
-
}
|
|
2178
|
-
return .object(result)
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
static func moveOverlayActor(_ params: JSON?) throws -> JSON {
|
|
2182
|
-
guard let params else {
|
|
2183
|
-
throw RouterError.missingParam("id")
|
|
2184
|
-
}
|
|
2185
|
-
let id = try requiredString(params, "id")
|
|
2186
|
-
guard let x = params["x"]?.numericDouble else {
|
|
2187
|
-
throw RouterError.missingParam("x")
|
|
2188
|
-
}
|
|
2189
|
-
guard let y = params["y"]?.numericDouble else {
|
|
2190
|
-
throw RouterError.missingParam("y")
|
|
2191
|
-
}
|
|
2192
|
-
let durationMs = params["durationMs"]?.intValue ?? 700
|
|
2193
|
-
let easing = params["easing"]?.stringValue
|
|
2194
|
-
var moved = false
|
|
2195
|
-
runOnMain {
|
|
2196
|
-
moved = ScreenOverlayCanvasController.shared.moveLayer(
|
|
2197
|
-
id: ScreenOverlayLayerID(id),
|
|
2198
|
-
to: CGPoint(x: x, y: y),
|
|
2199
|
-
durationMs: durationMs,
|
|
2200
|
-
easing: easing
|
|
2201
|
-
)
|
|
2202
|
-
}
|
|
2203
|
-
guard moved else {
|
|
2204
|
-
throw RouterError.custom("Overlay actor not found or not movable: \(id)")
|
|
2205
|
-
}
|
|
2206
|
-
return .object([
|
|
2207
|
-
"ok": .bool(true),
|
|
2208
|
-
"id": .string(id),
|
|
2209
|
-
"x": .double(x),
|
|
2210
|
-
"y": .double(y),
|
|
2211
|
-
])
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
static func runOnMain(_ work: @escaping () -> Void) {
|
|
2215
|
-
if Thread.isMainThread {
|
|
2216
|
-
work()
|
|
2217
|
-
} else {
|
|
2218
|
-
DispatchQueue.main.sync(execute: work)
|
|
2219
|
-
}
|
|
2220
|
-
}
|
|
2221
|
-
|
|
2222
|
-
static func parseOverlayScreen(_ displayIndex: Int?) throws -> ScreenOverlayScreenTarget {
|
|
2223
|
-
guard let displayIndex else { return .all }
|
|
2224
|
-
guard displayIndex >= 0, displayIndex < NSScreen.screens.count else {
|
|
2225
|
-
throw RouterError.custom("Invalid display index: \(displayIndex)")
|
|
2226
|
-
}
|
|
2227
|
-
return .screen(id: ScreenOverlayCanvasController.screenID(for: NSScreen.screens[displayIndex]))
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
static func parseOverlayPoint(_ params: JSON) -> CGPoint? {
|
|
2231
|
-
guard let x = params["x"]?.numericDouble,
|
|
2232
|
-
let y = params["y"]?.numericDouble else { return nil }
|
|
2233
|
-
return CGPoint(x: x, y: y)
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
static func parseOverlayRect(_ params: JSON) -> CGRect? {
|
|
2237
|
-
guard let x = params["x"]?.numericDouble,
|
|
2238
|
-
let y = params["y"]?.numericDouble,
|
|
2239
|
-
let w = params["w"]?.numericDouble,
|
|
2240
|
-
let h = params["h"]?.numericDouble else { return nil }
|
|
2241
|
-
return CGRect(x: x, y: y, width: w, height: h)
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
static func parseOverlayPlacement(_ value: String?) throws -> ScreenOverlayPlacement {
|
|
2245
|
-
let raw = value?.lowercased() ?? ScreenOverlayPlacement.top.rawValue
|
|
2246
|
-
guard let placement = ScreenOverlayPlacement(rawValue: raw) else {
|
|
2247
|
-
throw RouterError.custom("Unsupported overlay placement: \(raw)")
|
|
2248
|
-
}
|
|
2249
|
-
return placement
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
static func parseOverlayStyle(_ value: String?) throws -> ScreenOverlayStyle {
|
|
2253
|
-
let raw = value?.lowercased() ?? ScreenOverlayStyle.info.rawValue
|
|
2254
|
-
guard let style = ScreenOverlayStyle(rawValue: raw) else {
|
|
2255
|
-
throw RouterError.custom("Unsupported overlay style: \(raw)")
|
|
2256
|
-
}
|
|
2257
|
-
return style
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
static func parseOverlayOwner(_ value: String?) throws -> ScreenOverlayOwner {
|
|
2261
|
-
let raw = value ?? ScreenOverlayOwner.agentApi.rawValue
|
|
2262
|
-
guard let owner = ScreenOverlayOwner(rawValue: raw) else {
|
|
2263
|
-
throw RouterError.custom("Unsupported overlay owner: \(raw)")
|
|
2264
|
-
}
|
|
2265
|
-
return owner
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
static func defaultOverlayTTL(for kind: String) -> Int {
|
|
2269
|
-
switch kind {
|
|
2270
|
-
case "highlight":
|
|
2271
|
-
return 2500
|
|
2272
|
-
case "pet":
|
|
2273
|
-
return 4200
|
|
2274
|
-
default:
|
|
2275
|
-
return 2800
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
static func requiredString(_ params: JSON, _ key: String) throws -> String {
|
|
2280
|
-
guard let value = params[key]?.stringValue, !value.isEmpty else {
|
|
2281
|
-
throw RouterError.missingParam(key)
|
|
2282
|
-
}
|
|
2283
|
-
return value
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
static func decodeDeckActionRequest(from json: JSON?) throws -> DeckActionRequest {
|
|
2287
|
-
guard let json else {
|
|
2288
|
-
throw RouterError.missingParam("actionID")
|
|
2289
|
-
}
|
|
2290
|
-
guard case .object(var object) = json else {
|
|
2291
|
-
throw RouterError.custom("Invalid deck action request: params must be an object")
|
|
2292
|
-
}
|
|
2293
|
-
object["payload"] = object["payload"] ?? .object([:])
|
|
2294
|
-
let data = try JSONEncoder().encode(JSON.object(object))
|
|
2295
|
-
do {
|
|
2296
|
-
return try JSONDecoder().decode(DeckActionRequest.self, from: data)
|
|
2297
|
-
} catch {
|
|
2298
|
-
throw RouterError.custom("Invalid deck action request: \(error.localizedDescription)")
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
static func encodeDeckValue<T: Encodable>(_ value: T) throws -> JSON {
|
|
2303
|
-
let data = try JSONEncoder().encode(value)
|
|
2304
|
-
return try JSONDecoder().decode(JSON.self, from: data)
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
static func parsePlacement(from json: JSON?) -> PlacementSpec? {
|
|
2308
|
-
PlacementSpec(json: json)
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
|
-
static func resolveTargetScreen(for entry: WindowEntry?, displayIndex: Int?) -> NSScreen {
|
|
2312
|
-
if let displayIndex, displayIndex >= 0, displayIndex < NSScreen.screens.count {
|
|
2313
|
-
return NSScreen.screens[displayIndex]
|
|
2314
|
-
}
|
|
2315
|
-
if let entry {
|
|
2316
|
-
return WindowTiler.screenForWindowFrame(entry.frame)
|
|
2317
|
-
}
|
|
2318
|
-
return NSScreen.main ?? NSScreen.screens[0]
|
|
2319
|
-
}
|
|
2320
|
-
|
|
2321
|
-
static func frontmostWindowTarget() -> (wid: UInt32, pid: Int32)? {
|
|
2322
|
-
guard let app = NSWorkspace.shared.frontmostApplication,
|
|
2323
|
-
app.bundleIdentifier != "com.arach.lattices" else {
|
|
2324
|
-
return nil
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
let appRef = AXUIElementCreateApplication(app.processIdentifier)
|
|
2328
|
-
var focusedRef: CFTypeRef?
|
|
2329
|
-
guard AXUIElementCopyAttributeValue(appRef, kAXFocusedWindowAttribute as CFString, &focusedRef) == .success,
|
|
2330
|
-
let focusedWindow = focusedRef else {
|
|
2331
|
-
return nil
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
var wid: CGWindowID = 0
|
|
2335
|
-
guard _AXUIElementGetWindow(focusedWindow as! AXUIElement, &wid) == .success else {
|
|
2336
|
-
return nil
|
|
2337
|
-
}
|
|
2338
|
-
return (UInt32(wid), app.processIdentifier)
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
static func executeWindowPlacement(params: JSON?) throws -> JSON {
|
|
2342
|
-
guard let placement = parsePlacement(from: params?["placement"] ?? params?["position"]) else {
|
|
2343
|
-
throw RouterError.missingParam("placement")
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
let displayIndex = params?["display"]?.intValue
|
|
2347
|
-
var trace: [JSON] = []
|
|
2348
|
-
|
|
2349
|
-
if let wid = params?["wid"]?.uint32Value {
|
|
2350
|
-
guard let entry = DesktopModel.shared.windows[wid] else {
|
|
2351
|
-
throw RouterError.notFound("window \(wid)")
|
|
2352
|
-
}
|
|
2353
|
-
let screen = resolveTargetScreen(for: entry, displayIndex: displayIndex)
|
|
2354
|
-
trace.append(.string("resolved target by wid"))
|
|
2355
|
-
trace.append(.string("placement \(placement.wireValue)"))
|
|
2356
|
-
DispatchQueue.main.async {
|
|
2357
|
-
WindowTiler.tileWindowById(wid: wid, pid: entry.pid, to: placement, on: screen)
|
|
2358
|
-
}
|
|
2359
|
-
return .object([
|
|
2360
|
-
"ok": .bool(true),
|
|
2361
|
-
"target": .string("wid"),
|
|
2362
|
-
"wid": .int(Int(wid)),
|
|
2363
|
-
"app": .string(entry.app),
|
|
2364
|
-
"placement": placement.jsonValue,
|
|
2365
|
-
"trace": .array(trace),
|
|
2366
|
-
])
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
if let session = params?["session"]?.stringValue {
|
|
2370
|
-
let screen = resolveTargetScreen(
|
|
2371
|
-
for: DesktopModel.shared.windowForSession(session),
|
|
2372
|
-
displayIndex: displayIndex
|
|
2373
|
-
)
|
|
2374
|
-
trace.append(.string("resolved target by session"))
|
|
2375
|
-
trace.append(.string("placement \(placement.wireValue)"))
|
|
2376
|
-
|
|
2377
|
-
if let entry = DesktopModel.shared.windowForSession(session) {
|
|
2378
|
-
DispatchQueue.main.async {
|
|
2379
|
-
WindowTiler.tileWindowById(wid: entry.wid, pid: entry.pid, to: placement, on: screen)
|
|
2380
|
-
}
|
|
2381
|
-
return .object([
|
|
2382
|
-
"ok": .bool(true),
|
|
2383
|
-
"target": .string("session"),
|
|
2384
|
-
"session": .string(session),
|
|
2385
|
-
"wid": .int(Int(entry.wid)),
|
|
2386
|
-
"placement": placement.jsonValue,
|
|
2387
|
-
"trace": .array(trace),
|
|
2388
|
-
])
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
let terminal = Preferences.shared.terminal
|
|
2392
|
-
trace.append(.string("session window not in DesktopModel; using terminal fallback"))
|
|
2393
|
-
DispatchQueue.main.async {
|
|
2394
|
-
WindowTiler.tile(session: session, terminal: terminal, to: placement, on: screen)
|
|
2395
|
-
}
|
|
2396
|
-
return .object([
|
|
2397
|
-
"ok": .bool(true),
|
|
2398
|
-
"target": .string("session"),
|
|
2399
|
-
"session": .string(session),
|
|
2400
|
-
"placement": placement.jsonValue,
|
|
2401
|
-
"trace": .array(trace),
|
|
2402
|
-
])
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
if let app = params?["app"]?.stringValue {
|
|
2406
|
-
let title = params?["title"]?.stringValue
|
|
2407
|
-
guard let entry = DesktopModel.shared.windowForApp(app: app, title: title) else {
|
|
2408
|
-
throw RouterError.notFound("window for app \(app)")
|
|
2409
|
-
}
|
|
2410
|
-
let screen = resolveTargetScreen(for: entry, displayIndex: displayIndex)
|
|
2411
|
-
trace.append(.string("resolved target by app/title match"))
|
|
2412
|
-
trace.append(.string("placement \(placement.wireValue)"))
|
|
2413
|
-
DispatchQueue.main.async {
|
|
2414
|
-
WindowTiler.tileWindowById(wid: entry.wid, pid: entry.pid, to: placement, on: screen)
|
|
2415
|
-
}
|
|
2416
|
-
return .object([
|
|
2417
|
-
"ok": .bool(true),
|
|
2418
|
-
"target": .string("app"),
|
|
2419
|
-
"app": .string(entry.app),
|
|
2420
|
-
"wid": .int(Int(entry.wid)),
|
|
2421
|
-
"placement": placement.jsonValue,
|
|
2422
|
-
"trace": .array(trace),
|
|
2423
|
-
])
|
|
2424
|
-
}
|
|
2425
|
-
|
|
2426
|
-
if let target = frontmostWindowTarget() {
|
|
2427
|
-
let wid = target.wid
|
|
2428
|
-
let entry = DesktopModel.shared.windows[wid]
|
|
2429
|
-
let screen = resolveTargetScreen(for: entry, displayIndex: displayIndex)
|
|
2430
|
-
trace.append(.string("resolved target by frontmost window"))
|
|
2431
|
-
trace.append(.string("placement \(placement.wireValue)"))
|
|
2432
|
-
DispatchQueue.main.async {
|
|
2433
|
-
WindowTiler.tileWindowById(wid: wid, pid: target.pid, to: placement, on: screen)
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
var response: [String: JSON] = [
|
|
2437
|
-
"ok": .bool(true),
|
|
2438
|
-
"target": .string("frontmost"),
|
|
2439
|
-
"wid": .int(Int(wid)),
|
|
2440
|
-
"placement": placement.jsonValue,
|
|
2441
|
-
"trace": .array(trace),
|
|
2442
|
-
]
|
|
2443
|
-
if let entry {
|
|
2444
|
-
response["app"] = .string(entry.app)
|
|
2445
|
-
}
|
|
2446
|
-
return .object(response)
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
|
-
throw RouterError.custom("Could not resolve a window target for placement")
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
static func executeLayerActivation(params: JSON?) throws -> JSON {
|
|
2453
|
-
let wm = WorkspaceManager.shared
|
|
2454
|
-
guard let layers = wm.config?.layers, !layers.isEmpty else {
|
|
2455
|
-
throw RouterError.notFound("workspace layers")
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
let index: Int
|
|
2459
|
-
var trace: [JSON] = []
|
|
2460
|
-
|
|
2461
|
-
if let value = params?["index"]?.intValue {
|
|
2462
|
-
index = value
|
|
2463
|
-
trace.append(.string("resolved layer by index"))
|
|
2464
|
-
} else if let name = params?["name"]?.stringValue, let value = wm.layerIndex(named: name) {
|
|
2465
|
-
index = value
|
|
2466
|
-
trace.append(.string("resolved layer by name"))
|
|
2467
|
-
} else {
|
|
2468
|
-
throw RouterError.missingParam("index or name")
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
guard index >= 0, index < layers.count else {
|
|
2472
|
-
throw RouterError.notFound("layer \(index)")
|
|
2473
|
-
}
|
|
2474
|
-
|
|
2475
|
-
let mode = try parseLayerActivationMode(params?["mode"]?.stringValue)
|
|
2476
|
-
let layer = layers[index]
|
|
2477
|
-
let previousIndex = wm.activeLayerIndex
|
|
2478
|
-
trace.append(.string("activation mode \(mode)"))
|
|
2479
|
-
|
|
2480
|
-
DispatchQueue.main.async {
|
|
2481
|
-
switch mode {
|
|
2482
|
-
case "focus":
|
|
2483
|
-
wm.focusLayer(index: index)
|
|
2484
|
-
case "retile":
|
|
2485
|
-
wm.tileLayer(index: index, launch: false, force: true)
|
|
2486
|
-
default:
|
|
2487
|
-
wm.tileLayer(index: index, launch: true, force: true)
|
|
2488
|
-
}
|
|
2489
|
-
|
|
2490
|
-
if previousIndex != index || mode != "focus" {
|
|
2491
|
-
EventBus.shared.post(.layerSwitched(index: index))
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
return .object([
|
|
2496
|
-
"ok": .bool(true),
|
|
2497
|
-
"index": .int(index),
|
|
2498
|
-
"id": .string(layer.id),
|
|
2499
|
-
"label": .string(layer.label),
|
|
2500
|
-
"mode": .string(mode),
|
|
2501
|
-
"trace": .array(trace),
|
|
2502
|
-
])
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
static func defaultSpaceName(for index: Int) -> String {
|
|
2506
|
-
if let layers = WorkspaceManager.shared.config?.layers,
|
|
2507
|
-
layers.indices.contains(index - 1) {
|
|
2508
|
-
return layers[index - 1].label
|
|
2509
|
-
}
|
|
2510
|
-
|
|
2511
|
-
let defaults = ["main", "code", "chat", "review", "media", "notes", "ops", "admin", "scratch"]
|
|
2512
|
-
if defaults.indices.contains(index - 1) {
|
|
2513
|
-
return defaults[index - 1]
|
|
2514
|
-
}
|
|
2515
|
-
return "space \(index)"
|
|
2516
|
-
}
|
|
2517
|
-
|
|
2518
|
-
static func executeSpaceOptimization(params: JSON?) throws -> JSON {
|
|
2519
|
-
let scope = try parseOptimizationScope(from: params)
|
|
2520
|
-
let strategy = try parseOptimizationStrategy(params?["strategy"]?.stringValue)
|
|
2521
|
-
var trace: [JSON] = [.string("resolved scope \(scope)"), .string("resolved strategy \(strategy)")]
|
|
2522
|
-
let windows = resolveOptimizationTargets(scope: scope, params: params, trace: &trace)
|
|
2523
|
-
|
|
2524
|
-
// Resolve optional region constraint (e.g. "right" → right half of screen)
|
|
2525
|
-
var region: (CGFloat, CGFloat, CGFloat, CGFloat)? = nil
|
|
2526
|
-
if let regionStr = params?["region"]?.stringValue {
|
|
2527
|
-
if let spec = PlacementSpec(string: regionStr) {
|
|
2528
|
-
region = spec.fractions
|
|
2529
|
-
trace.append(.string("region \(regionStr) → fractions \(spec.fractions)"))
|
|
2530
|
-
} else {
|
|
2531
|
-
trace.append(.string("unknown region \(regionStr), using full screen"))
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
if strategy == "mosaic" {
|
|
2536
|
-
trace.append(.string("strategy mosaic currently uses the smart-grid distributor"))
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
guard !windows.isEmpty else {
|
|
2540
|
-
trace.append(.string("no eligible windows resolved"))
|
|
2541
|
-
return .object([
|
|
2542
|
-
"ok": .bool(true),
|
|
2543
|
-
"scope": .string(scope),
|
|
2544
|
-
"strategy": .string(strategy),
|
|
2545
|
-
"windowCount": .int(0),
|
|
2546
|
-
"wids": .array([]),
|
|
2547
|
-
"trace": .array(trace),
|
|
2548
|
-
])
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
let targets = windows.map { (wid: $0.wid, pid: $0.pid) }
|
|
2552
|
-
DispatchQueue.main.async {
|
|
2553
|
-
WindowTiler.batchRaiseAndDistribute(windows: targets, region: region)
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
return .object([
|
|
2557
|
-
"ok": .bool(true),
|
|
2558
|
-
"scope": .string(scope),
|
|
2559
|
-
"strategy": .string(strategy),
|
|
2560
|
-
"windowCount": .int(windows.count),
|
|
2561
|
-
"wids": .array(windows.map { .int(Int($0.wid)) }),
|
|
2562
|
-
"trace": .array(trace),
|
|
2563
|
-
])
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
static func parseLayerActivationMode(_ raw: String?) throws -> String {
|
|
2567
|
-
let mode = normalizeToken(raw ?? "launch")
|
|
2568
|
-
switch mode {
|
|
2569
|
-
case "launch", "focus", "retile":
|
|
2570
|
-
return mode
|
|
2571
|
-
default:
|
|
2572
|
-
throw RouterError.custom("Unsupported layer activation mode: \(raw ?? mode)")
|
|
2573
|
-
}
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
static func parseOptimizationScope(from params: JSON?) throws -> String {
|
|
2577
|
-
if params?["windowIds"] != nil {
|
|
2578
|
-
return "selection"
|
|
2579
|
-
}
|
|
2580
|
-
if params?["app"] != nil {
|
|
2581
|
-
return "app"
|
|
2582
|
-
}
|
|
2583
|
-
if params?["type"] != nil {
|
|
2584
|
-
return "type"
|
|
2585
|
-
}
|
|
2586
|
-
|
|
2587
|
-
let scope = normalizeToken(params?["scope"]?.stringValue ?? "visible")
|
|
2588
|
-
switch scope {
|
|
2589
|
-
case "visible", "selection", "app", "type",
|
|
2590
|
-
"active-app", "frontmost-app", "current-app",
|
|
2591
|
-
"active-type", "frontmost-type", "current-type":
|
|
2592
|
-
return scope
|
|
2593
|
-
default:
|
|
2594
|
-
throw RouterError.custom("Unsupported optimization scope: \(params?["scope"]?.stringValue ?? scope)")
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
|
|
2598
|
-
static func parseOptimizationStrategy(_ raw: String?) throws -> String {
|
|
2599
|
-
let strategy = normalizeToken(raw ?? "balanced")
|
|
2600
|
-
switch strategy {
|
|
2601
|
-
case "balanced", "mosaic":
|
|
2602
|
-
return strategy
|
|
2603
|
-
default:
|
|
2604
|
-
throw RouterError.custom("Unsupported optimization strategy: \(raw ?? strategy)")
|
|
2605
|
-
}
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
static func resolveOptimizationTargets(scope: String, params: JSON?, trace: inout [JSON]) -> [WindowEntry] {
|
|
2609
|
-
let visible = distributableWindows()
|
|
2610
|
-
let titleFilter = params?["title"]?.stringValue
|
|
2611
|
-
|
|
2612
|
-
switch scope {
|
|
2613
|
-
case "selection":
|
|
2614
|
-
let ids = selectedWindowIds(from: params?["windowIds"])
|
|
2615
|
-
trace.append(.string("selection size \(ids.count)"))
|
|
2616
|
-
return dedupeWindows(visible.filter { ids.contains($0.wid) })
|
|
2617
|
-
|
|
2618
|
-
case "app":
|
|
2619
|
-
guard let app = params?["app"]?.stringValue else {
|
|
2620
|
-
trace.append(.string("missing app for app scope"))
|
|
2621
|
-
return []
|
|
2622
|
-
}
|
|
2623
|
-
trace.append(.string("filtered by app \(app)"))
|
|
2624
|
-
if let titleFilter {
|
|
2625
|
-
trace.append(.string("title contains \(titleFilter)"))
|
|
2626
|
-
}
|
|
2627
|
-
return dedupeWindows(visible.filter {
|
|
2628
|
-
$0.app.localizedCaseInsensitiveCompare(app) == .orderedSame &&
|
|
2629
|
-
(titleFilter == nil || $0.title.localizedCaseInsensitiveContains(titleFilter!))
|
|
2630
|
-
})
|
|
2631
|
-
|
|
2632
|
-
case "type":
|
|
2633
|
-
guard let typeName = params?["type"]?.stringValue,
|
|
2634
|
-
let appType = parseOptimizationAppType(typeName) else {
|
|
2635
|
-
trace.append(.string("missing or unknown type for type scope"))
|
|
2636
|
-
return []
|
|
2637
|
-
}
|
|
2638
|
-
trace.append(.string("filtered by type \(appType.rawValue)"))
|
|
2639
|
-
if let titleFilter {
|
|
2640
|
-
trace.append(.string("title contains \(titleFilter)"))
|
|
2641
|
-
}
|
|
2642
|
-
return dedupeWindows(visible.filter {
|
|
2643
|
-
AppTypeClassifier.matches($0.app, type: appType) &&
|
|
2644
|
-
(titleFilter == nil || $0.title.localizedCaseInsensitiveContains(titleFilter!))
|
|
2645
|
-
})
|
|
2646
|
-
|
|
2647
|
-
case "active-app", "frontmost-app", "current-app":
|
|
2648
|
-
let activeApp = params?["app"]?.stringValue ?? frontmostOptimizableApp()
|
|
2649
|
-
guard let activeApp else {
|
|
2650
|
-
trace.append(.string("no active app available"))
|
|
2651
|
-
return []
|
|
2652
|
-
}
|
|
2653
|
-
trace.append(.string("resolved active app \(activeApp)"))
|
|
2654
|
-
if let titleFilter {
|
|
2655
|
-
trace.append(.string("title contains \(titleFilter)"))
|
|
2656
|
-
}
|
|
2657
|
-
return dedupeWindows(visible.filter {
|
|
2658
|
-
$0.app.localizedCaseInsensitiveCompare(activeApp) == .orderedSame &&
|
|
2659
|
-
(titleFilter == nil || $0.title.localizedCaseInsensitiveContains(titleFilter!))
|
|
2660
|
-
})
|
|
2661
|
-
|
|
2662
|
-
case "active-type", "frontmost-type", "current-type":
|
|
2663
|
-
let activeApp = params?["app"]?.stringValue ?? frontmostOptimizableApp()
|
|
2664
|
-
guard let activeApp else {
|
|
2665
|
-
trace.append(.string("no active app available"))
|
|
2666
|
-
return []
|
|
2667
|
-
}
|
|
2668
|
-
let grouping = AppTypeClassifier.grouping(for: activeApp)
|
|
2669
|
-
trace.append(.string("resolved active type \(grouping.label) from \(activeApp)"))
|
|
2670
|
-
if let titleFilter {
|
|
2671
|
-
trace.append(.string("title contains \(titleFilter)"))
|
|
2672
|
-
}
|
|
2673
|
-
return dedupeWindows(visible.filter {
|
|
2674
|
-
AppTypeClassifier.matches($0.app, grouping: grouping) &&
|
|
2675
|
-
(titleFilter == nil || $0.title.localizedCaseInsensitiveContains(titleFilter!))
|
|
2676
|
-
})
|
|
2677
|
-
|
|
2678
|
-
default:
|
|
2679
|
-
trace.append(.string("using visible window scope"))
|
|
2680
|
-
return dedupeWindows(visible)
|
|
2681
|
-
}
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
static func parseOptimizationAppType(_ raw: String) -> AppType? {
|
|
2685
|
-
let normalized = normalizeToken(raw)
|
|
2686
|
-
return AppType.allCases.first { $0.rawValue == normalized }
|
|
2687
|
-
}
|
|
2688
|
-
|
|
2689
|
-
static func selectedWindowIds(from json: JSON?) -> [UInt32] {
|
|
2690
|
-
guard case .array(let values) = json else { return [] }
|
|
2691
|
-
return values.compactMap(\.uint32Value)
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
static func distributableWindows() -> [WindowEntry] {
|
|
2695
|
-
DesktopModel.shared.allWindows().filter { entry in
|
|
2696
|
-
entry.isOnScreen &&
|
|
2697
|
-
entry.app != "Lattices" &&
|
|
2698
|
-
entry.frame.w > 50 &&
|
|
2699
|
-
entry.frame.h > 50
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
static func dedupeWindows(_ windows: [WindowEntry]) -> [WindowEntry] {
|
|
2704
|
-
var seen: Set<UInt32> = []
|
|
2705
|
-
var result: [WindowEntry] = []
|
|
2706
|
-
for window in windows where !seen.contains(window.wid) {
|
|
2707
|
-
seen.insert(window.wid)
|
|
2708
|
-
result.append(window)
|
|
2709
|
-
}
|
|
2710
|
-
return result
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
|
-
static func frontmostOptimizableApp() -> String? {
|
|
2714
|
-
if let app = NSWorkspace.shared.frontmostApplication?.localizedName,
|
|
2715
|
-
!app.localizedCaseInsensitiveContains("lattices") {
|
|
2716
|
-
return app
|
|
2717
|
-
}
|
|
2718
|
-
return distributableWindows().first?.app
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
static func normalizeToken(_ raw: String) -> String {
|
|
2722
|
-
raw
|
|
2723
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
2724
|
-
.lowercased()
|
|
2725
|
-
.replacingOccurrences(of: "_", with: "-")
|
|
2726
|
-
.replacingOccurrences(of: " ", with: "-")
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
|
|
2730
|
-
// MARK: - Encoders
|
|
2731
|
-
|
|
2732
|
-
enum Encoders {
|
|
2733
|
-
static func window(_ w: WindowEntry) -> JSON {
|
|
2734
|
-
var obj: [String: JSON] = [
|
|
2735
|
-
"wid": .int(Int(w.wid)),
|
|
2736
|
-
"app": .string(w.app),
|
|
2737
|
-
"pid": .int(Int(w.pid)),
|
|
2738
|
-
"title": .string(w.title),
|
|
2739
|
-
"frame": .object([
|
|
2740
|
-
"x": .double(w.frame.x),
|
|
2741
|
-
"y": .double(w.frame.y),
|
|
2742
|
-
"w": .double(w.frame.w),
|
|
2743
|
-
"h": .double(w.frame.h)
|
|
2744
|
-
]),
|
|
2745
|
-
"spaceIds": .array(w.spaceIds.map { .int($0) }),
|
|
2746
|
-
"isOnScreen": .bool(w.isOnScreen),
|
|
2747
|
-
"axVerified": .bool(w.axVerified)
|
|
2748
|
-
]
|
|
2749
|
-
if let session = w.latticesSession {
|
|
2750
|
-
obj["latticesSession"] = .string(session)
|
|
2751
|
-
}
|
|
2752
|
-
if let layerTag = DesktopModel.shared.windowLayerTags[w.wid] {
|
|
2753
|
-
obj["layerTag"] = .string(layerTag)
|
|
2754
|
-
}
|
|
2755
|
-
return .object(obj)
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
static func session(_ s: TmuxSession) -> JSON {
|
|
2759
|
-
.object([
|
|
2760
|
-
"name": .string(s.name),
|
|
2761
|
-
"windowCount": .int(s.windowCount),
|
|
2762
|
-
"attached": .bool(s.attached),
|
|
2763
|
-
"panes": .array(s.panes.map { pane in
|
|
2764
|
-
.object([
|
|
2765
|
-
"id": .string(pane.id),
|
|
2766
|
-
"windowIndex": .int(pane.windowIndex),
|
|
2767
|
-
"windowName": .string(pane.windowName),
|
|
2768
|
-
"title": .string(pane.title),
|
|
2769
|
-
"currentCommand": .string(pane.currentCommand),
|
|
2770
|
-
"pid": .int(pane.pid),
|
|
2771
|
-
"isActive": .bool(pane.isActive)
|
|
2772
|
-
])
|
|
2773
|
-
})
|
|
2774
|
-
])
|
|
2775
|
-
}
|
|
2776
|
-
|
|
2777
|
-
static func process(_ e: ProcessModel.Enrichment) -> JSON {
|
|
2778
|
-
var obj: [String: JSON] = [
|
|
2779
|
-
"pid": .int(e.process.pid),
|
|
2780
|
-
"ppid": .int(e.process.ppid),
|
|
2781
|
-
"command": .string(e.process.comm),
|
|
2782
|
-
"args": .string(e.process.args),
|
|
2783
|
-
"tty": .string(e.process.tty),
|
|
2784
|
-
]
|
|
2785
|
-
if let cwd = e.process.cwd { obj["cwd"] = .string(cwd) }
|
|
2786
|
-
if let s = e.tmuxSession { obj["tmuxSession"] = .string(s) }
|
|
2787
|
-
if let p = e.tmuxPaneId { obj["tmuxPaneId"] = .string(p) }
|
|
2788
|
-
if let w = e.windowId { obj["windowId"] = .int(Int(w)) }
|
|
2789
|
-
return .object(obj)
|
|
2790
|
-
}
|
|
2791
|
-
|
|
2792
|
-
static func paneChild(_ entry: ProcessEntry) -> JSON {
|
|
2793
|
-
var obj: [String: JSON] = [
|
|
2794
|
-
"pid": .int(entry.pid),
|
|
2795
|
-
"command": .string(entry.comm),
|
|
2796
|
-
"args": .string(entry.args),
|
|
2797
|
-
]
|
|
2798
|
-
if let cwd = entry.cwd { obj["cwd"] = .string(cwd) }
|
|
2799
|
-
return .object(obj)
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
static func terminalInstance(_ inst: TerminalInstance) -> JSON {
|
|
2803
|
-
var obj: [String: JSON] = [
|
|
2804
|
-
"tty": .string(inst.tty),
|
|
2805
|
-
"isActiveTab": .bool(inst.isActiveTab),
|
|
2806
|
-
"hasClaude": .bool(inst.hasClaude),
|
|
2807
|
-
"displayName": .string(inst.displayName),
|
|
2808
|
-
"processes": .array(inst.processes.map { entry in
|
|
2809
|
-
var p: [String: JSON] = [
|
|
2810
|
-
"pid": .int(entry.pid),
|
|
2811
|
-
"ppid": .int(entry.ppid),
|
|
2812
|
-
"command": .string(entry.comm),
|
|
2813
|
-
"args": .string(entry.args),
|
|
2814
|
-
"tty": .string(entry.tty),
|
|
2815
|
-
]
|
|
2816
|
-
if let cwd = entry.cwd { p["cwd"] = .string(cwd) }
|
|
2817
|
-
return .object(p)
|
|
2818
|
-
}),
|
|
2819
|
-
]
|
|
2820
|
-
if let app = inst.app { obj["app"] = .string(app.rawValue) }
|
|
2821
|
-
if let wi = inst.windowIndex { obj["windowIndex"] = .int(wi) }
|
|
2822
|
-
if let ti = inst.tabIndex { obj["tabIndex"] = .int(ti) }
|
|
2823
|
-
if let title = inst.tabTitle { obj["tabTitle"] = .string(title) }
|
|
2824
|
-
if let sid = inst.terminalSessionId { obj["terminalSessionId"] = .string(sid) }
|
|
2825
|
-
if let pid = inst.shellPid { obj["shellPid"] = .int(pid) }
|
|
2826
|
-
if let cwd = inst.cwd { obj["cwd"] = .string(cwd) }
|
|
2827
|
-
if let s = inst.tmuxSession { obj["tmuxSession"] = .string(s) }
|
|
2828
|
-
if let p = inst.tmuxPaneId { obj["tmuxPaneId"] = .string(p) }
|
|
2829
|
-
if let w = inst.windowId { obj["windowId"] = .int(Int(w)) }
|
|
2830
|
-
if let t = inst.windowTitle { obj["windowTitle"] = .string(t) }
|
|
2831
|
-
return .object(obj)
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
static func ocrResult(_ r: OcrWindowResult) -> JSON {
|
|
2835
|
-
.object([
|
|
2836
|
-
"wid": .int(Int(r.wid)),
|
|
2837
|
-
"app": .string(r.app),
|
|
2838
|
-
"title": .string(r.title),
|
|
2839
|
-
"frame": .object([
|
|
2840
|
-
"x": .double(r.frame.x),
|
|
2841
|
-
"y": .double(r.frame.y),
|
|
2842
|
-
"w": .double(r.frame.w),
|
|
2843
|
-
"h": .double(r.frame.h)
|
|
2844
|
-
]),
|
|
2845
|
-
"fullText": .string(r.fullText),
|
|
2846
|
-
"blocks": .array(r.texts.map { block in
|
|
2847
|
-
.object([
|
|
2848
|
-
"text": .string(block.text),
|
|
2849
|
-
"confidence": .double(Double(block.confidence)),
|
|
2850
|
-
"x": .double(block.boundingBox.origin.x),
|
|
2851
|
-
"y": .double(block.boundingBox.origin.y),
|
|
2852
|
-
"w": .double(block.boundingBox.size.width),
|
|
2853
|
-
"h": .double(block.boundingBox.size.height)
|
|
2854
|
-
])
|
|
2855
|
-
}),
|
|
2856
|
-
"timestamp": .double(r.timestamp.timeIntervalSince1970),
|
|
2857
|
-
"source": .string(r.source.rawValue)
|
|
2858
|
-
])
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
static func ocrSearchResult(_ r: OcrSearchResult) -> JSON {
|
|
2862
|
-
.object([
|
|
2863
|
-
"id": .int(Int(r.id)),
|
|
2864
|
-
"wid": .int(Int(r.wid)),
|
|
2865
|
-
"app": .string(r.app),
|
|
2866
|
-
"title": .string(r.title),
|
|
2867
|
-
"frame": .object([
|
|
2868
|
-
"x": .double(r.frame.x),
|
|
2869
|
-
"y": .double(r.frame.y),
|
|
2870
|
-
"w": .double(r.frame.w),
|
|
2871
|
-
"h": .double(r.frame.h)
|
|
2872
|
-
]),
|
|
2873
|
-
"fullText": .string(r.fullText),
|
|
2874
|
-
"snippet": .string(r.snippet),
|
|
2875
|
-
"timestamp": .double(r.timestamp.timeIntervalSince1970),
|
|
2876
|
-
"source": .string(r.source.rawValue)
|
|
2877
|
-
])
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
static func enrichedSession(_ s: TmuxSession) -> JSON {
|
|
2881
|
-
let pm = ProcessModel.shared
|
|
2882
|
-
return .object([
|
|
2883
|
-
"name": .string(s.name),
|
|
2884
|
-
"windowCount": .int(s.windowCount),
|
|
2885
|
-
"attached": .bool(s.attached),
|
|
2886
|
-
"panes": .array(s.panes.map { pane in
|
|
2887
|
-
let children = pm.interestingDescendants(of: pane.pid)
|
|
2888
|
-
var obj: [String: JSON] = [
|
|
2889
|
-
"id": .string(pane.id),
|
|
2890
|
-
"windowIndex": .int(pane.windowIndex),
|
|
2891
|
-
"windowName": .string(pane.windowName),
|
|
2892
|
-
"title": .string(pane.title),
|
|
2893
|
-
"currentCommand": .string(pane.currentCommand),
|
|
2894
|
-
"pid": .int(pane.pid),
|
|
2895
|
-
"isActive": .bool(pane.isActive),
|
|
2896
|
-
]
|
|
2897
|
-
if !children.isEmpty {
|
|
2898
|
-
obj["children"] = .array(children.map { Encoders.paneChild($0) })
|
|
2899
|
-
}
|
|
2900
|
-
return .object(obj)
|
|
2901
|
-
})
|
|
2902
|
-
])
|
|
2903
|
-
}
|
|
2904
|
-
|
|
2905
|
-
static func project(_ p: Project) -> JSON {
|
|
2906
|
-
var obj: [String: JSON] = [
|
|
2907
|
-
"path": .string(p.path),
|
|
2908
|
-
"name": .string(p.name),
|
|
2909
|
-
"sessionName": .string(p.sessionName),
|
|
2910
|
-
"isRunning": .bool(p.isRunning),
|
|
2911
|
-
"hasConfig": .bool(p.hasConfig),
|
|
2912
|
-
"paneCount": .int(p.paneCount),
|
|
2913
|
-
"paneNames": .array(p.paneNames.map { .string($0) })
|
|
2914
|
-
]
|
|
2915
|
-
if let cmd = p.devCommand { obj["devCommand"] = .string(cmd) }
|
|
2916
|
-
if let pm = p.packageManager { obj["packageManager"] = .string(pm) }
|
|
2917
|
-
return .object(obj)
|
|
2918
|
-
}
|
|
2919
|
-
|
|
2920
|
-
static func windowRef(_ ref: WindowRef) -> JSON {
|
|
2921
|
-
var obj: [String: JSON] = [
|
|
2922
|
-
"id": .string(ref.id),
|
|
2923
|
-
"app": .string(ref.app),
|
|
2924
|
-
]
|
|
2925
|
-
if let hint = ref.contentHint { obj["contentHint"] = .string(hint) }
|
|
2926
|
-
if let tile = ref.tile { obj["tile"] = .string(tile) }
|
|
2927
|
-
if let display = ref.display { obj["display"] = .int(display) }
|
|
2928
|
-
if let wid = ref.wid { obj["wid"] = .int(Int(wid)) }
|
|
2929
|
-
if let pid = ref.pid { obj["pid"] = .int(Int(pid)) }
|
|
2930
|
-
if let title = ref.title { obj["title"] = .string(title) }
|
|
2931
|
-
if let frame = ref.frame {
|
|
2932
|
-
obj["frame"] = .object([
|
|
2933
|
-
"x": .double(frame.x), "y": .double(frame.y),
|
|
2934
|
-
"w": .double(frame.w), "h": .double(frame.h)
|
|
2935
|
-
])
|
|
2936
|
-
}
|
|
2937
|
-
return .object(obj)
|
|
2938
|
-
}
|
|
2939
|
-
|
|
2940
|
-
static func sessionLayer(_ layer: SessionLayer) -> JSON {
|
|
2941
|
-
.object([
|
|
2942
|
-
"id": .string(layer.id),
|
|
2943
|
-
"name": .string(layer.name),
|
|
2944
|
-
"windows": .array(layer.windows.map { windowRef($0) })
|
|
2945
|
-
])
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// MARK: - Errors
|
|
2950
|
-
|
|
2951
|
-
enum RouterError: LocalizedError {
|
|
2952
|
-
case unknownMethod(String)
|
|
2953
|
-
case missingParam(String)
|
|
2954
|
-
case notFound(String)
|
|
2955
|
-
case custom(String)
|
|
2956
|
-
|
|
2957
|
-
var errorDescription: String? {
|
|
2958
|
-
switch self {
|
|
2959
|
-
case .unknownMethod(let m): return "Unknown method: \(m)"
|
|
2960
|
-
case .missingParam(let p): return "Missing parameter: \(p)"
|
|
2961
|
-
case .notFound(let what): return "Not found: \(what)"
|
|
2962
|
-
case .custom(let msg): return msg
|
|
2963
|
-
}
|
|
2964
|
-
}
|
|
2965
|
-
}
|