Skip to content

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

Description

@DeLuke84

Describe the bug

When dictating repeatedly into Ghostty, the first insertion is instant (~45 ms). A second dictation started shortly after is transcribed correctly, but the text insertion stalls for seconds before the text appears.

The stall happens entirely inside the insertion call. Nothing is logged during it. The same rapid-fire pattern in a normal Cocoa text view (CotEditor) never stalls.

Steps to Reproduce

  1. Focus Ghostty (com.mitchellh.ghostty).
  2. Dictate a short sentence, stop. Text appears immediately.
  3. Within a few seconds, dictate a second short sentence and stop.
  4. Observe the delay before the second text is inserted.
  5. Repeat in CotEditor (or any standard NSTextView) — no delay.

Actual behavior

Measured from my own ~/Library/Logs/Fluid/Fluid.log, one session, 40 insertions:

  • 37 insertions: totalMs between 42 and 53
  • 3 insertions: totalMs = 2342, 7450, 8889

All three slow insertions targeted Ghostty. All 8 CotEditor insertions in the same session completed at totalMs=0.

The full delay sits between insert_call and insert_return, with no log output in between:

[14:53:10.483] [INFO] [AppBenchmark]    APP_BENCH    text_ready chars=31
[14:53:10.483] [DEBUG] [ContentView]    Typing decision → frontmost: Ghostty, fluidFrontmost: false, editorFocused: true, willTypeExternally: true
[14:53:10.483] [INFO] [AppBenchmark]    APP_BENCH    focus_restore_result activated=false element=true elapsedMs=0 reason=already_focused
[14:53:10.483] [INFO] [TypingBenchmark] TYPING_BENCH request chars=31 mode=standard autocompleteSteps=1 preferredPID=2181 textReadyAgeMs=8
[14:53:10.483] [INFO] [TypingBenchmark] TYPING_BENCH worker_start queueDelayMs=0
[14:53:10.483] [INFO] [TypingBenchmark] TYPING_BENCH settle_delay_done delayMs=0 elapsedMs=0
[14:53:10.483] [INFO] [TypingBenchmark] TYPING_BENCH insert_call
[14:53:19.372] [INFO] [TypingBenchmark] TYPING_BENCH insert_return elapsedMs=8889 totalMs=8889

ASR is not involved: final_done elapsedMs=48, text_ready 8 ms later, focus restore 0 ms, queue delay 0 ms.

Expected behavior

Consecutive dictations into a terminal insert as fast as into any other app. Clipboard restoration is an internal concern and must not block the next insertion.

Root cause

In Sources/Fluid/Services/TypingService.swift:

  1. Ghostty is detected by bundle identifier and, in standard mode, forced onto the reliable-paste path:

    if self.textInsertionMode == .standard,
       let ghosttyTargetPID = self.ghosttyTargetPID(preferredTargetPID: preferredTargetPID)
    {
        self.log("[TypingService] Ghostty target detected in standard mode (PID \(ghosttyTargetPID)); forcing Reliable Paste path")
  2. That path calls withTemporaryPasteboardString(text, restoreDelayMicros: 5_000_000).

  3. withTemporaryPasteboardString acquires the process-global pasteboardSessionSemaphore (DispatchSemaphore(value: 1)) on entry and deliberately does not release it on return — it sets releasesPasteboardSessionOnReturn = false and hands the release to an async task on pasteboardRestoreQueue.

  4. That task first runs waitForFocusedTextVerification(..., timeoutMicros: 5_000_000), polling every 50 ms, before signalling the semaphore.

  5. waitForFocusedTextVerification can only succeed via one of four checks — appScriptValue.contains(expectedText), an AppleScript caret-distance match, current.value.contains(expectedText), or an AX caret-distance match. A GPU-rendered terminal exposes neither an AX text value nor a text-field-style selected range, so none of the four can ever match. The loop always runs the full 5 s and returns .timeout.

  6. The next insertion blocks at pasteboardSessionSemaphore.wait() for the remainder of that window. With several dictations in quick succession the windows stack, which is where the 7–9 s figures come from.

So the stall is not Ghostty being slow — it is a global lock held for a fixed 5 s whenever the target app cannot satisfy the verification, which is always true for terminals.

Suggested fix

The semaphore currently guards two unrelated concerns: (a) not clobbering another session's pasteboard snapshot, and (b) waiting long enough before restoring the clipboard. Only (a) needs to serialize insertions, and it does not need to be held across the poll.

Options, roughly in order of preference:

  1. Release the semaphore once the paste has been dispatched and the snapshot is owned, and let the restore task carry the snapshot as its own state. Serialize restores on pasteboardRestoreQueue rather than blocking the insertion path.
  2. Skip verification entirely when the target cannot support it — if captureFocusedTextSnapshot() yields no usable AX text value and no selected range (the terminal case), fall back to a short fixed delay instead of a 5 s poll. Ghostty is already special-cased by bundle ID, so the information is available at that point.
  3. Bound the wait for a queued insertion: if pasteboardSessionSemaphore.wait(timeout:) expires, proceed rather than block indefinitely.

Note that switching TextInsertionMode is not a workaround: standard explicitly routes Ghostty onto the reliable-paste path, and reliablePaste uses the same function with the same 5 s value.

Environment

  • FluidVoice: 1.6.7
  • macOS: 26.6 (25G72), Apple Silicon (arm64)
  • Target app: Ghostty 1.3.1 (com.mitchellh.ghostty)
  • Model: Parakeet TDT v3 (Multilingual)
  • TextInsertionMode = standard ("Clipboard Free Insert")

Possibly related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions