@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,224 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import SwiftUI
|
|
3
|
-
import Combine
|
|
4
|
-
import ScreenCaptureKit
|
|
5
|
-
|
|
6
|
-
final class PermissionChecker: ObservableObject {
|
|
7
|
-
static let shared = PermissionChecker()
|
|
8
|
-
|
|
9
|
-
@Published var accessibility: Bool = false
|
|
10
|
-
@Published var screenRecording: Bool = false
|
|
11
|
-
|
|
12
|
-
private var pollTimer: Timer?
|
|
13
|
-
private var hasLoggedInitial = false
|
|
14
|
-
|
|
15
|
-
var allGranted: Bool { accessibility && screenRecording }
|
|
16
|
-
|
|
17
|
-
var isSimulatingMissingPermissions: Bool {
|
|
18
|
-
CommandLine.arguments.contains("--simulate-missing-permissions")
|
|
19
|
-
|| UserDefaults.standard.bool(forKey: "permissions.simulateMissing")
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/// Check current permission state without prompting.
|
|
23
|
-
func check(pollIfMissing: Bool = false) {
|
|
24
|
-
let diag = DiagnosticLog.shared
|
|
25
|
-
|
|
26
|
-
let realAX = AXIsProcessTrusted()
|
|
27
|
-
let realSR = CGPreflightScreenCaptureAccess()
|
|
28
|
-
let simulating = isSimulatingMissingPermissions
|
|
29
|
-
let ax = simulating ? false : realAX
|
|
30
|
-
let sr = simulating ? false : realSR
|
|
31
|
-
|
|
32
|
-
// First check: log identity info only
|
|
33
|
-
if !hasLoggedInitial {
|
|
34
|
-
hasLoggedInitial = true
|
|
35
|
-
let bundleId = Bundle.main.bundleIdentifier ?? "<no bundle id>"
|
|
36
|
-
let execPath = Bundle.main.executablePath ?? ProcessInfo.processInfo.arguments.first ?? "<unknown>"
|
|
37
|
-
let pid = ProcessInfo.processInfo.processIdentifier
|
|
38
|
-
diag.info("PermissionChecker: bundleId=\(bundleId) pid=\(pid)")
|
|
39
|
-
diag.info("PermissionChecker: exec=\(execPath)")
|
|
40
|
-
diag.info("AXIsProcessTrusted() → \(realAX)")
|
|
41
|
-
diag.info("CGPreflightScreenCaptureAccess() → \(realSR)")
|
|
42
|
-
if simulating {
|
|
43
|
-
diag.warn("PermissionChecker: simulating missing permissions for UX preview")
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Log on state changes
|
|
48
|
-
if ax != accessibility || sr != screenRecording {
|
|
49
|
-
diag.info("Permissions: Accessibility \(ax ? "✓" : "✗"), Screen Recording \(sr ? "✓" : "✗")")
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
accessibility = ax
|
|
53
|
-
screenRecording = sr
|
|
54
|
-
|
|
55
|
-
// Only poll after an intentional permission request. A passive launch-time
|
|
56
|
-
// check should not keep nudging macOS privacy state in the background.
|
|
57
|
-
if allGranted {
|
|
58
|
-
stopPolling()
|
|
59
|
-
} else if pollIfMissing {
|
|
60
|
-
startPolling()
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/// Request Accessibility permission — shows the system dialog if not yet granted,
|
|
65
|
-
/// which adds lattices to the Accessibility list and asks the user to toggle it on.
|
|
66
|
-
func requestAccessibility() {
|
|
67
|
-
let diag = DiagnosticLog.shared
|
|
68
|
-
if isSimulatingMissingPermissions {
|
|
69
|
-
diag.warn("requestAccessibility: skipped because missing-permission simulation is enabled")
|
|
70
|
-
accessibility = false
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
let beforeCheck = AXIsProcessTrusted()
|
|
74
|
-
diag.info("requestAccessibility: before=\(beforeCheck), prompting…")
|
|
75
|
-
let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
|
|
76
|
-
let result = AXIsProcessTrustedWithOptions(opts)
|
|
77
|
-
diag.info("AXIsProcessTrustedWithOptions(prompt) → \(result)")
|
|
78
|
-
accessibility = result
|
|
79
|
-
if !result {
|
|
80
|
-
diag.warn("Accessibility not granted — opening System Settings. Toggle ON in Privacy → Accessibility.")
|
|
81
|
-
openAccessibilitySettings()
|
|
82
|
-
startPolling()
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/// Request Screen Recording permission — triggers the system prompt on first call,
|
|
87
|
-
/// which adds lattices to the Screen Recording list. The user toggles it on in Settings.
|
|
88
|
-
func requestScreenRecording() {
|
|
89
|
-
let diag = DiagnosticLog.shared
|
|
90
|
-
if isSimulatingMissingPermissions {
|
|
91
|
-
diag.warn("requestScreenRecording: skipped because missing-permission simulation is enabled")
|
|
92
|
-
screenRecording = false
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
let beforeCheck = CGPreflightScreenCaptureAccess()
|
|
96
|
-
diag.info("requestScreenRecording: before=\(beforeCheck), probing…")
|
|
97
|
-
|
|
98
|
-
// On newer macOS releases TCC no longer reliably prompts for screen capture
|
|
99
|
-
// through the legacy CoreGraphics request API. Warm up ScreenCaptureKit first,
|
|
100
|
-
// then fall back to opening System Settings if access is still denied.
|
|
101
|
-
if #available(macOS 15.0, *) {
|
|
102
|
-
NSApp.activate(ignoringOtherApps: true)
|
|
103
|
-
Task { @MainActor [weak self] in
|
|
104
|
-
guard let self else { return }
|
|
105
|
-
|
|
106
|
-
let shareableProbe = await self.probeScreenCaptureShareableContent()
|
|
107
|
-
diag.info("ScreenCaptureKit shareable probe → \(shareableProbe)")
|
|
108
|
-
|
|
109
|
-
if #available(macOS 15.2, *) {
|
|
110
|
-
let afterShareable = CGPreflightScreenCaptureAccess()
|
|
111
|
-
if !afterShareable {
|
|
112
|
-
let screenshotProbe = await self.probeScreenCaptureScreenshot()
|
|
113
|
-
diag.info("ScreenCaptureKit screenshot probe → \(screenshotProbe)")
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let afterCheck = CGPreflightScreenCaptureAccess()
|
|
118
|
-
diag.info("requestScreenRecording: after=\(afterCheck)")
|
|
119
|
-
self.screenRecording = afterCheck
|
|
120
|
-
|
|
121
|
-
if !afterCheck {
|
|
122
|
-
diag.warn("Screen capture not granted — opening System Settings. On newer macOS versions this may require enabling Lattices in Privacy → Screen & System Audio Recording.")
|
|
123
|
-
self.openScreenRecordingSettings()
|
|
124
|
-
self.startPolling()
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
diag.info("requestScreenRecording: using legacy CoreGraphics request API")
|
|
131
|
-
let result = CGRequestScreenCaptureAccess()
|
|
132
|
-
diag.info("CGRequestScreenCaptureAccess() → \(result)")
|
|
133
|
-
screenRecording = result
|
|
134
|
-
if !result {
|
|
135
|
-
diag.warn("Screen capture not granted — opening System Settings. Toggle ON in Privacy → Screen Recording.")
|
|
136
|
-
openScreenRecordingSettings()
|
|
137
|
-
startPolling()
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/// Opens System Settings → Privacy & Security → Accessibility
|
|
142
|
-
func openAccessibilitySettings() {
|
|
143
|
-
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
|
144
|
-
NSWorkspace.shared.open(url)
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// Opens System Settings → Privacy & Security → Screen Recording
|
|
149
|
-
func openScreenRecordingSettings() {
|
|
150
|
-
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
|
|
151
|
-
NSWorkspace.shared.open(url)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/// Opens System Settings → Privacy & Security → Automation.
|
|
156
|
-
func openAutomationSettings() {
|
|
157
|
-
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Automation") {
|
|
158
|
-
NSWorkspace.shared.open(url)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/// Opens System Settings → Privacy & Security → Input Monitoring.
|
|
163
|
-
func openInputMonitoringSettings() {
|
|
164
|
-
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent") {
|
|
165
|
-
NSWorkspace.shared.open(url)
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
@available(macOS 15.0, *)
|
|
170
|
-
private func probeScreenCaptureShareableContent() async -> String {
|
|
171
|
-
await withCheckedContinuation { continuation in
|
|
172
|
-
SCShareableContent.getExcludingDesktopWindows(true, onScreenWindowsOnly: true) { content, error in
|
|
173
|
-
if let error {
|
|
174
|
-
continuation.resume(returning: "error \(Self.describe(error))")
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
let windows = content?.windows.count ?? 0
|
|
179
|
-
let displays = content?.displays.count ?? 0
|
|
180
|
-
let apps = content?.applications.count ?? 0
|
|
181
|
-
continuation.resume(returning: "ok windows=\(windows) displays=\(displays) apps=\(apps)")
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
@available(macOS 15.2, *)
|
|
187
|
-
private func probeScreenCaptureScreenshot() async -> String {
|
|
188
|
-
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
189
|
-
return await withCheckedContinuation { continuation in
|
|
190
|
-
SCScreenshotManager.captureImage(in: rect) { _, error in
|
|
191
|
-
if let error {
|
|
192
|
-
continuation.resume(returning: "error \(Self.describe(error))")
|
|
193
|
-
} else {
|
|
194
|
-
continuation.resume(returning: "ok")
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
private static func describe(_ error: Error) -> String {
|
|
201
|
-
let nsError = error as NSError
|
|
202
|
-
if nsError.localizedDescription.isEmpty {
|
|
203
|
-
return "\(nsError.domain)#\(nsError.code)"
|
|
204
|
-
}
|
|
205
|
-
return "\(nsError.domain)#\(nsError.code) \(nsError.localizedDescription)"
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// MARK: - Polling
|
|
209
|
-
|
|
210
|
-
/// Poll every 2 seconds to detect permission changes made in System Settings.
|
|
211
|
-
private func startPolling() {
|
|
212
|
-
guard pollTimer == nil else { return }
|
|
213
|
-
pollTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
|
|
214
|
-
DispatchQueue.main.async {
|
|
215
|
-
self?.check()
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private func stopPolling() {
|
|
221
|
-
pollTimer?.invalidate()
|
|
222
|
-
pollTimer = nil
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
final class ProcessModel: ObservableObject {
|
|
4
|
-
static let shared = ProcessModel()
|
|
5
|
-
|
|
6
|
-
@Published private(set) var processTable: [Int: ProcessEntry] = [:]
|
|
7
|
-
@Published private(set) var childrenMap: [Int: [Int]] = [:] // ppid → [child pids]
|
|
8
|
-
@Published private(set) var interesting: [ProcessEntry] = []
|
|
9
|
-
|
|
10
|
-
private var timer: DispatchSourceTimer?
|
|
11
|
-
private var lastInterestingPids: Set<Int> = []
|
|
12
|
-
|
|
13
|
-
// Terminal tab cache — refreshed lazily when terminals are queried
|
|
14
|
-
private var cachedTerminalTabs: [TerminalTab] = []
|
|
15
|
-
private var lastTabQueryTime: Date = .distantPast
|
|
16
|
-
private static let tabCacheTTL: TimeInterval = 300.0 // 5 minutes
|
|
17
|
-
|
|
18
|
-
/// Background queue for process polling — avoids blocking the main thread
|
|
19
|
-
/// with posix_spawn calls (waitUntilExit deadlocks on macOS 26 main run loop).
|
|
20
|
-
private let pollQueue = DispatchQueue(label: "lattices.process-poll", qos: .userInitiated)
|
|
21
|
-
|
|
22
|
-
func start(interval: TimeInterval = 5.0) {
|
|
23
|
-
guard timer == nil else { return }
|
|
24
|
-
DiagnosticLog.shared.info("ProcessModel: starting (interval=\(interval)s)")
|
|
25
|
-
|
|
26
|
-
let source = DispatchSource.makeTimerSource(queue: pollQueue)
|
|
27
|
-
source.schedule(deadline: .now(), repeating: interval)
|
|
28
|
-
source.setEventHandler { [weak self] in
|
|
29
|
-
self?.poll()
|
|
30
|
-
}
|
|
31
|
-
source.resume()
|
|
32
|
-
timer = source
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
func stop() {
|
|
36
|
-
timer?.cancel()
|
|
37
|
-
timer = nil
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// MARK: - Query Methods
|
|
41
|
-
|
|
42
|
-
/// All interesting developer processes with CWDs resolved.
|
|
43
|
-
func interestingProcesses() -> [ProcessEntry] {
|
|
44
|
-
interesting
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/// BFS walk all descendants of a given PID.
|
|
48
|
-
func descendants(of pid: Int) -> [ProcessEntry] {
|
|
49
|
-
var result: [ProcessEntry] = []
|
|
50
|
-
var queue = childrenMap[pid] ?? []
|
|
51
|
-
var visited: Set<Int> = [pid]
|
|
52
|
-
|
|
53
|
-
while !queue.isEmpty {
|
|
54
|
-
let childPid = queue.removeFirst()
|
|
55
|
-
guard !visited.contains(childPid) else { continue }
|
|
56
|
-
visited.insert(childPid)
|
|
57
|
-
if let entry = processTable[childPid] {
|
|
58
|
-
result.append(entry)
|
|
59
|
-
}
|
|
60
|
-
if let grandchildren = childrenMap[childPid] {
|
|
61
|
-
queue.append(contentsOf: grandchildren)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// BFS descendants filtered to interesting commands only.
|
|
68
|
-
func interestingDescendants(of pid: Int) -> [ProcessEntry] {
|
|
69
|
-
descendants(of: pid).filter { ProcessQuery.interestingCommands.contains($0.comm) }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// MARK: - Enrichment
|
|
73
|
-
|
|
74
|
-
struct Enrichment {
|
|
75
|
-
let process: ProcessEntry
|
|
76
|
-
let tmuxSession: String?
|
|
77
|
-
let tmuxPaneId: String?
|
|
78
|
-
let windowId: UInt32?
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/// Walk ppid chain from a process upward until we find a tmux pane_pid.
|
|
82
|
-
/// Returns (sessionName, paneId) or nil.
|
|
83
|
-
func tmuxLinkage(for entry: ProcessEntry) -> (session: String, paneId: String)? {
|
|
84
|
-
let paneLookup = buildPaneLookup()
|
|
85
|
-
var current = entry.pid
|
|
86
|
-
// Walk up at most 10 hops (typically 2-3)
|
|
87
|
-
for _ in 0..<10 {
|
|
88
|
-
if let match = paneLookup[current] {
|
|
89
|
-
return match
|
|
90
|
-
}
|
|
91
|
-
guard let parent = processTable[current]?.ppid, parent != current, parent > 1 else {
|
|
92
|
-
break
|
|
93
|
-
}
|
|
94
|
-
current = parent
|
|
95
|
-
}
|
|
96
|
-
return nil
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/// Enrich a single process with tmux + window linkage.
|
|
100
|
-
func enrich(_ entry: ProcessEntry) -> Enrichment {
|
|
101
|
-
if let link = tmuxLinkage(for: entry) {
|
|
102
|
-
let win = DesktopModel.shared.windowForSession(link.session)
|
|
103
|
-
return Enrichment(
|
|
104
|
-
process: entry,
|
|
105
|
-
tmuxSession: link.session,
|
|
106
|
-
tmuxPaneId: link.paneId,
|
|
107
|
-
windowId: win?.wid
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
return Enrichment(process: entry, tmuxSession: nil, tmuxPaneId: nil, windowId: nil)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/// Enrich all interesting processes.
|
|
114
|
-
func enrichedProcesses() -> [Enrichment] {
|
|
115
|
-
interesting.map { enrich($0) }
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// MARK: - Terminal Synthesis (on-demand)
|
|
119
|
-
|
|
120
|
-
/// Synthesize terminal instances on demand. Merges the current process table,
|
|
121
|
-
/// tmux sessions, terminal tabs (cached), and window list into a unified view.
|
|
122
|
-
/// Called by API endpoints — no background polling needed.
|
|
123
|
-
func synthesizeTerminals() -> [TerminalInstance] {
|
|
124
|
-
// Refresh tab cache if stale
|
|
125
|
-
let now = Date()
|
|
126
|
-
if now.timeIntervalSince(lastTabQueryTime) >= Self.tabCacheTTL {
|
|
127
|
-
cachedTerminalTabs = TerminalQuery.queryAll()
|
|
128
|
-
lastTabQueryTime = now
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return TerminalSynthesizer.synthesize(
|
|
132
|
-
processTable: processTable,
|
|
133
|
-
interesting: interesting,
|
|
134
|
-
tmuxSessions: TmuxModel.shared.sessions,
|
|
135
|
-
terminalTabs: cachedTerminalTabs,
|
|
136
|
-
windows: DesktopModel.shared.windows
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/// Force-refresh the terminal tab cache (e.g. on first query or explicit refresh).
|
|
141
|
-
func refreshTerminalTabs() {
|
|
142
|
-
cachedTerminalTabs = TerminalQuery.queryAll()
|
|
143
|
-
lastTabQueryTime = Date()
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// MARK: - Polling (runs on pollQueue)
|
|
147
|
-
|
|
148
|
-
func poll() {
|
|
149
|
-
// 1. Full process snapshot
|
|
150
|
-
var table = ProcessQuery.snapshot()
|
|
151
|
-
|
|
152
|
-
// 2. Build parent → children map
|
|
153
|
-
var children: [Int: [Int]] = [:]
|
|
154
|
-
for (pid, entry) in table {
|
|
155
|
-
children[entry.ppid, default: []].append(pid)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// 3. Filter interesting, batch-resolve CWDs
|
|
159
|
-
let interestingEntries = ProcessQuery.filterInteresting(table)
|
|
160
|
-
let pids = interestingEntries.map(\.pid)
|
|
161
|
-
let cwds = ProcessQuery.batchCWD(pids: pids)
|
|
162
|
-
|
|
163
|
-
// 4. Merge CWDs back into table
|
|
164
|
-
for (pid, cwd) in cwds {
|
|
165
|
-
table[pid]?.cwd = cwd
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
let freshInteresting = pids.compactMap { table[$0] }
|
|
169
|
-
let freshPidSet = Set(pids)
|
|
170
|
-
|
|
171
|
-
// 5. Detect change
|
|
172
|
-
let changed = freshPidSet != lastInterestingPids
|
|
173
|
-
|
|
174
|
-
DispatchQueue.main.async {
|
|
175
|
-
self.processTable = table
|
|
176
|
-
self.childrenMap = children
|
|
177
|
-
self.interesting = freshInteresting
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
lastInterestingPids = freshPidSet
|
|
181
|
-
|
|
182
|
-
if changed {
|
|
183
|
-
EventBus.shared.post(.processesChanged(interesting: Array(freshPidSet)))
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// MARK: - Private
|
|
188
|
-
|
|
189
|
-
/// Build [pane_pid: (sessionName, paneId)] from current TmuxModel state.
|
|
190
|
-
private func buildPaneLookup() -> [Int: (session: String, paneId: String)] {
|
|
191
|
-
var lookup: [Int: (session: String, paneId: String)] = [:]
|
|
192
|
-
for session in TmuxModel.shared.sessions {
|
|
193
|
-
for pane in session.panes {
|
|
194
|
-
lookup[pane.pid] = (session: session.name, paneId: pane.id)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return lookup
|
|
198
|
-
}
|
|
199
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
// MARK: - Data Models
|
|
4
|
-
|
|
5
|
-
struct ProcessEntry {
|
|
6
|
-
let pid: Int
|
|
7
|
-
let ppid: Int
|
|
8
|
-
let pgid: Int
|
|
9
|
-
let tty: String // "ttys003" or "??"
|
|
10
|
-
let comm: String // basename, e.g. "node"
|
|
11
|
-
let args: String // full command line
|
|
12
|
-
var cwd: String? // filled by batchCWD
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// MARK: - Query
|
|
16
|
-
|
|
17
|
-
enum ProcessQuery {
|
|
18
|
-
|
|
19
|
-
/// Process names we care about for developer workspace enrichment
|
|
20
|
-
static let interestingCommands: Set<String> = [
|
|
21
|
-
"claude", "node", "bun", "deno", "python", "python3",
|
|
22
|
-
"ruby", "go", "cargo", "nvim", "vim", "npm", "npx",
|
|
23
|
-
"pnpm", "swift", "make", "git"
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
/// Snapshot the full process table in a single `ps` call.
|
|
27
|
-
/// Returns [pid: ProcessEntry].
|
|
28
|
-
static func snapshot() -> [Int: ProcessEntry] {
|
|
29
|
-
let raw = shell([
|
|
30
|
-
"/bin/ps", "-eo", "pid,ppid,pgid,tty,comm,args"
|
|
31
|
-
])
|
|
32
|
-
guard !raw.isEmpty else { return [:] }
|
|
33
|
-
|
|
34
|
-
var table: [Int: ProcessEntry] = [:]
|
|
35
|
-
let lines = raw.split(separator: "\n", omittingEmptySubsequences: true)
|
|
36
|
-
|
|
37
|
-
for line in lines.dropFirst() { // skip header
|
|
38
|
-
let str = String(line)
|
|
39
|
-
// Columns are whitespace-separated; args can contain spaces.
|
|
40
|
-
// Format: " PID PPID PGID TTY COMM ARGS"
|
|
41
|
-
let trimmed = str.trimmingCharacters(in: .whitespaces)
|
|
42
|
-
let parts = trimmed.split(separator: " ", maxSplits: 5, omittingEmptySubsequences: true)
|
|
43
|
-
guard parts.count >= 6 else { continue }
|
|
44
|
-
|
|
45
|
-
guard let pid = Int(parts[0]),
|
|
46
|
-
let ppid = Int(parts[1]),
|
|
47
|
-
let pgid = Int(parts[2]) else { continue }
|
|
48
|
-
|
|
49
|
-
let tty = String(parts[3])
|
|
50
|
-
let commFull = String(parts[4])
|
|
51
|
-
let args = String(parts[5])
|
|
52
|
-
|
|
53
|
-
// comm from ps is the full path; take basename
|
|
54
|
-
let comm = (commFull as NSString).lastPathComponent
|
|
55
|
-
|
|
56
|
-
table[pid] = ProcessEntry(
|
|
57
|
-
pid: pid, ppid: ppid, pgid: pgid,
|
|
58
|
-
tty: tty, comm: comm, args: args, cwd: nil
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return table
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// Batch-resolve working directories for a set of PIDs via a single `lsof` call.
|
|
66
|
-
/// Returns [pid: cwdPath].
|
|
67
|
-
static func batchCWD(pids: [Int]) -> [Int: String] {
|
|
68
|
-
guard !pids.isEmpty else { return [:] }
|
|
69
|
-
|
|
70
|
-
let pidList = pids.map(String.init).joined(separator: ",")
|
|
71
|
-
let raw = shell([
|
|
72
|
-
"/usr/sbin/lsof", "-a", "-d", "cwd", "-p", pidList, "-Fn"
|
|
73
|
-
])
|
|
74
|
-
guard !raw.isEmpty else { return [:] }
|
|
75
|
-
|
|
76
|
-
var result: [Int: String] = [:]
|
|
77
|
-
var currentPid: Int?
|
|
78
|
-
|
|
79
|
-
for line in raw.split(separator: "\n", omittingEmptySubsequences: true) {
|
|
80
|
-
let s = String(line)
|
|
81
|
-
if s.hasPrefix("p") {
|
|
82
|
-
currentPid = Int(s.dropFirst())
|
|
83
|
-
} else if s.hasPrefix("n"), let pid = currentPid {
|
|
84
|
-
result[pid] = String(s.dropFirst())
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return result
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/// Filter a process table down to interesting developer processes.
|
|
92
|
-
static func filterInteresting(_ table: [Int: ProcessEntry]) -> [ProcessEntry] {
|
|
93
|
-
table.values.filter { interestingCommands.contains($0.comm) }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// MARK: - Shell helper
|
|
97
|
-
|
|
98
|
-
/// Run a command and capture stdout using posix_spawn + waitpid.
|
|
99
|
-
/// Avoids Process/NSTask's waitUntilExit() which deadlocks on macOS 26
|
|
100
|
-
/// when called from GUI apps (CFRunLoop issue).
|
|
101
|
-
static func shell(_ args: [String]) -> String {
|
|
102
|
-
// Set up stdout pipe
|
|
103
|
-
var pipeFds: [Int32] = [0, 0]
|
|
104
|
-
guard pipe(&pipeFds) == 0 else { return "" }
|
|
105
|
-
|
|
106
|
-
// File actions: stdout → write end of pipe, stderr → /dev/null
|
|
107
|
-
var fileActions: posix_spawn_file_actions_t?
|
|
108
|
-
posix_spawn_file_actions_init(&fileActions)
|
|
109
|
-
posix_spawn_file_actions_adddup2(&fileActions, pipeFds[1], STDOUT_FILENO)
|
|
110
|
-
posix_spawn_file_actions_addopen(&fileActions, STDERR_FILENO, "/dev/null", O_WRONLY, 0)
|
|
111
|
-
posix_spawn_file_actions_addclose(&fileActions, pipeFds[0])
|
|
112
|
-
posix_spawn_file_actions_addclose(&fileActions, pipeFds[1])
|
|
113
|
-
|
|
114
|
-
// Build C strings
|
|
115
|
-
let cPath = args[0]
|
|
116
|
-
let cArgs = args.map { strdup($0) } + [nil]
|
|
117
|
-
defer { cArgs.compactMap({ $0 }).forEach { free($0) } }
|
|
118
|
-
|
|
119
|
-
var pid: pid_t = 0
|
|
120
|
-
let spawnResult = cPath.withCString { path in
|
|
121
|
-
posix_spawn(&pid, path, &fileActions, nil, cArgs, environ)
|
|
122
|
-
}
|
|
123
|
-
posix_spawn_file_actions_destroy(&fileActions)
|
|
124
|
-
|
|
125
|
-
// Close write end in parent
|
|
126
|
-
close(pipeFds[1])
|
|
127
|
-
|
|
128
|
-
guard spawnResult == 0 else {
|
|
129
|
-
close(pipeFds[0])
|
|
130
|
-
return ""
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Read all stdout
|
|
134
|
-
var data = Data()
|
|
135
|
-
let bufSize = 65536
|
|
136
|
-
var buf = [UInt8](repeating: 0, count: bufSize)
|
|
137
|
-
while true {
|
|
138
|
-
let n = read(pipeFds[0], &buf, bufSize)
|
|
139
|
-
if n <= 0 { break }
|
|
140
|
-
data.append(buf, count: n)
|
|
141
|
-
}
|
|
142
|
-
close(pipeFds[0])
|
|
143
|
-
|
|
144
|
-
// Wait for child
|
|
145
|
-
var status: Int32 = 0
|
|
146
|
-
waitpid(pid, &status, 0)
|
|
147
|
-
|
|
148
|
-
guard status == 0 else { return "" }
|
|
149
|
-
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
150
|
-
}
|
|
151
|
-
}
|