Skip to content

fix(typing): short-circuit verification for targets with no verifiable text - #809

Open
YuriNachos wants to merge 2 commits into
altic-dev:mainfrom
YuriNachos:fix/typing-unverifiable-shortcut
Open

fix(typing): short-circuit verification for targets with no verifiable text#809
YuriNachos wants to merge 2 commits into
altic-dev:mainfrom
YuriNachos:fix/typing-unverifiable-shortcut

Conversation

@YuriNachos

@YuriNachos YuriNachos commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

On targets that expose no verifiable accessibility text — GPU-rendered terminals such as Ghostty — waitForFocusedTextVerification polled for the full 5 s timeoutMicros while holding the process-global pasteboard session semaphore. That is the multi-second stall measured in #802 (sub-second → ~8.9 s) whenever dictation was injected into such a target.

The fix short-circuits verification when none of the four verifiable focused-text channels is present (value, selectedRange, appScriptValue, appScriptSelectedRange all nil): sleep a pasteboard-consume window, then release the session as .unavailable. Verifiable targets — any one channel present — keep their existing fast path unchanged.

The consume window is 500 ms (pasteConsumeWindowMicros). action() only enqueues Cmd+V via postToPid; the target then reads NSPasteboard.general asynchronously on its own main thread, and under load (a large paste, a busy main thread during a build) that read can land hundreds of milliseconds later — so restoring the prior pasteboard too eagerly would clobber the transcript before the target consumes it. 500 ms is a conservative floor for a busy terminal to service the paste, yet ~10× under the 5 s timeout, so the #802 stall is gone.

Diff: Sources/Fluid/Services/TypingService.swift plus one test file.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Closes #802.

Testing

  • Tested on Apple Silicon Mac
  • Tested on Intel Mac
  • Tested on macOS version: 26.0 (Darwin 25.6.0), Xcode latest-stable
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources0 violations, 0 serious in 139 files
  • Ran tests locally: xcodebuild test -project Fluid.xcodeproj -scheme Fluid -destination 'platform=macOS,arch=arm64' CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO -skip-testing:FluidDictationIntegrationTests/DictationE2ETests/testDictationEndToEnd_whisperTiny_transcribesFixture

Two new cases, both in TypingServiceTransientPasteboardTests (suite passes in full):

  • testFocusedTextSnapshotWithoutVerifiableChannelShortCircuitsVerification — pins the predicate: all-four-nil is unverifiable; any single non-nil channel is verifiable.
  • testWaitForFocusedTextVerificationShortCircuitsUnverifiableTargetInBoundedTimebehavioural: drives the real waitForFocusedTextVerification with an unverifiable snapshot (real pid, all four channels nil) and asserts it returns .unavailable in under timeoutMicros / 2. Remove the short-circuit and it polls to .timeout at ~5 s, failing both asserts.

Two unrelated tests are red on my machine only, for the environment reasons this repo's own prerequisites document, and both are red on a clean main here too:

  • HotkeyShortcutTests.testKeyboardPayloadIgnoresStrayMouseButtonField — asserts the ASCII glyph at a keyCode; my host has a non-U.S. input source active.
  • DictationE2ETests.testAppPromptBinding_defaultFallbackIgnoresGlobalSelection — reads real com.FluidApp.app defaults, which are customised on this machine.

Neither touches the typing-verification path. CI runs on a clean U.S.-layout runner, so both should be green there.

Screenshots / Video

  • No UI/visual changes; screenshots/video are not applicable.

This is a backend typing-verification timing path — no view, layout, styling, settings, overlay or menu-bar surface is touched. The user-observable effect is latency: dictating into Ghostty returns in well under a second instead of ~8.9 s.

Notes

  • Verifiable targets are byte-identical to main; only the all-channels-nil case takes the new branch.
  • The consume window is a named constant (pasteConsumeWindowMicros) so it is easy to tune if a slower target turns up.
  • A few private declarations (FocusedTextSnapshot, PasteVerificationResult, waitForFocusedTextVerification) became internal so the short-circuit path can be exercised by tests; none of them are public API.

@github-actions github-actions Bot added needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence. labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Testing
  • Screenshots / Video

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@greptile-apps

greptile-apps Bot commented Aug 7, 2026

Copy link
Copy Markdown

Greptile Summary

The PR avoids a five-second focused-text verification poll when the target exposes no verifiable accessibility or AppleScript text channel.

  • Adds a 500 ms pasteboard-consumption window before restoring clipboard contents for unverifiable targets.
  • Adds predicate and behavioral tests covering channel detection and bounded short-circuit timing.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "fix(typing): give unverifiable targets a..." | Re-trigger Greptile

@github-actions github-actions Bot removed needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence. labels Aug 8, 2026
@YuriNachos
YuriNachos force-pushed the fix/typing-unverifiable-shortcut branch from 0af41ae to 05de5e8 Compare August 8, 2026 11:24
YuriNachos and others added 2 commits August 10, 2026 14:03
… targets

Consecutive dictations into a GPU-rendered/terminal target (e.g. Ghostty)
that exposes no verifiable accessibility text channel stalled for seconds:
withTemporaryPasteboardString holds the process-global pasteboard session
semaphore across waitForFocusedTextVerification, which polled the full 5s
timeoutMicros before returning .timeout because every if let-gated channel
check fell through (all four channels nil) while the non-nil snapshot (real
pid) skipped the only existing nil-snapshot early-out.

Add a pure static predicate focusedTextSnapshotSupportsVerification (returns
true iff at least one of value/selectedRange/appScriptValue/
appScriptSelectedRange is non-nil) and call it right after the guard let
snapshot block: when it returns false, return .unavailable after a single
bounded poll interval (usleep(min(timeoutMicros, 50_000))) instead of
entering the 5s poll loop. Verifiable targets keep their exact current
behaviour: the short-circuit sits strictly above the unchanged poll loop
and only fires when all four channels are nil.

Closes altic-dev#802

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Yurii Chukhlib <yurii.chukhlib@viber.com>
…d add a behavioral test

Address two review findings on the unverifiable-target short-circuit:

1. The 50 ms paste window was too aggressive. `action()` only enqueues Cmd+V
   (postToPid); the target reads NSPasteboard.general asynchronously on its own
   main thread, which under load (large paste, busy main thread during a build)
   can land hundreds of milliseconds later. Restoring the pasteboard after 50 ms
   could clobber the transcript before the target consumed it. Raise the window
   to a conservative 500 ms (pasteConsumeWindowMicros) — ~10x under the 5 s
   timeoutMicros, so the pasteboard session semaphore is still not held across
   multi-second windows (altic-dev#802), but the target gets a real consume chance.

2. The existing test only covered the classification predicate, not the
   short-circuit behavior. Add a behavioral test that drives the real
   waitForFocusedTextVerification with an unverifiable snapshot (real pid, all
   four verifiable channels nil) and asserts it returns .unavailable in bounded
   time well under timeoutMicros. This is an assertion-level guard: remove the
   short-circuit and it returns .timeout after ~5 s, failing both asserts.
   FocusedTextSnapshot / PasteVerificationResult / waitForFocusedTextVerification
   move from private to internal (module-scoped, not public) to make the
   deterministic short-circuit path exercisable headlessly; the live-AX poll
   loop is unchanged and not driven from tests.

Closes altic-dev#802

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Yurii Chukhlib <yurii.chukhlib@viber.com>
@altic-dev
altic-dev force-pushed the fix/typing-unverifiable-shortcut branch from 05de5e8 to 33cba5d Compare August 10, 2026 21:04
@YuriNachos

Copy link
Copy Markdown
Contributor Author

Saw the 1.6.9 fixes ship earlier today — nice to see the release come together. This PR has been green for most of a week now (build, SwiftLint, the policy check and Greptile all pass), so I wanted to ask whether anything in the approach gives you pause — for example the 500 ms paste-consume window before the clipboard is restored — or whether it just hasn't had a review pass yet. Happy to make the window tunable, extend the behavioural tests, or rebase onto the release if any of that would help.

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.

[BUG] Consecutive dictations into Ghostty stall for seconds — pasteboard semaphore held across a 5 s verification that can never succeed in a terminal

1 participant