Skip to content

Improve auto-send reliability with AX-based paste detection - #616

Open
gulliversgames wants to merge 1 commit into
Beingpax:mainfrom
gulliversgames:fix/reliable-auto-send
Open

gulliversgames wants to merge 1 commit into
Beingpax:mainfrom
gulliversgames:fix/reliable-auto-send

Conversation

@gulliversgames

@gulliversgames gulliversgames commented Mar 28, 2026

Copy link
Copy Markdown

Summary

  • Dismiss recorder before pasting so macOS refocuses the target window first — previously paste keystrokes could miss the target window because the recorder panel was still visible
  • Capture auto-send key before dismissdismissMiniRecorder clears the Power Mode config, so the auto-send key was always nil by the time it was read
  • Replace fixed delay with AX-based detection — polls the focused text field's AXValue every 50ms (up to 3s) to confirm the paste has landed before sending the auto-send keystroke. Works for any text length. Falls back to a timeout for apps where AXValue isn't readable (e.g. some web apps)

Problem

The auto-send feature (sending Enter/Shift+Enter/Cmd+Enter after paste) was unreliable:

  1. The recorder panel was still visible when paste fired, so keystrokes sometimes went to the wrong window
  2. The Power Mode config was cleared during dismiss before the auto-send key could be read
  3. The fixed 100ms delay between paste and auto-send was too short for long texts — the target app hadn't finished processing the paste

Test plan

  • Record with a Power Mode that has auto-send enabled (e.g. Return key)
  • Verify paste lands in the correct window
  • Verify Enter fires after the text appears (not before)
  • Test with short text ("yes") — Enter should fire almost immediately
  • Test with long text (30+ seconds of dictation) — Enter should wait for the full text to appear
  • Test with apps where AXValue may not be readable (e.g. browser text fields) — should fall back to 3s timeout

🤖 Generated with Claude Code


Summary by cubic

Improves auto-send reliability by dismissing the recorder before paste, capturing the auto-send key earlier, and using AX-based detection to wait for the paste to land. Prevents misdirected keystrokes and sends Enter only after text appears.

  • Bug Fixes
    • Dismiss mini recorder before paste so macOS refocuses the target window; wait ~150ms before pasting to let focus settle.
    • Capture the auto-send key before dismiss clears the Power Mode config.
    • Replace fixed delay with AX polling of the focused field’s value (snapshot pre-paste, poll every 50ms up to 3s); fall back to a short 300ms delay when AX isn’t readable.

Written for commit 6dc3f12. Summary will update on new commits.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="VoiceInk/CursorPaster.swift">

<violation number="1" location="VoiceInk/CursorPaster.swift:116">
P1: Auto-send polling runs in an uncancelled detached task and can fire Enter into a different app/field after focus changes.</violation>

<violation number="2" location="VoiceInk/CursorPaster.swift:126">
P2: Paste completion detection is too weak (`contains(prefix)`), allowing false positives (including empty-text immediate match) and premature auto-send before paste is actually applied.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

guard autoSendKey.isEnabled else { return }

// Poll the focused element's AXValue to detect when paste has landed.
Task.detached {

@cubic-dev-ai cubic-dev-ai Bot Mar 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Auto-send polling runs in an uncancelled detached task and can fire Enter into a different app/field after focus changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At VoiceInk/CursorPaster.swift, line 116:

<comment>Auto-send polling runs in an uncancelled detached task and can fire Enter into a different app/field after focus changes.</comment>

<file context>
@@ -100,6 +100,66 @@ class CursorPaster {
+        guard autoSendKey.isEnabled else { return }
+
+        // Poll the focused element's AXValue to detect when paste has landed.
+        Task.detached {
+            let maxWait: TimeInterval = 3.0
+            let pollInterval: UInt64 = 50_000_000 // 50ms
</file context>
Fix with Cubic

Comment thread VoiceInk/CursorPaster.swift Outdated
Three fixes for paste and auto-send reliability:

1. Dismiss recorder before pasting so macOS refocuses the target window
   first (was: paste fired while recorder was still visible, causing
   keystrokes to miss the target window)

2. Capture auto-send key before dismissing, since dismissMiniRecorder
   clears the Power Mode configuration (was: autoSendKey was always nil
   by the time it was read)

3. Replace fixed auto-send delay with AX-based paste detection: polls
   the focused text field's AXValue every 50ms (up to 3s) to confirm
   the paste has landed before sending the auto-send keystroke. This
   works reliably for any text length — short messages send immediately,
   long pastes wait until the app has finished processing. Falls back
   to a timeout for apps where AXValue isn't readable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gulliversgames
gulliversgames force-pushed the fix/reliable-auto-send branch from a4f417e to 6dc3f12 Compare March 28, 2026 21:50
@brutes1

brutes1 commented Apr 12, 2026

Copy link
Copy Markdown

Code review

Found 3 issues:

  1. AutoLearnVocabularyService calls are dropped on merge. This PR branched before commit ca705f9 added AutoLearnVocabularyService.shared.prepareMonitoring() and AutoLearnVocabularyService.shared.beginMonitoring() into the same paste dispatch block this PR rewrites. The new DispatchQueue.main.asyncAfter block only calls pasteAndAutoSend or pasteAtCursor — neither service call is present. Merging as-is will silently stop the auto-learn vocabulary feature from firing on every transcription.

\n\(textToPaste)
"""
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
if let autoSendKey, autoSendKey.isEnabled {
CursorPaster.pasteAndAutoSend(textToPaste, autoSendKey: autoSendKey)
} else {
let appendSpace = UserDefaults.standard.bool(forKey: "AppendTrailingSpace")
CursorPaster.pasteAtCursor(textToPaste + (appendSpace ? " " : ""))

  1. Strategy A fires auto-send into the wrong window when focus changes during polling. baselineValue is non-nil (guaranteed by canReadField). If the user switches apps during the 3-second window, getFocusedElementValue() returns nil for the new element. In Swift, nil != "previous text" evaluates to true, so the poll immediately concludes "paste landed" and fires performAutoSend into whichever window is now focused. The fix is to skip the comparison when currentValue is nil rather than treating it as a changed value.

while Date().timeIntervalSince(startTime) < maxWait {
let currentValue = await Self.getFocusedElementValue()
if currentValue != baselineValue {
// Field changed — paste landed
await MainActor.run { performAutoSend(autoSendKey) }
return
}
try? await Task.sleep(nanoseconds: pollInterval)

  1. Strategy B's 300ms delay regresses the terminal fix from commit a75ef6f. That commit raised the auto-send delay from 0.1s to 0.5s specifically because terminal emulators (Terminal.app, iTerm2, Warp, Ghostty) need 200–350ms for PTY/readline to process longer dictations. Strategy B fires performAutoSend 300ms after pasteAndAutoSend is called — but pasteAtCursor's internal 50ms delay means Cmd+V doesn't fire until 50ms later, leaving only 250ms between the paste keystroke and the auto-send. That's inside the 200–350ms danger zone for longer dictations in terminals. Strategy B's sleep should be at least 500_000_000 ns (500ms) to restore the prior guarantee.

} else {
// Strategy B: fixed delay for apps where AXValue isn't readable
// 300ms after paste keystroke (50ms internal + 250ms buffer)
try? await Task.sleep(nanoseconds: 300_000_000)
await MainActor.run { performAutoSend(autoSendKey) }
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

This branch has not been deployed

No deployments
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.

2 participants