@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,111 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
|
|
3
|
-
// Private API: get CGWindowID from an AXUIElement (already declared in WindowTiler.swift)
|
|
4
|
-
// We reuse the same _AXUIElementGetWindow binding.
|
|
5
|
-
|
|
6
|
-
struct AXTextResult {
|
|
7
|
-
let wid: UInt32
|
|
8
|
-
let texts: [String]
|
|
9
|
-
let fullText: String
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
final class AccessibilityTextExtractor {
|
|
13
|
-
static let timeoutSeconds: TimeInterval = 0.2
|
|
14
|
-
static let maxDepth: Int = 4
|
|
15
|
-
static let maxChildrenPerNode: Int = 30
|
|
16
|
-
|
|
17
|
-
/// Extract text from a window's AX element tree.
|
|
18
|
-
/// Returns nil if AX fails or yields fewer than `minChars` characters.
|
|
19
|
-
func extract(pid: Int32, wid: UInt32, minChars: Int = 12) -> AXTextResult? {
|
|
20
|
-
let appRef = AXUIElementCreateApplication(pid)
|
|
21
|
-
|
|
22
|
-
// Find the AXUIElement matching this wid
|
|
23
|
-
var windowsRef: CFTypeRef?
|
|
24
|
-
let err = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &windowsRef)
|
|
25
|
-
guard err == .success, let axWindows = windowsRef as? [AXUIElement] else { return nil }
|
|
26
|
-
|
|
27
|
-
var targetWindow: AXUIElement?
|
|
28
|
-
for axWin in axWindows {
|
|
29
|
-
var winID: CGWindowID = 0
|
|
30
|
-
if _AXUIElementGetWindow(axWin, &winID) == .success, winID == CGWindowID(wid) {
|
|
31
|
-
targetWindow = axWin
|
|
32
|
-
break
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
guard let window = targetWindow else { return nil }
|
|
37
|
-
|
|
38
|
-
let deadline = Date().addingTimeInterval(Self.timeoutSeconds)
|
|
39
|
-
var collected: [String] = []
|
|
40
|
-
|
|
41
|
-
walkChildren(element: window, depth: 0, deadline: deadline, collected: &collected)
|
|
42
|
-
|
|
43
|
-
let fullText = collected.joined(separator: "\n")
|
|
44
|
-
guard fullText.count >= minChars else { return nil }
|
|
45
|
-
|
|
46
|
-
return AXTextResult(wid: wid, texts: collected, fullText: fullText)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// MARK: - Tree Walker
|
|
50
|
-
|
|
51
|
-
private func walkChildren(
|
|
52
|
-
element: AXUIElement,
|
|
53
|
-
depth: Int,
|
|
54
|
-
deadline: Date,
|
|
55
|
-
collected: inout [String]
|
|
56
|
-
) {
|
|
57
|
-
guard depth < Self.maxDepth, Date() < deadline else { return }
|
|
58
|
-
|
|
59
|
-
// Extract text attributes from this element
|
|
60
|
-
extractText(from: element, into: &collected)
|
|
61
|
-
|
|
62
|
-
// Get children — prefer visible children, fall back to all children
|
|
63
|
-
var childrenRef: CFTypeRef?
|
|
64
|
-
var gotChildren = false
|
|
65
|
-
|
|
66
|
-
let visErr = AXUIElementCopyAttributeValue(element, kAXVisibleChildrenAttribute as CFString, &childrenRef)
|
|
67
|
-
if visErr == .success, let children = childrenRef as? [AXUIElement], !children.isEmpty {
|
|
68
|
-
gotChildren = true
|
|
69
|
-
let capped = children.prefix(Self.maxChildrenPerNode)
|
|
70
|
-
for child in capped {
|
|
71
|
-
guard Date() < deadline else { return }
|
|
72
|
-
walkChildren(element: child, depth: depth + 1, deadline: deadline, collected: &collected)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if !gotChildren {
|
|
77
|
-
childrenRef = nil
|
|
78
|
-
let childErr = AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &childrenRef)
|
|
79
|
-
if childErr == .success, let children = childrenRef as? [AXUIElement] {
|
|
80
|
-
let capped = children.prefix(Self.maxChildrenPerNode)
|
|
81
|
-
for child in capped {
|
|
82
|
-
guard Date() < deadline else { return }
|
|
83
|
-
walkChildren(element: child, depth: depth + 1, deadline: deadline, collected: &collected)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private func extractText(from element: AXUIElement, into collected: inout [String]) {
|
|
90
|
-
// kAXValueAttribute — text field contents, labels, etc.
|
|
91
|
-
var valueRef: CFTypeRef?
|
|
92
|
-
if AXUIElementCopyAttributeValue(element, kAXValueAttribute as CFString, &valueRef) == .success,
|
|
93
|
-
let str = valueRef as? String, !str.isEmpty, str.count > 1 {
|
|
94
|
-
collected.append(str)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// kAXTitleAttribute — window/button titles
|
|
98
|
-
var titleRef: CFTypeRef?
|
|
99
|
-
if AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &titleRef) == .success,
|
|
100
|
-
let str = titleRef as? String, !str.isEmpty, str.count > 1 {
|
|
101
|
-
collected.append(str)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// kAXDescriptionAttribute — accessible descriptions
|
|
105
|
-
var descRef: CFTypeRef?
|
|
106
|
-
if AXUIElementCopyAttributeValue(element, kAXDescriptionAttribute as CFString, &descRef) == .success,
|
|
107
|
-
let str = descRef as? String, !str.isEmpty, str.count > 1 {
|
|
108
|
-
collected.append(str)
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
enum AppType: String, CaseIterable {
|
|
4
|
-
case terminal
|
|
5
|
-
case editor
|
|
6
|
-
case browser
|
|
7
|
-
case chat
|
|
8
|
-
case media
|
|
9
|
-
case design
|
|
10
|
-
case system
|
|
11
|
-
case other
|
|
12
|
-
|
|
13
|
-
var label: String { rawValue }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
enum AppGrouping {
|
|
17
|
-
case type(AppType)
|
|
18
|
-
case exactApp(String)
|
|
19
|
-
|
|
20
|
-
var label: String {
|
|
21
|
-
switch self {
|
|
22
|
-
case .type(let type):
|
|
23
|
-
return type.label
|
|
24
|
-
case .exactApp(let appName):
|
|
25
|
-
return appName
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
enum AppTypeClassifier {
|
|
31
|
-
private static let nameMap: [String: AppType] = [
|
|
32
|
-
// Terminals
|
|
33
|
-
"iTerm2": .terminal, "Terminal": .terminal, "Alacritty": .terminal,
|
|
34
|
-
"kitty": .terminal, "Warp": .terminal, "Hyper": .terminal,
|
|
35
|
-
"WezTerm": .terminal, "Rio": .terminal, "Ghostty": .terminal,
|
|
36
|
-
|
|
37
|
-
// Editors / IDEs
|
|
38
|
-
"Xcode": .editor, "Code": .editor, "Visual Studio Code": .editor,
|
|
39
|
-
"Cursor": .editor, "Sublime Text": .editor, "TextEdit": .editor,
|
|
40
|
-
"Nova": .editor, "BBEdit": .editor, "Zed": .editor,
|
|
41
|
-
"IntelliJ IDEA": .editor, "WebStorm": .editor, "PyCharm": .editor,
|
|
42
|
-
"CLion": .editor, "GoLand": .editor, "RustRover": .editor,
|
|
43
|
-
"Android Studio": .editor, "Fleet": .editor, "Neovide": .editor,
|
|
44
|
-
|
|
45
|
-
// Browsers
|
|
46
|
-
"Safari": .browser, "Google Chrome": .browser, "Firefox": .browser,
|
|
47
|
-
"Arc": .browser, "Brave Browser": .browser, "Microsoft Edge": .browser,
|
|
48
|
-
"Orion": .browser, "Vivaldi": .browser, "Opera": .browser,
|
|
49
|
-
"Chrome": .browser, "Zen Browser": .browser,
|
|
50
|
-
|
|
51
|
-
// Chat / Communication
|
|
52
|
-
"Slack": .chat, "Discord": .chat, "Messages": .chat,
|
|
53
|
-
"Telegram": .chat, "WhatsApp": .chat, "Signal": .chat,
|
|
54
|
-
"Teams": .chat, "Microsoft Teams": .chat, "Zoom": .chat,
|
|
55
|
-
"FaceTime": .chat, "Skype": .chat,
|
|
56
|
-
|
|
57
|
-
// Media
|
|
58
|
-
"Spotify": .media, "Music": .media, "QuickTime Player": .media,
|
|
59
|
-
"VLC": .media, "IINA": .media, "Podcasts": .media,
|
|
60
|
-
"Photos": .media, "Preview": .media, "mpv": .media,
|
|
61
|
-
|
|
62
|
-
// Design
|
|
63
|
-
"Figma": .design, "Sketch": .design, "Pixelmator Pro": .design,
|
|
64
|
-
"Affinity Designer 2": .design, "Affinity Photo 2": .design,
|
|
65
|
-
"Adobe Photoshop": .design, "Adobe Illustrator": .design,
|
|
66
|
-
"Blender": .design, "OmniGraffle": .design,
|
|
67
|
-
|
|
68
|
-
// System
|
|
69
|
-
"Finder": .system, "System Preferences": .system, "System Settings": .system,
|
|
70
|
-
"Activity Monitor": .system, "Console": .system, "Disk Utility": .system,
|
|
71
|
-
"Keychain Access": .system,
|
|
72
|
-
]
|
|
73
|
-
|
|
74
|
-
static func classify(_ appName: String) -> AppType {
|
|
75
|
-
if let exact = nameMap[appName] { return exact }
|
|
76
|
-
// Substring fallback
|
|
77
|
-
let lower = appName.lowercased()
|
|
78
|
-
if lower.contains("terminal") || lower.contains("term") { return .terminal }
|
|
79
|
-
if lower.contains("code") || lower.contains("studio") || lower.contains("edit") { return .editor }
|
|
80
|
-
if lower.contains("chrome") || lower.contains("firefox") || lower.contains("safari") || lower.contains("browser") { return .browser }
|
|
81
|
-
if lower.contains("slack") || lower.contains("discord") || lower.contains("chat") || lower.contains("teams") { return .chat }
|
|
82
|
-
return .other
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
static func grouping(for appName: String) -> AppGrouping {
|
|
86
|
-
switch classify(appName) {
|
|
87
|
-
case .system, .other:
|
|
88
|
-
return .exactApp(appName)
|
|
89
|
-
case let type:
|
|
90
|
-
return .type(type)
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
static func matches(_ appName: String, grouping: AppGrouping) -> Bool {
|
|
95
|
-
switch grouping {
|
|
96
|
-
case .type(let type):
|
|
97
|
-
return classify(appName) == type
|
|
98
|
-
case .exactApp(let exactApp):
|
|
99
|
-
return appName.localizedCaseInsensitiveCompare(exactApp) == .orderedSame
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
static func matches(_ appName: String, type: AppType) -> Bool {
|
|
104
|
-
classify(appName) == type
|
|
105
|
-
}
|
|
106
|
-
}
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import AppKit
|
|
2
|
-
import ApplicationServices
|
|
3
|
-
import CoreGraphics
|
|
4
|
-
|
|
5
|
-
final class DesktopModel: ObservableObject {
|
|
6
|
-
static let shared = DesktopModel()
|
|
7
|
-
|
|
8
|
-
/// System helper processes that should never appear in search results or window lists.
|
|
9
|
-
/// These are XPC services, agents, and background helpers — not user-facing apps.
|
|
10
|
-
private static let systemHelperProcesses: Set<String> = [
|
|
11
|
-
// Apple system helpers
|
|
12
|
-
"CredentialsProviderExtensionHost",
|
|
13
|
-
"AuthenticationServicesAgent",
|
|
14
|
-
"SafariPasswordExtension",
|
|
15
|
-
"com.apple.WebKit.WebAuthn",
|
|
16
|
-
"SharedWebCredentialRunner",
|
|
17
|
-
"ViewBridgeAuxiliary",
|
|
18
|
-
"universalaccessd",
|
|
19
|
-
"CoreServicesUIAgent",
|
|
20
|
-
"UserNotificationCenter",
|
|
21
|
-
"AutoFillPanelService",
|
|
22
|
-
"AutoFill",
|
|
23
|
-
"CoreLocationAgent",
|
|
24
|
-
"SecurityAgent",
|
|
25
|
-
"coreautha",
|
|
26
|
-
"coreauth",
|
|
27
|
-
"talagent",
|
|
28
|
-
"CommCenter",
|
|
29
|
-
"AXVisualSupportAgent",
|
|
30
|
-
"SystemUIServer",
|
|
31
|
-
"Dock",
|
|
32
|
-
"Window Server",
|
|
33
|
-
"WindowManager",
|
|
34
|
-
"NotificationCenter",
|
|
35
|
-
"ControlCenter",
|
|
36
|
-
"Spotlight",
|
|
37
|
-
"Keychain Access",
|
|
38
|
-
"loginwindow",
|
|
39
|
-
"ScreenSaverEngine",
|
|
40
|
-
"SoftwareUpdateNotificationManager",
|
|
41
|
-
"WiFiAgent",
|
|
42
|
-
"pboard",
|
|
43
|
-
"storeuid",
|
|
44
|
-
// Third-party helpers
|
|
45
|
-
"CursorUIViewService",
|
|
46
|
-
"Codex Computer Use",
|
|
47
|
-
"Electron Helper",
|
|
48
|
-
"Google Chrome Helper",
|
|
49
|
-
]
|
|
50
|
-
|
|
51
|
-
/// Suffixes that indicate a helper/service process, not a user-facing app
|
|
52
|
-
private static let helperSuffixes = ["Service", "Agent", "Helper", "Extension", "Daemon", "XPCService"]
|
|
53
|
-
|
|
54
|
-
/// Real apps that happen to match helper suffixes — don't filter these
|
|
55
|
-
private static let knownRealApps: Set<String> = [
|
|
56
|
-
"Finder",
|
|
57
|
-
"Activity Monitor",
|
|
58
|
-
]
|
|
59
|
-
|
|
60
|
-
@Published private(set) var windows: [UInt32: WindowEntry] = [:]
|
|
61
|
-
@Published private(set) var interactionDates: [UInt32: Date] = [:]
|
|
62
|
-
/// In-memory layer tags: wid → layer id (e.g. "lattices", "vox", "hudson")
|
|
63
|
-
private(set) var windowLayerTags: [UInt32: String] = [:]
|
|
64
|
-
private var timer: Timer?
|
|
65
|
-
private var lastFrontmostWid: UInt32?
|
|
66
|
-
|
|
67
|
-
func start(interval: TimeInterval = 1.5) {
|
|
68
|
-
guard timer == nil else { return }
|
|
69
|
-
DiagnosticLog.shared.info("DesktopModel: starting (interval=\(interval)s)")
|
|
70
|
-
poll()
|
|
71
|
-
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
72
|
-
self?.poll()
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
func stop() {
|
|
77
|
-
timer?.invalidate()
|
|
78
|
-
timer = nil
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
func allWindows() -> [WindowEntry] {
|
|
82
|
-
Array(windows.values).sorted { $0.zIndex < $1.zIndex }
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
func frontmostWindow() -> WindowEntry? {
|
|
86
|
-
windows.values.min { $0.zIndex < $1.zIndex }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
func lastInteractionDate(for wid: UInt32) -> Date? {
|
|
90
|
-
interactionDates[wid]
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
func markInteraction(wid: UInt32, at date: Date = Date()) {
|
|
94
|
-
DispatchQueue.main.async {
|
|
95
|
-
self.interactionDates[wid] = date
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
func markInteraction(wids: [UInt32], at date: Date = Date()) {
|
|
100
|
-
guard !wids.isEmpty else { return }
|
|
101
|
-
let unique = Set(wids)
|
|
102
|
-
DispatchQueue.main.async {
|
|
103
|
-
for wid in unique {
|
|
104
|
-
self.interactionDates[wid] = date
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
func windowForSession(_ session: String) -> WindowEntry? {
|
|
110
|
-
SessionWindowLocator.cachedWindow(forSession: session, in: windows)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/// Assign a layer tag to a window (in-memory only)
|
|
114
|
-
func assignLayer(wid: UInt32, layerId: String) {
|
|
115
|
-
windowLayerTags[wid] = layerId
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// Remove layer tag from a window
|
|
119
|
-
func removeLayerTag(wid: UInt32) {
|
|
120
|
-
windowLayerTags.removeValue(forKey: wid)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/// Clear all layer tags
|
|
124
|
-
func clearLayerTags() {
|
|
125
|
-
windowLayerTags.removeAll()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/// Find a window by app name and optional title substring (case-insensitive)
|
|
129
|
-
func windowForApp(app: String, title: String?) -> WindowEntry? {
|
|
130
|
-
let matches = windows.values.filter {
|
|
131
|
-
$0.app.localizedCaseInsensitiveContains(app)
|
|
132
|
-
}
|
|
133
|
-
if let title {
|
|
134
|
-
return matches.first { $0.title.localizedCaseInsensitiveContains(title) }
|
|
135
|
-
}
|
|
136
|
-
return matches.first
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// MARK: - Polling
|
|
140
|
-
|
|
141
|
-
private var lastPollTime: Date = .distantPast
|
|
142
|
-
private static let minPollInterval: TimeInterval = 1.0
|
|
143
|
-
|
|
144
|
-
/// Poll only if stale. Call `forcePoll()` to bypass the freshness check.
|
|
145
|
-
func poll() {
|
|
146
|
-
let now = Date()
|
|
147
|
-
guard now.timeIntervalSince(lastPollTime) >= Self.minPollInterval else { return }
|
|
148
|
-
lastPollTime = now
|
|
149
|
-
performPoll()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/// Force a poll regardless of freshness — use sparingly.
|
|
153
|
-
func forcePoll() {
|
|
154
|
-
lastPollTime = Date()
|
|
155
|
-
performPoll()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private func performPoll() {
|
|
159
|
-
guard let list = CGWindowListCopyWindowInfo(
|
|
160
|
-
[.optionAll, .excludeDesktopElements],
|
|
161
|
-
kCGNullWindowID
|
|
162
|
-
) as? [[String: Any]] else { return }
|
|
163
|
-
|
|
164
|
-
var fresh: [UInt32: WindowEntry] = [:]
|
|
165
|
-
var zCounter = 0
|
|
166
|
-
|
|
167
|
-
for info in list {
|
|
168
|
-
guard let wid = info[kCGWindowNumber as String] as? UInt32,
|
|
169
|
-
let ownerName = info[kCGWindowOwnerName as String] as? String,
|
|
170
|
-
let pid = info[kCGWindowOwnerPID as String] as? Int32,
|
|
171
|
-
let boundsDict = info[kCGWindowBounds as String] as? NSDictionary
|
|
172
|
-
else { continue }
|
|
173
|
-
|
|
174
|
-
// Skip tiny windows (menu extras, status items)
|
|
175
|
-
var rect = CGRect.zero
|
|
176
|
-
guard CGRectMakeWithDictionaryRepresentation(boundsDict, &rect),
|
|
177
|
-
rect.width >= 50, rect.height >= 50 else { continue }
|
|
178
|
-
|
|
179
|
-
let title = info[kCGWindowName as String] as? String ?? ""
|
|
180
|
-
let layer = info[kCGWindowLayer as String] as? Int ?? 0
|
|
181
|
-
let isOnScreen = info[kCGWindowIsOnscreen as String] as? Bool ?? false
|
|
182
|
-
|
|
183
|
-
// Skip non-standard layers (menus, overlays)
|
|
184
|
-
guard layer == 0 else { continue }
|
|
185
|
-
|
|
186
|
-
// Skip system helper processes (autofill, credential providers, etc.)
|
|
187
|
-
if Self.systemHelperProcesses.contains(ownerName) { continue }
|
|
188
|
-
|
|
189
|
-
// Skip processes whose name ends with common helper suffixes
|
|
190
|
-
// (e.g. "CursorUIViewService", "AutoFillPanelService", "SecurityAgent")
|
|
191
|
-
// but not known real apps that happen to have these words
|
|
192
|
-
let isHelperByName = Self.helperSuffixes.contains(where: { ownerName.hasSuffix($0) })
|
|
193
|
-
&& !Self.knownRealApps.contains(ownerName)
|
|
194
|
-
if isHelperByName { continue }
|
|
195
|
-
|
|
196
|
-
// Skip windows with no title from processes containing "com.apple."
|
|
197
|
-
if ownerName.hasPrefix("com.apple.") && title.isEmpty { continue }
|
|
198
|
-
|
|
199
|
-
let frame = WindowFrame(
|
|
200
|
-
x: Double(rect.origin.x),
|
|
201
|
-
y: Double(rect.origin.y),
|
|
202
|
-
w: Double(rect.width),
|
|
203
|
-
h: Double(rect.height)
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
let spaceIds = WindowTiler.getSpacesForWindow(wid)
|
|
207
|
-
|
|
208
|
-
let latticesSession = SessionWindowLocator.extractSessionName(from: title)
|
|
209
|
-
|
|
210
|
-
var entry = WindowEntry(
|
|
211
|
-
wid: wid,
|
|
212
|
-
app: ownerName,
|
|
213
|
-
pid: pid,
|
|
214
|
-
title: title,
|
|
215
|
-
frame: frame,
|
|
216
|
-
spaceIds: spaceIds,
|
|
217
|
-
isOnScreen: isOnScreen,
|
|
218
|
-
latticesSession: latticesSession
|
|
219
|
-
)
|
|
220
|
-
entry.zIndex = zCounter
|
|
221
|
-
zCounter += 1
|
|
222
|
-
fresh[wid] = entry
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// AX reconciliation: check which CG windows actually exist in Accessibility
|
|
226
|
-
reconcileWithAX(&fresh)
|
|
227
|
-
|
|
228
|
-
// Diff
|
|
229
|
-
let oldKeys = Set(windows.keys)
|
|
230
|
-
let newKeys = Set(fresh.keys)
|
|
231
|
-
let added = Array(newKeys.subtracting(oldKeys))
|
|
232
|
-
let removed = Array(oldKeys.subtracting(newKeys))
|
|
233
|
-
|
|
234
|
-
let changed = added.count > 0 || removed.count > 0 || windowsContentChanged(old: windows, new: fresh)
|
|
235
|
-
let frontmostWid = fresh.values.min(by: { $0.zIndex < $1.zIndex })?.wid
|
|
236
|
-
let markFrontmost = frontmostWid != nil && frontmostWid != lastFrontmostWid
|
|
237
|
-
let interactionTime = Date()
|
|
238
|
-
|
|
239
|
-
DispatchQueue.main.async {
|
|
240
|
-
var interactions = self.interactionDates.filter { fresh[$0.key] != nil }
|
|
241
|
-
if markFrontmost, let frontmostWid {
|
|
242
|
-
interactions[frontmostWid] = interactionTime
|
|
243
|
-
}
|
|
244
|
-
// Only publish if something actually changed — avoids unnecessary SwiftUI re-renders
|
|
245
|
-
if changed || markFrontmost {
|
|
246
|
-
self.windows = fresh
|
|
247
|
-
self.interactionDates = interactions
|
|
248
|
-
}
|
|
249
|
-
self.lastFrontmostWid = frontmostWid
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if changed {
|
|
253
|
-
EventBus.shared.post(.windowsChanged(
|
|
254
|
-
windows: Array(fresh.values),
|
|
255
|
-
added: added,
|
|
256
|
-
removed: removed
|
|
257
|
-
))
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private func reconcileWithAX(_ fresh: inout [UInt32: WindowEntry]) {
|
|
262
|
-
// Get currently active Space IDs — AX only returns windows on these
|
|
263
|
-
let currentSpaceIds = Set(WindowTiler.getDisplaySpaces().map(\.currentSpaceId))
|
|
264
|
-
guard !currentSpaceIds.isEmpty else { return }
|
|
265
|
-
|
|
266
|
-
// Group CG windows by PID — only titled windows on current Spaces
|
|
267
|
-
var byPid: [Int32: [UInt32]] = [:]
|
|
268
|
-
for (wid, entry) in fresh where !entry.title.isEmpty {
|
|
269
|
-
let onCurrentSpace = entry.spaceIds.contains { currentSpaceIds.contains($0) }
|
|
270
|
-
if onCurrentSpace {
|
|
271
|
-
byPid[entry.pid, default: []].append(wid)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
for (pid, wids) in byPid {
|
|
276
|
-
let axApp = AXUIElementCreateApplication(pid)
|
|
277
|
-
|
|
278
|
-
// Set a timeout so unresponsive apps (video calls, etc.) don't block the poll
|
|
279
|
-
AXUIElementSetMessagingTimeout(axApp, 0.3)
|
|
280
|
-
|
|
281
|
-
var axWindowsRef: CFTypeRef?
|
|
282
|
-
guard AXUIElementCopyAttributeValue(axApp, kAXWindowsAttribute as CFString, &axWindowsRef) == .success,
|
|
283
|
-
let axWindows = axWindowsRef as? [AXUIElement] else { continue }
|
|
284
|
-
|
|
285
|
-
// Collect AX window titles
|
|
286
|
-
var axTitles: [String] = []
|
|
287
|
-
for axWin in axWindows {
|
|
288
|
-
var titleRef: CFTypeRef?
|
|
289
|
-
AXUIElementCopyAttributeValue(axWin, kAXTitleAttribute as CFString, &titleRef)
|
|
290
|
-
if let title = titleRef as? String, !title.isEmpty {
|
|
291
|
-
axTitles.append(title)
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Mark CG windows that have no matching AX title.
|
|
296
|
-
// AX titles often have suffixes like " - Google Chrome - Profile"
|
|
297
|
-
// so check if any AX title starts with the CG title (stripped of emoji).
|
|
298
|
-
for wid in wids {
|
|
299
|
-
guard let entry = fresh[wid], !entry.title.isEmpty else { continue }
|
|
300
|
-
let cgClean = stripForMatch(entry.title)
|
|
301
|
-
let matched = axTitles.contains { axTitle in
|
|
302
|
-
let axClean = stripForMatch(axTitle)
|
|
303
|
-
return axClean.hasPrefix(cgClean) || axClean.contains(cgClean) || cgClean.hasPrefix(axClean)
|
|
304
|
-
}
|
|
305
|
-
if !matched {
|
|
306
|
-
fresh[wid]?.axVerified = false
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private func stripForMatch(_ text: String) -> String {
|
|
313
|
-
// Remove emoji and non-ASCII symbols, lowercase, collapse whitespace
|
|
314
|
-
let scalar = text.unicodeScalars.filter { scalar in
|
|
315
|
-
scalar.isASCII || CharacterSet.letters.contains(scalar)
|
|
316
|
-
}
|
|
317
|
-
return String(scalar).lowercased()
|
|
318
|
-
.split(separator: " ").joined(separator: " ")
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
private func windowsContentChanged(old: [UInt32: WindowEntry], new: [UInt32: WindowEntry]) -> Bool {
|
|
322
|
-
// Quick check: if titles or frames changed for any existing window
|
|
323
|
-
for (wid, newEntry) in new {
|
|
324
|
-
guard let oldEntry = old[wid] else { continue }
|
|
325
|
-
if oldEntry.title != newEntry.title || oldEntry.frame != newEntry.frame {
|
|
326
|
-
return true
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return false
|
|
330
|
-
}
|
|
331
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
struct WindowEntry: Codable, Identifiable {
|
|
4
|
-
let wid: UInt32
|
|
5
|
-
let app: String
|
|
6
|
-
let pid: Int32
|
|
7
|
-
let title: String
|
|
8
|
-
let frame: WindowFrame
|
|
9
|
-
let spaceIds: [Int]
|
|
10
|
-
let isOnScreen: Bool
|
|
11
|
-
let latticesSession: String?
|
|
12
|
-
var axVerified: Bool = true
|
|
13
|
-
var zIndex: Int = 0 // 0 = frontmost, from CGWindowList order
|
|
14
|
-
|
|
15
|
-
var id: UInt32 { wid }
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
struct WindowFrame: Codable, Equatable {
|
|
19
|
-
let x: Double
|
|
20
|
-
let y: Double
|
|
21
|
-
let w: Double
|
|
22
|
-
let h: Double
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// MARK: - Desktop Inventory Snapshot
|
|
26
|
-
|
|
27
|
-
struct DesktopInventorySnapshot {
|
|
28
|
-
let displays: [DisplayInfo]
|
|
29
|
-
let timestamp: Date
|
|
30
|
-
|
|
31
|
-
struct DisplayInfo: Identifiable {
|
|
32
|
-
let id: String // display UUID or index
|
|
33
|
-
let name: String // e.g. "Built-in Retina", "LG UltraFine"
|
|
34
|
-
let resolution: (w: Int, h: Int)
|
|
35
|
-
let visibleFrame: (w: Int, h: Int)
|
|
36
|
-
let isMain: Bool
|
|
37
|
-
let spaceCount: Int
|
|
38
|
-
let currentSpaceIndex: Int
|
|
39
|
-
let spaces: [SpaceGroup]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
struct SpaceGroup: Identifiable {
|
|
43
|
-
let id: Int // CGS space ID
|
|
44
|
-
let index: Int // 1-based index within display
|
|
45
|
-
let isCurrent: Bool
|
|
46
|
-
let apps: [AppGroup]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
struct AppGroup: Identifiable {
|
|
50
|
-
let id: String // unique key (spaceId-appName)
|
|
51
|
-
let appName: String
|
|
52
|
-
let windows: [InventoryWindowInfo]
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
struct InventoryWindowInfo: Identifiable {
|
|
56
|
-
let id: UInt32 // CGWindowID
|
|
57
|
-
let pid: Int32 // owner PID for AX operations
|
|
58
|
-
let title: String
|
|
59
|
-
let frame: WindowFrame
|
|
60
|
-
let tilePosition: TilePosition?
|
|
61
|
-
let isLattices: Bool
|
|
62
|
-
let latticesSession: String?
|
|
63
|
-
let spaceIndex: Int? // 1-based space index within display
|
|
64
|
-
let isOnScreen: Bool // on current space
|
|
65
|
-
var inventoryPath: InventoryPath?
|
|
66
|
-
var appName: String? // owner app name for filtering
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/// Flat list of all windows across all displays/spaces/apps
|
|
70
|
-
var allWindows: [InventoryWindowInfo] {
|
|
71
|
-
displays.flatMap { $0.spaces.flatMap { $0.apps.flatMap { $0.windows } } }
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
|
|
3
|
-
class InventoryManager: ObservableObject {
|
|
4
|
-
static let shared = InventoryManager()
|
|
5
|
-
|
|
6
|
-
@Published var orphans: [TmuxSession] = []
|
|
7
|
-
@Published var allSessions: [TmuxSession] = []
|
|
8
|
-
|
|
9
|
-
func refresh() {
|
|
10
|
-
// Always query fresh — this is called on explicit user refresh
|
|
11
|
-
let sessions = TmuxQuery.listSessions()
|
|
12
|
-
|
|
13
|
-
// Build set of managed session names
|
|
14
|
-
var managed = Set<String>()
|
|
15
|
-
|
|
16
|
-
// From scanned projects
|
|
17
|
-
for project in ProjectScanner.shared.projects {
|
|
18
|
-
managed.insert(project.sessionName)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// From workspace tab groups
|
|
22
|
-
if let groups = WorkspaceManager.shared.config?.groups {
|
|
23
|
-
for group in groups {
|
|
24
|
-
for tab in group.tabs {
|
|
25
|
-
managed.insert(WorkspaceManager.sessionName(for: tab.path))
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
DispatchQueue.main.async {
|
|
31
|
-
self.allSessions = sessions
|
|
32
|
-
self.orphans = sessions.filter { !managed.contains($0.name) }
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|