fix: address review comments on PR #671

- Replace lastEmittedAssetId with a process-wide Set<String> so each unique
  cursor shape is serialised at most once even across non-adjacent repeats
  (e.g. arrow → text → arrow no longer resends the arrow bitmap)
- Wrap the sampling loop body in autoreleasepool{} to prevent Cocoa objects
  (NSBitmapImageRep, PNG Data, base64 String) from accumulating for the
  lifetime of the helper during long recordings
- Update stale Accessibility comments: missing Accessibility only disables
  text/pointer affordance detection; native bitmap capture is unaffected
This commit is contained in:
Kelly Yang
2026-05-29 13:48:44 -07:00
parent e82fc0d8cc
commit eac5cc63b4
2 changed files with 35 additions and 31 deletions
@@ -210,7 +210,8 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
try {
systemPreferences.isTrustedAccessibilityClient(true);
} catch {
// Link cursor detection degrades to arrow when Accessibility is unavailable.
// Without Accessibility, text/pointer affordance detection is unavailable;
// cursor bitmaps are still captured natively via NSCursor.
}
const helperPath = findMacCursorHelperPath();
@@ -333,7 +334,7 @@ export class MacNativeCursorRecordingSession implements CursorRecordingSession {
if (payload.type === "ready") {
if (payload.accessibilityTrusted === false) {
console.warn(
"[cursor-macos] Accessibility is not trusted; cursor shape detection will be arrow-only.",
"[cursor-macos] Accessibility is not trusted; text/pointer affordance detection disabled (bitmap capture still active).",
);
}
this.resolveReady();
@@ -314,36 +314,39 @@ emit([
"mouseTapReady": mouseTapReady,
])
var lastEmittedAssetId: String?
// Process-wide set so each unique cursor shape is serialised at most once,
// even if the user alternates between shapes (e.g. arrow → text → arrow).
var emittedAssetIds = Set<String>()
while true {
mouseTracker.pump()
let mouseEvents = mouseTracker.consume()
let asset = currentCursorAsset()
// Only ship the (large) base64 payload the first time a cursor shape is seen;
// subsequent samples reference it by assetId so stdout stays small.
var assetPayload: [String: Any]?
if let asset, asset.id != lastEmittedAssetId {
lastEmittedAssetId = asset.id
assetPayload = [
"id": asset.id,
"imageDataUrl": asset.imageDataUrl,
"width": asset.width,
"height": asset.height,
"hotspotX": asset.hotspotX,
"hotspotY": asset.hotspotY,
"scaleFactor": asset.scaleFactor,
]
autoreleasepool {
mouseTracker.pump()
let mouseEvents = mouseTracker.consume()
let asset = currentCursorAsset()
// Only ship the (large) base64 payload the first time a cursor shape is seen;
// subsequent samples reference it by assetId so stdout stays small.
var assetPayload: [String: Any]?
if let asset, emittedAssetIds.insert(asset.id).inserted {
assetPayload = [
"id": asset.id,
"imageDataUrl": asset.imageDataUrl,
"width": asset.width,
"height": asset.height,
"hotspotX": asset.hotspotX,
"hotspotY": asset.hotspotY,
"scaleFactor": asset.scaleFactor,
]
}
emit([
"type": "sample",
"timestampMs": timestampMs(),
"cursorType": currentCursorType(),
"assetId": asset?.id,
"asset": assetPayload,
"leftButtonDown": leftButtonDown(),
"leftButtonPressed": mouseEvents.leftDownCount > 0,
"leftButtonReleased": mouseEvents.leftUpCount > 0,
])
Thread.sleep(forTimeInterval: Double(intervalMs) / 1000.0)
}
emit([
"type": "sample",
"timestampMs": timestampMs(),
"cursorType": currentCursorType(),
"assetId": asset?.id,
"asset": assetPayload,
"leftButtonDown": leftButtonDown(),
"leftButtonPressed": mouseEvents.leftDownCount > 0,
"leftButtonReleased": mouseEvents.leftUpCount > 0,
])
Thread.sleep(forTimeInterval: Double(intervalMs) / 1000.0)
}