Skip to content

fix: align clipboard-first runtime and DMG notarization#3

Open
bishoe01 wants to merge 1 commit into
mainfrom
codex/clipboard-dmg-cleanup
Open

fix: align clipboard-first runtime and DMG notarization#3
bishoe01 wants to merge 1 commit into
mainfrom
codex/clipboard-dmg-cleanup

Conversation

@bishoe01

@bishoe01 bishoe01 commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Align the app runtime with the clipboard-first MVP by removing Accessibility selected-text monitoring and permission UI from the active path.
  • Harden DMG distribution scripts with Release optimization, signing verification, notarization preflight, and latest-DMG discovery.
  • Refresh bilingual docs, PRD/ADR decision logs, roadmap progress, and the session log for the clarified product boundary.

Motivation

The repository documentation now describes clipboard polling as the stable MVP input path, but the app still blocked the overlay on Accessibility permission and kept unused selected-text monitoring code. The distribution scripts also needed a safer signed DMG/notarization handoff before web release.

Changes

File Action Reason
transScan/TextDetector.swift 수정 Keep detection focused on NSPasteboard polling and remove AX observer state.
transScan/TranslationViewModel.swift 수정 Start monitoring without Accessibility permission and simplify event state.
transScan/SettingsView.swift, MenuBarContentView.swift, TranslationResultView.swift 수정 Remove permission CTA/error surfaces from the current MVP UI.
transScan/PermissionManager.swift, PermissionBannerView.swift 삭제 Remove unused Accessibility permission infrastructure from the active app.
scripts/create_dmg.sh, scripts/notarize_dmg.sh 추가/수정 Improve Release DMG signing verification and notarization workflow.
README.md, README.ko.md, docs/PRD.md, docs/ADR.md, roadmap* 수정 Keep docs aligned with clipboard-first behavior and distribution handoff.
transScanTests/* 수정 Update tests for clipboard-only detection and monitoring startup.

Test Plan

  • xcodebuild -project transScan.xcodeproj -scheme transScan test -quiet
  • xcodebuild -project transScan.xcodeproj -scheme transScan build -quiet
  • bash -n scripts/create_dmg.sh scripts/notarize_dmg.sh
  • node -e "JSON.parse(require('fs').readFileSync('roadmap.progress.json','utf8')); console.log('roadmap.progress.json OK')"
  • git diff HEAD --check
  • Real Developer ID DMG notarization and spctl verification not run yet.

Summary by CodeRabbit

Release Notes

  • Documentation

    • Updated guides and roadmap to reflect clipboard-based text detection workflow
    • Updated UI guidance to instruct users to copy text instead of drag-or-copy
  • Chores

    • Removed accessibility permission requirement and related UI elements
    • Simplified settings interface; removed permission controls
    • Enhanced DMG build and notarization scripts with signing and verification capabilities
    • Updated test suite for clipboard-focused detection approach

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

transScan's text detection shifts from Accessibility API primary with clipboard fallback to clipboard-only polling via NSPasteboard. Removes PermissionManager, simplifies TextDetector and event model, eliminates permission-gated UI and translation flow, updates all documentation and tests, and adds DMG notarization infrastructure for official releases.

Changes

Clipboard-first text detection refactor

Layer / File(s) Summary
Documentation alignment and decision log
README.md, README.ko.md, AGENTS.md, CLAUDE.md, docs/ADR.md, docs/PRD.md, docs/UI_GUIDE.md, docs/session-logs/2026-05-28.md, roadmap.md, roadmap.progress.json
All user-facing and architectural documentation updated to describe clipboard-first MVP using NSPasteboard polling. Accessibility-based direct selection detection deferred as future stabilization goal. Decision logs record rationale, test strategy adjusted from "fallback" to "polling" terminology.
TextDetector clipboard-only implementation
transScan/TextDetector.swift
Eliminates ApplicationServices import and AX observer setup. TextDetectedEvent simplified to carry only text (TextDetectionSource enum removed). New processRawInput helper normalizes and deduplicates incoming text. Monitoring lifecycle reduced to clipboard timer management.
Permission manager and barrier removal
transScan/AppDelegate.swift, transScan/MenuBarContentView.swift
Delete PermissionManager.swift singleton and PermissionBannerView.swift entirely. Remove startup permission checks from AppDelegate. Remove "open settings" button and divider from menu.
TranslationViewModel and error handling refactor
transScan/TranslationViewModel.swift
Remove detectionSource published state and permission-check closure from initializer. Drop permissionDenied error case. Remove onAppear permission guard. Text detection handler now only updates lastInputText and triggers translation.
TranslationResultView permission UI removal
transScan/TranslationResultView.swift
Remove detectionSource and onOpenSettings stored properties. Delete permissionDenied switch branches from errorTitle, errorDescription, and errorAction methods.
View layer integration cleanup
transScan/OverlayContentView.swift, transScan/SettingsView.swift
OverlayContentView stops passing detectionSource and onOpenSettings to result view. SettingsView removes PermissionManager observation and permission UI, retains only clipboard-to-translation explanation and overlay-open button.
Unit tests for clipboard-only model
transScanTests/TextDetectorTests.swift, transScanTests/TranslationViewModelTests.swift
Update TextDetectorTests to call processRawInput without source argument; remove MockAXReader. Simplify TranslationViewModelTests makeVM() helper, update all TextDetectedEvent publications to text-only form across test cases, track startMonitoring call counts, and replace permission-denied test with clipboard-monitoring assertion.

DMG signing and notarization automation

Layer / File(s) Summary
DMG creation with code signing
scripts/create_dmg.sh
Add configurable signing: detect available Developer ID identities, wire manual-signing arguments to xcodebuild when SIGNING_IDENTITY is set. Set distribution-specific derived data paths, run clean build, add pre-packaging check to refuse debug/preview dylibs, and conditionally codesign and verify final DMG with timestamp.
DMG notarization and Gatekeeper verification
scripts/notarize_dmg.sh
New script automates macOS notarization workflow: accept optional DMG path or auto-detect latest build, validate code-signing, ensure xcrun notarytool keychain profile exists, submit for notarization and wait, staple ticket, and verify Gatekeeper acceptance via spctl with explicit error handling and credential setup guidance.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Clipboard polls with NSPasteboard cheer,
No Accessibility gates appear,
Permissions gone, the flow runs clear—
Drag-free translation drawing near,
With notarization standing here!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: align clipboard-first runtime and DMG notarization' accurately and specifically summarizes the two main changes: removing Accessibility-based text detection and hardening DMG distribution with notarization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/clipboard-dmg-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8603aed590

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.font(.system(size: 13, weight: .medium))

Text(permissionDescription)
Text("텍스트를 복사한 뒤 오버레이를 열면 최근 클립보드 내용을 번역합니다.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle the preexisting clipboard text on open

This copy now tells users that copying text and then opening the overlay will translate the recent clipboard contents, but the runtime path only subscribes to future pasteboard changes: TextDetector.startMonitoring() seeds lastClipboardChangeCount with the current NSPasteboard.general.changeCount, and TranslationViewModel.onAppear() never calls latestClipboardText(). In the advertised flow, the overlay remains idle until the user copies again, so either translate the current clipboard snapshot when opening or avoid promising copy-before-open behavior.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
transScanTests/TranslationViewModelTests.swift (1)

440-449: ⚡ Quick win

Add the paired onDisappear() lifecycle assertion test.

You now track stopMonitoringCallCount, but only onAppear() is asserted. Add one focused test for onDisappear() to lock the lifecycle contract.

Proposed test addition
 func testStartsClipboardMonitoringOnAppear() {
     let vm = makeVM()

     vm.onAppear()

     XCTAssertEqual(vm.state, .idle)
     XCTAssertEqual(mockDetector.startMonitoringCallCount, 1)
 }
+
+func testStopsClipboardMonitoringOnDisappear() {
+    let vm = makeVM()
+    vm.onAppear()
+
+    vm.onDisappear()
+
+    XCTAssertEqual(mockDetector.stopMonitoringCallCount, 1)
+}

As per coding guidelines: Add XCTest coverage for new app behavior in transScanTests/ and prefer focused tests around view model state transitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transScanTests/TranslationViewModelTests.swift` around lines 440 - 449, Add a
focused XCTest that verifies the view model calls stop monitoring on lifecycle
end: create the VM via makeVM(), invoke vm.onAppear() (to mirror the start
path), then call vm.onDisappear(), and assert
mockDetector.stopMonitoringCallCount == 1 and vm.state == .idle (and optionally
that startMonitoringCallCount == 1 to show the paired lifecycle). Name the test
e.g. testStopsClipboardMonitoringOnDisappear and reference vm.onDisappear() and
mockDetector.stopMonitoringCallCount in the assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@roadmap.progress.json`:
- Line 5: The JSON contains a machine-identifying value for the key
"deviceLabel" (currently "bishoe01s-MacBook-Air-4.local"); replace that value
with a neutral identifier (e.g. "local-device" or "unknown-device") or remove
the "deviceLabel" entry entirely to avoid leaking hostname information, then
amend the commit so the sanitized value is stored; ensure any local
Xcode/machine state files that may regenerate this field are excluded from
version control going forward.

In `@scripts/notarize_dmg.sh`:
- Around line 12-16: The current fallback that sets DMG_PATH uses lexicographic
sort which can pick the wrong file; change the fallback to select the most
recently modified DMG by mtime instead. In the branch that sets DMG_PATH when no
argument is given (the find ... -name 'transScan-*.dmg' | sort | tail -n 1
pipeline), replace the lexicographic sort with a pipeline that sorts by
modification time (e.g., use find to list matches and pipe to ls -t / sort -n on
file mtimes or use find -printf '%T@ %p\n' | sort -n | tail -n1) so the code
that assigns DMG_PATH picks the newest file by modification time.
- Around line 30-43: The notarytool invocation currently discards all output and
treats any failure as a missing profile; change the logic in
scripts/notarize_dmg.sh around the xcrun notarytool history --keychain-profile
"$PROFILE" call to capture stderr/stdout into a variable, test that output (or
the command exit status) for the specific "No Keychain password item found for
profile" message (or exact error code/message returned by notarytool) and only
print the existing "No notarytool Keychain profile found: $PROFILE" help text in
that case; for any other failure, echo the captured notarytool error to stderr
and exit with the original exit code so callers see the real cause (keep the
existing exit 1 for the profile-missing branch and propagate other non-zero
statuses instead).

In `@transScan/TextDetector.swift`:
- Around line 64-68: The clipboard polling Timer created in
startClipboardPolling uses Timer.scheduledTimer(...) which registers in the
default run loop mode and can pause during UI interactions; change
startClipboardPolling to create the Timer (e.g.,
Timer(timeInterval:repeats:block:) or scheduledTimer then remove automatic
scheduling) and explicitly add it to RunLoop.main in .common modes
(RunLoop.main.add(clipboardTimer!, forMode: .common)) so clipboardTimer runs in
RunLoop.Mode.common and won't be paused during menu/drag interactions.

---

Nitpick comments:
In `@transScanTests/TranslationViewModelTests.swift`:
- Around line 440-449: Add a focused XCTest that verifies the view model calls
stop monitoring on lifecycle end: create the VM via makeVM(), invoke
vm.onAppear() (to mirror the start path), then call vm.onDisappear(), and assert
mockDetector.stopMonitoringCallCount == 1 and vm.state == .idle (and optionally
that startMonitoringCallCount == 1 to show the paired lifecycle). Name the test
e.g. testStopsClipboardMonitoringOnDisappear and reference vm.onDisappear() and
mockDetector.stopMonitoringCallCount in the assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 029ee371-e709-4315-a209-4c0b712e8859

📥 Commits

Reviewing files that changed from the base of the PR and between 740747c and 8603aed.

📒 Files selected for processing (23)
  • AGENTS.md
  • CLAUDE.md
  • README.ko.md
  • README.md
  • docs/ADR.md
  • docs/PRD.md
  • docs/UI_GUIDE.md
  • docs/session-logs/2026-05-28.md
  • roadmap.md
  • roadmap.progress.json
  • scripts/create_dmg.sh
  • scripts/notarize_dmg.sh
  • transScan/AppDelegate.swift
  • transScan/MenuBarContentView.swift
  • transScan/OverlayContentView.swift
  • transScan/PermissionBannerView.swift
  • transScan/PermissionManager.swift
  • transScan/SettingsView.swift
  • transScan/TextDetector.swift
  • transScan/TranslationResultView.swift
  • transScan/TranslationViewModel.swift
  • transScanTests/TextDetectorTests.swift
  • transScanTests/TranslationViewModelTests.swift
💤 Files with no reviewable changes (5)
  • transScan/AppDelegate.swift
  • transScan/TranslationResultView.swift
  • transScan/PermissionManager.swift
  • transScan/PermissionBannerView.swift
  • transScan/MenuBarContentView.swift

Comment thread roadmap.progress.json
"updatedAt": "2026-05-08 23:36:37 JST (+0900)",
"deviceLabel": "bishoe01s-MacBook-Air-2.local",
"updatedAt": "2026-05-28 23:47:34 JST (+0900)",
"deviceLabel": "bishoe01s-MacBook-Air-4.local",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove machine-identifying hostname from tracked metadata.

Line 5 exposes a local host identifier (bishoe01s-MacBook-Air-4.local) in source control. Replace it with a neutral label to avoid leaking machine-specific information.

Based on learnings: Do not commit local Xcode or machine state files, including machine-specific paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@roadmap.progress.json` at line 5, The JSON contains a machine-identifying
value for the key "deviceLabel" (currently "bishoe01s-MacBook-Air-4.local");
replace that value with a neutral identifier (e.g. "local-device" or
"unknown-device") or remove the "deviceLabel" entry entirely to avoid leaking
hostname information, then amend the commit so the sanitized value is stored;
ensure any local Xcode/machine state files that may regenerate this field are
excluded from version control going forward.

Comment thread scripts/notarize_dmg.sh
Comment on lines +12 to +16
if [[ $# -eq 1 ]]; then
DMG_PATH="$1"
else
DMG_PATH="$(find "$ROOT_DIR/build" -maxdepth 1 -type f -name 'transScan-*.dmg' -print 2>/dev/null | sort | tail -n 1 || true)"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Pick the newest DMG by modification time, not filename sort.

Line 15 uses lexicographic sorting, so transScan-1.10.dmg sorts before transScan-1.9.dmg. That can notarize an older artifact by default instead of the most recently built one, which breaks the “latest build” behavior described for this flow in roadmap.progress.json:12-13.

💡 Proposed fix
-    DMG_PATH="$(find "$ROOT_DIR/build" -maxdepth 1 -type f -name 'transScan-*.dmg' -print 2>/dev/null | sort | tail -n 1 || true)"
+    DMG_PATH="$(
+        find "$ROOT_DIR/build" -maxdepth 1 -type f -name 'transScan-*.dmg' -exec stat -f '%m %N' {} + 2>/dev/null \
+            | sort -n \
+            | tail -n 1 \
+            | cut -d' ' -f2-
+    )"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/notarize_dmg.sh` around lines 12 - 16, The current fallback that sets
DMG_PATH uses lexicographic sort which can pick the wrong file; change the
fallback to select the most recently modified DMG by mtime instead. In the
branch that sets DMG_PATH when no argument is given (the find ... -name
'transScan-*.dmg' | sort | tail -n 1 pipeline), replace the lexicographic sort
with a pipeline that sorts by modification time (e.g., use find to list matches
and pipe to ls -t / sort -n on file mtimes or use find -printf '%T@ %p\n' | sort
-n | tail -n1) so the code that assigns DMG_PATH picks the newest file by
modification time.

Comment thread scripts/notarize_dmg.sh
Comment on lines +30 to +43
if ! xcrun notarytool history --keychain-profile "$PROFILE" >/dev/null 2>&1; then
cat >&2 <<MESSAGE
No notarytool Keychain profile found: $PROFILE

Create it once with:

xcrun notarytool store-credentials "$PROFILE" \\
--apple-id "YOUR_APPLE_ID_EMAIL" \\
--team-id 9A96KXU385 \\
--password "APP_SPECIFIC_PASSWORD"

Use an Apple ID app-specific password, not your normal Apple ID password.
MESSAGE
exit 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For Apple xcrun notarytool, what failure modes can notarytool history --keychain-profile return, and is it a reliable way to test whether a keychain profile exists locally?

💡 Result:

What can fail with xcrun notarytool history --keychain-profile <profile>?

history needs two things to succeed: (a) it must be able to read credentials from Keychain for <profile>, and (b) it must be able to authenticate to Apple’s notary service and fetch history.

Common failure modes include:

  • Keychain item for the profile can’t be found/read
    Typically surfaces as: Error: No Keychain password item found for profile: <profile> (often reported with exit code 69). This can happen when the profile truly doesn’t exist, or when it exists but isn’t readable (wrong keychain, access issues, corruption, etc.). [2], [3], [4]

  • Keychain is locked / requires unlock UI
    The man page notes that if the specified keychain is locked, notarytool will prompt to unlock it—this can break non-interactive/CI usage and lead to failure. [1]

  • No usable credentials were ultimately provided
    If notarytool can’t resolve usable credentials from the profile (or otherwise), it can fail with errors like Error: Must provide credentials. [5]

  • Credentials resolve but authentication is rejected (server-side auth failure)
    For example, the man page explicitly mentions cases that yield a 401 Unauthorized response (e.g., wrong issuer usage for certain API keys). Any invalid/revoked credential could similarly cause history to fail even though the profile exists locally. [1]

Is history --keychain-profile <profile> a reliable “does this profile exist locally?” test?

Not fully.

  • A failure does not reliably mean “profile doesn’t exist”, because it can also fail due to locked keychain, unreadable keychain item, bad/expired credentials, or server-side auth issues. [1], [4]
  • A success does strongly imply the profile exists and is usable (it was read from Keychain and authenticated).

If your goal is specifically “does the keychain item exist locally?”, prefer directly checking Keychain for the saved-credentials item. Apple’s stored profile naming convention shows up in Keychain with an account attribute like com.apple.gke.notary.tool.saved-creds.<ProfileName>. [6]


Don’t collapse every xcrun notarytool history --keychain-profile failure into “profile missing” (scripts/notarize_dmg.sh, lines 30-43).

  • The script discards all notarytool output (>/dev/null 2>&1) and treats any non-zero exit as “No notarytool Keychain profile found”.
  • notarytool history --keychain-profile ... can fail for reasons unrelated to a missing Keychain item (e.g., locked keychain, unreadable/corrupt credential item, missing/invalid credentials, or server-side auth/API errors such as 401 Unauthorized).
  • Show the real notarytool error (stderr) and only display the “profile missing” message for the specific “No Keychain password item found for profile …” failure (or matching exit/error code); otherwise surface the underlying cause.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/notarize_dmg.sh` around lines 30 - 43, The notarytool invocation
currently discards all output and treats any failure as a missing profile;
change the logic in scripts/notarize_dmg.sh around the xcrun notarytool history
--keychain-profile "$PROFILE" call to capture stderr/stdout into a variable,
test that output (or the command exit status) for the specific "No Keychain
password item found for profile" message (or exact error code/message returned
by notarytool) and only print the existing "No notarytool Keychain profile
found: $PROFILE" help text in that case; for any other failure, echo the
captured notarytool error to stderr and exit with the original exit code so
callers see the real cause (keep the existing exit 1 for the profile-missing
branch and propagate other non-zero statuses instead).

Comment on lines 64 to 68
private func startClipboardPolling() {
clipboardTimer?.invalidate()
clipboardTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.checkClipboard()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Timer.scheduledTimer(withTimeInterval:repeats:block:)on macOS add the timer only to the default run loop mode, and should a polling timer use.common modes to keep firing during menu tracking or drag interactions?

💡 Result:

Yes. Apple documents that Timer.scheduledTimer(withTimeInterval:repeats:block:) “schedules it on the current run loop in the default mode”, i.e. only in the default run-loop mode rather than the menu/drag tracking modes.[1][2] For a polling/repeating timer that you need to keep firing during menu tracking or drag interactions, you generally should add the timer to the run loop’s “common modes” (e.g. RunLoop.Mode.common) rather than only the default mode, because common modes share sources/timers across the default/modal/event-tracking modes. Apple’s run-loop documentation explains that timers are associated with specific modes and won’t invoke their handlers unless the run loop is running in a mode that the timer is registered for.[3] Core Foundation further explains that kCFRunLoopCommonModes is a pseudo-mode used so objects added there are monitored by all run loop modes that are in the common set (including the default mode, and typically the tracking modes once they’re part of the common set).[4] Practical Swift pattern (add to common modes instead of relying on the convenience scheduler): let timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in // polling work } RunLoop.current.add(timer, forMode:.common) Or create the timer without scheduling and add it manually to .common (so it’s not limited to the default mode only).[2] In short: the scheduledTimer(withTimeInterval:repeats:block:) convenience API is default-mode-only; use .common for repeating polling timers when you must keep firing during UI tracking/drag/menu interactions.

Citations:


Run clipboard polling timer in .common run loop modes.

Timer.scheduledTimer(withTimeInterval:repeats:block:) schedules the timer on the run loop in the default mode, which can pause during menu tracking/drag interactions; this can delay the clipboard-only ingestion path.

♻️ Proposed fix
 private func startClipboardPolling() {
     clipboardTimer?.invalidate()
-    clipboardTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
-        self?.checkClipboard()
-    }
+    let timer = Timer(timeInterval: 0.5, repeats: true) { [weak self] _ in
+        self?.checkClipboard()
+    }
+    RunLoop.main.add(timer, forMode: .common)
+    clipboardTimer = timer
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func startClipboardPolling() {
clipboardTimer?.invalidate()
clipboardTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
self?.checkClipboard()
}
private func startClipboardPolling() {
clipboardTimer?.invalidate()
let timer = Timer(timeInterval: 0.5, repeats: true) { [weak self] _ in
self?.checkClipboard()
}
RunLoop.main.add(timer, forMode: .common)
clipboardTimer = timer
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@transScan/TextDetector.swift` around lines 64 - 68, The clipboard polling
Timer created in startClipboardPolling uses Timer.scheduledTimer(...) which
registers in the default run loop mode and can pause during UI interactions;
change startClipboardPolling to create the Timer (e.g.,
Timer(timeInterval:repeats:block:) or scheduledTimer then remove automatic
scheduling) and explicitly add it to RunLoop.main in .common modes
(RunLoop.main.add(clipboardTimer!, forMode: .common)) so clipboardTimer runs in
RunLoop.Mode.common and won't be paused during menu/drag interactions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant