@lattices/cli 0.4.9 → 0.4.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -13
  3. package/{app → apps/mac}/Lattices.app/Contents/Info.plist +10 -2
  4. package/{app → apps/mac}/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/{app → apps/mac}/Package.swift +2 -1
  6. package/apps/mac/Resources/Pets/assistant-spark/pet.json +62 -0
  7. package/apps/mac/Resources/Pets/assistant-spark/spritesheet.webp +0 -0
  8. package/apps/mac/Resources/Pets/scout-ranger/pet.json +6 -0
  9. package/apps/mac/Resources/Pets/scout-ranger/spritesheet.webp +0 -0
  10. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +27 -0
  11. package/apps/mac/Sources/AppShell/AppDelegate.swift +189 -0
  12. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +25 -0
  13. package/{app → apps/mac}/Sources/AppShell/AppShellView.swift +18 -3
  14. package/{app → apps/mac}/Sources/AppShell/AppUpdater.swift +4 -3
  15. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +87 -0
  16. package/{app → apps/mac}/Sources/AppShell/LatticesRuntime.swift +43 -0
  17. package/{app → apps/mac}/Sources/AppShell/MainView.swift +116 -63
  18. package/apps/mac/Sources/AppShell/MenuBarController.swift +177 -0
  19. package/{app → apps/mac}/Sources/AppShell/OnboardingView.swift +72 -60
  20. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +366 -0
  21. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +70 -0
  22. package/{app → apps/mac}/Sources/AppShell/Preferences.swift +37 -2
  23. package/{app → apps/mac}/Sources/AppShell/SettingsView.swift +815 -156
  24. package/{app → apps/mac}/Sources/AppShell/SettingsWindow.swift +10 -0
  25. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +13 -0
  26. package/{app → apps/mac}/Sources/Core/Actions/HotkeyStore.swift +6 -1
  27. package/{app → apps/mac}/Sources/Core/Actions/IntentEngine.swift +2 -0
  28. package/{app → apps/mac}/Sources/Core/Daemon/DaemonServer.swift +5 -0
  29. package/{app → apps/mac}/Sources/Core/Daemon/LatticesApi.swift +365 -0
  30. package/{app → apps/mac}/Sources/Core/Desktop/DesktopModel.swift +1 -0
  31. package/{app → apps/mac}/Sources/Core/Desktop/OcrModel.swift +17 -13
  32. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +33 -0
  33. package/{app → apps/mac}/Sources/Core/Desktop/WindowDragSnapController.swift +18 -217
  34. package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewStore.swift +4 -5
  35. package/{app → apps/mac}/Sources/Core/Desktop/WindowTiler.swift +19 -13
  36. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +124 -0
  37. package/apps/mac/Sources/Core/Input/EventTapThread.swift +54 -0
  38. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +20 -0
  39. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +335 -0
  40. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +141 -0
  41. package/{app → apps/mac}/Sources/Core/Input/MouseGestureConfig.swift +155 -20
  42. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +2259 -0
  43. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +170 -0
  44. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +39 -0
  45. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +624 -0
  46. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +56 -0
  47. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +46 -27
  48. package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +580 -162
  49. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +1240 -0
  50. package/{app → apps/mac}/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +11 -23
  51. package/{app → apps/mac}/Sources/Core/Pi/PiChatDock.swift +90 -43
  52. package/{app → apps/mac}/Sources/Core/Pi/PiChatSession.swift +676 -43
  53. package/{app → apps/mac}/Sources/Core/Pi/PiProviderSetupCallout.swift +5 -5
  54. package/{app → apps/mac}/Sources/Core/Pi/PiWorkspaceView.swift +93 -44
  55. package/apps/mac/Sources/Core/System/Capability.swift +79 -0
  56. package/{app → apps/mac}/Sources/Core/System/PermissionChecker.swift +43 -8
  57. package/{app → apps/mac}/Sources/Core/Voice/AudioProvider.swift +225 -56
  58. package/bin/handsoff-infer.ts +14 -5
  59. package/bin/handsoff-worker.ts +11 -7
  60. package/bin/infer.ts +406 -0
  61. package/bin/lattices-app.ts +57 -7
  62. package/bin/lattices-dev +40 -1
  63. package/bin/lattices.ts +1 -1
  64. package/docs/agent-execution-plan.md +9 -9
  65. package/docs/api.md +119 -0
  66. package/docs/app.md +1 -0
  67. package/docs/companion-deck.md +1 -1
  68. package/docs/gesture-customization-proposal.md +520 -0
  69. package/docs/mouse-gestures.md +79 -0
  70. package/docs/overview.md +2 -2
  71. package/docs/presentation-execution-review.md +9 -9
  72. package/docs/proposals/LAT-001-gesture-visual-customization.md +522 -0
  73. package/docs/proposals/LAT-002-shared-overlay-canvas.md +353 -0
  74. package/docs/proposals/LAT-003-menu-bar-controller-architecture.md +291 -0
  75. package/docs/proposals/LAT-004-interactive-overlay-actors.md +534 -0
  76. package/docs/reference/dewey.config.ts +74 -0
  77. package/docs/reference/install-agent.md +79 -0
  78. package/docs/repo-structure.md +100 -0
  79. package/docs/voice-error-model.md +7 -7
  80. package/docs/voice.md +18 -0
  81. package/package.json +23 -13
  82. package/swift/Package.swift +20 -0
  83. package/swift/Sources/DeckKit/DeckAction.swift +51 -0
  84. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +152 -0
  85. package/swift/Sources/DeckKit/DeckCockpit.swift +82 -0
  86. package/swift/Sources/DeckKit/DeckHost.swift +7 -0
  87. package/swift/Sources/DeckKit/DeckManifest.swift +145 -0
  88. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +533 -0
  89. package/swift/Sources/DeckKit/DeckTrackpad.swift +63 -0
  90. package/swift/Sources/DeckKit/DeckValue.swift +93 -0
  91. package/swift/Sources/DeckKit/DeckVoiceError.swift +88 -0
  92. package/swift/Tests/DeckKitTests/DeckKitTests.swift +286 -0
  93. package/app/Sources/AppShell/AppDelegate.swift +0 -408
  94. package/app/Sources/Core/Input/KeyboardRemapController.swift +0 -184
  95. package/app/Sources/Core/Input/KeyboardRemapStore.swift +0 -84
  96. package/app/Sources/Core/Input/MouseGestureController.swift +0 -1203
  97. package/app/Sources/Core/Input/MouseShortcutStore.swift +0 -107
  98. /package/{app → apps/mac}/Info.plist +0 -0
  99. /package/{app → apps/mac}/Lattices.app/Contents/Resources/AppIcon.icns +0 -0
  100. /package/{app → apps/mac}/Lattices.app/Contents/Resources/tap.wav +0 -0
  101. /package/{app → apps/mac}/Lattices.app/Contents/_CodeSignature/CodeResources +0 -0
  102. /package/{app → apps/mac}/Lattices.entitlements +0 -0
  103. /package/{app → apps/mac}/Resources/tap.wav +0 -0
  104. /package/{app → apps/mac}/Sources/AppShell/App.swift +0 -0
  105. /package/{app → apps/mac}/Sources/AppShell/CliActionLauncher.swift +0 -0
  106. /package/{app → apps/mac}/Sources/AppShell/HomeDashboardView.swift +0 -0
  107. /package/{app → apps/mac}/Sources/AppShell/KeyRecorderView.swift +0 -0
  108. /package/{app → apps/mac}/Sources/AppShell/MainWindow.swift +0 -0
  109. /package/{app → apps/mac}/Sources/Core/Actions/HotkeyManager.swift +0 -0
  110. /package/{app → apps/mac}/Sources/Core/Actions/IntentSchema.swift +0 -0
  111. /package/{app → apps/mac}/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -0
  112. /package/{app → apps/mac}/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -0
  113. /package/{app → apps/mac}/Sources/Core/Actions/Intents/FocusIntent.swift +0 -0
  114. /package/{app → apps/mac}/Sources/Core/Actions/Intents/HelpIntent.swift +0 -0
  115. /package/{app → apps/mac}/Sources/Core/Actions/Intents/KillIntent.swift +0 -0
  116. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -0
  117. /package/{app → apps/mac}/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -0
  118. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -0
  119. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -0
  120. /package/{app → apps/mac}/Sources/Core/Actions/Intents/ScanIntent.swift +0 -0
  121. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SearchIntent.swift +0 -0
  122. /package/{app → apps/mac}/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -0
  123. /package/{app → apps/mac}/Sources/Core/Actions/Intents/TileIntent.swift +0 -0
  124. /package/{app → apps/mac}/Sources/Core/Actions/PaletteCommand.swift +0 -0
  125. /package/{app → apps/mac}/Sources/Core/Actions/VoiceIntentResolver.swift +0 -0
  126. /package/{app → apps/mac}/Sources/Core/Companion/CompanionActivityLog.swift +0 -0
  127. /package/{app → apps/mac}/Sources/Core/Companion/CompanionKeyboardController.swift +0 -0
  128. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -0
  129. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -0
  130. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -0
  131. /package/{app → apps/mac}/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -0
  132. /package/{app → apps/mac}/Sources/Core/Companion/LatticesDeckHost.swift +0 -0
  133. /package/{app → apps/mac}/Sources/Core/Daemon/DaemonProtocol.swift +0 -0
  134. /package/{app → apps/mac}/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -0
  135. /package/{app → apps/mac}/Sources/Core/Desktop/AppTypeClassifier.swift +0 -0
  136. /package/{app → apps/mac}/Sources/Core/Desktop/DesktopModelTypes.swift +0 -0
  137. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryManager.swift +0 -0
  138. /package/{app → apps/mac}/Sources/Core/Desktop/InventoryPath.swift +0 -0
  139. /package/{app → apps/mac}/Sources/Core/Desktop/MouseFinder.swift +0 -0
  140. /package/{app → apps/mac}/Sources/Core/Desktop/OcrStore.swift +0 -0
  141. /package/{app → apps/mac}/Sources/Core/Desktop/PlacementSpec.swift +0 -0
  142. /package/{app → apps/mac}/Sources/Core/Desktop/SessionWindowLocator.swift +0 -0
  143. /package/{app → apps/mac}/Sources/Core/Desktop/TilePickerView.swift +0 -0
  144. /package/{app → apps/mac}/Sources/Core/Desktop/WindowPreviewCard.swift +0 -0
  145. /package/{app → apps/mac}/Sources/Core/Desktop/WindowSelectionStore.swift +0 -0
  146. /package/{app → apps/mac}/Sources/Core/Input/KeyboardRemapConfig.swift +0 -0
  147. /package/{app → apps/mac}/Sources/Core/Input/MouseInputDeviceStore.swift +0 -0
  148. /package/{app → apps/mac}/Sources/Core/Input/MouseInputEventViewer.swift +0 -0
  149. /package/{app → apps/mac}/Sources/Core/Overlays/AppWindowShell.swift +0 -0
  150. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -0
  151. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -0
  152. /package/{app → apps/mac}/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -0
  153. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -0
  154. /package/{app → apps/mac}/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -0
  155. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -0
  156. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -0
  157. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDController.swift +0 -0
  158. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -0
  159. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -0
  160. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -0
  161. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDState.swift +0 -0
  162. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -0
  163. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -0
  164. /package/{app → apps/mac}/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -0
  165. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -0
  166. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -0
  167. /package/{app → apps/mac}/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -0
  168. /package/{app → apps/mac}/Sources/Core/Overlays/OverlayPanelShell.swift +0 -0
  169. /package/{app → apps/mac}/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -0
  170. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -0
  171. /package/{app → apps/mac}/Sources/Core/Pi/PiAuthPromptCard.swift +0 -0
  172. /package/{app → apps/mac}/Sources/Core/Pi/PiInstallCallout.swift +0 -0
  173. /package/{app → apps/mac}/Sources/Core/System/DiagnosticLog.swift +0 -0
  174. /package/{app → apps/mac}/Sources/Core/System/EventBus.swift +0 -0
  175. /package/{app → apps/mac}/Sources/Core/System/ProcessModel.swift +0 -0
  176. /package/{app → apps/mac}/Sources/Core/System/ProcessQuery.swift +0 -0
  177. /package/{app → apps/mac}/Sources/Core/System/SystemTelemetryMonitor.swift +0 -0
  178. /package/{app → apps/mac}/Sources/Core/Voice/AdvisorLearningStore.swift +0 -0
  179. /package/{app → apps/mac}/Sources/Core/Voice/AgentSession.swift +0 -0
  180. /package/{app → apps/mac}/Sources/Core/Voice/HandsOffSession.swift +0 -0
  181. /package/{app → apps/mac}/Sources/Core/Voice/VoiceChatView.swift +0 -0
  182. /package/{app → apps/mac}/Sources/Core/Voice/VoxClient.swift +0 -0
  183. /package/{app → apps/mac}/Sources/Core/Workspace/Project.swift +0 -0
  184. /package/{app → apps/mac}/Sources/Core/Workspace/ProjectScanner.swift +0 -0
  185. /package/{app → apps/mac}/Sources/Core/Workspace/SessionLayerStore.swift +0 -0
  186. /package/{app → apps/mac}/Sources/Core/Workspace/SessionManager.swift +0 -0
  187. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/Terminal.swift +0 -0
  188. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -0
  189. /package/{app → apps/mac}/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -0
  190. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -0
  191. /package/{app → apps/mac}/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -0
  192. /package/{app → apps/mac}/Sources/Core/Workspace/WorkspaceManager.swift +0 -0
  193. /package/{app → apps/mac}/Sources/UI/ActionRow.swift +0 -0
  194. /package/{app → apps/mac}/Sources/UI/OrphanRow.swift +0 -0
  195. /package/{app → apps/mac}/Sources/UI/ProjectRow.swift +0 -0
  196. /package/{app → apps/mac}/Sources/UI/TabGroupRow.swift +0 -0
  197. /package/{app → apps/mac}/Sources/UI/Theme.swift +0 -0
  198. /package/{app → apps/mac}/Tests/StageDragTests.swift +0 -0
  199. /package/{app → apps/mac}/Tests/StageJoinTests.swift +0 -0
  200. /package/{app → apps/mac}/Tests/StageManagerTests.swift +0 -0
  201. /package/{app → apps/mac}/Tests/StageTileTests.swift +0 -0
@@ -0,0 +1,1240 @@
1
+ import AppKit
2
+
3
+ struct ScreenOverlayLayerID: Hashable {
4
+ let rawValue: String
5
+
6
+ init(_ rawValue: String) {
7
+ self.rawValue = rawValue
8
+ }
9
+ }
10
+
11
+ enum ScreenOverlayOwner: String {
12
+ case dragSnap
13
+ case mouseGesture
14
+ case hotkeyHints
15
+ case focusHighlight
16
+ case agentApi
17
+ }
18
+
19
+ enum ScreenOverlayScreenTarget: Equatable {
20
+ case screen(id: String)
21
+ case all
22
+ }
23
+
24
+ struct ScreenOverlayLayerSnapshot {
25
+ let id: ScreenOverlayLayerID
26
+ let owner: ScreenOverlayOwner
27
+ let screen: ScreenOverlayScreenTarget
28
+ let zIndex: Int
29
+ let opacity: CGFloat
30
+ let payload: ScreenOverlayPayload
31
+ let expiresAt: Date?
32
+ }
33
+
34
+ enum ScreenOverlayPayload {
35
+ case snapZones(ScreenOverlaySnapZonesPayload)
36
+ case toast(ScreenOverlayTextPayload)
37
+ case label(ScreenOverlayTextPayload)
38
+ case highlight(ScreenOverlayHighlightPayload)
39
+ case pet(ScreenOverlayPetPayload)
40
+ }
41
+
42
+ struct ScreenOverlaySnapZone {
43
+ let id: String
44
+ let label: String
45
+ let rect: CGRect
46
+ let isHovered: Bool
47
+ }
48
+
49
+ struct ScreenOverlaySnapZonesPayload {
50
+ let zones: [ScreenOverlaySnapZone]
51
+ let previewRect: CGRect?
52
+ let previewLabel: String?
53
+ let zoneOpacity: CGFloat
54
+ let highlightOpacity: CGFloat
55
+ let previewOpacity: CGFloat
56
+ let cornerRadius: CGFloat
57
+ }
58
+
59
+ struct ScreenOverlayTextPayload {
60
+ let text: String
61
+ let detail: String?
62
+ let point: CGPoint?
63
+ let placement: ScreenOverlayPlacement
64
+ let style: ScreenOverlayStyle
65
+ }
66
+
67
+ struct ScreenOverlayHighlightPayload {
68
+ let rect: CGRect
69
+ let label: String?
70
+ let style: ScreenOverlayStyle
71
+ let cornerRadius: CGFloat
72
+ }
73
+
74
+ struct ScreenOverlayPetPayload {
75
+ let glyph: String
76
+ let petID: String?
77
+ let state: String?
78
+ let name: String?
79
+ let message: String?
80
+ let point: CGPoint?
81
+ let placement: ScreenOverlayPlacement
82
+ let style: ScreenOverlayStyle
83
+ let isDragging: Bool
84
+ let dismissible: Bool
85
+
86
+ func moved(to point: CGPoint, state nextState: String?, isDragging nextIsDragging: Bool? = nil) -> ScreenOverlayPetPayload {
87
+ ScreenOverlayPetPayload(
88
+ glyph: glyph,
89
+ petID: petID,
90
+ state: nextState ?? state,
91
+ name: name,
92
+ message: message,
93
+ point: point,
94
+ placement: .point,
95
+ style: style,
96
+ isDragging: nextIsDragging ?? isDragging,
97
+ dismissible: dismissible
98
+ )
99
+ }
100
+ }
101
+
102
+ enum ScreenOverlayPlacement: String {
103
+ case top
104
+ case bottom
105
+ case center
106
+ case cursor
107
+ case point
108
+ }
109
+
110
+ enum ScreenOverlayStyle: String {
111
+ case info
112
+ case success
113
+ case warning
114
+ case danger
115
+ case playful
116
+ }
117
+
118
+ final class ScreenOverlayCanvasController {
119
+ static let shared = ScreenOverlayCanvasController()
120
+
121
+ private var windowsByScreenID: [String: ScreenOverlayWindow] = [:]
122
+ private var layersByID: [ScreenOverlayLayerID: ScreenOverlayLayerSnapshot] = [:]
123
+ private var motionsByLayerID: [ScreenOverlayLayerID: OverlayLayerMotion] = [:]
124
+ private var animationTimer: Timer?
125
+ private var globalDismissMonitor: Any?
126
+ private var localDismissMonitor: Any?
127
+ private var dragState: OverlayActorDragState?
128
+ private var agentActorsHidden = false
129
+ private let maxActorDragDuration: TimeInterval = 8.0
130
+
131
+ private init() {}
132
+
133
+ func warmUp() {
134
+ reconcileScreens()
135
+ for window in windowsByScreenID.values {
136
+ window.orderFrontRegardless()
137
+ window.alphaValue = 0
138
+ }
139
+ }
140
+
141
+ func reconcileScreens() {
142
+ let currentScreenIDs = Set(NSScreen.screens.map(Self.screenID(for:)))
143
+ for staleID in windowsByScreenID.keys where !currentScreenIDs.contains(staleID) {
144
+ windowsByScreenID[staleID]?.orderOut(nil)
145
+ windowsByScreenID.removeValue(forKey: staleID)
146
+ }
147
+
148
+ for screen in NSScreen.screens {
149
+ let screenID = Self.screenID(for: screen)
150
+ let window = windowsByScreenID[screenID] ?? makeWindow(for: screen)
151
+ window.setFrame(screen.frame, display: false)
152
+ window.overlayView.frame = NSRect(origin: .zero, size: screen.frame.size)
153
+ windowsByScreenID[screenID] = window
154
+ }
155
+ }
156
+
157
+ func publishLayer(_ layer: ScreenOverlayLayerSnapshot) {
158
+ layersByID[layer.id] = layer
159
+ scheduleExpiration(for: layer)
160
+ render()
161
+ updateLifecycleMonitors()
162
+ }
163
+
164
+ func replaceLayers(owner: ScreenOverlayOwner, with layers: [ScreenOverlayLayerSnapshot]) {
165
+ layersByID = layersByID.filter { _, layer in layer.owner != owner }
166
+ for layer in layers {
167
+ layersByID[layer.id] = layer
168
+ scheduleExpiration(for: layer)
169
+ }
170
+ render()
171
+ updateLifecycleMonitors()
172
+ }
173
+
174
+ func removeLayer(id: ScreenOverlayLayerID) {
175
+ layersByID.removeValue(forKey: id)
176
+ motionsByLayerID.removeValue(forKey: id)
177
+ if dragState?.id == id {
178
+ dragState = nil
179
+ resetPointerCapture()
180
+ }
181
+ render()
182
+ updateLifecycleMonitors()
183
+ }
184
+
185
+ func removeLayers(owner: ScreenOverlayOwner) {
186
+ let removedIDs = Set(layersByID.values.filter { $0.owner == owner }.map(\.id))
187
+ layersByID = layersByID.filter { _, layer in layer.owner != owner }
188
+ motionsByLayerID = motionsByLayerID.filter { id, _ in layersByID[id] != nil }
189
+ if let dragState, removedIDs.contains(dragState.id) {
190
+ self.dragState = nil
191
+ resetPointerCapture()
192
+ }
193
+ render()
194
+ updateLifecycleMonitors()
195
+ }
196
+
197
+ func toggleAgentActorsVisibility() {
198
+ agentActorsHidden.toggle()
199
+ if agentActorsHidden {
200
+ dragState = nil
201
+ resetPointerCapture()
202
+ }
203
+ render()
204
+ updateLifecycleMonitors()
205
+ }
206
+
207
+ func resetInputCapture(reason: String) {
208
+ dragState = nil
209
+ resetPointerCapture()
210
+ DiagnosticLog.shared.warn("ScreenOverlay: input capture reset for \(reason)")
211
+ }
212
+
213
+ @discardableResult
214
+ func moveLayer(id: ScreenOverlayLayerID, to target: CGPoint, durationMs: Int, easing: String?) -> Bool {
215
+ guard let layer = layersByID[id],
216
+ case .pet(let payload) = layer.payload else { return false }
217
+ let now = Date()
218
+ let currentPoint = motionsByLayerID[id]?.point(at: now) ?? payload.point ?? target
219
+ let duration = max(0.08, min(Double(durationMs) / 1000.0, 8.0))
220
+ let restingState = payload.state == "run_left" || payload.state == "run_right" ? "idle" : payload.state
221
+ let movingState = target.x < currentPoint.x - 2 ? "run_left" : "run_right"
222
+
223
+ motionsByLayerID[id] = OverlayLayerMotion(
224
+ from: currentPoint,
225
+ to: target,
226
+ startedAt: now,
227
+ duration: duration,
228
+ easing: OverlayLayerMotion.Easing.parse(easing),
229
+ restingState: restingState
230
+ )
231
+ layersByID[id] = layer.replacingPayload(.pet(payload.moved(to: currentPoint, state: movingState)))
232
+ render()
233
+ updateLifecycleMonitors()
234
+ return true
235
+ }
236
+
237
+ static func screenID(for screen: NSScreen) -> String {
238
+ let key = NSDeviceDescriptionKey("NSScreenNumber")
239
+ if let number = screen.deviceDescription[key] as? NSNumber {
240
+ return number.stringValue
241
+ }
242
+ return screen.localizedName
243
+ }
244
+
245
+ private func makeWindow(for screen: NSScreen) -> ScreenOverlayWindow {
246
+ let window = ScreenOverlayWindow(frame: screen.frame)
247
+ windowsByScreenID[Self.screenID(for: screen)] = window
248
+ return window
249
+ }
250
+
251
+ private func render() {
252
+ reconcileScreens()
253
+ dropExpiredLayers()
254
+
255
+ for screen in NSScreen.screens {
256
+ let screenID = Self.screenID(for: screen)
257
+ guard let window = windowsByScreenID[screenID] else { continue }
258
+ let visibleLayers = layersByID.values
259
+ .filter { layer in
260
+ if agentActorsHidden && layer.isParkableActor {
261
+ return false
262
+ }
263
+ switch layer.screen {
264
+ case .all:
265
+ return true
266
+ case .screen(let targetID):
267
+ return targetID == screenID
268
+ }
269
+ }
270
+ .sorted { left, right in
271
+ if left.zIndex != right.zIndex {
272
+ return left.zIndex < right.zIndex
273
+ }
274
+ return left.id.rawValue < right.id.rawValue
275
+ }
276
+
277
+ window.overlayView.layers = visibleLayers
278
+ if visibleLayers.isEmpty {
279
+ window.alphaValue = 0
280
+ } else {
281
+ window.alphaValue = 1
282
+ window.orderFrontRegardless()
283
+ }
284
+ }
285
+ }
286
+
287
+ private func dropExpiredLayers() {
288
+ let now = Date()
289
+ layersByID = layersByID.filter { _, layer in
290
+ guard let expiresAt = layer.expiresAt else { return true }
291
+ return expiresAt > now
292
+ }
293
+ motionsByLayerID = motionsByLayerID.filter { id, _ in layersByID[id] != nil }
294
+ }
295
+
296
+ private func scheduleExpiration(for layer: ScreenOverlayLayerSnapshot) {
297
+ guard let expiresAt = layer.expiresAt else { return }
298
+ let delay = max(0, expiresAt.timeIntervalSinceNow)
299
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
300
+ guard let self,
301
+ let current = self.layersByID[layer.id],
302
+ current.expiresAt == expiresAt else { return }
303
+ self.layersByID.removeValue(forKey: layer.id)
304
+ self.motionsByLayerID.removeValue(forKey: layer.id)
305
+ self.render()
306
+ self.updateLifecycleMonitors()
307
+ }
308
+ }
309
+
310
+ private func updateLifecycleMonitors() {
311
+ updateAnimationTimer()
312
+ updateDismissMonitors()
313
+ }
314
+
315
+ private func updateAnimationTimer() {
316
+ let needsAnimation = !motionsByLayerID.isEmpty || layersByID.values.contains { layer in
317
+ if case .pet = layer.payload { return true }
318
+ return false
319
+ }
320
+
321
+ if needsAnimation, animationTimer == nil {
322
+ animationTimer = Timer.scheduledTimer(withTimeInterval: 1.0 / 60.0, repeats: true) { [weak self] _ in
323
+ self?.tickAnimation()
324
+ }
325
+ } else if !needsAnimation {
326
+ animationTimer?.invalidate()
327
+ animationTimer = nil
328
+ }
329
+ }
330
+
331
+ private func tickAnimation() {
332
+ let now = Date()
333
+ var completedIDs: [ScreenOverlayLayerID] = []
334
+ for (id, motion) in motionsByLayerID {
335
+ guard let layer = layersByID[id],
336
+ case .pet(let payload) = layer.payload else {
337
+ completedIDs.append(id)
338
+ continue
339
+ }
340
+
341
+ let point = motion.point(at: now)
342
+ let isComplete = motion.isComplete(at: now)
343
+ let state: String?
344
+ if isComplete {
345
+ state = motion.restingState
346
+ completedIDs.append(id)
347
+ } else {
348
+ state = motion.to.x < motion.from.x ? "run_left" : "run_right"
349
+ }
350
+ layersByID[id] = layer.replacingPayload(.pet(payload.moved(to: point, state: state)))
351
+ }
352
+ for id in completedIDs {
353
+ motionsByLayerID.removeValue(forKey: id)
354
+ }
355
+
356
+ if !completedIDs.isEmpty {
357
+ updateLifecycleMonitors()
358
+ }
359
+ render()
360
+ for window in windowsByScreenID.values {
361
+ window.overlayView.needsDisplay = true
362
+ }
363
+ }
364
+
365
+ private func updateDismissMonitors() {
366
+ let hasAgentLayer = layersByID.values.contains { $0.owner == .agentApi }
367
+ if hasAgentLayer, globalDismissMonitor == nil {
368
+ let mask: NSEvent.EventTypeMask = [
369
+ .mouseMoved,
370
+ .leftMouseDown,
371
+ .leftMouseUp,
372
+ .rightMouseDown,
373
+ .otherMouseDown,
374
+ .leftMouseDragged,
375
+ .rightMouseDragged,
376
+ .otherMouseDragged,
377
+ ]
378
+ globalDismissMonitor = NSEvent.addGlobalMonitorForEvents(matching: mask) { [weak self] event in
379
+ DispatchQueue.main.async {
380
+ _ = self?.handlePointerEvent(event)
381
+ }
382
+ }
383
+ localDismissMonitor = NSEvent.addLocalMonitorForEvents(matching: mask.union(.keyDown)) { [weak self] event in
384
+ if event.type == .keyDown {
385
+ if event.keyCode == 53 {
386
+ self?.dismissAgentOverlays()
387
+ return nil
388
+ }
389
+ } else {
390
+ if self?.handlePointerEvent(event) == true {
391
+ return nil
392
+ }
393
+ }
394
+ return event
395
+ }
396
+ } else if !hasAgentLayer {
397
+ if let globalDismissMonitor {
398
+ NSEvent.removeMonitor(globalDismissMonitor)
399
+ self.globalDismissMonitor = nil
400
+ }
401
+ if let localDismissMonitor {
402
+ NSEvent.removeMonitor(localDismissMonitor)
403
+ self.localDismissMonitor = nil
404
+ }
405
+ dragState = nil
406
+ resetPointerCapture()
407
+ }
408
+ }
409
+
410
+ @discardableResult
411
+ private func handlePointerEvent(_ event: NSEvent) -> Bool {
412
+ switch event.type {
413
+ case .mouseMoved:
414
+ updatePointerCapture(at: NSEvent.mouseLocation)
415
+ return false
416
+ case .leftMouseDown:
417
+ updatePointerCapture(at: NSEvent.mouseLocation)
418
+ if beginActorDrag(at: NSEvent.mouseLocation) {
419
+ return true
420
+ }
421
+ dismissAgentOverlays()
422
+ return false
423
+ case .leftMouseDragged:
424
+ if dragActor(to: NSEvent.mouseLocation) {
425
+ return true
426
+ }
427
+ dismissAgentOverlays()
428
+ return false
429
+ case .leftMouseUp:
430
+ let wasDragging = dragState != nil
431
+ endActorDrag()
432
+ updatePointerCapture(at: NSEvent.mouseLocation)
433
+ return wasDragging
434
+ case .rightMouseDown:
435
+ updatePointerCapture(at: NSEvent.mouseLocation)
436
+ if closeActor(at: NSEvent.mouseLocation) {
437
+ return true
438
+ }
439
+ dismissAgentOverlays()
440
+ return false
441
+ case .otherMouseDown, .rightMouseDragged, .otherMouseDragged:
442
+ dismissAgentOverlays()
443
+ return false
444
+ default:
445
+ return false
446
+ }
447
+ }
448
+
449
+ private func updatePointerCapture(at globalPoint: CGPoint) {
450
+ clearStaleActorDragIfNeeded()
451
+ let captureWindow: ScreenOverlayWindow?
452
+ if let dragState {
453
+ captureWindow = windowsByScreenID[dragState.screenID]
454
+ } else {
455
+ captureWindow = hitActor(at: globalPoint)?.window
456
+ }
457
+ for window in windowsByScreenID.values {
458
+ window.ignoresMouseEvents = window !== captureWindow
459
+ }
460
+ }
461
+
462
+ private func beginActorDrag(at globalPoint: CGPoint) -> Bool {
463
+ guard let hit = hitActor(at: globalPoint),
464
+ let layer = layersByID[hit.id],
465
+ case .pet(let payload) = layer.payload else { return false }
466
+ let currentPoint = motionsByLayerID[hit.id]?.point(at: Date()) ?? payload.point ?? hit.localPoint
467
+ motionsByLayerID.removeValue(forKey: hit.id)
468
+ dragState = OverlayActorDragState(
469
+ id: hit.id,
470
+ screenID: hit.screenID,
471
+ offset: CGPoint(x: hit.localPoint.x - currentPoint.x, y: hit.localPoint.y - currentPoint.y),
472
+ lastPoint: currentPoint,
473
+ startedAt: Date()
474
+ )
475
+ layersByID[hit.id] = layer.replacingPayload(.pet(payload.moved(to: currentPoint, state: "idle", isDragging: true)))
476
+ render()
477
+ updateLifecycleMonitors()
478
+ updatePointerCapture(at: globalPoint)
479
+ return true
480
+ }
481
+
482
+ private func dragActor(to globalPoint: CGPoint) -> Bool {
483
+ guard var dragState,
484
+ let hit = screenLocalPoint(for: globalPoint),
485
+ let layer = layersByID[dragState.id],
486
+ case .pet(let payload) = layer.payload else { return false }
487
+ clearStaleActorDragIfNeeded()
488
+ guard self.dragState != nil else { return false }
489
+ let nextPoint = CGPoint(
490
+ x: hit.localPoint.x - dragState.offset.x,
491
+ y: hit.localPoint.y - dragState.offset.y
492
+ )
493
+ let state = nextPoint.x < dragState.lastPoint.x - 1 ? "run_left" : "run_right"
494
+ layersByID[dragState.id] = layer.replacingPayload(.pet(payload.moved(to: nextPoint, state: state, isDragging: true)))
495
+ dragState.screenID = hit.screenID
496
+ dragState.lastPoint = nextPoint
497
+ self.dragState = dragState
498
+ render()
499
+ updatePointerCapture(at: globalPoint)
500
+ return true
501
+ }
502
+
503
+ private func endActorDrag() {
504
+ guard let dragState,
505
+ let layer = layersByID[dragState.id],
506
+ case .pet(let payload) = layer.payload else {
507
+ self.dragState = nil
508
+ resetPointerCapture()
509
+ return
510
+ }
511
+ layersByID[dragState.id] = layer.replacingPayload(.pet(payload.moved(to: dragState.lastPoint, state: "idle", isDragging: false)))
512
+ self.dragState = nil
513
+ render()
514
+ updateLifecycleMonitors()
515
+ updatePointerCapture(at: NSEvent.mouseLocation)
516
+ }
517
+
518
+ private func clearStaleActorDragIfNeeded() {
519
+ guard let dragState,
520
+ Date().timeIntervalSince(dragState.startedAt) > maxActorDragDuration else { return }
521
+ DiagnosticLog.shared.warn("ScreenOverlay: stale actor drag cleared for \(dragState.id.rawValue)")
522
+ endActorDrag()
523
+ }
524
+
525
+ private func resetPointerCapture() {
526
+ for window in windowsByScreenID.values {
527
+ window.ignoresMouseEvents = true
528
+ }
529
+ }
530
+
531
+ private func closeActor(at globalPoint: CGPoint) -> Bool {
532
+ guard let hit = hitActor(at: globalPoint),
533
+ let layer = layersByID[hit.id],
534
+ layer.isParkableActor else { return false }
535
+ layersByID.removeValue(forKey: hit.id)
536
+ motionsByLayerID.removeValue(forKey: hit.id)
537
+ if dragState?.id == hit.id {
538
+ dragState = nil
539
+ }
540
+ render()
541
+ updateLifecycleMonitors()
542
+ updatePointerCapture(at: globalPoint)
543
+ return true
544
+ }
545
+
546
+ private func hitActor(at globalPoint: CGPoint) -> (id: ScreenOverlayLayerID, window: ScreenOverlayWindow, screenID: String, localPoint: CGPoint)? {
547
+ guard let hit = screenLocalPoint(for: globalPoint) else { return nil }
548
+ guard let id = hit.window.overlayView.layerID(at: hit.localPoint) else { return nil }
549
+ return (id, hit.window, hit.screenID, hit.localPoint)
550
+ }
551
+
552
+ private func screenLocalPoint(for globalPoint: CGPoint) -> (window: ScreenOverlayWindow, screenID: String, localPoint: CGPoint)? {
553
+ for (screenID, window) in windowsByScreenID where window.frame.contains(globalPoint) {
554
+ let localPoint = CGPoint(
555
+ x: globalPoint.x - window.frame.minX,
556
+ y: globalPoint.y - window.frame.minY
557
+ )
558
+ return (window, screenID, localPoint)
559
+ }
560
+ return nil
561
+ }
562
+
563
+ private func dismissAgentOverlays() {
564
+ let before = layersByID.count
565
+ layersByID = layersByID.filter { _, layer in
566
+ guard layer.owner == .agentApi else { return true }
567
+ return !layer.isDismissible
568
+ }
569
+ motionsByLayerID = motionsByLayerID.filter { id, _ in layersByID[id] != nil }
570
+ if let dragState, layersByID[dragState.id] == nil {
571
+ self.dragState = nil
572
+ resetPointerCapture()
573
+ }
574
+ guard layersByID.count != before else { return }
575
+ render()
576
+ updateLifecycleMonitors()
577
+ }
578
+ }
579
+
580
+ private struct OverlayActorDragState {
581
+ let id: ScreenOverlayLayerID
582
+ var screenID: String
583
+ let offset: CGPoint
584
+ var lastPoint: CGPoint
585
+ let startedAt: Date
586
+ }
587
+
588
+ private extension ScreenOverlayLayerSnapshot {
589
+ var isDismissible: Bool {
590
+ switch payload {
591
+ case .pet(let payload):
592
+ return payload.dismissible
593
+ default:
594
+ return true
595
+ }
596
+ }
597
+
598
+ var isParkableActor: Bool {
599
+ owner == .agentApi && !isDismissible
600
+ }
601
+
602
+ func replacingPayload(_ payload: ScreenOverlayPayload) -> ScreenOverlayLayerSnapshot {
603
+ ScreenOverlayLayerSnapshot(
604
+ id: id,
605
+ owner: owner,
606
+ screen: screen,
607
+ zIndex: zIndex,
608
+ opacity: opacity,
609
+ payload: payload,
610
+ expiresAt: expiresAt
611
+ )
612
+ }
613
+ }
614
+
615
+ private struct OverlayLayerMotion {
616
+ enum Easing: String {
617
+ case linear
618
+ case easeInOut
619
+ case spring
620
+
621
+ static func parse(_ value: String?) -> Easing {
622
+ switch value?.lowercased() {
623
+ case "linear":
624
+ return .linear
625
+ case "easeinout", "ease-in-out", "ease_in_out":
626
+ return .easeInOut
627
+ case "spring", nil:
628
+ return .spring
629
+ default:
630
+ return .spring
631
+ }
632
+ }
633
+ }
634
+
635
+ let from: CGPoint
636
+ let to: CGPoint
637
+ let startedAt: Date
638
+ let duration: TimeInterval
639
+ let easing: Easing
640
+ let restingState: String?
641
+
642
+ func isComplete(at date: Date) -> Bool {
643
+ date.timeIntervalSince(startedAt) >= duration
644
+ }
645
+
646
+ func point(at date: Date) -> CGPoint {
647
+ let rawProgress = duration <= 0 ? 1 : min(max(date.timeIntervalSince(startedAt) / duration, 0), 1)
648
+ let progress = eased(rawProgress)
649
+ return CGPoint(
650
+ x: from.x + (to.x - from.x) * progress,
651
+ y: from.y + (to.y - from.y) * progress
652
+ )
653
+ }
654
+
655
+ private func eased(_ progress: Double) -> Double {
656
+ switch easing {
657
+ case .linear:
658
+ return progress
659
+ case .easeInOut:
660
+ return progress * progress * (3 - 2 * progress)
661
+ case .spring:
662
+ let damping = exp(-6.8 * progress)
663
+ let oscillation = cos(10.5 * progress)
664
+ return min(max(1 - damping * oscillation, 0), 1)
665
+ }
666
+ }
667
+ }
668
+
669
+ private final class ScreenOverlayWindow: NSPanel {
670
+ let overlayView = ScreenOverlayCanvasView(frame: .zero)
671
+
672
+ init(frame: CGRect) {
673
+ super.init(
674
+ contentRect: frame,
675
+ styleMask: [.borderless, .nonactivatingPanel],
676
+ backing: .buffered,
677
+ defer: false
678
+ )
679
+
680
+ isOpaque = false
681
+ backgroundColor = .clear
682
+ hasShadow = false
683
+ ignoresMouseEvents = true
684
+ level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
685
+ collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
686
+ isMovable = false
687
+ hidesOnDeactivate = false
688
+ animationBehavior = .none
689
+ alphaValue = 0
690
+ overlayView.frame = NSRect(origin: .zero, size: frame.size)
691
+ overlayView.autoresizingMask = [.width, .height]
692
+ contentView = overlayView
693
+ }
694
+
695
+ override var canBecomeKey: Bool { false }
696
+ override var canBecomeMain: Bool { false }
697
+ }
698
+
699
+ private final class ScreenOverlayCanvasView: NSView {
700
+ var layers: [ScreenOverlayLayerSnapshot] = [] {
701
+ didSet { needsDisplay = true }
702
+ }
703
+ private var interactiveRectsByLayerID: [ScreenOverlayLayerID: CGRect] = [:]
704
+
705
+ override init(frame frameRect: NSRect) {
706
+ super.init(frame: frameRect)
707
+ wantsLayer = true
708
+ layer?.backgroundColor = NSColor.clear.cgColor
709
+ }
710
+
711
+ required init?(coder: NSCoder) {
712
+ fatalError("init(coder:) has not been implemented")
713
+ }
714
+
715
+ override func draw(_ dirtyRect: NSRect) {
716
+ NSColor.clear.setFill()
717
+ bounds.fill()
718
+ interactiveRectsByLayerID.removeAll()
719
+
720
+ for layer in layers {
721
+ NSGraphicsContext.saveGraphicsState()
722
+ NSColor.black.withAlphaComponent(0).set()
723
+ switch layer.payload {
724
+ case .snapZones(let payload):
725
+ drawSnapZones(payload, opacity: layer.opacity)
726
+ case .toast(let payload):
727
+ drawTextPill(payload, opacity: layer.opacity, isToast: true)
728
+ case .label(let payload):
729
+ drawTextPill(payload, opacity: layer.opacity, isToast: false)
730
+ case .highlight(let payload):
731
+ drawHighlight(payload, opacity: layer.opacity)
732
+ case .pet(let payload):
733
+ drawPet(payload, id: layer.id, opacity: layer.opacity)
734
+ }
735
+ NSGraphicsContext.restoreGraphicsState()
736
+ }
737
+ }
738
+
739
+ func layerID(at point: CGPoint) -> ScreenOverlayLayerID? {
740
+ interactiveRectsByLayerID
741
+ .first { _, rect in rect.contains(point) }
742
+ .map(\.key)
743
+ }
744
+
745
+ private func drawTextPill(_ payload: ScreenOverlayTextPayload, opacity: CGFloat, isToast: Bool) {
746
+ let titleFont = NSFont.monospacedSystemFont(ofSize: isToast ? 13 : 11, weight: .semibold)
747
+ let detailFont = NSFont.systemFont(ofSize: 11, weight: .regular)
748
+ let maxWidth = min(bounds.width - 64, isToast ? 460 : 320)
749
+ let title = attributed(payload.text, font: titleFont, color: NSColor.white.withAlphaComponent(0.94 * opacity))
750
+ let detail = payload.detail.map {
751
+ attributed($0, font: detailFont, color: NSColor.white.withAlphaComponent(0.66 * opacity))
752
+ }
753
+ let detailSize = detail?.boundingRect(
754
+ with: CGSize(width: maxWidth - 28, height: 120),
755
+ options: [.usesLineFragmentOrigin, .usesFontLeading]
756
+ ).size ?? .zero
757
+ let titleSize = title.boundingRect(
758
+ with: CGSize(width: maxWidth - 28, height: 80),
759
+ options: [.usesLineFragmentOrigin, .usesFontLeading]
760
+ ).size
761
+ let width = min(maxWidth, max(110, max(titleSize.width, detailSize.width) + 28))
762
+ let height = max(30, titleSize.height + (detail == nil ? 12 : detailSize.height + 18))
763
+ let origin = overlayOrigin(
764
+ placement: payload.placement,
765
+ point: payload.point,
766
+ size: CGSize(width: width, height: height),
767
+ margin: isToast ? 42 : 18
768
+ )
769
+ let rect = CGRect(origin: origin, size: CGSize(width: width, height: height))
770
+
771
+ drawPanel(rect, style: payload.style, opacity: opacity, radius: min(16, height / 2))
772
+ title.draw(with: CGRect(x: rect.minX + 14, y: rect.maxY - titleSize.height - (detail == nil ? 8 : 10), width: width - 28, height: titleSize.height), options: [.usesLineFragmentOrigin])
773
+ if let detail {
774
+ detail.draw(with: CGRect(x: rect.minX + 14, y: rect.minY + 8, width: width - 28, height: detailSize.height + 2), options: [.usesLineFragmentOrigin])
775
+ }
776
+ }
777
+
778
+ private func drawHighlight(_ payload: ScreenOverlayHighlightPayload, opacity: CGFloat) {
779
+ let rect = payload.rect.insetBy(dx: -3, dy: -3)
780
+ let radius = min(payload.cornerRadius, min(rect.width, rect.height) * 0.2)
781
+ let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
782
+ let tint = color(for: payload.style)
783
+
784
+ let shadow = NSShadow()
785
+ shadow.shadowBlurRadius = 18
786
+ shadow.shadowOffset = .zero
787
+ shadow.shadowColor = tint.withAlphaComponent(0.25 * opacity)
788
+ NSGraphicsContext.saveGraphicsState()
789
+ shadow.set()
790
+ tint.withAlphaComponent(0.08 * opacity).setFill()
791
+ path.fill()
792
+ NSGraphicsContext.restoreGraphicsState()
793
+
794
+ path.lineWidth = 2
795
+ tint.withAlphaComponent(0.82 * opacity).setStroke()
796
+ path.stroke()
797
+
798
+ if let label = payload.label, !label.isEmpty {
799
+ let textPayload = ScreenOverlayTextPayload(
800
+ text: label,
801
+ detail: nil,
802
+ point: CGPoint(x: rect.minX + 14, y: rect.maxY + 18),
803
+ placement: .point,
804
+ style: payload.style
805
+ )
806
+ drawTextPill(textPayload, opacity: opacity, isToast: false)
807
+ }
808
+ }
809
+
810
+ private func drawPet(_ payload: ScreenOverlayPetPayload, id: ScreenOverlayLayerID, opacity: CGFloat) {
811
+ let glyphFont = NSFont.systemFont(ofSize: 44, weight: .regular)
812
+ let nameFont = NSFont.systemFont(ofSize: 12, weight: .semibold)
813
+ let messageFont = NSFont.systemFont(ofSize: 12, weight: .regular)
814
+ let glyph = attributed(payload.glyph, font: glyphFont, color: NSColor.white.withAlphaComponent(0.96 * opacity))
815
+ let name = payload.name.map { attributed($0, font: nameFont, color: NSColor.white.withAlphaComponent(0.96 * opacity)) }
816
+ let message = payload.message.map {
817
+ attributed($0, font: messageFont, color: NSColor.white.withAlphaComponent(0.86 * opacity))
818
+ }
819
+ let artSize = CGSize(width: 96, height: 104)
820
+ let textWidth: CGFloat = (name == nil && message == nil) ? 0 : 228
821
+ let textHeight = textPlateHeight(name: name, message: message, width: textWidth)
822
+ let bubbleWidth = artSize.width + (textWidth > 0 ? textWidth + 10 : 0)
823
+ let bubbleHeight = max(artSize.height, textHeight)
824
+ let origin = overlayOrigin(
825
+ placement: payload.placement,
826
+ point: payload.point,
827
+ size: CGSize(width: bubbleWidth, height: bubbleHeight),
828
+ margin: 30
829
+ )
830
+ let rect = CGRect(origin: origin, size: CGSize(width: bubbleWidth, height: bubbleHeight))
831
+ let artRect = CGRect(
832
+ x: rect.minX,
833
+ y: rect.midY - artSize.height / 2,
834
+ width: artSize.width,
835
+ height: artSize.height
836
+ )
837
+ let dragPhase = Date().timeIntervalSinceReferenceDate * 11
838
+ let dragLift: CGFloat = payload.isDragging ? 8 + CGFloat(sin(dragPhase)) * 2.5 : 0
839
+ let dragTilt: CGFloat = payload.isDragging
840
+ ? (payload.state == "run_left" ? 7 : -7) + CGFloat(sin(dragPhase * 0.72)) * 2.5
841
+ : 0
842
+ let dragScaleX: CGFloat = payload.isDragging ? 1.05 + CGFloat(sin(dragPhase * 0.9)) * 0.018 : 1
843
+ let dragScaleY: CGFloat = payload.isDragging ? 0.98 + CGFloat(cos(dragPhase * 0.9)) * 0.018 : 1
844
+ let bodyRect = artRect.offsetBy(dx: 0, dy: dragLift)
845
+
846
+ NSGraphicsContext.saveGraphicsState()
847
+ if payload.isDragging {
848
+ let transform = NSAffineTransform()
849
+ transform.translateX(by: bodyRect.midX, yBy: bodyRect.midY)
850
+ transform.rotate(byDegrees: dragTilt)
851
+ transform.scaleX(by: dragScaleX, yBy: dragScaleY)
852
+ transform.translateX(by: -bodyRect.midX, yBy: -bodyRect.midY)
853
+ transform.concat()
854
+ }
855
+ if let petID = payload.petID,
856
+ let frame = CodexPetAssetCache.shared.frame(for: petID, state: payload.state) {
857
+ frame.image.draw(
858
+ in: bodyRect,
859
+ from: frame.sourceRect,
860
+ operation: .sourceOver,
861
+ fraction: opacity,
862
+ respectFlipped: true,
863
+ hints: [.interpolation: NSImageInterpolation.high]
864
+ )
865
+ } else {
866
+ glyph.draw(
867
+ with: CGRect(x: bodyRect.midX - 26, y: bodyRect.midY - 26, width: 52, height: 52),
868
+ options: [.usesLineFragmentOrigin]
869
+ )
870
+ }
871
+ NSGraphicsContext.restoreGraphicsState()
872
+
873
+ guard textWidth > 0 else {
874
+ interactiveRectsByLayerID[id] = artRect.insetBy(dx: -8, dy: -8)
875
+ return
876
+ }
877
+ let textRect = CGRect(
878
+ x: artRect.maxX + 10,
879
+ y: rect.midY - textHeight / 2,
880
+ width: textWidth,
881
+ height: textHeight
882
+ )
883
+ interactiveRectsByLayerID[id] = artRect.union(textRect).insetBy(dx: -8, dy: -8)
884
+ drawTranslucentTextWash(textRect, opacity: opacity)
885
+
886
+ var cursorY = textRect.maxY - 10
887
+ if let name {
888
+ let nameRect = CGRect(x: textRect.minX + 12, y: cursorY - 16, width: textRect.width - 24, height: 16)
889
+ drawCrispOverlayText(name, in: nameRect, opacity: opacity)
890
+ cursorY = nameRect.minY - 4
891
+ }
892
+ if let message {
893
+ let messageRect = CGRect(x: textRect.minX + 12, y: textRect.minY + 10, width: textRect.width - 24, height: max(18, cursorY - textRect.minY - 10))
894
+ drawCrispOverlayText(message, in: messageRect, opacity: opacity)
895
+ }
896
+ }
897
+
898
+ private func textPlateHeight(name: NSAttributedString?, message: NSAttributedString?, width: CGFloat) -> CGFloat {
899
+ guard width > 0 else { return 0 }
900
+ let messageSize = message?.boundingRect(
901
+ with: CGSize(width: width - 24, height: 72),
902
+ options: [.usesLineFragmentOrigin, .usesFontLeading]
903
+ ).size ?? .zero
904
+ return max(38, (name == nil ? 0 : 20) + (message == nil ? 0 : ceil(messageSize.height) + 10) + 18)
905
+ }
906
+
907
+ private func drawTranslucentTextWash(_ rect: CGRect, opacity: CGFloat) {
908
+ let path = NSBezierPath(roundedRect: rect, xRadius: 8, yRadius: 8)
909
+ NSColor(calibratedWhite: 0.02, alpha: 0.34 * opacity).setFill()
910
+ path.fill()
911
+
912
+ path.lineWidth = 0.5
913
+ NSColor.white.withAlphaComponent(0.10 * opacity).setStroke()
914
+ path.stroke()
915
+ }
916
+
917
+ private func drawCrispOverlayText(_ text: NSAttributedString, in rect: CGRect, opacity: CGFloat) {
918
+ let shadow = NSShadow()
919
+ shadow.shadowBlurRadius = 2
920
+ shadow.shadowOffset = NSSize(width: 0, height: -1)
921
+ shadow.shadowColor = NSColor.black.withAlphaComponent(0.72 * opacity)
922
+
923
+ NSGraphicsContext.saveGraphicsState()
924
+ shadow.set()
925
+ text.draw(with: rect.offsetBy(dx: 0, dy: -0.5), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine])
926
+ NSGraphicsContext.restoreGraphicsState()
927
+
928
+ let halo = NSMutableAttributedString(attributedString: text)
929
+ halo.addAttribute(.foregroundColor, value: NSColor.black.withAlphaComponent(0.36 * opacity), range: NSRange(location: 0, length: halo.length))
930
+ halo.draw(with: rect.offsetBy(dx: 0.5, dy: -0.5), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine])
931
+ halo.draw(with: rect.offsetBy(dx: -0.5, dy: -0.5), options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine])
932
+
933
+ text.draw(with: rect, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine])
934
+ }
935
+
936
+ private func drawSnapZones(_ payload: ScreenOverlaySnapZonesPayload, opacity: CGFloat) {
937
+ for zone in payload.zones {
938
+ drawSnapZone(zone, payload: payload, opacity: opacity)
939
+ }
940
+
941
+ if let previewRect = payload.previewRect {
942
+ drawSnapPreview(previewRect, label: payload.previewLabel, payload: payload, opacity: opacity)
943
+ }
944
+ }
945
+
946
+ private func drawSnapZone(
947
+ _ zone: ScreenOverlaySnapZone,
948
+ payload: ScreenOverlaySnapZonesPayload,
949
+ opacity: CGFloat
950
+ ) {
951
+ let rect = zone.rect.insetBy(dx: 1.5, dy: 1.5)
952
+ let radius = min(payload.cornerRadius, min(rect.width, rect.height) * 0.34)
953
+ let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
954
+ let idleStrength = max(0.35, min(payload.zoneOpacity / 0.10, 1.4))
955
+ let hoverStrength = max(0.35, min(payload.highlightOpacity / 0.22, 1.4))
956
+
957
+ let shadow = NSShadow()
958
+ shadow.shadowBlurRadius = zone.isHovered ? 18 : 10
959
+ shadow.shadowOffset = NSSize(width: 0, height: -2)
960
+ shadow.shadowColor = NSColor.black.withAlphaComponent((zone.isHovered ? 0.20 : 0.10) * opacity)
961
+
962
+ NSGraphicsContext.saveGraphicsState()
963
+ shadow.set()
964
+ let baseTop = NSColor(
965
+ calibratedWhite: 0.13,
966
+ alpha: (zone.isHovered ? 0.42 * hoverStrength : 0.22 * idleStrength) * opacity
967
+ )
968
+ let baseBottom = NSColor(
969
+ calibratedWhite: 0.07,
970
+ alpha: (zone.isHovered ? 0.34 * hoverStrength : 0.15 * idleStrength) * opacity
971
+ )
972
+ NSGradient(starting: baseTop, ending: baseBottom)?.draw(in: path, angle: -90)
973
+ NSGraphicsContext.restoreGraphicsState()
974
+
975
+ if zone.isHovered {
976
+ let glowPath = path.copy() as! NSBezierPath
977
+ glowPath.lineWidth = 6
978
+ NSColor(
979
+ calibratedRed: 0.25,
980
+ green: 0.84,
981
+ blue: 0.58,
982
+ alpha: payload.highlightOpacity * 0.28 * opacity
983
+ ).setStroke()
984
+ glowPath.stroke()
985
+ }
986
+
987
+ path.lineWidth = zone.isHovered ? 1.6 : 1.0
988
+ NSColor(
989
+ calibratedRed: 0.52,
990
+ green: 0.94,
991
+ blue: 0.72,
992
+ alpha: (zone.isHovered ? 0.54 * hoverStrength : 0.10 * idleStrength) * opacity
993
+ ).setStroke()
994
+ path.stroke()
995
+
996
+ let lipRect = CGRect(x: rect.minX + 1.5, y: rect.maxY - 2.5, width: rect.width - 3, height: 2)
997
+ if lipRect.width > 0 {
998
+ let lipPath = NSBezierPath(roundedRect: lipRect, xRadius: 1, yRadius: 1)
999
+ NSColor.white.withAlphaComponent((zone.isHovered ? 0.18 : 0.08) * opacity).setFill()
1000
+ lipPath.fill()
1001
+ }
1002
+
1003
+ drawLabel(zone.label, in: rect, emphasized: zone.isHovered, opacity: opacity)
1004
+ }
1005
+
1006
+ private func drawSnapPreview(
1007
+ _ rect: CGRect,
1008
+ label: String?,
1009
+ payload: ScreenOverlaySnapZonesPayload,
1010
+ opacity: CGFloat
1011
+ ) {
1012
+ let previewRect = rect.insetBy(dx: 10, dy: 10)
1013
+ let radius = min(payload.cornerRadius, min(previewRect.width, previewRect.height) * 0.14)
1014
+ let path = NSBezierPath(roundedRect: previewRect, xRadius: radius, yRadius: radius)
1015
+
1016
+ NSColor(calibratedWhite: 1.0, alpha: payload.previewOpacity * 0.22 * opacity).setFill()
1017
+ path.fill()
1018
+
1019
+ path.lineWidth = 1.6
1020
+ path.setLineDash([10, 8], count: 2, phase: 0)
1021
+ NSColor(
1022
+ calibratedRed: 0.44,
1023
+ green: 0.90,
1024
+ blue: 0.68,
1025
+ alpha: max(0.34, payload.previewOpacity * 3.2) * opacity
1026
+ ).setStroke()
1027
+ path.stroke()
1028
+ path.setLineDash([], count: 0, phase: 0)
1029
+
1030
+ let innerPath = NSBezierPath(
1031
+ roundedRect: previewRect.insetBy(dx: 7, dy: 7),
1032
+ xRadius: max(radius - 4, 8),
1033
+ yRadius: max(radius - 4, 8)
1034
+ )
1035
+ innerPath.lineWidth = 1
1036
+ NSColor.white.withAlphaComponent(max(0.08, payload.previewOpacity * 1.2) * opacity).setStroke()
1037
+ innerPath.stroke()
1038
+
1039
+ if let label {
1040
+ let tagRect = CGRect(x: previewRect.minX + 14, y: previewRect.maxY - 34, width: 110, height: 24)
1041
+ let tagPath = NSBezierPath(roundedRect: tagRect, xRadius: 12, yRadius: 12)
1042
+ NSColor(calibratedWhite: 0.08, alpha: 0.62 * opacity).setFill()
1043
+ tagPath.fill()
1044
+ NSColor.white.withAlphaComponent(0.10 * opacity).setStroke()
1045
+ tagPath.lineWidth = 1
1046
+ tagPath.stroke()
1047
+ drawLabel(label, in: tagRect, emphasized: true, opacity: opacity)
1048
+ }
1049
+ }
1050
+
1051
+ private func drawLabel(_ label: String, in rect: CGRect, emphasized: Bool, opacity: CGFloat) {
1052
+ let font = NSFont.monospacedSystemFont(ofSize: emphasized ? 11 : 10, weight: emphasized ? .semibold : .medium)
1053
+ let attributes: [NSAttributedString.Key: Any] = [
1054
+ .font: font,
1055
+ .foregroundColor: NSColor.white.withAlphaComponent((emphasized ? 0.92 : 0.72) * opacity),
1056
+ ]
1057
+ let attributed = NSAttributedString(string: label.uppercased(), attributes: attributes)
1058
+ let size = attributed.size()
1059
+ let drawPoint = CGPoint(
1060
+ x: rect.midX - size.width / 2,
1061
+ y: rect.midY - size.height / 2
1062
+ )
1063
+ attributed.draw(at: drawPoint)
1064
+ }
1065
+
1066
+ private func attributed(_ text: String, font: NSFont, color: NSColor) -> NSAttributedString {
1067
+ NSAttributedString(string: text, attributes: [
1068
+ .font: font,
1069
+ .foregroundColor: color,
1070
+ ])
1071
+ }
1072
+
1073
+ private func overlayOrigin(
1074
+ placement: ScreenOverlayPlacement,
1075
+ point: CGPoint?,
1076
+ size: CGSize,
1077
+ margin: CGFloat
1078
+ ) -> CGPoint {
1079
+ let cursor = convertGlobalPointToLocal(NSEvent.mouseLocation)
1080
+ let anchor: CGPoint
1081
+ switch placement {
1082
+ case .top:
1083
+ anchor = CGPoint(x: bounds.midX, y: bounds.maxY - margin - size.height / 2)
1084
+ case .bottom:
1085
+ anchor = CGPoint(x: bounds.midX, y: bounds.minY + margin + size.height / 2)
1086
+ case .center:
1087
+ anchor = CGPoint(x: bounds.midX, y: bounds.midY)
1088
+ case .cursor:
1089
+ anchor = cursor
1090
+ case .point:
1091
+ anchor = point ?? cursor
1092
+ }
1093
+
1094
+ return CGPoint(
1095
+ x: min(max(anchor.x - size.width / 2, 16), bounds.maxX - size.width - 16),
1096
+ y: min(max(anchor.y - size.height / 2, 16), bounds.maxY - size.height - 16)
1097
+ )
1098
+ }
1099
+
1100
+ private func convertGlobalPointToLocal(_ point: CGPoint) -> CGPoint {
1101
+ guard let window else { return point }
1102
+ return CGPoint(x: point.x - window.frame.minX, y: point.y - window.frame.minY)
1103
+ }
1104
+
1105
+ private func drawPanel(_ rect: CGRect, style: ScreenOverlayStyle, opacity: CGFloat, radius: CGFloat) {
1106
+ let tint = color(for: style)
1107
+ let path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius)
1108
+ let shadow = NSShadow()
1109
+ shadow.shadowBlurRadius = 18
1110
+ shadow.shadowOffset = NSSize(width: 0, height: -4)
1111
+ shadow.shadowColor = NSColor.black.withAlphaComponent(0.26 * opacity)
1112
+
1113
+ NSGraphicsContext.saveGraphicsState()
1114
+ shadow.set()
1115
+ NSGradient(
1116
+ starting: NSColor(calibratedWhite: 0.12, alpha: 0.90 * opacity),
1117
+ ending: NSColor(calibratedWhite: 0.06, alpha: 0.90 * opacity)
1118
+ )?.draw(in: path, angle: -90)
1119
+ NSGraphicsContext.restoreGraphicsState()
1120
+
1121
+ path.lineWidth = 1
1122
+ tint.withAlphaComponent(0.34 * opacity).setStroke()
1123
+ path.stroke()
1124
+
1125
+ let lipRect = CGRect(x: rect.minX + 10, y: rect.maxY - 2, width: rect.width - 20, height: 1)
1126
+ NSColor.white.withAlphaComponent(0.10 * opacity).setFill()
1127
+ NSBezierPath(roundedRect: lipRect, xRadius: 0.5, yRadius: 0.5).fill()
1128
+ }
1129
+
1130
+ private func color(for style: ScreenOverlayStyle) -> NSColor {
1131
+ switch style {
1132
+ case .info:
1133
+ return NSColor(calibratedRed: 0.36, green: 0.72, blue: 1.0, alpha: 1)
1134
+ case .success:
1135
+ return NSColor(calibratedRed: 0.38, green: 0.92, blue: 0.62, alpha: 1)
1136
+ case .warning:
1137
+ return NSColor(calibratedRed: 1.0, green: 0.66, blue: 0.24, alpha: 1)
1138
+ case .danger:
1139
+ return NSColor(calibratedRed: 1.0, green: 0.36, blue: 0.38, alpha: 1)
1140
+ case .playful:
1141
+ return NSColor(calibratedRed: 0.95, green: 0.66, blue: 1.0, alpha: 1)
1142
+ }
1143
+ }
1144
+ }
1145
+
1146
+ private final class CodexPetAssetCache {
1147
+ static let shared = CodexPetAssetCache()
1148
+
1149
+ struct Frame {
1150
+ let image: NSImage
1151
+ let sourceRect: CGRect
1152
+ }
1153
+
1154
+ private struct Metadata: Decodable {
1155
+ struct State: Decodable {
1156
+ let row: Int
1157
+ let frames: Int
1158
+ let frameWidth: CGFloat
1159
+ let frameHeight: CGFloat
1160
+ }
1161
+
1162
+ let spritesheetPath: String?
1163
+ let states: [String: State]?
1164
+ }
1165
+
1166
+ private var cache: [String: (image: NSImage, metadata: Metadata?)] = [:]
1167
+
1168
+ private init() {}
1169
+
1170
+ func frame(for petID: String, state requestedState: String?) -> Frame? {
1171
+ guard let asset = load(petID: petID),
1172
+ let size = asset.image.representations.first.map({ CGSize(width: $0.pixelsWide, height: $0.pixelsHigh) }) else {
1173
+ return nil
1174
+ }
1175
+
1176
+ let state = requestedState.flatMap { asset.metadata?.states?[$0] }
1177
+ ?? asset.metadata?.states?["idle"]
1178
+ ?? Metadata.State(row: 0, frames: 1, frameWidth: 192, frameHeight: 208)
1179
+ let frameWidth = max(1, state.frameWidth)
1180
+ let frameHeight = max(1, state.frameHeight)
1181
+ let frameCount = max(1, state.frames)
1182
+ let frameIndex = Int(Date().timeIntervalSinceReferenceDate * 8) % frameCount
1183
+ let row = max(0, state.row)
1184
+ let maxX = max(0, size.width - frameWidth)
1185
+ let y = max(0, size.height - CGFloat(row + 1) * frameHeight)
1186
+ return Frame(
1187
+ image: asset.image,
1188
+ sourceRect: CGRect(x: min(CGFloat(frameIndex) * frameWidth, maxX), y: y, width: frameWidth, height: frameHeight)
1189
+ )
1190
+ }
1191
+
1192
+ private func load(petID: String) -> (image: NSImage, metadata: Metadata?)? {
1193
+ if let cached = cache[petID] {
1194
+ return cached
1195
+ }
1196
+
1197
+ guard petID.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil else {
1198
+ return nil
1199
+ }
1200
+
1201
+ let root = bundledPetRoot(petID: petID) ?? codexPetRoot(petID: petID)
1202
+ let metadataURL = root.appendingPathComponent("pet.json")
1203
+ let metadata = try? JSONDecoder().decode(Metadata.self, from: Data(contentsOf: metadataURL))
1204
+ let spritesheetURL = root.appendingPathComponent(metadata?.spritesheetPath ?? "spritesheet.webp")
1205
+ guard let image = NSImage(contentsOf: spritesheetURL) else {
1206
+ return nil
1207
+ }
1208
+
1209
+ let asset = (image, metadata)
1210
+ cache[petID] = asset
1211
+ return asset
1212
+ }
1213
+
1214
+ private func bundledPetRoot(petID: String) -> URL? {
1215
+ let candidateRoots = [
1216
+ Bundle.main.resourceURL?.appendingPathComponent("Pets"),
1217
+ Bundle.main.bundleURL.appendingPathComponent("Contents/Resources/Pets"),
1218
+ URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
1219
+ .appendingPathComponent("apps/mac/Resources/Pets"),
1220
+ URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
1221
+ .appendingPathComponent("Resources/Pets"),
1222
+ ].compactMap { $0 }
1223
+
1224
+ for petsRoot in candidateRoots {
1225
+ let root = petsRoot.appendingPathComponent(petID)
1226
+ if FileManager.default.fileExists(atPath: root.appendingPathComponent("spritesheet.webp").path) {
1227
+ return root
1228
+ }
1229
+ }
1230
+
1231
+ return nil
1232
+ }
1233
+
1234
+ private func codexPetRoot(petID: String) -> URL {
1235
+ URL(fileURLWithPath: NSHomeDirectory())
1236
+ .appendingPathComponent(".codex")
1237
+ .appendingPathComponent("pets")
1238
+ .appendingPathComponent(petID)
1239
+ }
1240
+ }