diff --git a/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift b/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift index 14013aed0..8569f7b07 100644 --- a/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift +++ b/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift @@ -27,6 +27,15 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate, NS func applicationWillFinishLaunching(_ notification: Notification) { // Prevent focus steal on launch — no Dock icon, no Cmd+Tab entry NSApp.setActivationPolicy(.prohibited) + + // Disable macOS automatic text substitutions app-wide (issue #538). + // Smart-dash substitution rewrites "--" as an em-dash "—", which + // silently corrupts CLI flags typed into server Command/Arguments/Env + // fields (e.g. "--flag" → "—flag"), producing broken configs. Done + // before any window (and thus any NSTextView field editor) is created + // so every text field inherits the disabled state. See + // TextSubstitution.disableAutomaticTextSubstitutions. + TextSubstitution.disableAutomaticTextSubstitutions() } func applicationDidFinishLaunching(_ notification: Notification) { diff --git a/native/macos/MCPProxy/MCPProxy/TextSubstitution.swift b/native/macos/MCPProxy/MCPProxy/TextSubstitution.swift new file mode 100644 index 000000000..7f67e10f7 --- /dev/null +++ b/native/macos/MCPProxy/MCPProxy/TextSubstitution.swift @@ -0,0 +1,43 @@ +// TextSubstitution.swift +// MCPProxy +// +// Disables macOS automatic text substitutions (smart dashes, smart quotes, +// text replacement, spelling autocorrection, autocapitalization) for the whole +// app. See issue #538: smart-dash substitution rewrites "--" as an em-dash +// "—", silently corrupting CLI flags typed into server Command/Arguments/Env +// fields (e.g. "--flag" → "—flag"), producing broken configs. +// +// None of this app's text fields hold prose — they hold commands, flags, +// paths, URLs, KEY=VALUE pairs, names, and tokens — so every automatic +// substitution is unwanted everywhere. SwiftUI exposes no modifier for smart +// dashes (`.autocorrectionDisabled()` does not affect dash substitution), so +// the reliable lever is the AppKit defaults the field editor (NSTextView) +// reads at creation time. + +import AppKit + +enum TextSubstitution { + /// The automatic-substitution default keys NSTextView consults when a field + /// editor is created. Writing them to the standard (application) defaults + /// domain overrides the user's system-wide NSGlobalDomain setting for this + /// app only. + static let substitutionDefaultsKeys = [ + "NSAutomaticDashSubstitutionEnabled", + "NSAutomaticQuoteSubstitutionEnabled", + "NSAutomaticTextReplacementEnabled", + "NSAutomaticSpellingCorrectionEnabled", + "NSAutomaticCapitalizationEnabled", + ] + + /// Disable all automatic text substitutions app-wide. MUST be called before + /// any window / text field editor is created (e.g. from + /// `applicationWillFinishLaunching`) so every NSTextView inherits the + /// disabled state. + static func disableAutomaticTextSubstitutions( + defaults: UserDefaults = .standard + ) { + for key in substitutionDefaultsKeys { + defaults.set(false, forKey: key) + } + } +} diff --git a/native/macos/MCPProxy/MCPProxyTests/ModelsTests.swift b/native/macos/MCPProxy/MCPProxyTests/ModelsTests.swift index 44e7b83ae..6510c9fee 100644 --- a/native/macos/MCPProxy/MCPProxyTests/ModelsTests.swift +++ b/native/macos/MCPProxy/MCPProxyTests/ModelsTests.swift @@ -643,7 +643,7 @@ final class ModelsTests: XCTestCase { XCTAssertEqual(stats.tokenMetrics?.totalServerToolListSize, 120000) XCTAssertEqual(stats.tokenMetrics?.averageQueryResultSize, 5000) XCTAssertEqual(stats.tokenMetrics?.savedTokens, 115000) - XCTAssertEqual(stats.tokenMetrics?.savedTokensPercentage, 95.83, accuracy: 0.01) + XCTAssertEqual(try XCTUnwrap(stats.tokenMetrics?.savedTokensPercentage), 95.83, accuracy: 0.01) XCTAssertEqual(stats.tokenMetrics?.perServerToolListSizes?["github"], 80000) XCTAssertEqual(stats.tokenMetrics?.perServerToolListSizes?["gitlab"], 40000) } diff --git a/native/macos/MCPProxy/MCPProxyTests/TextSubstitutionTests.swift b/native/macos/MCPProxy/MCPProxyTests/TextSubstitutionTests.swift new file mode 100644 index 000000000..6beca8be7 --- /dev/null +++ b/native/macos/MCPProxy/MCPProxyTests/TextSubstitutionTests.swift @@ -0,0 +1,72 @@ +import XCTest +import AppKit +@testable import MCPProxy + +/// Regression tests for issue #538 — "Editor autocorrect creates broken +/// configs". macOS smart-dash substitution turns "--" into "—" in the tray's +/// Add Server / config text fields. `TextSubstitution.disableAutomatic...` +/// must turn every automatic substitution off so a freshly created NSTextView +/// field editor (the type SwiftUI TextField/TextEditor use on macOS) has dash +/// substitution disabled. +final class TextSubstitutionTests: XCTestCase { + + /// After calling the helper, the substitution defaults are all false. + @MainActor + func test_disableAutomaticTextSubstitutions_setsAllDefaultsFalse() { + let suiteName = "TextSubstitutionTests.\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suiteName)! + defer { defaults.removePersistentDomain(forName: suiteName) } + + // Pre-condition: simulate a machine where the user has smart dashes ON. + for key in TextSubstitution.substitutionDefaultsKeys { + defaults.set(true, forKey: key) + } + + TextSubstitution.disableAutomaticTextSubstitutions(defaults: defaults) + + for key in TextSubstitution.substitutionDefaultsKeys { + XCTAssertFalse( + defaults.bool(forKey: key), + "\(key) must be disabled after disableAutomaticTextSubstitutions() (issue #538)" + ) + } + } + + /// The dash-substitution key (the one that produces the em-dash bug) is + /// among the keys the helper disables. + func test_substitutionKeys_includeDashSubstitution() { + XCTAssertTrue( + TextSubstitution.substitutionDefaultsKeys.contains("NSAutomaticDashSubstitutionEnabled"), + "Dash substitution is the key that causes '--' → '—' (issue #538)" + ) + } + + /// End-to-end mechanism check: with the substitution defaults written to + /// the STANDARD domain (what the app does at launch) BEFORE the field + /// editor exists, a freshly created NSTextView reports dash substitution + /// disabled. This is the property AppKit consults before rewriting "--". + @MainActor + func test_freshNSTextView_hasDashSubstitutionDisabled_afterHelper() { + // Capture and restore the real standard-domain values so the test is + // side-effect free regardless of the host machine's settings. + let keys = TextSubstitution.substitutionDefaultsKeys + let saved = keys.map { ($0, UserDefaults.standard.object(forKey: $0)) } + defer { + for (key, value) in saved { + if let value { UserDefaults.standard.set(value, forKey: key) } + else { UserDefaults.standard.removeObject(forKey: key) } + } + } + + // Model the buggy condition, then apply the fix to the standard domain. + UserDefaults.standard.set(true, forKey: "NSAutomaticDashSubstitutionEnabled") + TextSubstitution.disableAutomaticTextSubstitutions() + + // A field editor created now must inherit the disabled state. + let textView = NSTextView(frame: .zero) + XCTAssertFalse( + textView.isAutomaticDashSubstitutionEnabled, + "A fresh NSTextView must have smart-dash substitution OFF so '--' is preserved (issue #538)" + ) + } +}