Skip to content

Add automatic typo fixing and correction explanations#654

Open
FuJacob wants to merge 1 commit into
mainfrom
codex/automatic-typo-fixing
Open

Add automatic typo fixing and correction explanations#654
FuJacob wants to merge 1 commit into
mainfrom
codex/automatic-typo-fixing

Conversation

@FuJacob

@FuJacob FuJacob commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds always-visible explanatory text to the typo correction settings and introduces an opt-in automatic typo-fixing mode. Automatic fixes run only after the user commits a misspelled word with Space, then revalidate the exact live trailing word and preserve its spacing before replacement.

The correction preference is persisted through the settings store/model/snapshot pipeline, indexed for Settings search, and shares one fail-closed replacement planner with manual correction acceptance.

Validation

  • xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build-for-testing -derivedDataPath build/DerivedData
    • ** TEST BUILD SUCCEEDED **
  • Focused tests were compiled for TypoGateTests, CurrentWordExtractorTests, and SuggestionSettingsStoreTests.
  • Focused test execution was attempted, but the app-hosted bundle could not load because the host app and CotabbyTests.xctest were signed with different Team IDs. This is the documented local test-host signing limitation.
  • git diff --check passed.
  • Post-change UI screenshot was not captured in this environment.

Linked issues

None.

Risk / rollout notes

  • Adds the cotabbyAutomaticallyFixTypos UserDefaults key. It defaults to false, so existing behavior is unchanged until the user opts in.
  • Automatic replacement requires typo suppression and fires only after one literal trailing Space. It does not mutate an unfinished word when typing pauses.
  • The replacement planner fails closed if the live trailing word changed, preventing stale Accessibility state from deleting unrelated text.

Greptile Summary

This PR introduces an opt-in "Automatically Fix Typos" mode that replaces a completed misspelled word immediately after Space, without requiring the user's accept key. The correction preference is persisted through the full settings pipeline (store → model → snapshot) and the replacement logic is unified in a new TypoCorrectionReplacementPlanner shared by both auto-fix and manual-acceptance paths.

  • New TypoCorrectionReplacementPlanner centralises the word-match validation, UTF-16 delete-count computation, and trailing-space preservation that were previously duplicated in the acceptance path; the new auto-fix path uses the same planner with requiresTrailingSpace: true to fail-closed if the user has already continued typing.
  • TypoGate gains a .applyCorrection case that fires only when there is exactly one trailing Space, preventing destructive edits on unfinished words; the gate already guarded all fix features behind suppressCompletionsOnTypo.
  • Settings pipeline, UI toggle, and search index are consistently updated, and focused tests are added for the planner's edge cases and the new gate decision.

Confidence Score: 4/5

Safe to merge; the new auto-fix path is fail-closed and defaults to off, leaving existing behaviour unchanged until the user opts in.

The core replacement planner is well-tested, both fix paths share the same validation logic, and the feature defaults to off. Two small rough edges: bestCorrection (a SymSpell + NSSpellChecker chain) is now called on every typo-detected cycle even when both offer and auto-fix flags are off, adding unnecessary work in suppression-only mode; and normalizedOutput in the auto-correction log diverges from the accepted-correction log, which could make diagnostic comparisons awkward.

Cotabby/Support/TypoGate.swift — the bestCorrection guard placement; Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift — the normalizedOutput argument in both log calls inside applyAutomaticCorrection.

Important Files Changed

Filename Overview
Cotabby/Support/TypoGate.swift Adds .applyCorrection case and re-structures resolution logic; bestCorrection is now always evaluated when a typo is detected regardless of which feature flags are set, which is an unnecessary call in suppression-only mode.
Cotabby/Support/CurrentWordExtractor.swift Adds TypoCorrectionReplacement value type and TypoCorrectionReplacementPlanner – well-designed shared planner that centralises word-match validation and space-preservation logic for both manual-accept and auto-fix paths.
Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift Adds applyAutomaticCorrection with correct fail-closed planner guard; normalizedOutput logs correctedWord (raw, no trailing space) on both success and failure while the acceptance path consistently logs replacement.replacementText.
Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift Acceptance path refactored to use shared TypoCorrectionReplacementPlanner; removes the explicit empty-correction guard but equivalently handled by the planner returning nil.
Cotabby/Models/SuggestionSettingsModel.swift Correctly expands Combine typo-toggles pipeline from CombineLatest to CombineLatest3 and threads automaticallyFixTypos through the full snapshot pipeline.
Cotabby/UI/Settings/Panes/WritingPaneView.swift Converts help-text popups to always-visible SettingsRowLabel descriptions and adds the new Automatically Fix Typos toggle; correctly disabled when suppressCompletionsOnTypo is off.
CotabbyTests/TypoGateTests.swift New tests cover .applyCorrection after Space and confirm auto-fix does not fire on unfinished words; existing tests updated for renamed .offerCorrection case.
CotabbyTests/CurrentWordExtractorTests.swift Adds three planner tests covering the committed-space case, rejection before space, and rejection on word change.

Sequence Diagram

sequenceDiagram
    participant User
    participant Coordinator as SuggestionCoordinator
    participant Gate as TypoGate
    participant Planner as TypoCorrectionReplacementPlanner
    participant Inserter as SuggestionInserter

    User->>Coordinator: keystroke (debounced)
    Coordinator->>Gate: resolve(precedingText, suppress, offer, autoFix)
    alt no typo / suppression off
        Gate-->>Coordinator: .proceed
        Coordinator->>Coordinator: normal generation
    else typo, no correction available
        Gate-->>Coordinator: .suppress
        Coordinator->>Coordinator: clearSuggestion / hideOverlay
    else typo, offer on (no trailing space or autoFix off)
        Gate-->>Coordinator: .offerCorrection(word, correctedWord)
        Coordinator->>Coordinator: presentCorrection (green session)
        User->>Coordinator: presses accept key
        Coordinator->>Planner: plan(precedingText, expectedTypo, correctedWord, requiresTrailingSpace: false)
        Planner-->>Coordinator: TypoCorrectionReplacement
        Coordinator->>Inserter: replace(deletingUTF16Count, replacementText)
        Inserter-->>User: corrected text
    else "typo, autoFix on, trailing space = 1"
        Gate-->>Coordinator: .applyCorrection(word, correctedWord)
        Coordinator->>Planner: plan(precedingText, expectedTypo, correctedWord, requiresTrailingSpace: true)
        Planner-->>Coordinator: TypoCorrectionReplacement (or nil, abort)
        Coordinator->>Inserter: replace(deletingUTF16Count, replacementText)
        Inserter-->>User: corrected text (no key press needed)
        Coordinator->>Coordinator: schedulePredictionAfterHostPublishDelay
    end
Loading

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat: add automatic typo fixing" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Comment on lines +48 to 57
guard let corrected = bestCorrection(current.result.word) else {
return .suppress
}
if automaticallyFixTypos, current.trailingSpaceCount == 1 {
return .applyCorrection(word: current.result.word, correctedWord: corrected)
}
if offerTypoCorrections {
return .offerCorrection(word: current.result.word, correctedWord: corrected)
}
return .suppress

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.

P2 In suppression-only mode (suppressCompletionsOnTypo: true, both offerTypoCorrections and automaticallyFixTypos false), bestCorrection is now invoked on every prediction cycle that hits a typo, even though its result is never used. The old code guarded the call behind offerTypoCorrections. The bestCorrection closure chains symSpellCorrector then falls back to spellChecker.bestCorrection, so an NSSpellChecker round-trip can fire even when neither fix feature is on.

Suggested change
guard let corrected = bestCorrection(current.result.word) else {
return .suppress
}
if automaticallyFixTypos, current.trailingSpaceCount == 1 {
return .applyCorrection(word: current.result.word, correctedWord: corrected)
}
if offerTypoCorrections {
return .offerCorrection(word: current.result.word, correctedWord: corrected)
}
return .suppress
guard automaticallyFixTypos || offerTypoCorrections else {
return .suppress
}
guard let corrected = bestCorrection(current.result.word) else {
return .suppress
}
if automaticallyFixTypos, current.trailingSpaceCount == 1 {
return .applyCorrection(word: current.result.word, correctedWord: corrected)
}
if offerTypoCorrections {
return .offerCorrection(word: current.result.word, correctedWord: corrected)
}
return .suppress

Fix in Codex Fix in Claude Code

Comment on lines +229 to +250
logStage(
"typo-auto-correction-failed",
workID: workID,
generation: liveContext.generation,
message: message,
normalizedOutput: correctedWord
)
return
}

cancelPredictionWork()
clearSuggestion(clearDiagnostics: false)
hideOverlay(reason: "Overlay hidden because Cotabby automatically fixed a typo.")
latestAcceptanceAction = "Automatically corrected \"\(typoWord)\" to \"\(correctedWord)\"."
state = .idle
logStage(
"typo-auto-corrected",
workID: workID,
generation: liveContext.generation,
message: "Automatically replaced the completed misspelled word after Space.",
normalizedOutput: correctedWord
)

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.

P2 normalizedOutput diverges from the acceptance path on both branches

Both the failure log (line 234) and success log (line 249) pass correctedWord — the raw string from bestCorrection, before normalization and before trailing-space preservation. The manual-acceptance path in acceptCorrection consistently passes replacement.replacementText for normalizedOutput on its failure and success branches. Using replacement.replacementText here as well would make both paths comparable when correlating auto-corrected and manually-accepted events in diagnostics.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

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.

1 participant