@lattices/cli 0.4.2 → 0.4.5

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 (70) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +90 -34
  8. package/app/Sources/AppShellView.swift +2 -0
  9. package/app/Sources/AppTypeClassifier.swift +36 -0
  10. package/app/Sources/AppUpdater.swift +92 -0
  11. package/app/Sources/CheatSheetHUD.swift +1 -0
  12. package/app/Sources/CliActionLauncher.swift +50 -0
  13. package/app/Sources/CommandModeView.swift +4 -24
  14. package/app/Sources/CompanionActivityLog.swift +70 -0
  15. package/app/Sources/CompanionKeyboardController.swift +141 -0
  16. package/app/Sources/DesktopModel.swift +4 -0
  17. package/app/Sources/HandsOffSession.swift +15 -4
  18. package/app/Sources/HomeDashboardView.swift +18 -10
  19. package/app/Sources/HotkeyStore.swift +8 -5
  20. package/app/Sources/IntentEngine.swift +7 -1
  21. package/app/Sources/LatticesApi.swift +125 -4
  22. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  23. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  24. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  25. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  26. package/app/Sources/LatticesDeckHost.swift +1463 -0
  27. package/app/Sources/LatticesRuntime.swift +61 -0
  28. package/app/Sources/MainView.swift +351 -191
  29. package/app/Sources/MouseFinder.swift +335 -30
  30. package/app/Sources/MouseGestureConfig.swift +364 -0
  31. package/app/Sources/MouseGestureController.swift +1203 -0
  32. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  33. package/app/Sources/MouseInputEventViewer.swift +272 -0
  34. package/app/Sources/MouseShortcutStore.swift +107 -0
  35. package/app/Sources/OmniSearchView.swift +136 -2
  36. package/app/Sources/OmniSearchWindow.swift +65 -5
  37. package/app/Sources/OnboardingView.swift +30 -16
  38. package/app/Sources/PaletteCommand.swift +26 -6
  39. package/app/Sources/PermissionChecker.swift +76 -2
  40. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  41. package/app/Sources/PiAuthPromptCard.swift +90 -0
  42. package/app/Sources/PiChatDock.swift +137 -74
  43. package/app/Sources/PiChatSession.swift +608 -108
  44. package/app/Sources/PiInstallCallout.swift +86 -0
  45. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  46. package/app/Sources/PiWorkspaceView.swift +174 -77
  47. package/app/Sources/Preferences.swift +78 -0
  48. package/app/Sources/ScreenMapState.swift +91 -31
  49. package/app/Sources/ScreenMapView.swift +510 -524
  50. package/app/Sources/ScreenMapWindowController.swift +12 -4
  51. package/app/Sources/SettingsView.swift +869 -152
  52. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  53. package/app/Sources/VoiceCommandWindow.swift +23 -2
  54. package/app/Sources/WindowDragSnapController.swift +628 -0
  55. package/app/Sources/WindowTiler.swift +328 -65
  56. package/app/Sources/WorkspaceManager.swift +288 -0
  57. package/bin/assistant-intelligence.ts +874 -0
  58. package/bin/handsoff-infer.ts +16 -209
  59. package/bin/handsoff-worker.ts +45 -258
  60. package/bin/lattices-app.ts +62 -0
  61. package/bin/lattices-dev +4 -0
  62. package/bin/lattices.ts +125 -14
  63. package/docs/agents.md +14 -0
  64. package/docs/api.md +55 -0
  65. package/docs/app.md +3 -0
  66. package/docs/companion-deck.md +180 -0
  67. package/docs/config.md +25 -0
  68. package/docs/tiling-reference.md +55 -0
  69. package/docs/voice-error-model.md +73 -0
  70. package/package.json +2 -1
@@ -1,23 +1,78 @@
1
1
  import AppKit
2
2
  import CoreGraphics
3
3
 
4
- /// Locates the mouse cursor with an animated sonar pulse overlay.
5
- /// "Find" shows rings at the current cursor position.
6
- /// "Summon" warps the cursor to screen center (or a given point).
4
+ private enum SpotlightConfig {
5
+ static let overlayAlpha: CGFloat = 0.75
6
+ static let dimAlpha: CGFloat = 0.85
7
+ static let spotlightRadius: CGFloat = 200
8
+ static let sonarDelay: TimeInterval = 1.0
9
+ static let totalDuration: TimeInterval = 2.5
10
+ static let fadeInDuration: TimeInterval = 0.15
11
+ static let fadeOutDuration: TimeInterval = 0.4
12
+ static let accentColor = NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: 1.0)
13
+ }
14
+
15
+ private struct DotMatrixConfig {
16
+ var dotRadius: CGFloat = 2.2
17
+ var dotSpacing: CGFloat = 6.0
18
+ var arrowCols: Int = 13
19
+ var arrowRows: Int = 7 // must be odd
20
+
21
+ static let shared: DotMatrixConfig = {
22
+ let path = NSHomeDirectory() + "/.lattices/mouse-finder.json"
23
+ guard FileManager.default.fileExists(atPath: path),
24
+ let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
25
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
26
+ else { return DotMatrixConfig() }
27
+
28
+ var config = DotMatrixConfig()
29
+ if let v = json["dotRadius"] as? Double { config.dotRadius = CGFloat(v) }
30
+ if let v = json["dotSpacing"] as? Double { config.dotSpacing = CGFloat(v) }
31
+ if let v = json["arrowCols"] as? Int { config.arrowCols = max(3, v) }
32
+ if let v = json["arrowRows"] as? Int { config.arrowRows = max(3, v | 1) }
33
+ return config
34
+ }()
35
+
36
+ func generatePattern() -> [(col: Int, row: Int)] {
37
+ let center = arrowRows / 2
38
+ let shaftHalf = center / 2
39
+ var dots: [(Int, Int)] = []
40
+
41
+ for r in 0..<arrowRows {
42
+ let d = abs(r - center)
43
+ if d <= shaftHalf {
44
+ for c in 0...(arrowCols - 1 - d) { dots.append((c, r)) }
45
+ } else {
46
+ let headTip = arrowCols - 1 - d
47
+ let headStart = max(0, headTip - 1)
48
+ for c in headStart...headTip { dots.append((c, r)) }
49
+ }
50
+ }
51
+ return dots
52
+ }
53
+ }
54
+
55
+ /// Locates the mouse cursor with a spotlight + sonar pulse effect.
56
+ /// Dims all screens, spotlights the cursor area, shows directional arrows on off-screens,
57
+ /// then plays sonar rings on top.
7
58
  final class MouseFinder {
8
59
  static let shared = MouseFinder()
9
60
 
10
61
  private var overlayWindows: [NSWindow] = []
62
+ private var sonarWindows: [NSWindow] = []
11
63
  private var dismissTimer: Timer?
12
64
  private var animationTimer: Timer?
65
+ private var sonarDelayTimer: Timer?
13
66
  private var animationStart: CFTimeInterval = 0
14
67
  private let animationDuration: CFTimeInterval = 1.5
68
+ private var globalEventMonitor: Any?
69
+ private var localEventMonitor: Any?
15
70
 
16
71
  // MARK: - Find (highlight current position)
17
72
 
18
73
  func find() {
19
74
  let pos = NSEvent.mouseLocation
20
- showSonar(at: pos)
75
+ showSpotlight(at: pos)
21
76
  }
22
77
 
23
78
  // MARK: - Summon (warp to center of the screen the mouse is on, or a specific point)
@@ -32,30 +87,90 @@ final class MouseFinder {
32
87
  target = NSPoint(x: frame.midX, y: frame.midY)
33
88
  }
34
89
 
35
- // CGWarpMouseCursorPosition uses top-left origin
36
90
  let primaryHeight = NSScreen.screens.first?.frame.height ?? 0
37
91
  let cgPoint = CGPoint(x: target.x, y: primaryHeight - target.y)
38
92
  CGWarpMouseCursorPosition(cgPoint)
39
- // Re-associate mouse with cursor position after warp
40
93
  CGAssociateMouseAndMouseCursorPosition(1)
41
94
 
42
- showSonar(at: target)
95
+ showSpotlight(at: target, mode: .summon)
43
96
  }
44
97
 
45
- // MARK: - Sonar Animation
98
+ // MARK: - Spotlight Effect
46
99
 
47
- private func showSonar(at nsPoint: NSPoint) {
100
+ private func showSpotlight(at nsPoint: NSPoint, mode: SpotlightMode = .find) {
48
101
  dismiss()
49
102
 
50
103
  let screens = NSScreen.screens
51
104
  guard !screens.isEmpty else { return }
52
105
 
106
+ let cursorScreen = screens.first(where: { $0.frame.contains(nsPoint) }) ?? screens[0]
107
+ let otherScreens = screens.filter { $0 !== cursorScreen }
108
+ let windowLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
109
+
110
+ // Spotlight overlay on cursor screen
111
+ let localCursor = NSPoint(
112
+ x: nsPoint.x - cursorScreen.frame.origin.x,
113
+ y: nsPoint.y - cursorScreen.frame.origin.y
114
+ )
115
+ let spotlightWindow = makeOverlayWindow(frame: cursorScreen.frame, level: windowLevel)
116
+ spotlightWindow.contentView = SpotlightView(
117
+ frame: NSRect(origin: .zero, size: cursorScreen.frame.size),
118
+ cursorPoint: localCursor,
119
+ mode: mode
120
+ )
121
+ overlayWindows.append(spotlightWindow)
122
+
123
+ // Dim overlays with directional arrows on other screens
124
+ for screen in otherScreens {
125
+ let screenCenter = NSPoint(
126
+ x: screen.frame.midX,
127
+ y: screen.frame.midY
128
+ )
129
+ let angle = atan2(nsPoint.y - screenCenter.y, nsPoint.x - screenCenter.x)
130
+
131
+ let dimWindow = makeOverlayWindow(frame: screen.frame, level: windowLevel)
132
+ dimWindow.contentView = DimOverlayView(
133
+ frame: NSRect(origin: .zero, size: screen.frame.size),
134
+ cursorAngle: angle
135
+ )
136
+ overlayWindows.append(dimWindow)
137
+ }
138
+
139
+ // Fade all in
140
+ for window in overlayWindows {
141
+ window.alphaValue = 0
142
+ window.orderFrontRegardless()
143
+ NSAnimationContext.runAnimationGroup { ctx in
144
+ ctx.duration = SpotlightConfig.fadeInDuration
145
+ window.animator().alphaValue = 1.0
146
+ }
147
+ }
148
+
149
+ installEventMonitors()
150
+
151
+ // Start sonar after delay
152
+ sonarDelayTimer = Timer.scheduledTimer(withTimeInterval: SpotlightConfig.sonarDelay, repeats: false) { [weak self] _ in
153
+ self?.showSonar(at: nsPoint)
154
+ }
155
+
156
+ // Auto-dismiss
157
+ dismissTimer = Timer.scheduledTimer(withTimeInterval: SpotlightConfig.totalDuration, repeats: false) { [weak self] _ in
158
+ self?.fadeOut()
159
+ }
160
+ }
161
+
162
+ // MARK: - Sonar Animation (plays on top of spotlight)
163
+
164
+ private func showSonar(at nsPoint: NSPoint) {
165
+ let screens = NSScreen.screens
166
+ guard !screens.isEmpty else { return }
167
+
53
168
  let ringCount = 3
54
169
  let maxRadius: CGFloat = 120
55
170
  let totalSize = maxRadius * 2 + 20
171
+ let sonarLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)) + 1)
56
172
 
57
173
  for screen in screens {
58
- // Only show on screens near the cursor
59
174
  let extendedBounds = screen.frame.insetBy(dx: -maxRadius, dy: -maxRadius)
60
175
  guard extendedBounds.contains(nsPoint) else { continue }
61
176
 
@@ -74,7 +189,7 @@ final class MouseFinder {
74
189
  )
75
190
  window.isOpaque = false
76
191
  window.backgroundColor = .clear
77
- window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
192
+ window.level = sonarLevel
78
193
  window.hasShadow = false
79
194
  window.ignoresMouseEvents = true
80
195
  window.collectionBehavior = [.canJoinAllSpaces, .stationary]
@@ -88,16 +203,14 @@ final class MouseFinder {
88
203
 
89
204
  window.alphaValue = 0
90
205
  window.orderFrontRegardless()
91
- overlayWindows.append(window)
206
+ sonarWindows.append(window)
92
207
 
93
- // Fade in
94
208
  NSAnimationContext.runAnimationGroup { ctx in
95
209
  ctx.duration = 0.1
96
210
  window.animator().alphaValue = 1.0
97
211
  }
98
212
  }
99
213
 
100
- // Animate the rings expanding using CACurrentMediaTime for state
101
214
  animationStart = CACurrentMediaTime()
102
215
  let interval = 1.0 / 60.0
103
216
 
@@ -106,7 +219,7 @@ final class MouseFinder {
106
219
  let elapsed = CACurrentMediaTime() - self.animationStart
107
220
  let progress = CGFloat(min(elapsed / self.animationDuration, 1.0))
108
221
 
109
- for window in self.overlayWindows {
222
+ for window in self.sonarWindows {
110
223
  (window.contentView as? SonarView)?.progress = progress
111
224
  window.contentView?.needsDisplay = true
112
225
  }
@@ -116,18 +229,15 @@ final class MouseFinder {
116
229
  self.animationTimer = nil
117
230
  }
118
231
  }
119
-
120
- // Auto-dismiss after animation + hold
121
- dismissTimer = Timer.scheduledTimer(withTimeInterval: 2.5, repeats: false) { [weak self] _ in
122
- self?.fadeOut()
123
- }
124
232
  }
125
233
 
234
+ // MARK: - Lifecycle
235
+
126
236
  private func fadeOut() {
127
- let windows = overlayWindows
237
+ let allWindows = overlayWindows + sonarWindows
128
238
  NSAnimationContext.runAnimationGroup({ ctx in
129
- ctx.duration = 0.4
130
- for window in windows {
239
+ ctx.duration = SpotlightConfig.fadeOutDuration
240
+ for window in allWindows {
131
241
  window.animator().alphaValue = 0
132
242
  }
133
243
  }, completionHandler: { [weak self] in
@@ -136,14 +246,57 @@ final class MouseFinder {
136
246
  }
137
247
 
138
248
  func dismiss() {
249
+ removeEventMonitors()
139
250
  animationTimer?.invalidate()
140
251
  animationTimer = nil
141
252
  dismissTimer?.invalidate()
142
253
  dismissTimer = nil
143
- for window in overlayWindows {
254
+ sonarDelayTimer?.invalidate()
255
+ sonarDelayTimer = nil
256
+ for window in overlayWindows + sonarWindows {
144
257
  window.orderOut(nil)
145
258
  }
146
259
  overlayWindows.removeAll()
260
+ sonarWindows.removeAll()
261
+ }
262
+
263
+ // MARK: - Event Monitors
264
+
265
+ private func installEventMonitors() {
266
+ globalEventMonitor = NSEvent.addGlobalMonitorForEvents(
267
+ matching: [.leftMouseDown, .rightMouseDown, .keyDown]
268
+ ) { [weak self] _ in
269
+ self?.dismiss()
270
+ }
271
+ localEventMonitor = NSEvent.addLocalMonitorForEvents(
272
+ matching: [.leftMouseDown, .rightMouseDown, .keyDown]
273
+ ) { [weak self] event in
274
+ self?.dismiss()
275
+ return event
276
+ }
277
+ }
278
+
279
+ private func removeEventMonitors() {
280
+ if let m = globalEventMonitor { NSEvent.removeMonitor(m); globalEventMonitor = nil }
281
+ if let m = localEventMonitor { NSEvent.removeMonitor(m); localEventMonitor = nil }
282
+ }
283
+
284
+ // MARK: - Helpers
285
+
286
+ private func makeOverlayWindow(frame: NSRect, level: NSWindow.Level) -> NSWindow {
287
+ let window = NSWindow(
288
+ contentRect: frame,
289
+ styleMask: .borderless,
290
+ backing: .buffered,
291
+ defer: false
292
+ )
293
+ window.isOpaque = false
294
+ window.backgroundColor = .clear
295
+ window.level = level
296
+ window.hasShadow = false
297
+ window.ignoresMouseEvents = true
298
+ window.collectionBehavior = [.canJoinAllSpaces, .stationary]
299
+ return window
147
300
  }
148
301
 
149
302
  private func mouseScreen() -> NSScreen {
@@ -152,6 +305,164 @@ final class MouseFinder {
152
305
  }
153
306
  }
154
307
 
308
+ // MARK: - Spotlight View (radial gradient cutout on cursor screen)
309
+
310
+ enum SpotlightMode {
311
+ case find // single arrow at screen center pointing TO the cursor
312
+ case summon // four arrows around the cursor pointing INWARD ("conjured here")
313
+ }
314
+
315
+ private class SpotlightView: NSView {
316
+ let cursorPoint: CGPoint
317
+ let mode: SpotlightMode
318
+ private let config = DotMatrixConfig.shared
319
+ private lazy var dotPattern = config.generatePattern()
320
+
321
+ init(frame: NSRect, cursorPoint: CGPoint, mode: SpotlightMode = .find) {
322
+ self.cursorPoint = cursorPoint
323
+ self.mode = mode
324
+ super.init(frame: frame)
325
+ }
326
+
327
+ required init?(coder: NSCoder) { fatalError() }
328
+
329
+ override func draw(_ dirtyRect: NSRect) {
330
+ guard let ctx = NSGraphicsContext.current?.cgContext else { return }
331
+
332
+ ctx.setFillColor(NSColor.black.withAlphaComponent(SpotlightConfig.overlayAlpha).cgColor)
333
+ ctx.fill(bounds)
334
+
335
+ // Punch a radial gradient hole using destinationOut blend mode
336
+ ctx.setBlendMode(.destinationOut)
337
+
338
+ let colorSpace = CGColorSpaceCreateDeviceRGB()
339
+ let components: [CGFloat] = [
340
+ 1, 1, 1, 1.0,
341
+ 1, 1, 1, 0.8,
342
+ 1, 1, 1, 0.0,
343
+ ]
344
+ let locations: [CGFloat] = [0.0, 0.3, 1.0]
345
+
346
+ guard let gradient = CGGradient(
347
+ colorSpace: colorSpace,
348
+ colorComponents: components,
349
+ locations: locations,
350
+ count: 3
351
+ ) else { return }
352
+
353
+ ctx.drawRadialGradient(
354
+ gradient,
355
+ startCenter: cursorPoint,
356
+ startRadius: 0,
357
+ endCenter: cursorPoint,
358
+ endRadius: SpotlightConfig.spotlightRadius,
359
+ options: []
360
+ )
361
+
362
+ ctx.setBlendMode(.normal)
363
+
364
+ switch mode {
365
+ case .find:
366
+ // Single arrow at screen center pointing toward the cursor.
367
+ let center = CGPoint(x: bounds.midX, y: bounds.midY)
368
+ let angle = atan2(cursorPoint.y - center.y, cursorPoint.x - center.x)
369
+ drawDotMatrixArrow(in: ctx, at: center, angle: angle)
370
+
371
+ case .summon:
372
+ // Four arrows around the cursor, all heads pointing inward toward it —
373
+ // the visual joke is that the mouse was just summoned here, so everything
374
+ // is converging on the new cursor position.
375
+ let arrowLen = CGFloat(config.arrowCols - 1) * config.dotSpacing
376
+ let offset = arrowLen / 2 + SpotlightConfig.spotlightRadius * 0.55
377
+ let placements: [(CGPoint, CGFloat)] = [
378
+ (CGPoint(x: cursorPoint.x, y: cursorPoint.y + offset), -.pi / 2), // above → points down
379
+ (CGPoint(x: cursorPoint.x, y: cursorPoint.y - offset), .pi / 2), // below → points up
380
+ (CGPoint(x: cursorPoint.x - offset, y: cursorPoint.y), 0), // left → points right
381
+ (CGPoint(x: cursorPoint.x + offset, y: cursorPoint.y), .pi), // right → points left
382
+ ]
383
+ for (origin, angle) in placements {
384
+ drawDotMatrixArrow(in: ctx, at: origin, angle: angle)
385
+ }
386
+ }
387
+ }
388
+
389
+ private func drawDotMatrixArrow(in ctx: CGContext, at point: CGPoint, angle: CGFloat) {
390
+ ctx.saveGState()
391
+ ctx.translateBy(x: point.x, y: point.y)
392
+ ctx.rotate(by: angle)
393
+
394
+ let originX = -CGFloat(config.arrowCols - 1) * config.dotSpacing / 2
395
+ let originY = -CGFloat(config.arrowRows - 1) * config.dotSpacing / 2
396
+
397
+ for (col, row) in dotPattern {
398
+ let x = originX + CGFloat(col) * config.dotSpacing
399
+ let y = originY + CGFloat(row) * config.dotSpacing
400
+
401
+ let t = CGFloat(col) / CGFloat(max(1, config.arrowCols - 1))
402
+ let alpha = 0.35 + t * 0.5
403
+
404
+ ctx.setFillColor(SpotlightConfig.accentColor.withAlphaComponent(alpha).cgColor)
405
+ ctx.fillEllipse(in: CGRect(
406
+ x: x - config.dotRadius,
407
+ y: y - config.dotRadius,
408
+ width: config.dotRadius * 2,
409
+ height: config.dotRadius * 2
410
+ ))
411
+ }
412
+
413
+ ctx.restoreGState()
414
+ }
415
+ }
416
+
417
+ // MARK: - Dim Overlay View (dark fill + dot matrix arrow centered on off-screens)
418
+
419
+ private class DimOverlayView: NSView {
420
+ let cursorAngle: CGFloat
421
+ private let config = DotMatrixConfig.shared
422
+ private lazy var dotPattern = config.generatePattern()
423
+
424
+ init(frame: NSRect, cursorAngle: CGFloat) {
425
+ self.cursorAngle = cursorAngle
426
+ super.init(frame: frame)
427
+ }
428
+
429
+ required init?(coder: NSCoder) { fatalError() }
430
+
431
+ override func draw(_ dirtyRect: NSRect) {
432
+ guard let ctx = NSGraphicsContext.current?.cgContext else { return }
433
+
434
+ ctx.setFillColor(NSColor.black.withAlphaComponent(SpotlightConfig.dimAlpha).cgColor)
435
+ ctx.fill(bounds)
436
+
437
+ let center = CGPoint(x: bounds.midX, y: bounds.midY)
438
+
439
+ ctx.saveGState()
440
+ ctx.translateBy(x: center.x, y: center.y)
441
+ ctx.rotate(by: cursorAngle)
442
+
443
+ let originX = -CGFloat(config.arrowCols - 1) * config.dotSpacing / 2
444
+ let originY = -CGFloat(config.arrowRows - 1) * config.dotSpacing / 2
445
+
446
+ for (col, row) in dotPattern {
447
+ let x = originX + CGFloat(col) * config.dotSpacing
448
+ let y = originY + CGFloat(row) * config.dotSpacing
449
+
450
+ let t = CGFloat(col) / CGFloat(max(1, config.arrowCols - 1))
451
+ let alpha = 0.35 + t * 0.5
452
+
453
+ ctx.setFillColor(SpotlightConfig.accentColor.withAlphaComponent(alpha).cgColor)
454
+ ctx.fillEllipse(in: CGRect(
455
+ x: x - config.dotRadius,
456
+ y: y - config.dotRadius,
457
+ width: config.dotRadius * 2,
458
+ height: config.dotRadius * 2
459
+ ))
460
+ }
461
+
462
+ ctx.restoreGState()
463
+ }
464
+ }
465
+
155
466
  // MARK: - Sonar Ring View
156
467
 
157
468
  private class SonarView: NSView {
@@ -172,7 +483,6 @@ private class SonarView: NSView {
172
483
 
173
484
  let center = CGPoint(x: bounds.midX, y: bounds.midY)
174
485
 
175
- // Draw rings from outermost to innermost
176
486
  for i in 0..<ringCount {
177
487
  let ringDelay = CGFloat(i) * 0.15
178
488
  let denom = 1.0 - ringDelay * CGFloat(ringCount - 1) / CGFloat(ringCount)
@@ -180,13 +490,10 @@ private class SonarView: NSView {
180
490
 
181
491
  guard ringProgress > 0 else { continue }
182
492
 
183
- // Ease out cubic
184
493
  let eased = 1.0 - pow(1.0 - ringProgress, 3)
185
-
186
494
  let radius = maxRadius * eased
187
495
  let alpha = (1.0 - eased) * 0.8
188
496
 
189
- // Ring stroke
190
497
  ctx.setStrokeColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: alpha).cgColor)
191
498
  ctx.setLineWidth(2.5 - CGFloat(i) * 0.5)
192
499
  ctx.addEllipse(in: CGRect(
@@ -198,7 +505,6 @@ private class SonarView: NSView {
198
505
  ctx.strokePath()
199
506
  }
200
507
 
201
- // Center dot — stays visible
202
508
  let dotRadius: CGFloat = 6
203
509
  let dotAlpha = max(0.3, 1.0 - progress * 0.5)
204
510
  ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha).cgColor)
@@ -209,7 +515,6 @@ private class SonarView: NSView {
209
515
  height: dotRadius * 2
210
516
  ))
211
517
 
212
- // Outer glow on center dot
213
518
  ctx.setFillColor(NSColor(calibratedRed: 0.4, green: 0.7, blue: 1.0, alpha: dotAlpha * 0.2).cgColor)
214
519
  let glowRadius: CGFloat = 12
215
520
  ctx.fillEllipse(in: CGRect(