Skip to content

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

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

psdewar wants to merge 2 commits 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

RetriggerConfidence Score: 4/5

The PR should not merge until the outstanding scoped-change requirement is addressed; the shortened clipboard restoration window still risks breaking existing paste targets.

Summary

This PR routes Safari Google Docs and Google Slides dictation through the existing reliable clipboard-paste path while preserving direct typing for other sites, browsers, and native applications.

  • Expands paste-only target detection from Ghostty to affected Safari editor windows.
  • Adds bounded Accessibility window-title lookup for routing.
  • Adds predicate-level tests and registers them with the Xcode test target.

Reviews (3) · Last reviewed commit: "fix(typing): drop routing predicate doc ..."

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

@grohith327 grohith327 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good overall, just a couple of minor comments.

Note - this would be a temp fix though, when #811 is merged the clipboard method will become the default paste method which should fix all such issues

Comment on lines +913 to +962
return self.withTemporaryPasteboardString(text, restoreDelayMicros: 5_000_000) {
return self.withTemporaryPasteboardString(text, restoreDelayMicros: 1_500_000) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would recommend leaving this as is, this is already updated in the clipboard paste PR we have open

Comment on lines +341 to +345
/// Safari turns one synthesized unicode key event into a single `keypress` carrying only the
/// first character. Editors that build their text from `keypress`, which is what Google Docs and
/// Slides appear to do, therefore drop everything after it and need the clipboard path. The
/// title is looked up lazily so targets that match on bundle ID alone, and the far more common
/// targets that match nothing, never pay for an Accessibility round trip.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Minor - you can drop this comment

@altic-dev

altic-dev commented Sep 13, 2026 via email

Copy link
Copy Markdown
Owner

@psdewar
psdewar force-pushed the fix/safari-google-docs-first-character branch from c2d6476 to 6b9d527 Compare September 18, 2026 00:19
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

3 participants