Skip to content

fix(typing): route Safari Google Docs dictation through the clipboard - #960

Open
psdewar wants to merge 1 commit into
altic-dev:mainfrom
psdewar:fix/safari-google-docs-first-character
Open

fix(typing): route Safari Google Docs dictation through the clipboard#960
psdewar wants to merge 1 commit into
altic-dev:mainfrom
psdewar:fix/safari-google-docs-first-character

Conversation

@psdewar

@psdewar psdewar commented Sep 10, 2026

Copy link
Copy Markdown

Description

FluidVoice types a whole chunk of text as one synthetic keystroke. Safari turns that into a single keypress event carrying only the first character, and Google Docs and Slides build their text from keypress, so that first character is all they get. Sheets and ordinary text boxes read the full string from a different event, so they are fine, and so is every browser that is not Safari.

The keystroke is fine. Safari still hands the whole string to the page through textInput, so CGEventKeyboardSetUnicodeString is doing the right thing. Sending one character at a time would dodge keypress entirely, but f0d3855 moved away from that on purpose, because long text got cut off.

This sends the text to the affected apps through the clipboard instead, the same path you get today by choosing Clipboard Paste in settings. TypingService already did that for Ghostty, and now does the same when Safari's focused window is a Google Docs or Slides document. The rule keys on where dictation is known to break, not on the browser internals behind the failure. Sheets, every other site, and every other browser keep typing directly.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Screenshots / Video

before-after.mp4

Related Issue or Discussion

Closes #958

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version: 26.6.2
  • Ran linter locally
  • Ran formatter locally
  • Ran tests locally

Manual, against the released 1.6.9 build and then this branch: Google Docs and Google Slides in Safari now receive the whole phrase. Sheets, Chrome, and Firefox are unchanged from the matrix in the issue.

Native targets (Notes, Xcode) still take the direct-typing path.

TypingServicePasteOnlyRoutingTests covers the routing predicate directly: known bundle IDs, Docs and Slides in Safari, Sheets staying on the direct path, Chromium and Gecko staying on the direct path, ordinary browsing, and native apps.

@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 Sep 10, 2026
@github-actions

Copy link
Copy Markdown

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

Please update the PR description with:

  • Type of Change
  • Related Issue or Discussion
  • 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.

@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: 2d26f9ae63

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


guard Self.webKitBrowserBundleIdentifiers.contains(bundleIdentifier),
let title = focusedWindowTitle(),
let editor = Self.keypressDrivenEditorTitles.first(where: { title.hasSuffix($0) })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle localized Google editor window titles

When the Google account/UI language localizes the product name (for example, Safari exposes a title ending in the localized equivalent of Google Docs), this exact English suffix check returns nil, so the affected editor stays on the direct-typing path and continues dropping all but the first character. The routing signal needs to be independent of the localized window title, or explicitly support localized variants.

Useful? React with 👍 / 👎.

self.bench("paste_target_prepared elapsedMs=\(Self.elapsedMs(since: targetStartedAt))")

return self.withTemporaryPasteboardString(text, restoreDelayMicros: 5_000_000) {
return self.withTemporaryPasteboardString(text, restoreDelayMicros: 1_500_000) {

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 Keep the temporary clipboard alive for delayed paste events

When the target app's event loop takes more than 1.5 seconds to handle the posted Cmd+V and the focused-text snapshot cannot verify completion—as is common for web editors—postToPid has already returned successfully but cleanup restores the previous clipboard before the app reads it. This can paste the user's old clipboard contents instead of the transcript; the previous five-second grace period should not be shortened for unverifiable asynchronous paste delivery.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds destination-aware paste routing so Safari and Safari Technology Preview use clipboard insertion for Google Docs and Slides, while preserving direct typing for other sites and browsers. It also adds routing tests and project references.

  • Generalizes the existing Ghostty paste-only routing predicate.
  • Reads the focused Safari window title through Accessibility to identify affected editors.
  • Adds focused tests for supported and excluded bundle/title combinations.
  • Also shortens the shared clipboard verification and restoration window from five seconds to 1.5 seconds.

Confidence Score: 4/5

The routing approach is narrowly tested, but the PR should not merge until the unrelated global paste timeout regression risk is removed or validated as required by the repository instruction.

The Safari routing itself covers the reported matrix, but the same change globally shortens clipboard availability for all paste paths and uses a page-controlled title to decide when dictated text enters the general pasteboard.

Files Needing Attention: Sources/Fluid/Services/TypingService.swift

Security Review

Safari routing relies on a page-controlled title suffix, allowing unrelated pages to cause dictated text to pass through the general pasteboard and become visible to non-cooperative clipboard observers.

Fix all with Greploop Fix All in Codex

Prompt To Fix All With AI
### Issue 1
Sources/Fluid/Services/TypingService.swift:962
**Clipboard Window Shortened Globally**

This Safari-specific fix also reduces the clipboard restoration window from five seconds to 1.5 seconds for every reliable, fallback, and AppleScript paste target, including the same changes at lines 1087 and 1127. If a slow destination has not consumed the transient paste when verification times out, the previous clipboard contents are restored and no dictated text may be inserted even though dispatch was reported as successful. This violates the repository requirement that Swift changes stay within the stated scope and avoid risking existing features, so the requirement must be satisfied before merging. Retain the established timeout or validate and test the shorter window independently.

### Issue 2
Sources/Fluid/Services/TypingService.swift:356-358
**Page Title Controls Clipboard**

The routing predicate treats Safari’s page-controlled window title as proof that the destination is Google Docs or Slides. Any ordinary page whose title ends with one of those strings can therefore move otherwise directly typed dictation through `NSPasteboard.general`, where observers that do not honor the advisory transient markers can read or retain it. Verify the Google origin or another non-spoofable document identity before selecting the clipboard path.

**How this was verified:** Safari’s page-derived accessibility title is matched by suffix alone, after which the dictated string is written to the general pasteboard with only advisory markers.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(typing): route Safari Google Docs di..." | Re-trigger Greptile

self.bench("paste_target_prepared elapsedMs=\(Self.elapsedMs(since: targetStartedAt))")

return self.withTemporaryPasteboardString(text, restoreDelayMicros: 5_000_000) {
return self.withTemporaryPasteboardString(text, restoreDelayMicros: 1_500_000) {

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 Clipboard Window Shortened Globally

This Safari-specific fix also reduces the clipboard restoration window from five seconds to 1.5 seconds for every reliable, fallback, and AppleScript paste target, including the same changes at lines 1087 and 1127. If a slow destination has not consumed the transient paste when verification times out, the previous clipboard contents are restored and no dictated text may be inserted even though dispatch was reported as successful. This violates the repository requirement that Swift changes stay within the stated scope and avoid risking existing features, so the requirement must be satisfied before merging. Retain the established timeout or validate and test the shorter window independently.

Rule Used: What: Ensure macOS Swift PR changes match the stated scope, don’t introduce unrelated UI/UX/theming work, and don’t risk breaking existing features. Why: Keeps reviews focused, prevents scope creep (especially UI/UX), and avoids regressions or inc... (source)

Knowledge Base Used: Dictation processing and typing

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/Services/TypingService.swift
Line: 962

Comment:
**Clipboard Window Shortened Globally**

This Safari-specific fix also reduces the clipboard restoration window from five seconds to 1.5 seconds for every reliable, fallback, and AppleScript paste target, including the same changes at lines 1087 and 1127. If a slow destination has not consumed the transient paste when verification times out, the previous clipboard contents are restored and no dictated text may be inserted even though dispatch was reported as successful. This violates the repository requirement that Swift changes stay within the stated scope and avoid risking existing features, so the requirement must be satisfied before merging. Retain the established timeout or validate and test the shorter window independently.

**Rule Used:** What: Ensure macOS Swift PR changes match the stated scope, don’t introduce unrelated UI/UX/theming work, and don’t risk breaking existing features.    Why: Keeps reviews focused, prevents scope creep (especially UI/UX), and avoids regressions or inc... ([source](https://app.greptile.com/altic/-/custom-context?memory=c54a31bd-761f-45ed-8fcb-a3cb1158d02e))

**Knowledge Base Used:** [Dictation processing and typing](https://app.greptile.com/altic/-/custom-context/knowledge-base/altic-dev/fluidvoice/-/docs/dictation-processing-and-typing.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

Comment on lines +356 to +358
guard Self.webKitBrowserBundleIdentifiers.contains(bundleIdentifier),
let title = focusedWindowTitle(),
let editor = Self.keypressDrivenEditorTitles.first(where: { title.hasSuffix($0) })

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 security Page Title Controls Clipboard

The routing predicate treats Safari’s page-controlled window title as proof that the destination is Google Docs or Slides. Any ordinary page whose title ends with one of those strings can therefore move otherwise directly typed dictation through NSPasteboard.general, where observers that do not honor the advisory transient markers can read or retain it. Verify the Google origin or another non-spoofable document identity before selecting the clipboard path.

How this was verified: Safari’s page-derived accessibility title is matched by suffix alone, after which the dictated string is written to the general pasteboard with only advisory markers.

Knowledge Base Used: Dictation processing and typing

Prompt To Fix With AI
This is a comment left during a code review.
Path: Sources/Fluid/Services/TypingService.swift
Line: 356-358

Comment:
**Page Title Controls Clipboard**

The routing predicate treats Safari’s page-controlled window title as proof that the destination is Google Docs or Slides. Any ordinary page whose title ends with one of those strings can therefore move otherwise directly typed dictation through `NSPasteboard.general`, where observers that do not honor the advisory transient markers can read or retain it. Verify the Google origin or another non-spoofable document identity before selecting the clipboard path.

**How this was verified:** Safari’s page-derived accessibility title is matched by suffix alone, after which the dictated string is written to the general pasteboard with only advisory markers.

**Knowledge Base Used:** [Dictation processing and typing](https://app.greptile.com/altic/-/custom-context/knowledge-base/altic-dev/fluidvoice/-/docs/dictation-processing-and-typing.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

@psdewar

psdewar commented Sep 10, 2026

Copy link
Copy Markdown
Author

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

Please update the PR description with:

  • Type of Change
  • Related Issue or Discussion
  • 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.

@altic-dev any way to loosen the rules here since I have everything?

@altic-dev

Copy link
Copy Markdown
Owner

The PR template lets us keep track of things easily. thanks for the PR but please adher to the template when possible :)

@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 Sep 10, 2026
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] Google Docs in Safari receives only the first character of each dictation chunk

2 participants