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