From 7886c9fa8ae693d3f15ef8d4fcb7cd56ab97335b Mon Sep 17 00:00:00 2001 From: Mason363 Date: Tue, 8 Sep 2026 19:07:37 -0400 Subject: [PATCH 01/94] Rewrite inline ghost text rendering around the host's own font, baseline and lines The inline ghost is now laid out and drawn by CoreText in the host's exact typeface at the host's exact size, on the host's baseline, and wrapped where the host wraps: - GhostFontResolver picks the face from the field's reported PostScript name or family, else matches a web host's measured text width against common families at the reported size, else solves the size from the caret box. - GhostBaselinePolicy asks TextKit for its default baseline offset for native fields and reproduces Blink/WebKit's rounded ascent and descent (including the legacy Times/Helvetica/Courier ascent adjustment) for web fields. - GhostTextLayout builds rows from the caret with CTTypesetter line breaking inside a wrap band; accepted or typed-through prefixes only advance a consumed offset so remaining glyphs never move. GhostWrapBandPolicy derives the band from the element's real frame and its measured line-box inset rather than the widened card frame. - GhostTextPanelView draws rows with CTLineDraw at whole-point panel origins, since AppKit rounds window frames to whole points. - HostTextMetricsProbe/HostTextMetricsCache measure the host's width sample, line box and line pitch once per field, retrying Chromium's lazily loaded bounds and re-measuring when the frame moves or the caret first reaches measurable text. - HostBaselineCalibrator measures a web host's painted baseline from its own pixels (Screen Recording permitting) to recover the sub-point line position Accessibility rounds away; InkBaselineAnalyzer is the pure detector. - The caret resolver prefers the previous character's trailing edge (TextKit's zero-length rect is one line high), corrects zero-length rects from line boxes, and accepts zero-width marker rects. Removes GhostSuggestionLayout, GhostFontMetrics and GhostFontSizeStabilizer, whose caret-height-derived sizing and legibility floor were the source of oversized, drifting ghosts. Pixel-alignment tests render the ghost and an NSTextView offscreen and compare glyph positions. --- Cotabby.xcodeproj/project.pbxproj | 156 ++++- .../SuggestionCoordinator+Acceptance.swift | 19 +- .../SuggestionCoordinator+Prediction.swift | 27 +- Cotabby/App/Core/CotabbyAppEnvironment.swift | 12 +- Cotabby/Models/Focus/FocusModels.swift | 81 ++- .../Settings/CompletionRenderMode.swift | 5 + .../Request/FocusedInputContext.swift | 24 + .../SuggestionPresentationModels.swift | 31 +- .../SuggestionSubsystemContracts.swift | 7 + .../Focus/Caching/FieldStyleCache.swift | 90 ++- .../Focus/Caching/HostTextMetricsCache.swift | 89 +++ .../Resolution/AXTextGeometryResolver.swift | 179 ++++- .../Resolution/FocusSnapshotResolver.swift | 175 ++++- .../Resolution/HostTextMetricsProbe.swift | 179 +++++ .../Presentation/GhostTextPanelView.swift | 93 +++ .../Presentation/HostBaselineCalibrator.swift | 228 ++++++ .../Presentation/OverlayController.swift | 655 +++++++++++------- Cotabby/Support/Accessibility/AXHelper.swift | 60 +- .../Geometry/GhostBaselinePolicy.swift | 97 +++ .../Geometry/GhostSuggestionLayout.swift | 366 ---------- .../Geometry/GhostTextLayout.swift | 258 +++++++ .../Geometry/GhostWrapBandPolicy.swift | 58 ++ .../Geometry/InkBaselineAnalyzer.swift | 137 ++++ .../Geometry/MirrorOverlayLayout.swift | 6 +- .../SuggestionOverlayStabilityGate.swift | 4 +- .../Presentation/Style/GhostFontMetrics.swift | 62 -- .../Style/GhostFontResolver.swift | 269 +++++++ .../Style/GhostFontSizeStabilizer.swift | 43 -- .../Acceptance/InsertedTextAdvance.swift | 2 +- .../SpeculativeAcceptanceContext.swift | 5 +- .../UI/Overlays/SuggestionOverlayViews.swift | 75 -- .../Models/Focus/FocusModelsTests.swift | 12 +- .../ModelAndPresentationValueTests.swift | 38 - .../Caching/HostTextMetricsCacheTests.swift | 151 ++++ .../HostBaselineCalibratorTests.swift | 40 ++ .../Geometry/GhostBaselinePolicyTests.swift | 64 ++ .../Geometry/GhostSuggestionLayoutTests.swift | 567 --------------- .../Geometry/GhostTextLayoutTests.swift | 117 ++++ .../GhostTextPixelAlignmentTests.swift | 214 ++++++ .../Geometry/GhostWrapBandPolicyTests.swift | 94 +++ .../Geometry/InkBaselineAnalyzerTests.swift | 76 ++ .../Style/GhostFontMetricsTests.swift | 169 ----- .../Style/GhostFontResolverTests.swift | 130 ++++ .../Style/GhostFontSizeStabilizerTests.swift | 57 -- .../TestSupport/CotabbyTestFixtures.swift | 6 +- 45 files changed, 3455 insertions(+), 1772 deletions(-) create mode 100644 Cotabby/Services/Focus/Caching/HostTextMetricsCache.swift create mode 100644 Cotabby/Services/Focus/Resolution/HostTextMetricsProbe.swift create mode 100644 Cotabby/Services/Presentation/GhostTextPanelView.swift create mode 100644 Cotabby/Services/Presentation/HostBaselineCalibrator.swift create mode 100644 Cotabby/Support/Presentation/Geometry/GhostBaselinePolicy.swift delete mode 100644 Cotabby/Support/Presentation/Geometry/GhostSuggestionLayout.swift create mode 100644 Cotabby/Support/Presentation/Geometry/GhostTextLayout.swift create mode 100644 Cotabby/Support/Presentation/Geometry/GhostWrapBandPolicy.swift create mode 100644 Cotabby/Support/Presentation/Geometry/InkBaselineAnalyzer.swift delete mode 100644 Cotabby/Support/Presentation/Style/GhostFontMetrics.swift create mode 100644 Cotabby/Support/Presentation/Style/GhostFontResolver.swift delete mode 100644 Cotabby/Support/Presentation/Style/GhostFontSizeStabilizer.swift create mode 100644 CotabbyTests/Services/Focus/Caching/HostTextMetricsCacheTests.swift create mode 100644 CotabbyTests/Services/Presentation/HostBaselineCalibratorTests.swift create mode 100644 CotabbyTests/Support/Presentation/Geometry/GhostBaselinePolicyTests.swift delete mode 100644 CotabbyTests/Support/Presentation/Geometry/GhostSuggestionLayoutTests.swift create mode 100644 CotabbyTests/Support/Presentation/Geometry/GhostTextLayoutTests.swift create mode 100644 CotabbyTests/Support/Presentation/Geometry/GhostTextPixelAlignmentTests.swift create mode 100644 CotabbyTests/Support/Presentation/Geometry/GhostWrapBandPolicyTests.swift create mode 100644 CotabbyTests/Support/Presentation/Geometry/InkBaselineAnalyzerTests.swift delete mode 100644 CotabbyTests/Support/Presentation/Style/GhostFontMetricsTests.swift create mode 100644 CotabbyTests/Support/Presentation/Style/GhostFontResolverTests.swift delete mode 100644 CotabbyTests/Support/Presentation/Style/GhostFontSizeStabilizerTests.swift diff --git a/Cotabby.xcodeproj/project.pbxproj b/Cotabby.xcodeproj/project.pbxproj index 2ee749ed1..6510f1af8 100644 --- a/Cotabby.xcodeproj/project.pbxproj +++ b/Cotabby.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ 034B9FAFB0C719798D49BA0C /* SystemResourceSampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5B7FAD5A5E141AD9926D4A8 /* SystemResourceSampler.swift */; }; 037C23A24EF4BD5E21F526CA /* KeyRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1D44E50290A78AFAE7753DB /* KeyRecorderView.swift */; }; 039BC6D14C80C15841FE8019 /* OnboardingTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECF1C918E49607E99AE50C80 /* OnboardingTemplate.swift */; }; - 03C210D6D0BCC7DB9C4F46F0 /* GhostSuggestionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F1EE1DBFDC6CDC288DB98D /* GhostSuggestionLayout.swift */; }; 03FBB478CF7C51BDFB9AEB6D /* ScreenTextExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BDC778CCC14AC909D659D /* ScreenTextExtractorTests.swift */; }; 050750A0343235F426D8B47B /* ScreenshotContextGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 023BA8AA7DF3F8A45F9CE8FA /* ScreenshotContextGeneratorTests.swift */; }; 05CC25E51682528CE2E73446 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BC4F887528AE74AC0DD30314 /* Assets.xcassets */; }; @@ -58,9 +57,13 @@ 0DA822FDD75309CA14FCB915 /* WelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9805B5D25A6E261D92E0ADD /* WelcomeView.swift */; }; 0DE687D08BE8AA33F0A91787 /* TokenCountEstimatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AA97052545B05BA82450A6D /* TokenCountEstimatorTests.swift */; }; 0E6692D2A7AEEDF72FCF1AE2 /* ExtendedContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0447A27B293394D9379C48 /* ExtendedContextTests.swift */; }; + 0F413096D492794C5AA2E810 /* HostMarkedTextPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F7C9618CCEFD8430CEDB03 /* HostMarkedTextPolicy.swift */; }; 102E1EDC04636AFB607A8249 /* ActiveSuggestionSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F95814016D80E0A23974F0 /* ActiveSuggestionSession.swift */; }; 105187F185A579BC67CA2A93 /* AXHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D1A7D938F633C6EE094F7 /* AXHelper.swift */; }; + 1092E77D9D92DAC4808ABF9A /* GhostFontResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB26526298A85454330591 /* GhostFontResolver.swift */; }; + 109F9BD7E1AE3E7FB0C712AA /* GhostTextLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EB202C5AD6079174A14330C /* GhostTextLayoutTests.swift */; }; 1104ABD01283774EDF249EF0 /* LLMIOFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2F75C66BECA61A722D3FB /* LLMIOFileHandler.swift */; }; + 113D4A18C8782C578463A6C4 /* GhostTextLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C0792BF971B702DE0A095E /* GhostTextLayout.swift */; }; 11C548C2B50D9B27BD1B7293 /* SymSpell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2E9AF341F8F7043D5344B20 /* SymSpell.swift */; }; 11D3A5C8DA17BCDB79969FC4 /* FileLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C90C9EBCB70327D215EAE07 /* FileLogHandler.swift */; }; 12995E5DDB11E3395E6AF82F /* ShortcutsPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB630F9814388203DD1CA2EC /* ShortcutsPaneView.swift */; }; @@ -96,11 +99,11 @@ 1A4A2FCA8F08258A8A70A057 /* LGPL-3.0.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7513810E78F3C94FE972EB07 /* LGPL-3.0.txt */; }; 1AAC3A35721942F618E5C37F /* ApplicationBundleMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD1E28CF46BF59ABDC3056BF /* ApplicationBundleMetadata.swift */; }; 1AB037C6DC95769F4765B5CA /* SettingsAttentionEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AAAB1C46A0E5A9C6FC7D3D /* SettingsAttentionEvaluator.swift */; }; - 1AF051175E382193908AE73E /* GhostFontMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C68A9C7C77AF6B111782FD /* GhostFontMetrics.swift */; }; 1B1DDCEEA435F7DAB6C68B92 /* LlamaSuggestionEngineCancellationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC812721A3E5327154D6CD4E /* LlamaSuggestionEngineCancellationTests.swift */; }; 1B66CA97E53D1ADB9F4E9A4A /* InputMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB36177FC6C26F5E4CD1AD6E /* InputMonitorTests.swift */; }; 1B80604D77F1AF1DF96DDD45 /* MarkerSelectionSynthesizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E66CFA5AB733CE1F793A8 /* MarkerSelectionSynthesizerTests.swift */; }; 1C30344A181A9F6EBC4A9C8F /* DeviceInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6586D6D735F287F43D42520A /* DeviceInfo.swift */; }; + 1C40E805671D6EFC890113AA /* HostTextMetricsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B99181BF74C1F1DC5F6A7 /* HostTextMetricsCache.swift */; }; 1C776D5BC2DECFAE133B9A57 /* AppSurfaceClassifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21047E3C6ACA1035669A7E06 /* AppSurfaceClassifierTests.swift */; }; 1CCDD5419039F1151AF7A7B9 /* SuggestionOverlayViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9720913FECB22A9A28C96333 /* SuggestionOverlayViews.swift */; }; 1D5389E9562AF6315BFCDCE1 /* NOTICE.md in Resources */ = {isa = PBXBuildFile; fileRef = 66CF2A70D4699421AC9BD849 /* NOTICE.md */; }; @@ -110,6 +113,7 @@ 1EB5B904DB17460D839AD57F /* SpellingLanguageResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61CCBF33813CE5CD3B9EA37B /* SpellingLanguageResolver.swift */; }; 1EB90D3D8A5BA028B86E4D9F /* SettingsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0CE9AB1286367BA2E82392 /* SettingsContainerView.swift */; }; 1ED8785EB680045FF13270B4 /* MacroTriggerStateMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CDB11D96F343194B390321 /* MacroTriggerStateMachine.swift */; }; + 1F1131A36C6808948C6341AA /* SystemUIFocusShadowPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC384A90F3D8B71584B3449 /* SystemUIFocusShadowPolicy.swift */; }; 1F3171D691605978D51E1368 /* Aria2LocatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924714B1688E884D9AB2A79D /* Aria2LocatorTests.swift */; }; 1F39EE1D5FA0F5D32AFFB028 /* CurrencyEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67332497BB5448AEDA700BA7 /* CurrencyEvaluator.swift */; }; 1F728AD7AED5908FAA087EA9 /* EngineAndModelPaneView+AppleIntelligence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B891BA80475B61900A1D3C /* EngineAndModelPaneView+AppleIntelligence.swift */; }; @@ -131,6 +135,7 @@ 24D5BE34BE7B7B72A93C6A9F /* BundledRuntimeLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AB5EBB44EE64EB3EB1426F /* BundledRuntimeLocator.swift */; }; 24E0F4F0EC9148FB409FD112 /* InlinePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BC705A82669AEFBC704F59 /* InlinePreviewView.swift */; }; 257302D78C5AE9951C63FCEE /* SuggestionAnchorCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE7DB9EE77511823EDA7B52E /* SuggestionAnchorCacheTests.swift */; }; + 25F7E6EC713F8F71DEEEAAA3 /* SystemUIFocusShadowPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EC384A90F3D8B71584B3449 /* SystemUIFocusShadowPolicy.swift */; }; 26EA96EB13B94A68276FA15E /* MenuBarRecoveryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E267F3BB7FC8DEAEB5E841B /* MenuBarRecoveryPolicy.swift */; }; 27A09D81E47FA601F279EF11 /* FocusCapabilityResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56B8D2232F271197468CBC11 /* FocusCapabilityResolver.swift */; }; 27A7976690FC7FDA7EE364D8 /* SuggestionTextColorCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11CF768650A90705FC0D2730 /* SuggestionTextColorCodec.swift */; }; @@ -154,7 +159,6 @@ 2CF9087AE6FDB2A182BA9C85 /* MirrorOverlayLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3732CD6886AEAF3C01F543E /* MirrorOverlayLayout.swift */; }; 2D33BFF41BDE6CE7464885B8 /* BrowserAppDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 266C8BF167118855C56FC7F4 /* BrowserAppDetector.swift */; }; 2D83CECB0FF076323226449D /* SuggestionSettingsModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D253D06D941F60EDAF112625 /* SuggestionSettingsModelTests.swift */; }; - 2D845761E78C30D8BE11E2B7 /* GhostFontMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C68A9C7C77AF6B111782FD /* GhostFontMetrics.swift */; }; 2DD6D14058C7FD05DBE1AD60 /* UnitConversionEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6B0048AAD1B2A4EA09DCEE0 /* UnitConversionEvaluator.swift */; }; 2DF5A3826AAB99C279EBB8DE /* InputMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81DD30EB657368AACE9625A /* InputMonitor.swift */; }; 2E0AEA2199864F46F86FC0C0 /* TextDirectionDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD33A18BECBD7292ACD9F8B8 /* TextDirectionDetector.swift */; }; @@ -188,10 +192,10 @@ 39836F8D77DA6A4B3E3426FE /* GPL-3.0.txt in Resources */ = {isa = PBXBuildFile; fileRef = BEF60972B2D88E4EC4841AB0 /* GPL-3.0.txt */; }; 39910B6812663A61FC13D6BD /* HuggingFaceSearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF8401D3859F00CC94F8893 /* HuggingFaceSearchService.swift */; }; 39B6842EF482AFB6CBEE6816 /* GhostTextColorPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74AB26FDFBBAC0C6375C4192 /* GhostTextColorPreset.swift */; }; - 39F954287497C8B1253119C7 /* GhostSuggestionLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA6E6BC47B781B63640E25DA /* GhostSuggestionLayoutTests.swift */; }; 3A2A9FE7D529DDA2D471C8F5 /* Aria2StagingDirectoryResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA0ED5F9A319942CA024F93 /* Aria2StagingDirectoryResolver.swift */; }; 3A3B49975BCF1F648F8C1782 /* SuggestionEngineModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B494C76056D9B01D6A3ECABD /* SuggestionEngineModelsTests.swift */; }; 3A5BA316620E502DAED81EE1 /* LlamaSuggestionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = B19F5B58A9F2F20E7FA94AF8 /* LlamaSuggestionEngine.swift */; }; + 3A68F284E2C24093B115EC77 /* InkBaselineAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665A55CA2E7975CBFC63B4E2 /* InkBaselineAnalyzer.swift */; }; 3AB45217DFC86AFC98C374D6 /* WelcomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CB3008986BE7FD2A4D9132 /* WelcomeCoordinator.swift */; }; 3AC2F7F221F7C59242B06DFC /* InputSuppressionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1F9CEBAB0F330F8E7B61D8 /* InputSuppressionController.swift */; }; 3AD39D992243A7AA541A43C8 /* SecureFieldDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE99C84483D3A58D561C61D /* SecureFieldDetector.swift */; }; @@ -228,7 +232,6 @@ 430EB30535A955C3904C2AD9 /* PerDomainDisableSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F49C55FFF4352A6763BAE1A /* PerDomainDisableSettings.swift */; }; 44265797C6EC7798A5E61EF3 /* DateMacroEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A26D86030FC5FA67371C52 /* DateMacroEvaluator.swift */; }; 44B7F10676606D11D0B1D5D0 /* SuggestionCoordinator+Acceptance.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA85693671D2A705F544B566 /* SuggestionCoordinator+Acceptance.swift */; }; - 45172DA4CBEC1BD40126D190 /* GhostFontSizeStabilizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2C4ED070BBE86378229BB5 /* GhostFontSizeStabilizer.swift */; }; 4564DFEF1670EA473D9AE335 /* LlamaSuggestionEngineStreamingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD1FC547BE2E96D2ABB55DF4 /* LlamaSuggestionEngineStreamingTests.swift */; }; 45E72FDF94C0510AFCAA6C3E /* LlamaRuntimeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019029E24E67812E3FF63286 /* LlamaRuntimeCore.swift */; }; 4666D3B3A81CCB4CC68FC395 /* FocusModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B778713FE1C9481CE0DF065 /* FocusModels.swift */; }; @@ -248,6 +251,7 @@ 4A69EB9EEA4620D2EC35B9B2 /* ClipboardContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4B70455CA75A08FAD0F7409 /* ClipboardContextProvider.swift */; }; 4AC255BE2D0CCC67B8882C7A /* WelcomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21CB3008986BE7FD2A4D9132 /* WelcomeCoordinator.swift */; }; 4B1FD27F44D60A3B7CEF03D1 /* DebouncePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B630D1F95FD08BC30E624672 /* DebouncePolicy.swift */; }; + 4BDE48F1CCFAA097E95B53F6 /* GhostFontResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58FA6A8D05AEEBE3501B478F /* GhostFontResolverTests.swift */; }; 4C117D5B93C36D6C502AAC05 /* SuggestionAvailabilityEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12082948AEBC0DFE5ADC6961 /* SuggestionAvailabilityEvaluator.swift */; }; 4D2C1A0B4FBEF95CBFB9F481 /* ModelFileValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0259095D0A3E9B8DC26CB4B0 /* ModelFileValidatorTests.swift */; }; 4D32F003F78EAAA984E77D83 /* LLMIOFileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56A2F75C66BECA61A722D3FB /* LLMIOFileHandler.swift */; }; @@ -269,6 +273,7 @@ 52518CF0760DFEE9AF7C786C /* SuggestionEngineRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384FBCF5D7A3A446C5BE2B8D /* SuggestionEngineRouter.swift */; }; 526097DCFB81E01FD066915E /* Aria2Provisioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D2F4ED8C7493EF4B967BFF9 /* Aria2Provisioner.swift */; }; 52E6B0B61DA8AC33EF60BA04 /* FoundationModelSuggestionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17794AE5A5D5A93A7919E6BA /* FoundationModelSuggestionEngine.swift */; }; + 5313CEB1330FED9569FF55EF /* GhostBaselinePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08EC70547439E840417F5A5 /* GhostBaselinePolicy.swift */; }; 5392CB32A4E8BD0A4EC0B7AB /* SuggestionPauseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0ACD725A16F26E9F29124E4 /* SuggestionPauseModels.swift */; }; 53BA549ABCA25E9F44DF6341 /* DisplayCoordinateConverterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D881FED12A85FFC20F2C9D7 /* DisplayCoordinateConverterTests.swift */; }; 53F946FF09038C1C751C6E53 /* FoundationModelPromptRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A051152D3C503A2339BF0FA /* FoundationModelPromptRenderer.swift */; }; @@ -282,6 +287,7 @@ 55DAFC9D06629C23A6868565 /* WelcomeTemplateStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B53ED6F2F3354FB3E6065804 /* WelcomeTemplateStepView.swift */; }; 56D2FDE72FE21C35E8BDD06C /* ActivationIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E379AF08CDFF65D6EFC565E /* ActivationIndicatorController.swift */; }; 56D7E792922B83393A578E5A /* CompletionRenderModePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705E9119B1661BF464496D39 /* CompletionRenderModePolicy.swift */; }; + 5732858F8FF26E50AE4BFF8C /* DebugForcedSuggestionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8D50115291212D03414CBE /* DebugForcedSuggestionEngine.swift */; }; 57BCDFE786675C9793F3E08E /* ControlTokenMarkersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D6234872B84ECB56A707945 /* ControlTokenMarkersTests.swift */; }; 599DC847C11C4703DB4A1599 /* EmojiPickerPanelLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8418BB40BEE4BCAFB4516B /* EmojiPickerPanelLayout.swift */; }; 59F1CF8FC41EAFA625D0DEB0 /* AXHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B5D1A7D938F633C6EE094F7 /* AXHelper.swift */; }; @@ -315,6 +321,7 @@ 610650A14AC5E8EDDC81B4E9 /* PermissionReminderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 168A0DBF62BEF674B96CEBD2 /* PermissionReminderView.swift */; }; 6147AB8372652B7730749011 /* WelcomePermissionStepView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F952E54538EF82D1B1E672D8 /* WelcomePermissionStepView.swift */; }; 61932DE224F103A22C8E4EF7 /* MarkerSelectionSynthesizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FD2044CDEA95EC02C9B2FE /* MarkerSelectionSynthesizer.swift */; }; + 61DE0622990EA6A09D9F4769 /* HostMarkedTextPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59F8097C95B2D06300F5ECAA /* HostMarkedTextPolicyTests.swift */; }; 62B45766E8A2827E31AFD356 /* ScreenFrameReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B49898B64D4DB50736CBC46 /* ScreenFrameReader.swift */; }; 62DBCF429B7F464A6B467725 /* OnboardingFeatureShowcase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 926B332E7B4CFEE42C4CAA75 /* OnboardingFeatureShowcase.swift */; }; 635026FA5A2CDD7CB29AC20F /* LlamaSuggestionEnginePrewarmTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF5B9CC0B74A171519B6D8 /* LlamaSuggestionEnginePrewarmTests.swift */; }; @@ -364,6 +371,7 @@ 713DD4EE84CE1BE9ADEB8E45 /* EngineAndModelPaneView+Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A184538FD926947EBB88D9E /* EngineAndModelPaneView+Endpoint.swift */; }; 715BD5081873EF5FF070D3B0 /* CalendarAccessibilityCapturePolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 863B5A1DC3C4B9668F620245 /* CalendarAccessibilityCapturePolicyTests.swift */; }; 71DA26FE8F6758CAFE6F7042 /* EmojiPickerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8736D4321E8BF847D1F7338E /* EmojiPickerModels.swift */; }; + 71E039A6A5EF33DE3EB40328 /* HostTextMetricsProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4180FC6B2E085CEE93F23 /* HostTextMetricsProbe.swift */; }; 71E95FF86D5B3399FBD618A9 /* CompletionRenderModePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 705E9119B1661BF464496D39 /* CompletionRenderModePolicy.swift */; }; 7207434A045B4B372981BE2C /* CaretRunPlacementTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A8962FE08E1C76F0EEF423 /* CaretRunPlacementTests.swift */; }; 72672FBC57236EF484340659 /* ActivationIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E379AF08CDFF65D6EFC565E /* ActivationIndicatorController.swift */; }; @@ -371,7 +379,6 @@ 72FC053A621E4B00F7E52BD3 /* EmojiPopularity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DB52780A112A686C06D7E3 /* EmojiPopularity.swift */; }; 7324AD21F85FF6DDDD27EC84 /* SuggestionOverlayStabilityGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DB247AACE1B28D45DB52B0 /* SuggestionOverlayStabilityGate.swift */; }; 735C2E64CA51F58098B30A0D /* it.txt in Resources */ = {isa = PBXBuildFile; fileRef = 0397F1DACB094A0F6A66BC0E /* it.txt */; }; - 73B39F0BC9FFA95059AA7C7F /* GhostFontSizeStabilizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2C4ED070BBE86378229BB5 /* GhostFontSizeStabilizer.swift */; }; 73B7C4D5143F7AEC321BC873 /* PermissionAndContextModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEBBBAB9588AA8EDBA41520 /* PermissionAndContextModelTests.swift */; }; 744B06C2488156B178675615 /* PermissionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BF316556FDA64CB8AD07B6 /* PermissionManager.swift */; }; 749C4953D3AFE86AFABA968C /* OnboardingTemplateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4C5E3A714F5A7818337101C /* OnboardingTemplateTests.swift */; }; @@ -402,6 +409,7 @@ 7D87C0AEF85FA0653C9C6031 /* PerformanceMetricsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA991392A72658B758551BBA /* PerformanceMetricsStoreTests.swift */; }; 7DE4F91CADF4E65BCCF4DB17 /* SettingsRowLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44768629365FAD1A66DA3A51 /* SettingsRowLabel.swift */; }; 7E11BEAD32E9FF170C92000A /* HuggingFaceModelBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C696204AAEBA357129F77646 /* HuggingFaceModelBrowserView.swift */; }; + 7EA9216A909247A4F722C9C1 /* HostTextMetricsCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2BBA3187237680274BF80E /* HostTextMetricsCacheTests.swift */; }; 7F5DD6592FAA0BC2866634AB /* AboutPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2559228BB102022DC48193D9 /* AboutPaneView.swift */; }; 7FC103944F4EF39DB965F469 /* InMemoryLogging in Frameworks */ = {isa = PBXBuildFile; productRef = 88921938DC814625ED57D605 /* InMemoryLogging */; }; 800D96A3889A915779826D11 /* MidWordContinuationPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F56B79BF030507CC7FAE4D /* MidWordContinuationPolicy.swift */; }; @@ -431,7 +439,9 @@ 888FF8B27F8545F1EE9BBA93 /* OpenAICompatibleSuggestionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = B241105A4F9C29DD8C8D4A91 /* OpenAICompatibleSuggestionEngine.swift */; }; 88AAFB21F06EE37A392081E1 /* SuggestionSessionReconciler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CADFBB03C10309B9048C8E9 /* SuggestionSessionReconciler.swift */; }; 89329024F050602EFBC7CC6B /* FocusTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FDF029F7828CAF3FE8850 /* FocusTracker.swift */; }; + 8AE7B4DE4650D0DA8F6EFFE7 /* GhostTextLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5C0792BF971B702DE0A095E /* GhostTextLayout.swift */; }; 8AE7DC96D3DC615410A47B81 /* FoundationModelAvailabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA97ED2A10499771F9C32B64 /* FoundationModelAvailabilityService.swift */; }; + 8AFA546C09EE51078EFBEDAE /* GhostBaselinePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08EC70547439E840417F5A5 /* GhostBaselinePolicy.swift */; }; 8B36A3FDDBB803C72A1E521B /* PerDomainDisableSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A306C7B2B0790A0B98D8F353 /* PerDomainDisableSettingsTests.swift */; }; 8B8EA667506EB67DBCA35D57 /* EngineAndModelPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C35A0A68DA5D2B59460A67E /* EngineAndModelPaneView.swift */; }; 8C090BA472D90D9682F329BA /* KeyCodeLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF4D6CEDF032A25EF72D7228 /* KeyCodeLabels.swift */; }; @@ -443,13 +453,13 @@ 8E209BC819EBBCA0E7F10775 /* FoundationModelAvailabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA97ED2A10499771F9C32B64 /* FoundationModelAvailabilityService.swift */; }; 8E460A307B1ED0DA783CB247 /* FocusPollBackoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD5C8648740E7050CCA657F /* FocusPollBackoff.swift */; }; 8ED4542BF4F6BC1620D47A20 /* FocusDebugOverlayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B4F6E70B8A242F7BDE5361A /* FocusDebugOverlayController.swift */; }; - 8EFD4154069432EB43955033 /* GhostFontSizeStabilizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F3AC792296D57BE1AD0D308 /* GhostFontSizeStabilizerTests.swift */; }; 8F216FA9E4DD8C0317E28286 /* BundleVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A951C04DF783B1F1EFDD1E5 /* BundleVersion.swift */; }; 8F6B554D210B15AD99511D4D /* OnboardingTemplateRecommender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F86D2716CBE36970BB462E8 /* OnboardingTemplateRecommender.swift */; }; 902175A80CC3585095BE7D58 /* InlineCommandCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D68B5ABA427D3E87000E78 /* InlineCommandCoordinator.swift */; }; 907A0BF56C3BB0CBAF2649AB /* SettingsCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0AEFF86F8210CBE7CFCBAD /* SettingsCategory.swift */; }; 907D4369413BBD3715DB3436 /* PermissionHostApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32F6A7622BB9D537D157FA2 /* PermissionHostApp.swift */; }; 908D99B538976C07833157E8 /* OnboardingFeatureShowcase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 926B332E7B4CFEE42C4CAA75 /* OnboardingFeatureShowcase.swift */; }; + 9116AADE58B526162FBF4676 /* DebugForcedSuggestionEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C8D50115291212D03414CBE /* DebugForcedSuggestionEngine.swift */; }; 917E9217724C62C8C8E4E1C9 /* FocusCapabilityFlickerGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AA39C97E8A959E9A5B6F04 /* FocusCapabilityFlickerGate.swift */; }; 91979FC94BCE7A80F74EC47F /* Aria2StagingDirectoryResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA0ED5F9A319942CA024F93 /* Aria2StagingDirectoryResolver.swift */; }; 920DE073BEE1DC78B73108D0 /* OnboardingFlowSteps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AEF94A48DF96D6BEE3048C /* OnboardingFlowSteps.swift */; }; @@ -459,6 +469,7 @@ 9301B309462E4CF7F554578F /* OnboardingStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ABC808DE01AF9AD04E38943 /* OnboardingStyle.swift */; }; 930BA578E742D96FD9D340ED /* ContextPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89497C35D1825BAE9625EE06 /* ContextPaneView.swift */; }; 931D40674AF1CB11BE27327A /* OnboardingTemplateFeatureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7174EF88BA41ACC0A74CA582 /* OnboardingTemplateFeatureList.swift */; }; + 93483A25EA4434A05C6CCB2C /* GhostTextPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29163A3D0BB0F11EEAC2985A /* GhostTextPanelView.swift */; }; 935C8D87322CE57F91B673B6 /* FocusSnapshotExternalApplicationIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E97C4BFAF5EAF6792170AC /* FocusSnapshotExternalApplicationIdentityTests.swift */; }; 936F87925A22DAC58525B60F /* DownloadFileRescuer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC756DF8E0032B39099D2BF8 /* DownloadFileRescuer.swift */; }; 940E71A409B4FCDD63642471 /* EmojiVariantResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FCD0F17E1E2BB8FE75A6E75E /* EmojiVariantResolverTests.swift */; }; @@ -467,12 +478,14 @@ 948AD3BD25C113539DF12FD7 /* VisualContextModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A51B1559F4CE6728D39C959 /* VisualContextModels.swift */; }; 949409B5B133BF4960691AE8 /* SuggestionRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD834BCFF812935889E16D5A /* SuggestionRequestFactory.swift */; }; 94A93C9AECB84D56E3347601 /* FocusPollBackoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD5C8648740E7050CCA657F /* FocusPollBackoff.swift */; }; + 94FA4632A562E454472BE1C6 /* HostBaselineCalibrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC1ECBE9D1EDE855B57A913 /* HostBaselineCalibrator.swift */; }; 952729C6514FBC40D6E7D1D4 /* CotabbyInference in Frameworks */ = {isa = PBXBuildFile; productRef = D11AB0F962CCC5BD79B630CA /* CotabbyInference */; }; 95B1C40207F57D7E059B9965 /* de.txt in Resources */ = {isa = PBXBuildFile; fileRef = C648EBB10D7F8E0B904DEC91 /* de.txt */; }; 96117B3DC1292771EE6B8316 /* PerAppShortcutOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A9F3C0F7B53B7F0D3BA82A /* PerAppShortcutOverride.swift */; }; 9698DED033367C1E6F48D43E /* ModelFileValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36652DB88C5948AA4A31524A /* ModelFileValidator.swift */; }; 96CC39E6D6DF3F27ABA8019B /* SuggestionSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AB17696CE79982039061F29 /* SuggestionSettingsStore.swift */; }; 973171C8C049F8CC15F589F2 /* ShortcutConflictTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA02598F555696782E180DA5 /* ShortcutConflictTests.swift */; }; + 976835E71042F5755616F678 /* SuggestionCoordinator+HostMarkedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAD5637347E83DDCF515568 /* SuggestionCoordinator+HostMarkedText.swift */; }; 97EF76E6B7A1AFB3FA4879D1 /* LGPL-3.0.txt in Resources */ = {isa = PBXBuildFile; fileRef = 7513810E78F3C94FE972EB07 /* LGPL-3.0.txt */; }; 980E02175E2D83D4B60FE519 /* SuggestionQualityMetricsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50119F67E7BB9FBC8EA94BF2 /* SuggestionQualityMetricsStore.swift */; }; 985C583A41DEEE040F48D661 /* FocusModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B778713FE1C9481CE0DF065 /* FocusModels.swift */; }; @@ -496,6 +509,8 @@ 9EB8E3DC796A0C8BFDE8E683 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3E8E86A14090BC7BD13BA76 /* AppDelegate.swift */; }; 9F2FDCABCC941CBECAA3B4AB /* CotabbyInference in Frameworks */ = {isa = PBXBuildFile; productRef = 48A46AD6B613CF06072603E4 /* CotabbyInference */; }; A06D86F7475984F7AD810EFC /* InputModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A29DF1C68154C21E6943D38E /* InputModels.swift */; }; + A16DEE62FB45155A10E5DF74 /* SuggestionCoordinator+HostMarkedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAD5637347E83DDCF515568 /* SuggestionCoordinator+HostMarkedText.swift */; }; + A18051D870C0BEF082053D2C /* GhostFontResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BB26526298A85454330591 /* GhostFontResolver.swift */; }; A1A612C90221E0FE1195754A /* SettingsCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0AEFF86F8210CBE7CFCBAD /* SettingsCategory.swift */; }; A262EF1BD871C12840DA75F2 /* SentenceBoundaryClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FC72BD286ABA5B3CEF82F7 /* SentenceBoundaryClassifier.swift */; }; A275EDDC0B43865C587C33AA /* TrailingDuplicationFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD851386E85D3ED0A1F17E47 /* TrailingDuplicationFilter.swift */; }; @@ -522,6 +537,9 @@ A8854697A9EB29DB737C4A26 /* SelfCaptureGateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 571B221ACAFED86DABB10F48 /* SelfCaptureGateTests.swift */; }; A88F3C7039E8DDB71C5D6246 /* TypoGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCE0E0C38A776383B11809 /* TypoGate.swift */; }; A8DCC8CFAD1B698A32E1B077 /* SuggestionClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 743B7207A91EB1BFADD0C5E7 /* SuggestionClientError.swift */; }; + A966DCE17C5160F15F371372 /* HostBaselineCalibratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F23A711EA8EF13B480FF34DE /* HostBaselineCalibratorTests.swift */; }; + A974EF146F549AD4DFD11D66 /* GhostWrapBandPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B753DA96CA6BC168E675E61E /* GhostWrapBandPolicy.swift */; }; + A9C014BFFB4301320298BCE9 /* HostMarkedTextPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F7C9618CCEFD8430CEDB03 /* HostMarkedTextPolicy.swift */; }; AA00D42DFD1EE094E01A7EEA /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7024D32C43EAD2C5F8689B2D /* AcknowledgementsView.swift */; }; AA2E09FF7E430D66ECA8ECD5 /* CotabbyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1EDFB535AAA2EE0D67828A /* CotabbyApp.swift */; }; AAC519AC668EF430F68B06CA /* InlineCommandCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D68B5ABA427D3E87000E78 /* InlineCommandCoordinator.swift */; }; @@ -533,6 +551,7 @@ AC716016F8E26C1711C39F6D /* FileLogHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C90C9EBCB70327D215EAE07 /* FileLogHandler.swift */; }; ACC1D8C50007AA193271F977 /* PerformancePaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEBD6113A3C1038BECC99245 /* PerformancePaneView.swift */; }; AD0FE3F0F75A40B827109589 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3350EDE01ED5125520C79D53 /* SettingsCoordinator.swift */; }; + AD1BCDC0809B413FEC5F115B /* GhostTextPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29163A3D0BB0F11EEAC2985A /* GhostTextPanelView.swift */; }; AD7940C54015532D2E8973DD /* CompositionInputModeClassifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9316289DD11A72D3EAC0A929 /* CompositionInputModeClassifier.swift */; }; ADB10BCA76295920FE4A9FE1 /* SuggestionFadeInPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B351B5FDF077F8FF49A9A7 /* SuggestionFadeInPolicy.swift */; }; AE739989AC401113B99018BC /* SuggestionCoordinator+Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFA31F5FA8D164CB6C30D06 /* SuggestionCoordinator+Input.swift */; }; @@ -540,7 +559,6 @@ AEF6FC272F0B1B20B43FD0EE /* SuggestionTextColorCodecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 655F56C230F45CAC13EEE59C /* SuggestionTextColorCodecTests.swift */; }; AF55F1ABEDEA10C76C307CEC /* CotabbyAppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711293EA57808B9428C7B908 /* CotabbyAppEnvironment.swift */; }; AF5AF5B7D32EDC0AF1A31557 /* InsertedTextAdvance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8C1A4EC935BB837432BAF2 /* InsertedTextAdvance.swift */; }; - AFA6E84BC8C637CDBDF8BA9D /* GhostFontMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E3785ACD1BBD9D866ADE8DC /* GhostFontMetricsTests.swift */; }; AFFF05025897116EA6E58EA7 /* Aria2OutputParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83457CCF1A50CE83428C363D /* Aria2OutputParser.swift */; }; B00FDD3DEE0B73FF5136C91C /* FocusTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FDF029F7828CAF3FE8850 /* FocusTracker.swift */; }; B0D7A0BC5621C8D23A9145B7 /* CustomRulesEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7D1184F0BC707F0A9148DD /* CustomRulesEditor.swift */; }; @@ -561,11 +579,16 @@ B65B49F24F59154A7611FD22 /* HomePaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1123AB515110BD0CBA39490 /* HomePaneView.swift */; }; B6BA7DF77DCA55F9EF090AEE /* ScreenTextExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E299BE2E9D42A33D5D2F5D /* ScreenTextExtractor.swift */; }; B6F12EFB0AA3CC6BA8773476 /* CaretLinePosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE5E9B2A196282738F8D4BAD /* CaretLinePosition.swift */; }; + B76A1739BB3A656D6417C5D3 /* InkBaselineAnalyzerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC7FA0146AC6B78725105BA4 /* InkBaselineAnalyzerTests.swift */; }; B803D0491F5CF19735F23B65 /* CurrencyEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67332497BB5448AEDA700BA7 /* CurrencyEvaluator.swift */; }; B81FF9FB9A794656F63DEC01 /* SettingsIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 865F7C0490DAF7D77523BE84 /* SettingsIndexTests.swift */; }; B8B71602DFA0E6B7E61E2692 /* SuggestionSessionTypingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A61DC16ED76D694A59BF14 /* SuggestionSessionTypingTests.swift */; }; B9348E5D08C50B82D2C62BCF /* ContextBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7B8727111208CB506C25C4F /* ContextBuffer.swift */; }; + B97F5A32DC8C9CE048F231D7 /* InkBaselineAnalyzer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665A55CA2E7975CBFC63B4E2 /* InkBaselineAnalyzer.swift */; }; B9C7A729C0D0A8F7ED032A8E /* EmojiPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC8599C00D6C2CA5FB7A7E7 /* EmojiPickerController.swift */; }; + BA1EC288248A2E4508E4FE9A /* HostBaselineCalibrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC1ECBE9D1EDE855B57A913 /* HostBaselineCalibrator.swift */; }; + BACE17862C9D65881E0D5710 /* HostTextMetricsCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8B99181BF74C1F1DC5F6A7 /* HostTextMetricsCache.swift */; }; + BB9CA5E517347B478B4D52E9 /* GhostTextPixelAlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 406F3107DC345254BF028C63 /* GhostTextPixelAlignmentTests.swift */; }; BBBD7B4628BA1160DD6B4BDB /* MacroModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B41F06FEF208B30ECCF23A6F /* MacroModels.swift */; }; BBC7F32FC99DBC41B0102E54 /* OpenAICompatibleEndpointModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1ABCE733783332BE52E43D67 /* OpenAICompatibleEndpointModels.swift */; }; BBCF59701C5E1E77E7A3F62D /* AXTreeDumpWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB07C5F0319D124B6266BCF3 /* AXTreeDumpWriter.swift */; }; @@ -621,6 +644,8 @@ C8A3B724BB54A1209583B78B /* VisualContextStartCoalescer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEF8F1A40EAA82773C07F8B9 /* VisualContextStartCoalescer.swift */; }; C8B5E45A6F45B307C067CF61 /* SurfaceContextComposer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27B03589D82D0ADCF41B9CD0 /* SurfaceContextComposer.swift */; }; C95343A5A8496627F44233F0 /* InsertedTextAdvance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8C1A4EC935BB837432BAF2 /* InsertedTextAdvance.swift */; }; + CA959F082DB2C6E95C5743C8 /* GhostBaselinePolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25782BF21045F20B9721DB90 /* GhostBaselinePolicyTests.swift */; }; + CB2D43A2B7568173A8E82CE4 /* GhostWrapBandPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39AD68E34CF598F2C83D7C73 /* GhostWrapBandPolicyTests.swift */; }; CC54CAD96A348364989132D3 /* ActiveSuggestionSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04F95814016D80E0A23974F0 /* ActiveSuggestionSession.swift */; }; CC615916E478E98D403F5714 /* SettingsAttentionEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79AAAB1C46A0E5A9C6FC7D3D /* SettingsAttentionEvaluator.swift */; }; CC6BE0B13CCF9B1DE0466DC2 /* DecodeStopPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C319B4FF652512A69377DD5C /* DecodeStopPolicy.swift */; }; @@ -640,6 +665,7 @@ D01A732060A64634619980B1 /* DateMacroEvaluator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A26D86030FC5FA67371C52 /* DateMacroEvaluator.swift */; }; D07ED62B4BB3EEBB6566C1ED /* FocusedInputContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB06531A54FDCDBAAD14E8B /* FocusedInputContext.swift */; }; D0BA4677F47C6943310B8A28 /* PopupChrome.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C6335EED41343595F17A08 /* PopupChrome.swift */; }; + D18F11732EA94EA16CD8101F /* SystemUIFocusShadowPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209089420F2EE43C312657A7 /* SystemUIFocusShadowPolicyTests.swift */; }; D1B42567FEECD406CECFEC45 /* BrowserDomainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBD829565558CB1EA8CA69D5 /* BrowserDomainTests.swift */; }; D1CB2B2D33E5B6C76DDB2A9C /* FocusTrackingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 480596411FBB38BA3B993EDC /* FocusTrackingModel.swift */; }; D1D7D50E5C620042CEA3A77E /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E0AA9F90B83B823132880E6F /* LaunchAtLogin */; }; @@ -712,6 +738,7 @@ E96817EE505817F9E616B31C /* PerAppShortcutOverrideStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9227DE16CA5CE2E085C1A891 /* PerAppShortcutOverrideStoreTests.swift */; }; E9B34664449F7451581C6908 /* CustomRulesCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D93E3819F3BB0957FF3DF6 /* CustomRulesCatalog.swift */; }; E9B7E353B233F842AC859542 /* MenuBarRecoveryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E267F3BB7FC8DEAEB5E841B /* MenuBarRecoveryPolicy.swift */; }; + E9D65F5D5452B595886F43DC /* GhostWrapBandPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B753DA96CA6BC168E675E61E /* GhostWrapBandPolicy.swift */; }; E9E4CC657771DF9F4C56183C /* VisualContextCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A854CAFB1F557BC4CAED8819 /* VisualContextCoordinator.swift */; }; EA353CCECBFB4D297C865447 /* CurrentWordExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1AAC6367483952AB7D59A62 /* CurrentWordExtractor.swift */; }; EA7D6ECD050AC621B02E35F2 /* RequestID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F173F93FFA5FFBC169DA25 /* RequestID.swift */; }; @@ -726,7 +753,6 @@ EEC06CE37957B44E2B258DEE /* PromptContextSanitizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DA66559D50874865032EE8C /* PromptContextSanitizerTests.swift */; }; EF0DE5E045F328F1E912A02A /* AppsPaneView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C1C921A1CDA2ADFC39EA01 /* AppsPaneView.swift */; }; EF4C27A7BEB47AFC6635BD38 /* SuggestionPresentationModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3003832CAC19ED7AE371E845 /* SuggestionPresentationModels.swift */; }; - EF572E116EAD1FC1901B1BDA /* GhostSuggestionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F1EE1DBFDC6CDC288DB98D /* GhostSuggestionLayout.swift */; }; EFEA8845584EC9AE7D3B123D /* SelfCaptureGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13838E1372956FF2CFC5ECCC /* SelfCaptureGate.swift */; }; F00030AE79BA5E78B5615100 /* PermissionGuidanceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0C41A249D9538B91B7FA8A9 /* PermissionGuidanceController.swift */; }; F02C29667DEA43507521C87B /* LlamaRuntimeCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019029E24E67812E3FF63286 /* LlamaRuntimeCore.swift */; }; @@ -736,6 +762,7 @@ F10E203703E57C0EF2C0F98E /* StaticTextRunWalkThrottle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21E26C899DE4FE7AA9CC7869 /* StaticTextRunWalkThrottle.swift */; }; F13B85DD24398CD22C3560C1 /* InputModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A29DF1C68154C21E6943D38E /* InputModels.swift */; }; F140C1894C54E0A94EAE70BC /* BundledRuntimeLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AB5EBB44EE64EB3EB1426F /* BundledRuntimeLocator.swift */; }; + F229353A4D1AD42CBDFD5F8F /* HostTextMetricsProbe.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE4180FC6B2E085CEE93F23 /* HostTextMetricsProbe.swift */; }; F22FAA3449506ABBACB12BA0 /* WordCountFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E22DCC1852B8E4B235D256 /* WordCountFormatter.swift */; }; F25F1800B79BF85AF07048C3 /* TypoCaseTransferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5953E5EB042ABAF6F35A69 /* TypoCaseTransferTests.swift */; }; F26DD9AEAFECAAAD13F3A0A0 /* DownloadFileRescuer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC756DF8E0032B39099D2BF8 /* DownloadFileRescuer.swift */; }; @@ -853,6 +880,7 @@ 1F4A8BCFC1F9F09B10A4D6F1 /* SuggestionWordAcceptanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionWordAcceptanceTests.swift; sourceTree = ""; }; 1FB0AF4FEC5A3FD07652F718 /* CotabbyDebugOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CotabbyDebugOptionsTests.swift; sourceTree = ""; }; 202086735E28B96BBCE55E6B /* LlamaEvalScoring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LlamaEvalScoring.swift; sourceTree = ""; }; + 209089420F2EE43C312657A7 /* SystemUIFocusShadowPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemUIFocusShadowPolicyTests.swift; sourceTree = ""; }; 21047E3C6ACA1035669A7E06 /* AppSurfaceClassifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSurfaceClassifierTests.swift; sourceTree = ""; }; 21A5285E99934D0045CB1918 /* CompletionRenderModePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderModePolicyTests.swift; sourceTree = ""; }; 21CB3008986BE7FD2A4D9132 /* WelcomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeCoordinator.swift; sourceTree = ""; }; @@ -861,16 +889,19 @@ 22FA75F9AD318B9084B23ECF /* PostExhaustionAcceptanceState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostExhaustionAcceptanceState.swift; sourceTree = ""; }; 2559228BB102022DC48193D9 /* AboutPaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutPaneView.swift; sourceTree = ""; }; 2576B762374F4D3CD8347AD4 /* WelcomeKeybindStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeKeybindStepView.swift; sourceTree = ""; }; + 25782BF21045F20B9721DB90 /* GhostBaselinePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostBaselinePolicyTests.swift; sourceTree = ""; }; 25C4859A89B59938DB905377 /* SuggestionCoordinatorLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionCoordinatorLifecycleTests.swift; sourceTree = ""; }; 25D41CC179EC04093971A0D0 /* SpellingDictionaryCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpellingDictionaryCatalog.swift; sourceTree = ""; }; 262BE2F1E97389FE8D7A5FB9 /* Cotabby.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cotabby.app; sourceTree = BUILT_PRODUCTS_DIR; }; 264C3F355F2F82D1964DD263 /* EmojiQueryRun.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiQueryRun.swift; sourceTree = ""; }; 266C8BF167118855C56FC7F4 /* BrowserAppDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserAppDetector.swift; sourceTree = ""; }; 27B03589D82D0ADCF41B9CD0 /* SurfaceContextComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceContextComposer.swift; sourceTree = ""; }; + 29163A3D0BB0F11EEAC2985A /* GhostTextPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostTextPanelView.swift; sourceTree = ""; }; 2B23950F38B1B3C8A5CC5BE5 /* OpenAICompatibleCredentialStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAICompatibleCredentialStore.swift; sourceTree = ""; }; 2BF37C1A0F734BB940E371C2 /* ClipboardRelevanceFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardRelevanceFilter.swift; sourceTree = ""; }; 2C5953E5EB042ABAF6F35A69 /* TypoCaseTransferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypoCaseTransferTests.swift; sourceTree = ""; }; 2C693D7C76486C97DCB9FC56 /* AcceptanceModePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcceptanceModePickerView.swift; sourceTree = ""; }; + 2C8D50115291212D03414CBE /* DebugForcedSuggestionEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugForcedSuggestionEngine.swift; sourceTree = ""; }; 2CADFBB03C10309B9048C8E9 /* SuggestionSessionReconciler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSessionReconciler.swift; sourceTree = ""; }; 2CC8599C00D6C2CA5FB7A7E7 /* EmojiPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerController.swift; sourceTree = ""; }; 2D1F9CEBAB0F330F8E7B61D8 /* InputSuppressionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSuppressionController.swift; sourceTree = ""; }; @@ -880,13 +911,13 @@ 2E23EF696D34CFB8EF320668 /* SuggestionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionRequest.swift; sourceTree = ""; }; 2E379AF08CDFF65D6EFC565E /* ActivationIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivationIndicatorController.swift; sourceTree = ""; }; 2E7C883990AE384602BA9AF6 /* SuggestionSettingsDomainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSettingsDomainTests.swift; sourceTree = ""; }; + 2EC384A90F3D8B71584B3449 /* SystemUIFocusShadowPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemUIFocusShadowPolicy.swift; sourceTree = ""; }; 2F93E4EC64A8773A1A7F3E24 /* CompletionRenderMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionRenderMode.swift; sourceTree = ""; }; 2FCD2572B33EB6DDF1F2110A /* EmojiCatalogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCatalogTests.swift; sourceTree = ""; }; 3003832CAC19ED7AE371E845 /* SuggestionPresentationModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionPresentationModels.swift; sourceTree = ""; }; 308F8660F70ED64511BDB59F /* RandomMacroEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomMacroEvaluator.swift; sourceTree = ""; }; 30AB3A99BEB2EA978298FC6F /* MidWordContinuationPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MidWordContinuationPolicyTests.swift; sourceTree = ""; }; 31A62CE4DA3E55B5E117DBD8 /* LlamaSuggestionEvalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LlamaSuggestionEvalTests.swift; sourceTree = ""; }; - 32F1EE1DBFDC6CDC288DB98D /* GhostSuggestionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostSuggestionLayout.swift; sourceTree = ""; }; 3350EDE01ED5125520C79D53 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; 338EE4D92F8BE01CF43FE876 /* OllamaPreloadWorkControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OllamaPreloadWorkControllerTests.swift; sourceTree = ""; }; 33D4F378E5C70CAF7827A90A /* ControlTokenMarkers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlTokenMarkers.swift; sourceTree = ""; }; @@ -899,18 +930,20 @@ 384FBCF5D7A3A446C5BE2B8D /* SuggestionEngineRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionEngineRouter.swift; sourceTree = ""; }; 38D10022BEFABAD14D16EBF2 /* SuggestionOverlayStabilityGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionOverlayStabilityGateTests.swift; sourceTree = ""; }; 38FCE0E0C38A776383B11809 /* TypoGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypoGate.swift; sourceTree = ""; }; + 39AD68E34CF598F2C83D7C73 /* GhostWrapBandPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostWrapBandPolicyTests.swift; sourceTree = ""; }; 39B891BA80475B61900A1D3C /* EngineAndModelPaneView+AppleIntelligence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EngineAndModelPaneView+AppleIntelligence.swift"; sourceTree = ""; }; 3A051152D3C503A2339BF0FA /* FoundationModelPromptRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationModelPromptRenderer.swift; sourceTree = ""; }; 3A7A9C39A0713BD3B442C9AC /* AppUpdateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateManager.swift; sourceTree = ""; }; 3D6234872B84ECB56A707945 /* ControlTokenMarkersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlTokenMarkersTests.swift; sourceTree = ""; }; 3DB1A3F7D6C0FF28717E6D4C /* SuggestionEngineModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionEngineModels.swift; sourceTree = ""; }; 3DBD75CAF6CAC849D5ED9FA4 /* InputSuppressionControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSuppressionControllerTests.swift; sourceTree = ""; }; - 3E3785ACD1BBD9D866ADE8DC /* GhostFontMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontMetricsTests.swift; sourceTree = ""; }; 3E8BE4574F1770808AA848FE /* EmojiCatalogMatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCatalogMatcherTests.swift; sourceTree = ""; }; + 3EB202C5AD6079174A14330C /* GhostTextLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostTextLayoutTests.swift; sourceTree = ""; }; 3ED4E38B9FE65164C3ABEA12 /* LlamaEvalScoringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LlamaEvalScoringTests.swift; sourceTree = ""; }; 3FE69F822DE85087BCA01DF5 /* SymSpellCorrectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymSpellCorrectorTests.swift; sourceTree = ""; }; 3FE7D19D22434E3E24804999 /* CalendarAccessibilityCapturePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarAccessibilityCapturePolicy.swift; sourceTree = ""; }; 3FFA31F5FA8D164CB6C30D06 /* SuggestionCoordinator+Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionCoordinator+Input.swift"; sourceTree = ""; }; + 406F3107DC345254BF028C63 /* GhostTextPixelAlignmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostTextPixelAlignmentTests.swift; sourceTree = ""; }; 415E80FEA7C1330D502CE97F /* DownloadableModelCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadableModelCatalogView.swift; sourceTree = ""; }; 4189E399B4D9114816EEE93D /* Aria2OutputParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aria2OutputParserTests.swift; sourceTree = ""; }; 4213A267AE55FE56A071646F /* PermissionOverlayWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionOverlayWindowController.swift; sourceTree = ""; }; @@ -931,7 +964,6 @@ 4DF8401D3859F00CC94F8893 /* HuggingFaceSearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFaceSearchService.swift; sourceTree = ""; }; 4E1EBD88A434C325CC6EFBDF /* SuggestionCoordinatorAcceptanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionCoordinatorAcceptanceTests.swift; sourceTree = ""; }; 4E283DF8948B10268B46811F /* MPL-2.0.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "MPL-2.0.txt"; sourceTree = ""; }; - 4F3AC792296D57BE1AD0D308 /* GhostFontSizeStabilizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontSizeStabilizerTests.swift; sourceTree = ""; }; 4F86D2716CBE36970BB462E8 /* OnboardingTemplateRecommender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateRecommender.swift; sourceTree = ""; }; 4FFD3AFACAAC963DAF59D153 /* OnboardingTemplateRecommenderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTemplateRecommenderTests.swift; sourceTree = ""; }; 50119F67E7BB9FBC8EA94BF2 /* SuggestionQualityMetricsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionQualityMetricsStore.swift; sourceTree = ""; }; @@ -947,10 +979,13 @@ 56B8D2232F271197468CBC11 /* FocusCapabilityResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCapabilityResolver.swift; sourceTree = ""; }; 571B221ACAFED86DABB10F48 /* SelfCaptureGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfCaptureGateTests.swift; sourceTree = ""; }; 57B29DAC600626783649475A /* SettingsSearchRanker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchRanker.swift; sourceTree = ""; }; + 57F7C9618CCEFD8430CEDB03 /* HostMarkedTextPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostMarkedTextPolicy.swift; sourceTree = ""; }; 57F9A25D8935B8C2156D7CE5 /* SyntheticReplacePlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyntheticReplacePlannerTests.swift; sourceTree = ""; }; 589029534F91D2BE26A65263 /* BrowserAppDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserAppDetectorTests.swift; sourceTree = ""; }; 5897D28F4DBEAF4792FB4D24 /* ArithmeticEvaluator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArithmeticEvaluator.swift; sourceTree = ""; }; + 58FA6A8D05AEEBE3501B478F /* GhostFontResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontResolverTests.swift; sourceTree = ""; }; 59E299BE2E9D42A33D5D2F5D /* ScreenTextExtractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenTextExtractor.swift; sourceTree = ""; }; + 59F8097C95B2D06300F5ECAA /* HostMarkedTextPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostMarkedTextPolicyTests.swift; sourceTree = ""; }; 5ADC3A68EF5FD9A1E6B5D249 /* CustomRulesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRulesTests.swift; sourceTree = ""; }; 5B5D1A7D938F633C6EE094F7 /* AXHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AXHelper.swift; sourceTree = ""; }; 5B778713FE1C9481CE0DF065 /* FocusModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusModels.swift; sourceTree = ""; }; @@ -958,7 +993,6 @@ 5C9FDF029F7828CAF3FE8850 /* FocusTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusTracker.swift; sourceTree = ""; }; 5D0AEFF86F8210CBE7CFCBAD /* SettingsCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCategory.swift; sourceTree = ""; }; 5DCA3E46621815A19300365F /* fr.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = fr.txt; sourceTree = ""; }; - 5F2C4ED070BBE86378229BB5 /* GhostFontSizeStabilizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontSizeStabilizer.swift; sourceTree = ""; }; 6017E8AEDF88042933C61DFD /* SuggestionInteractionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionInteractionState.swift; sourceTree = ""; }; 601C120D3DB41535E1D90E24 /* WebContentFieldDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContentFieldDetectorTests.swift; sourceTree = ""; }; 604A895738E56186324D3387 /* SuggestionPauseModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionPauseModelsTests.swift; sourceTree = ""; }; @@ -973,6 +1007,7 @@ 655F56C230F45CAC13EEE59C /* SuggestionTextColorCodecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionTextColorCodecTests.swift; sourceTree = ""; }; 6586D6D735F287F43D42520A /* DeviceInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInfo.swift; sourceTree = ""; }; 65E97C4BFAF5EAF6792170AC /* FocusSnapshotExternalApplicationIdentityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusSnapshotExternalApplicationIdentityTests.swift; sourceTree = ""; }; + 665A55CA2E7975CBFC63B4E2 /* InkBaselineAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InkBaselineAnalyzer.swift; sourceTree = ""; }; 66B5FF2D1DCC846AABC134CF /* MenuBarRecoveryPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarRecoveryPolicyTests.swift; sourceTree = ""; }; 66CF2A70D4699421AC9BD849 /* NOTICE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = NOTICE.md; sourceTree = ""; }; 66D6D153C80A5BDE4905B81E /* MacroEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacroEngineTests.swift; sourceTree = ""; }; @@ -1057,6 +1092,7 @@ 8BE5F414704A8264C2946A50 /* TypoGateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypoGateTests.swift; sourceTree = ""; }; 8C151BF4D39485E5CFACFECB /* ApplicationBundleMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationBundleMetadataTests.swift; sourceTree = ""; }; 8D881FED12A85FFC20F2C9D7 /* DisplayCoordinateConverterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayCoordinateConverterTests.swift; sourceTree = ""; }; + 8DAD5637347E83DDCF515568 /* SuggestionCoordinator+HostMarkedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SuggestionCoordinator+HostMarkedText.swift"; sourceTree = ""; }; 8E89746E8CE7E9487337EE6F /* InsertionSafetyGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InsertionSafetyGate.swift; sourceTree = ""; }; 9227DE16CA5CE2E085C1A891 /* PerAppShortcutOverrideStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerAppShortcutOverrideStoreTests.swift; sourceTree = ""; }; 924714B1688E884D9AB2A79D /* Aria2LocatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aria2LocatorTests.swift; sourceTree = ""; }; @@ -1109,6 +1145,7 @@ A9805B5D25A6E261D92E0ADD /* WelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeView.swift; sourceTree = ""; }; A9FD2044CDEA95EC02C9B2FE /* MarkerSelectionSynthesizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerSelectionSynthesizer.swift; sourceTree = ""; }; AAD02052E51AFE56BECB92E4 /* EmojiPickerControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerControllerTests.swift; sourceTree = ""; }; + AAE4180FC6B2E085CEE93F23 /* HostTextMetricsProbe.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostTextMetricsProbe.swift; sourceTree = ""; }; AAE89BEAF57DF3A287DF56F1 /* PromptSectionBudget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptSectionBudget.swift; sourceTree = ""; }; AB84C175EA5B59E118F58C49 /* llama-eval-cases.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "llama-eval-cases.json"; sourceTree = ""; }; ACD93D7DE258C8E5367A433D /* EmojiCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiCatalog.swift; sourceTree = ""; }; @@ -1136,15 +1173,17 @@ B53ED6F2F3354FB3E6065804 /* WelcomeTemplateStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeTemplateStepView.swift; sourceTree = ""; }; B61134664FF616732705A89B /* DebouncePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncePolicyTests.swift; sourceTree = ""; }; B630D1F95FD08BC30E624672 /* DebouncePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncePolicy.swift; sourceTree = ""; }; + B753DA96CA6BC168E675E61E /* GhostWrapBandPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostWrapBandPolicy.swift; sourceTree = ""; }; B81DD30EB657368AACE9625A /* InputMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMonitor.swift; sourceTree = ""; }; B8EBC9F1890DD2FFC4884A5C /* SecureFieldDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureFieldDetectorTests.swift; sourceTree = ""; }; B9F5D16600EE9D4B76D04A26 /* CaretGeometrySelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretGeometrySelector.swift; sourceTree = ""; }; - BA6E6BC47B781B63640E25DA /* GhostSuggestionLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostSuggestionLayoutTests.swift; sourceTree = ""; }; + BA2BBA3187237680274BF80E /* HostTextMetricsCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostTextMetricsCacheTests.swift; sourceTree = ""; }; BA991392A72658B758551BBA /* PerformanceMetricsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetricsStoreTests.swift; sourceTree = ""; }; BADB38D0160B47637572FC5E /* SettingsSidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSidebarView.swift; sourceTree = ""; }; BB36177FC6C26F5E4CD1AD6E /* InputMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputMonitorTests.swift; sourceTree = ""; }; BC07B7CE715C69A63E6123DD /* SuggestionSettingsModelDisabledAppsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionSettingsModelDisabledAppsTests.swift; sourceTree = ""; }; BC4F887528AE74AC0DD30314 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + BC7FA0146AC6B78725105BA4 /* InkBaselineAnalyzerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InkBaselineAnalyzerTests.swift; sourceTree = ""; }; BC812721A3E5327154D6CD4E /* LlamaSuggestionEngineCancellationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LlamaSuggestionEngineCancellationTests.swift; sourceTree = ""; }; BCF2889446A21BC8AC773A81 /* CotabbyTestFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CotabbyTestFixtures.swift; sourceTree = ""; }; BD1E28CF46BF59ABDC3056BF /* ApplicationBundleMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationBundleMetadata.swift; sourceTree = ""; }; @@ -1157,6 +1196,7 @@ BFB53B865B0FA4378DEF2054 /* LanguageTagsEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageTagsEditor.swift; sourceTree = ""; }; C01F11F7F29EFC3A27A1EE5D /* FocusSnapshotResolverSelectionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusSnapshotResolverSelectionTests.swift; sourceTree = ""; }; C0894DA7EA3E157596DA0E66 /* SuggestionStaleAcceptanceEchoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionStaleAcceptanceEchoTests.swift; sourceTree = ""; }; + C08EC70547439E840417F5A5 /* GhostBaselinePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostBaselinePolicy.swift; sourceTree = ""; }; C1337F5C94FA9A6388841805 /* FocusPollBackoffTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusPollBackoffTests.swift; sourceTree = ""; }; C1A8962FE08E1C76F0EEF423 /* CaretRunPlacementTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretRunPlacementTests.swift; sourceTree = ""; }; C1D68B5ABA427D3E87000E78 /* InlineCommandCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineCommandCoordinator.swift; sourceTree = ""; }; @@ -1168,6 +1208,7 @@ C3F56B79BF030507CC7FAE4D /* MidWordContinuationPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MidWordContinuationPolicy.swift; sourceTree = ""; }; C4B70455CA75A08FAD0F7409 /* ClipboardContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardContextProvider.swift; sourceTree = ""; }; C57BCCAF434D44772948B696 /* FileLogHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogHandlerTests.swift; sourceTree = ""; }; + C5C0792BF971B702DE0A095E /* GhostTextLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostTextLayout.swift; sourceTree = ""; }; C648EBB10D7F8E0B904DEC91 /* de.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = de.txt; sourceTree = ""; }; C696204AAEBA357129F77646 /* HuggingFaceModelBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFaceModelBrowserView.swift; sourceTree = ""; }; C6A19E3F68A42D2FAC95E000 /* SuggestionTextNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionTextNormalizer.swift; sourceTree = ""; }; @@ -1182,11 +1223,11 @@ CC1EDFB535AAA2EE0D67828A /* CotabbyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CotabbyApp.swift; sourceTree = ""; }; CC5CD24D2FF35FD36B8D78DE /* SuggestionQualityMetricsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionQualityMetricsStoreTests.swift; sourceTree = ""; }; CD1FC547BE2E96D2ABB55DF4 /* LlamaSuggestionEngineStreamingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LlamaSuggestionEngineStreamingTests.swift; sourceTree = ""; }; + CE8B99181BF74C1F1DC5F6A7 /* HostTextMetricsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostTextMetricsCache.swift; sourceTree = ""; }; CF96999230079A768BEFD0FC /* KeycapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeycapView.swift; sourceTree = ""; }; CFB06531A54FDCDBAAD14E8B /* FocusedInputContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedInputContext.swift; sourceTree = ""; }; CFB3F1AA4007F0B44CA70E6F /* SymSpellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymSpellTests.swift; sourceTree = ""; }; D07B9AB842C16672AC490EFB /* FocusCapabilityResolverTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusCapabilityResolverTests.swift; sourceTree = ""; }; - D0C68A9C7C77AF6B111782FD /* GhostFontMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontMetrics.swift; sourceTree = ""; }; D0DB1F528070F8F9108720DB /* SettingsSearchResultRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSearchResultRow.swift; sourceTree = ""; }; D1123AB515110BD0CBA39490 /* HomePaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePaneView.swift; sourceTree = ""; }; D1499B80A697DAF5ABD8298E /* GhostTextPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostTextPreview.swift; sourceTree = ""; }; @@ -1216,6 +1257,7 @@ DD834BCFF812935889E16D5A /* SuggestionRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionRequestFactory.swift; sourceTree = ""; }; DDA0A1D64275BC85AEBBB852 /* Aria2DownloadServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aria2DownloadServiceTests.swift; sourceTree = ""; }; DDA17364BAFFA56E7DDF682D /* OpenAICompatibleAPIClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAICompatibleAPIClientTests.swift; sourceTree = ""; }; + DDC1ECBE9D1EDE855B57A913 /* HostBaselineCalibrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostBaselineCalibrator.swift; sourceTree = ""; }; DE29E54D78A9DD7CD87626DD /* LanguageSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSupportTests.swift; sourceTree = ""; }; DE6E2A19BF6B87A0C6FA9D49 /* Aria2DownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Aria2DownloadService.swift; sourceTree = ""; }; DEBD6113A3C1038BECC99245 /* PerformancePaneView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformancePaneView.swift; sourceTree = ""; }; @@ -1255,9 +1297,11 @@ F155556C751FB074B31CCC64 /* WelcomeHeroDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeHeroDemo.swift; sourceTree = ""; }; F1A4C5DEA32868F99E953D99 /* CompletionSeamGuard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSeamGuard.swift; sourceTree = ""; }; F1E69CFA319C5F9E4C30F699 /* SettingsQuickLinkCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsQuickLinkCard.swift; sourceTree = ""; }; + F23A711EA8EF13B480FF34DE /* HostBaselineCalibratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostBaselineCalibratorTests.swift; sourceTree = ""; }; F26F50467C83B202FB4BAC2A /* WelcomePersonalizeStepView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePersonalizeStepView.swift; sourceTree = ""; }; F2CC3E0A24F4608350657D80 /* LowPowerModeMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowPowerModeMonitor.swift; sourceTree = ""; }; F35CD1B715FCBB12BEB6CE62 /* FieldEdgeIconIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldEdgeIconIndicatorView.swift; sourceTree = ""; }; + F3BB26526298A85454330591 /* GhostFontResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhostFontResolver.swift; sourceTree = ""; }; F3D991D3536ED989C89AF44D /* EmojiUsageStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUsageStoreTests.swift; sourceTree = ""; }; F47CD9DF50E7192A70D1870E /* CotabbyDebugOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CotabbyDebugOptions.swift; sourceTree = ""; }; F4B98B1ABB36E600D7ECA185 /* SpeculativeAcceptanceContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeculativeAcceptanceContextTests.swift; sourceTree = ""; }; @@ -1334,6 +1378,7 @@ DD63AF32089738F21797C4D7 /* Focus */, 011A5248848010A06D9A182A /* Input */, 72E190A5A00C9BB76F46867A /* ModelManagement */, + 3EA5A1BC99AA99A0A910F324 /* Presentation */, 7630E49BE18AAF965F217AAA /* Runtime */, B0ACD82A4062021762443C9A /* Spelling */, 47C8100B9B0020FD0C25603E /* Suggestion */, @@ -1402,8 +1447,7 @@ 0F63ADF8231C373295DC37FD /* Style */ = { isa = PBXGroup; children = ( - 3E3785ACD1BBD9D866ADE8DC /* GhostFontMetricsTests.swift */, - 4F3AC792296D57BE1AD0D308 /* GhostFontSizeStabilizerTests.swift */, + 58FA6A8D05AEEBE3501B478F /* GhostFontResolverTests.swift */, 655F56C230F45CAC13EEE59C /* SuggestionTextColorCodecTests.swift */, ); path = Style; @@ -1441,7 +1485,10 @@ B9F5D16600EE9D4B76D04A26 /* CaretGeometrySelector.swift */, BE5E9B2A196282738F8D4BAD /* CaretLinePosition.swift */, ECC1416A95346CB72BE036FE /* DisplayCoordinateConverter.swift */, - 32F1EE1DBFDC6CDC288DB98D /* GhostSuggestionLayout.swift */, + C08EC70547439E840417F5A5 /* GhostBaselinePolicy.swift */, + C5C0792BF971B702DE0A095E /* GhostTextLayout.swift */, + B753DA96CA6BC168E675E61E /* GhostWrapBandPolicy.swift */, + 665A55CA2E7975CBFC63B4E2 /* InkBaselineAnalyzer.swift */, B3732CD6886AEAF3C01F543E /* MirrorOverlayLayout.swift */, 4861CC2533F3EA222A0833A9 /* SuggestionAnchorCache.swift */, BD33A18BECBD7292ACD9F8B8 /* TextDirectionDetector.swift */, @@ -1697,6 +1744,14 @@ path = Result; sourceTree = ""; }; + 3EA5A1BC99AA99A0A910F324 /* Presentation */ = { + isa = PBXGroup; + children = ( + F23A711EA8EF13B480FF34DE /* HostBaselineCalibratorTests.swift */, + ); + path = Presentation; + sourceTree = ""; + }; 3EFC2B112178CA99D6D76388 /* OpenAICompatible */ = { isa = PBXGroup; children = ( @@ -1747,8 +1802,7 @@ 4F37A3A455E254F04FB255D6 /* Style */ = { isa = PBXGroup; children = ( - D0C68A9C7C77AF6B111782FD /* GhostFontMetrics.swift */, - 5F2C4ED070BBE86378229BB5 /* GhostFontSizeStabilizer.swift */, + F3BB26526298A85454330591 /* GhostFontResolver.swift */, 74AB26FDFBBAC0C6375C4192 /* GhostTextColorPreset.swift */, 11CF768650A90705FC0D2730 /* SuggestionTextColorCodec.swift */, ); @@ -2187,6 +2241,7 @@ 98423150126CC5969C2886BA /* AXTextGeometryResolver.swift */, 9CCBD29CF7B0BA5A9CFDEEA3 /* DeepGeometryWalkThrottle.swift */, 6BEFDDFF24B78CDED4EB4206 /* FocusSnapshotResolver.swift */, + AAE4180FC6B2E085CEE93F23 /* HostTextMetricsProbe.swift */, 21E26C899DE4FE7AA9CC7869 /* StaticTextRunWalkThrottle.swift */, ); path = Resolution; @@ -2247,7 +2302,11 @@ children = ( FCE4F5CA49ED9988AC4F4C6C /* CaretLinePositionTests.swift */, 8D881FED12A85FFC20F2C9D7 /* DisplayCoordinateConverterTests.swift */, - BA6E6BC47B781B63640E25DA /* GhostSuggestionLayoutTests.swift */, + 25782BF21045F20B9721DB90 /* GhostBaselinePolicyTests.swift */, + 3EB202C5AD6079174A14330C /* GhostTextLayoutTests.swift */, + 406F3107DC345254BF028C63 /* GhostTextPixelAlignmentTests.swift */, + 39AD68E34CF598F2C83D7C73 /* GhostWrapBandPolicyTests.swift */, + BC7FA0146AC6B78725105BA4 /* InkBaselineAnalyzerTests.swift */, 2D962AC9D24D00957562CA7B /* MirrorOverlayLayoutTests.swift */, BE7DB9EE77511823EDA7B52E /* SuggestionAnchorCacheTests.swift */, 00BD4DE5D9D8C9BB66B78633 /* TextDirectionDetectorTests.swift */, @@ -2304,6 +2363,7 @@ children = ( 544C25CEF5C997F920C7BF0F /* FieldStyleCache.swift */, 43EBAC4CC8B8B8266F3E4DA1 /* FocusSessionScopedCache.swift */, + CE8B99181BF74C1F1DC5F6A7 /* HostTextMetricsCache.swift */, 7EFC3DDE7528C434BB3446C4 /* SurfaceContextCache.swift */, ); path = Caching; @@ -2335,6 +2395,7 @@ 21047E3C6ACA1035669A7E06 /* AppSurfaceClassifierTests.swift */, 589029534F91D2BE26A65263 /* BrowserAppDetectorTests.swift */, CBD829565558CB1EA8CA69D5 /* BrowserDomainTests.swift */, + 209089420F2EE43C312657A7 /* SystemUIFocusShadowPolicyTests.swift */, 52C557203A5A5F08E68BB11B /* TerminalAppDetectorTests.swift */, ); path = Applications; @@ -2513,6 +2574,7 @@ children = ( 9E76C76AEE756F43787EEECE /* SuggestionCoordinator.swift */, CA85693671D2A705F544B566 /* SuggestionCoordinator+Acceptance.swift */, + 8DAD5637347E83DDCF515568 /* SuggestionCoordinator+HostMarkedText.swift */, 3FFA31F5FA8D164CB6C30D06 /* SuggestionCoordinator+Input.swift */, 7814C85DA992C8124474FD81 /* SuggestionCoordinator+Lifecycle.swift */, ED7B674D586540F35682C573 /* SuggestionCoordinator+Prediction.swift */, @@ -2559,6 +2621,8 @@ 2E379AF08CDFF65D6EFC565E /* ActivationIndicatorController.swift */, 872C8DDC2E86A1C4C4BBD99F /* EmojiPickerPanelController.swift */, 8B4F6E70B8A242F7BDE5361A /* FocusDebugOverlayController.swift */, + 29163A3D0BB0F11EEAC2985A /* GhostTextPanelView.swift */, + DDC1ECBE9D1EDE855B57A913 /* HostBaselineCalibrator.swift */, 924CAA5E25C596A9FAB7602B /* InlinePreviewPanelController.swift */, 9A3C5A66AA93E50E4A64ED46 /* OverlayController.swift */, ); @@ -2578,6 +2642,7 @@ isa = PBXGroup; children = ( 487D8C5521411FC6845D4A51 /* FocusSessionScopedCacheTests.swift */, + BA2BBA3187237680274BF80E /* HostTextMetricsCacheTests.swift */, ); path = Caching; sourceTree = ""; @@ -2699,6 +2764,7 @@ 17A45844F33B55A3A58E9C71 /* AppSurfaceClassifier.swift */, 266C8BF167118855C56FC7F4 /* BrowserAppDetector.swift */, 81B57905DE20AD5E88BAE0B0 /* BrowserDomain.swift */, + 2EC384A90F3D8B71584B3449 /* SystemUIFocusShadowPolicy.swift */, E6DC2CCFDB2BF1F1E1375620 /* TerminalAppDetector.swift */, ); path = Applications; @@ -2728,6 +2794,7 @@ 9D5B9D87B5D7E7B8A42FEBDE /* Applications */, F2C5C6E620DD62A4496A539E /* Capability */, C1337F5C94FA9A6388841805 /* FocusPollBackoffTests.swift */, + 59F8097C95B2D06300F5ECAA /* HostMarkedTextPolicyTests.swift */, ); path = Focus; sourceTree = ""; @@ -2750,6 +2817,7 @@ CFA86D8DC87D79FD23899608 /* Applications */, 30BB2A9D0CE9D4B10795D789 /* Capability */, ECD5C8648740E7050CCA657F /* FocusPollBackoff.swift */, + 57F7C9618CCEFD8430CEDB03 /* HostMarkedTextPolicy.swift */, ); path = Focus; sourceTree = ""; @@ -3037,6 +3105,7 @@ 9DE7214E640C6F55297A245B /* AppleIntelligence */, F12953939DDAE88E16FF7509 /* Llama */, D1FD19C28ED1EA45A68ABE16 /* OpenAICompatible */, + 2C8D50115291212D03414CBE /* DebugForcedSuggestionEngine.swift */, 384FBCF5D7A3A446C5BE2B8D /* SuggestionEngineRouter.swift */, ); path = Runtime; @@ -3285,6 +3354,7 @@ B0D7A0BC5621C8D23A9145B7 /* CustomRulesEditor.swift in Sources */, 44265797C6EC7798A5E61EF3 /* DateMacroEvaluator.swift in Sources */, 4B1FD27F44D60A3B7CEF03D1 /* DebouncePolicy.swift in Sources */, + 5732858F8FF26E50AE4BFF8C /* DebugForcedSuggestionEngine.swift in Sources */, 29E20FF0F413E723D2A526C4 /* DecodeStopPolicy.swift in Sources */, 0A9F8963DABEDBFFD8D5A239 /* DeepGeometryWalkThrottle.swift in Sources */, 1C30344A181A9F6EBC4A9C8F /* DeviceInfo.swift in Sources */, @@ -3331,17 +3401,24 @@ 6B9B1630E1F30EAE429B5B60 /* FoundationModelPromptRenderer.swift in Sources */, 52E6B0B61DA8AC33EF60BA04 /* FoundationModelSuggestionEngine.swift in Sources */, 4882DF865737ECDC4925F44B /* GeneralPaneView.swift in Sources */, - 1AF051175E382193908AE73E /* GhostFontMetrics.swift in Sources */, - 45172DA4CBEC1BD40126D190 /* GhostFontSizeStabilizer.swift in Sources */, - 03C210D6D0BCC7DB9C4F46F0 /* GhostSuggestionLayout.swift in Sources */, + 8AFA546C09EE51078EFBEDAE /* GhostBaselinePolicy.swift in Sources */, + 1092E77D9D92DAC4808ABF9A /* GhostFontResolver.swift in Sources */, 39B6842EF482AFB6CBEE6816 /* GhostTextColorPreset.swift in Sources */, + 113D4A18C8782C578463A6C4 /* GhostTextLayout.swift in Sources */, + 93483A25EA4434A05C6CCB2C /* GhostTextPanelView.swift in Sources */, 9210DC383F2D181F617A2D74 /* GhostTextPreview.swift in Sources */, + E9D65F5D5452B595886F43DC /* GhostWrapBandPolicy.swift in Sources */, 31FE2E49FD1491E90B32B956 /* HardwareCapabilityProbe.swift in Sources */, 507E7BCCD189A64C3F8ECB79 /* HomePaneView.swift in Sources */, + 94FA4632A562E454472BE1C6 /* HostBaselineCalibrator.swift in Sources */, + 0F413096D492794C5AA2E810 /* HostMarkedTextPolicy.swift in Sources */, + BACE17862C9D65881E0D5710 /* HostTextMetricsCache.swift in Sources */, + F229353A4D1AD42CBDFD5F8F /* HostTextMetricsProbe.swift in Sources */, 09B092E8A682D127FC9872A7 /* HuggingFaceAPIClient.swift in Sources */, 7E11BEAD32E9FF170C92000A /* HuggingFaceModelBrowserView.swift in Sources */, FF46903861BA67AED8C24EF8 /* HuggingFaceModels.swift in Sources */, 6AF17537A3A91BAA875DEA0F /* HuggingFaceSearchService.swift in Sources */, + B97F5A32DC8C9CE048F231D7 /* InkBaselineAnalyzer.swift in Sources */, 902175A80CC3585095BE7D58 /* InlineCommandCoordinator.swift in Sources */, 87CE40ADBFCDBFD266BA9E3F /* InlinePreviewPanelController.swift in Sources */, 2C44C5142ED914BF99A70EC6 /* InlinePreviewView.swift in Sources */, @@ -3447,6 +3524,7 @@ 6A8A20016069E019AF9CBB12 /* SuggestionClientError.swift in Sources */, 778A81527D05314033B7521C /* SuggestionConfiguration.swift in Sources */, 44B7F10676606D11D0B1D5D0 /* SuggestionCoordinator+Acceptance.swift in Sources */, + A16DEE62FB45155A10E5DF74 /* SuggestionCoordinator+HostMarkedText.swift in Sources */, AE739989AC401113B99018BC /* SuggestionCoordinator+Input.swift in Sources */, 5CA62E4891EDC6F4E36B1F32 /* SuggestionCoordinator+Lifecycle.swift in Sources */, 148FE12B749A51872F54C8F8 /* SuggestionCoordinator+Prediction.swift in Sources */, @@ -3482,6 +3560,7 @@ C8682DF05F0B3C3950FB64B1 /* SystemMetricsStore.swift in Sources */, 034B9FAFB0C719798D49BA0C /* SystemResourceSampler.swift in Sources */, CD5740A7732F4ACA73CB3F64 /* SystemSettingsWindowLocator.swift in Sources */, + 1F1131A36C6808948C6341AA /* SystemUIFocusShadowPolicy.swift in Sources */, 77C40ACA872F20CFAB2BDD7F /* TagChip.swift in Sources */, 7C5BD8FDACC491EF62665FB2 /* TerminalAppDetector.swift in Sources */, D1F372A869DB29A8AC78C0CB /* TextDirectionDetector.swift in Sources */, @@ -3566,6 +3645,7 @@ 33EC776945F8CE612A8E85A3 /* CustomRulesEditor.swift in Sources */, D01A732060A64634619980B1 /* DateMacroEvaluator.swift in Sources */, DA8D967DFA3AFDE469D1FAE7 /* DebouncePolicy.swift in Sources */, + 9116AADE58B526162FBF4676 /* DebugForcedSuggestionEngine.swift in Sources */, CC6BE0B13CCF9B1DE0466DC2 /* DecodeStopPolicy.swift in Sources */, FECBB40B75788B449719170B /* DeepGeometryWalkThrottle.swift in Sources */, D3909BBB80781B0CE1982A8E /* DeviceInfo.swift in Sources */, @@ -3612,17 +3692,24 @@ 53F946FF09038C1C751C6E53 /* FoundationModelPromptRenderer.swift in Sources */, 54F87EB87E46B987E3679514 /* FoundationModelSuggestionEngine.swift in Sources */, 4134ADBE464D00BB748BD9AE /* GeneralPaneView.swift in Sources */, - 2D845761E78C30D8BE11E2B7 /* GhostFontMetrics.swift in Sources */, - 73B39F0BC9FFA95059AA7C7F /* GhostFontSizeStabilizer.swift in Sources */, - EF572E116EAD1FC1901B1BDA /* GhostSuggestionLayout.swift in Sources */, + 5313CEB1330FED9569FF55EF /* GhostBaselinePolicy.swift in Sources */, + A18051D870C0BEF082053D2C /* GhostFontResolver.swift in Sources */, 0C8D3E1DC9D7B0FC7664055A /* GhostTextColorPreset.swift in Sources */, + 8AE7B4DE4650D0DA8F6EFFE7 /* GhostTextLayout.swift in Sources */, + AD1BCDC0809B413FEC5F115B /* GhostTextPanelView.swift in Sources */, C657A9C35E432D0A21D96F9F /* GhostTextPreview.swift in Sources */, + A974EF146F549AD4DFD11D66 /* GhostWrapBandPolicy.swift in Sources */, 8D380BEC82C2969F3ED2161A /* HardwareCapabilityProbe.swift in Sources */, B65B49F24F59154A7611FD22 /* HomePaneView.swift in Sources */, + BA1EC288248A2E4508E4FE9A /* HostBaselineCalibrator.swift in Sources */, + A9C014BFFB4301320298BCE9 /* HostMarkedTextPolicy.swift in Sources */, + 1C40E805671D6EFC890113AA /* HostTextMetricsCache.swift in Sources */, + 71E039A6A5EF33DE3EB40328 /* HostTextMetricsProbe.swift in Sources */, 0B7C1A4F515F63462CCCE9EA /* HuggingFaceAPIClient.swift in Sources */, F0DCEFED640B453A5ECEB810 /* HuggingFaceModelBrowserView.swift in Sources */, AB1B25E213AE0A79C6993239 /* HuggingFaceModels.swift in Sources */, 39910B6812663A61FC13D6BD /* HuggingFaceSearchService.swift in Sources */, + 3A68F284E2C24093B115EC77 /* InkBaselineAnalyzer.swift in Sources */, AAC519AC668EF430F68B06CA /* InlineCommandCoordinator.swift in Sources */, E8768B22E683F35098DF4CCF /* InlinePreviewPanelController.swift in Sources */, 24E0F4F0EC9148FB409FD112 /* InlinePreviewView.swift in Sources */, @@ -3728,6 +3815,7 @@ A8DCC8CFAD1B698A32E1B077 /* SuggestionClientError.swift in Sources */, 832ACFA7639BA6F769CFA4E1 /* SuggestionConfiguration.swift in Sources */, CC6E722F33A2CCE2DEC6AA9D /* SuggestionCoordinator+Acceptance.swift in Sources */, + 976835E71042F5755616F678 /* SuggestionCoordinator+HostMarkedText.swift in Sources */, 830E59C6940485511EE3FA39 /* SuggestionCoordinator+Input.swift in Sources */, 5D6BF62973E33472E20E82B5 /* SuggestionCoordinator+Lifecycle.swift in Sources */, 64F9AE9BBCA9D09623246A14 /* SuggestionCoordinator+Prediction.swift in Sources */, @@ -3763,6 +3851,7 @@ 513BD87C18DAFB7EDC45B2FF /* SystemMetricsStore.swift in Sources */, C6EAB2503D7186C0B2CF417D /* SystemResourceSampler.swift in Sources */, 54290B1569A7568809014A23 /* SystemSettingsWindowLocator.swift in Sources */, + 25F7E6EC713F8F71DEEEAAA3 /* SystemUIFocusShadowPolicy.swift in Sources */, 1816D3D625F561209590D45D /* TagChip.swift in Sources */, 401409603DE521880F75C779 /* TerminalAppDetector.swift in Sources */, 2E0AEA2199864F46F86FC0C0 /* TextDirectionDetector.swift in Sources */, @@ -3856,11 +3945,17 @@ C6EE8C731928DB2A26945127 /* FocusTrackingModelTests.swift in Sources */, 5F2BFD0D7718CC774F03FBE0 /* FoundationModelAvailabilityServiceTests.swift in Sources */, AC3A30CDDFE6ED0A0D87005F /* FoundationModelDriftEvalTests.swift in Sources */, - AFA6E84BC8C637CDBDF8BA9D /* GhostFontMetricsTests.swift in Sources */, - 8EFD4154069432EB43955033 /* GhostFontSizeStabilizerTests.swift in Sources */, - 39F954287497C8B1253119C7 /* GhostSuggestionLayoutTests.swift in Sources */, + CA959F082DB2C6E95C5743C8 /* GhostBaselinePolicyTests.swift in Sources */, + 4BDE48F1CCFAA097E95B53F6 /* GhostFontResolverTests.swift in Sources */, + 109F9BD7E1AE3E7FB0C712AA /* GhostTextLayoutTests.swift in Sources */, + BB9CA5E517347B478B4D52E9 /* GhostTextPixelAlignmentTests.swift in Sources */, + CB2D43A2B7568173A8E82CE4 /* GhostWrapBandPolicyTests.swift in Sources */, 2240B5EC1DB9902E2D2E2CCD /* HardwareCapabilityProbeTests.swift in Sources */, + A966DCE17C5160F15F371372 /* HostBaselineCalibratorTests.swift in Sources */, + 61DE0622990EA6A09D9F4769 /* HostMarkedTextPolicyTests.swift in Sources */, + 7EA9216A909247A4F722C9C1 /* HostTextMetricsCacheTests.swift in Sources */, C080D50FA0ED1F32C92BCC36 /* HuggingFaceModelsTests.swift in Sources */, + B76A1739BB3A656D6417C5D3 /* InkBaselineAnalyzerTests.swift in Sources */, 1B66CA97E53D1ADB9F4E9A4A /* InputMonitorTests.swift in Sources */, 765A4E8212962910AA3FBA87 /* InputSuppressionControllerTests.swift in Sources */, 16BD2E6ECD6A818711A799D3 /* InsertedTextAdvanceTests.swift in Sources */, @@ -3959,6 +4054,7 @@ 833A5A25C0F5B0E9A46DA04D /* SyntheticReplacePlannerTests.swift in Sources */, E43B0EA04ED0B747CC8D9EBC /* SystemMetricsStoreTests.swift in Sources */, FBC44557CFBA826392930ABD /* SystemResourceSamplerTests.swift in Sources */, + D18F11732EA94EA16CD8101F /* SystemUIFocusShadowPolicyTests.swift in Sources */, 76D834C8C72F8AFD737D8433 /* TerminalAppDetectorTests.swift in Sources */, 74C07F4568C4FAE1C344E789 /* TextDirectionDetectorTests.swift in Sources */, 696514ADA5A7D085C333BE35 /* TextLayoutCaretEstimatorTests.swift in Sources */, diff --git a/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift b/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift index 97f302af6..e83487e86 100644 --- a/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift +++ b/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift @@ -543,9 +543,16 @@ extension SuggestionCoordinator { } state = .ready(text: advancedSession.remainingText, latency: advancedSession.latency) - // Same slide as Tab acceptance; the user typed the next characters, so the caret traveled - // by exactly them. Fall back to the (session-start) caret anchor only if the slide can't apply. - if !overlayController.advanceInline(to: advancedSession.remainingText, insertedText: typedCharacters) { + if isHoldingForHostMarkedText { + // The host's own prediction still occupies the ghost's spot; the advanced tail stays + // hidden until a snapshot without marked text reconciles and re-presents it. + if overlayState.isVisible { + hideOverlay(reason: Self.hostMarkedTextHoldReason) + } + } else if !overlayController.advanceInline(to: advancedSession.remainingText, insertedText: typedCharacters) { + // Same slide as Tab acceptance; the user typed the next characters, so the caret + // traveled by exactly them. Fall back to the (session-start) caret anchor only if the + // slide can't apply. presentOverlay( text: advancedSession.remainingText, at: session.baseContext.caretRect, @@ -745,7 +752,11 @@ extension SuggestionCoordinator { focusChangeSequence: context.focusChangeSequence, focusedInputIdentityKey: context.focusedInputIdentityKey, isCorrection: isCorrection, - resolvedFieldStyle: context.resolvedFieldStyle + resolvedFieldStyle: context.resolvedFieldStyle, + hostTextMetrics: context.hostTextMetrics, + isWebContentField: context.isWebContentField, + hasTrailingContent: context.hasTrailingContent, + elementFrameRect: context.elementFrameRect ) _ = overlayPresenter.present( text: text, diff --git a/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Prediction.swift b/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Prediction.swift index 59507e084..1536d1d73 100644 --- a/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Prediction.swift +++ b/Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Prediction.swift @@ -86,6 +86,12 @@ extension SuggestionCoordinator { return } + // No generation while the host shows its own inline prediction or composes text. + if rawContext.hasHostMarkedText { + holdForHostMarkedText() + return + } + guard SuggestionRequestFactory.shouldGenerateSuggestion(for: rawContext.precedingText) else { clearSuggestion() hideOverlay(reason: "Overlay hidden because the field has no typed text yet.") @@ -126,6 +132,9 @@ extension SuggestionCoordinator { latestRequestID = request.requestID state = .generating + // The model needs tens to hundreds of milliseconds; the overlay uses that time to measure + // the host's baseline so the ghost lands right the first time. + overlayController.prepareInlinePresentation(for: context) logStage( "generating", workID: workID, @@ -826,12 +835,18 @@ extension SuggestionCoordinator { ) state = .ready(text: session.remainingText, latency: session.latency) - presentOverlay( - text: session.remainingText, - at: liveContext.caretRect, - context: liveContext, - isRightToLeft: TextDirectionDetector.isRightToLeft(liveContext.precedingText) - ) + // A host prediction that appeared while the model was thinking owns the spot right now; + // the session is kept and shows itself once the host span clears. + if liveContext.hasHostMarkedText { + holdForHostMarkedText() + } else { + presentOverlay( + text: session.remainingText, + at: liveContext.caretRect, + context: liveContext, + isRightToLeft: TextDirectionDetector.isRightToLeft(liveContext.precedingText) + ) + } logStage( "ready", workID: workID, diff --git a/Cotabby/App/Core/CotabbyAppEnvironment.swift b/Cotabby/App/Core/CotabbyAppEnvironment.swift index b6a78a409..ce654578d 100644 --- a/Cotabby/App/Core/CotabbyAppEnvironment.swift +++ b/Cotabby/App/Core/CotabbyAppEnvironment.swift @@ -164,7 +164,10 @@ final class CotabbyAppEnvironment { suggestionInserter.isComposingIMEActiveProvider = { [weak keyboardInputSourceMonitor] in keyboardInputSourceMonitor?.isComposingIMEActive ?? false } - let overlayController = OverlayController(suggestionSettings: suggestionSettings) + let overlayController = OverlayController( + suggestionSettings: suggestionSettings, + baselineCalibrator: HostBaselineCalibrator() + ) let activationIndicatorController = ActivationIndicatorController() let clipboardContextProvider = ClipboardContextProvider() let clipboardRelevanceFilter = ClipboardRelevanceFilter() @@ -193,7 +196,7 @@ final class CotabbyAppEnvironment { CotabbyLogger.app.info("Foundation model engine unavailable (SDK)") #endif - let suggestionEngine: any SuggestionGenerating = SuggestionEngineRouter( + let routedEngine: any SuggestionGenerating = SuggestionEngineRouter( suggestionSettings: suggestionSettings, foundationModelEngine: foundationModelEngine, llamaEngine: LlamaSuggestionEngine(runtimeManager: runtimeManager), @@ -218,6 +221,11 @@ final class CotabbyAppEnvironment { suggestionSettings?.openAICompatibleModelName.nonEmpty } ) + // Under `-cotabby-debug` with `cotabbyDebugForcedSuggestion` set, every request answers with + // that fixed text so ghost placement can be measured deterministically without a model. + let suggestionEngine: any SuggestionGenerating = DebugForcedSuggestionEngine.isConfigured() + ? DebugForcedSuggestionEngine(wrapping: routedEngine) + : routedEngine // Per-user emoji recents/frequency. Built before the settings coordinator so the // "Clear History" control can reach it, and before the picker which reads and writes it. diff --git a/Cotabby/Models/Focus/FocusModels.swift b/Cotabby/Models/Focus/FocusModels.swift index 7bfdf42e5..879eb0a7d 100644 --- a/Cotabby/Models/Focus/FocusModels.swift +++ b/Cotabby/Models/Focus/FocusModels.swift @@ -111,15 +111,61 @@ enum FocusCapability: Equatable { /// back to its default styling. Stored as plain value types (no `NSFont`/`NSColor`) so the snapshot /// stays `Equatable`/`Sendable` and is cheap to carry across async boundaries. nonisolated struct ResolvedFieldStyle: Equatable, Sendable { - /// PostScript font name suitable for `NSFont(name:size:)`. + /// PostScript font name suitable for `NSFont(name:size:)`. Native AppKit hosts report it + /// (`Menlo-Regular`, `Helvetica`, `.AppleSystemUIFont`); web engines usually omit it. let fontName: String? - /// Host-reported point size, used only as the reference for scale-invariant metric sizing. + /// Family name (`AXFontFamily`), kept separately because a host can report the family without + /// a PostScript face, and the family alone still selects the right typeface. + let fontFamily: String? + /// Host-reported point size. This is the ghost font's rendered size whenever it is plausible: + /// matching the host's size exactly is what makes ghost glyphs line up with the host's text. + /// Chromium reports only this field, so a size-only style is a real, useful style. let fontPointSize: CGFloat? /// Foreground text color as a 6-digit hex string (see `SuggestionTextColorCodec`). let colorHex: String? + init(fontName: String?, fontFamily: String? = nil, fontPointSize: CGFloat?, colorHex: String?) { + self.fontName = fontName + self.fontFamily = fontFamily + self.fontPointSize = fontPointSize + self.colorHex = colorHex + } + + /// True when the host exposed nothing usable at all. A size-only style is NOT empty: the point + /// size is the single most important fact for matching the host's rendering. + var isEmpty: Bool { + fontName == nil && fontFamily == nil && fontPointSize == nil && colorHex == nil + } +} + +/// Facts about how the host actually renders the text next to the caret, measured from the host's +/// own Accessibility geometry rather than guessed from font tables. `GhostFontResolver` uses the +/// width sample to pick (or scale) a typeface when the host names no family, and the line pitch +/// lets a wrapped ghost place its second row exactly where the host would place the next line. +/// +/// Every field is optional: a host that exposes no line or range bounds simply contributes nothing +/// and the ghost falls back to font-table metrics. Plain value type so it rides along in the +/// `Equatable`/`Sendable` snapshot. +nonisolated struct HostTextMetrics: Equatable, Sendable { + /// Text immediately before the caret on the caret's line whose rendered width was measured. + let sampleText: String? + /// Rendered width of `sampleText` in points, from the host's own bounds-for-range answer. + let sampleWidth: CGFloat? + /// The host's rendered box for the caret's whole visual line, in global Cocoa coordinates. + /// Its `minX` is the real content left edge (where a wrapped ghost row starts). + let lineRect: CGRect? + /// Vertical distance between consecutive visual lines, when the host exposes line geometry. + let linePitch: CGFloat? + + init(sampleText: String? = nil, sampleWidth: CGFloat? = nil, lineRect: CGRect? = nil, linePitch: CGFloat? = nil) { + self.sampleText = sampleText + self.sampleWidth = sampleWidth + self.lineRect = lineRect + self.linePitch = linePitch + } + var isEmpty: Bool { - fontName == nil && colorHex == nil + sampleText == nil && lineRect == nil && linePitch == nil } } @@ -146,6 +192,16 @@ nonisolated struct FocusedInputSnapshot: Equatable { let subrole: String? let caretRect: CGRect let inputFrameRect: CGRect? + /// The focused element's own `AXFrame` in global Cocoa coordinates, read fresh on every poll and + /// never widened. `inputFrameRect` is grown to a parent container or a 500pt minimum for card + /// placement, so it cannot say where the host actually wraps; this can. + let elementFrameRect: CGRect? + /// The host's uncommitted (marked) text range in its own document coordinates, when it has one: + /// macOS inline predictive text shown after the caret, or an IME composition before it. The + /// host owns that span of the field until the user commits or dismisses it, so Cotabby must + /// neither generate against it nor paint a ghost over it (see `HostMarkedTextPolicy`). Marked + /// text after the caret is already excluded from `trailingText`. + let hostMarkedTextRange: NSRange? let caretSource: String let caretQuality: CaretGeometryQuality /// Average character width in points observed from AX child frame measurements. @@ -208,6 +264,12 @@ nonisolated struct FocusedInputSnapshot: Equatable { /// compiling unchanged. let fieldPlaceholder: String? + /// How the host renders text near the caret (measured widths, line box, line pitch), resolved + /// once per field so the ghost can match the host's typeface and wrap geometry. Nil when the + /// host exposes no measurable text geometry. The initializer default keeps existing call sites + /// compiling unchanged. + let hostTextMetrics: HostTextMetrics? + /// Explicit initializer keeps `focusChangeSequence` immutable while preserving the old /// memberwise-call ergonomics for tests that do not care about focus identity. /// @@ -237,7 +299,10 @@ nonisolated struct FocusedInputSnapshot: Equatable { focusedURLString: String? = nil, resolvedFieldStyle: ResolvedFieldStyle? = nil, windowTitle: String? = nil, - fieldPlaceholder: String? = nil + fieldPlaceholder: String? = nil, + hostTextMetrics: HostTextMetrics? = nil, + elementFrameRect: CGRect? = nil, + hostMarkedTextRange: NSRange? = nil ) { self.applicationName = applicationName self.bundleIdentifier = bundleIdentifier @@ -262,6 +327,9 @@ nonisolated struct FocusedInputSnapshot: Equatable { self.resolvedFieldStyle = resolvedFieldStyle self.windowTitle = windowTitle self.fieldPlaceholder = fieldPlaceholder + self.hostTextMetrics = hostTextMetrics + self.elementFrameRect = elementFrameRect + self.hostMarkedTextRange = hostMarkedTextRange } var identity: FocusedInputIdentity { @@ -271,6 +339,11 @@ nonisolated struct FocusedInputSnapshot: Equatable { ) } + /// True while the host shows uncommitted text of its own (see `hostMarkedTextRange`). + var hasHostMarkedText: Bool { + (hostMarkedTextRange?.length ?? 0) > 0 + } + /// The signature lets later pipeline stages detect whether a completion result is stale. /// This is the same idea you would use in a React app with a derived cache key. /// Content-only fingerprint for staleness detection. Deliberately excludes `elementIdentifier` diff --git a/Cotabby/Models/Settings/CompletionRenderMode.swift b/Cotabby/Models/Settings/CompletionRenderMode.swift index 14d56cec9..fd7ceb478 100644 --- a/Cotabby/Models/Settings/CompletionRenderMode.swift +++ b/Cotabby/Models/Settings/CompletionRenderMode.swift @@ -41,6 +41,11 @@ nonisolated enum CompletionRenderMode: Equatable, Sendable { /// completions render in, since a FIM result has no inline home. case caretMidLine + /// The policy chose inline, but no honest inline layout existed: the text needed a second + /// row and the host exposed no line pitch to place it on, or a second row would have painted + /// over the host's text below the caret. The card shows the whole suggestion instead. + case inlineLayoutUnavailable + /// User set their global preference to always use mirror mode. Phase 2 wiring. case userPreference diff --git a/Cotabby/Models/Suggestion/Request/FocusedInputContext.swift b/Cotabby/Models/Suggestion/Request/FocusedInputContext.swift index 3c5ec61c0..8d00e061d 100644 --- a/Cotabby/Models/Suggestion/Request/FocusedInputContext.swift +++ b/Cotabby/Models/Suggestion/Request/FocusedInputContext.swift @@ -36,6 +36,14 @@ struct FocusedInputContext: Equatable, Sendable { let isWebContentField: Bool /// The host field's own text font/color, carried through so the overlay can match it. let resolvedFieldStyle: ResolvedFieldStyle? + /// Measured host text geometry (width sample, line box, line pitch), carried through so the + /// overlay can match the host's typeface and wrap exactly where the host would. + let hostTextMetrics: HostTextMetrics? + /// The focused element's unwidened frame (see `FocusedInputSnapshot.elementFrameRect`), the + /// only frame the overlay may use to decide where a ghost row must wrap. + let elementFrameRect: CGRect? + /// The host's uncommitted text range, if any (see `FocusedInputSnapshot.hostMarkedTextRange`). + let hostMarkedTextRange: NSRange? /// Surface metadata captured once per field session, carried through so the request factory /// can condition the prompt on what the user is writing in (see `SurfaceContextComposer`). let windowTitle: String? @@ -67,6 +75,9 @@ struct FocusedInputContext: Equatable, Sendable { isSecure = snapshot.isSecure isWebContentField = snapshot.isWebContentField resolvedFieldStyle = snapshot.resolvedFieldStyle + hostTextMetrics = snapshot.hostTextMetrics + elementFrameRect = snapshot.elementFrameRect + hostMarkedTextRange = snapshot.hostMarkedTextRange windowTitle = snapshot.windowTitle fieldPlaceholder = snapshot.fieldPlaceholder focusedURLString = snapshot.focusedURLString @@ -82,6 +93,19 @@ struct FocusedInputContext: Equatable, Sendable { CaretLinePosition.isAtEndOfLine(trailingText: trailingText) } + /// True when real (non-whitespace) characters follow the caret anywhere in the captured + /// trailing window. A multi-row ghost would paint over that content, so the overlay keeps the + /// ghost to one row or promotes it to the card when this is set. + var hasTrailingContent: Bool { + trailingText.contains { !$0.isWhitespace } + } + + /// True while the host shows uncommitted text of its own: an inline prediction after the caret + /// or an IME composition before it. Generation and ghost rendering pause until it clears. + var hasHostMarkedText: Bool { + (hostMarkedTextRange?.length ?? 0) > 0 + } + /// Stable per-process key for the focused field, intentionally NOT including the input frame /// rect. The polling signature in `FocusTracker` bumps `focusChangeSequence` whenever the /// field's frame changes (e.g., a chat composer growing taller as the user types wraps onto a diff --git a/Cotabby/Models/Suggestion/Session/SuggestionPresentationModels.swift b/Cotabby/Models/Suggestion/Session/SuggestionPresentationModels.swift index 248e71c19..3b053b718 100644 --- a/Cotabby/Models/Suggestion/Session/SuggestionPresentationModels.swift +++ b/Cotabby/Models/Suggestion/Session/SuggestionPresentationModels.swift @@ -54,6 +54,20 @@ struct SuggestionOverlayGeometry: Equatable, Sendable { /// The host field's own text font/color, so the overlay can render ghost text that matches the /// field instead of always using the system font and a fixed gray. Nil falls back to defaults. let resolvedFieldStyle: ResolvedFieldStyle? + /// Measured host text geometry (see `HostTextMetrics`): width sample for typeface matching, + /// line box for the content left edge, and line pitch for exact multi-row placement. + let hostTextMetrics: HostTextMetrics? + /// True when the field's text is rendered by a web engine. The baseline rule differs between + /// TextKit (baseline at the layout manager's default offset) and web engines (rounded ascent + /// centered in the line box), so the renderer must know which one produced the caret box. + let isWebContentField: Bool + /// True when non-whitespace text follows the caret. A ghost may then occupy only one row: a + /// second row would paint over the host's own following lines. + let hasTrailingContent: Bool + /// The focused element's own frame, unwidened (see `FocusedInputSnapshot.elementFrameRect`). + /// Wrapped ghost rows must stay inside it; `inputFrameRect` is grown for card placement and + /// would let a row run past the host's real right edge. + let elementFrameRect: CGRect? init( caretRect: CGRect, @@ -66,7 +80,11 @@ struct SuggestionOverlayGeometry: Equatable, Sendable { focusChangeSequence: UInt64 = 0, focusedInputIdentityKey: UInt64 = 0, isCorrection: Bool = false, - resolvedFieldStyle: ResolvedFieldStyle? = nil + resolvedFieldStyle: ResolvedFieldStyle? = nil, + hostTextMetrics: HostTextMetrics? = nil, + isWebContentField: Bool = false, + hasTrailingContent: Bool = false, + elementFrameRect: CGRect? = nil ) { self.caretRect = caretRect self.inputFrameRect = inputFrameRect @@ -79,6 +97,10 @@ struct SuggestionOverlayGeometry: Equatable, Sendable { self.focusedInputIdentityKey = focusedInputIdentityKey self.isCorrection = isCorrection self.resolvedFieldStyle = resolvedFieldStyle + self.hostTextMetrics = hostTextMetrics + self.isWebContentField = isWebContentField + self.hasTrailingContent = hasTrailingContent + self.elementFrameRect = elementFrameRect } /// Returns a copy with only `caretRect` replaced. Used to advance the ghost by an exact measured @@ -94,7 +116,12 @@ struct SuggestionOverlayGeometry: Equatable, Sendable { isRightToLeft: isRightToLeft, focusChangeSequence: focusChangeSequence, focusedInputIdentityKey: focusedInputIdentityKey, - resolvedFieldStyle: resolvedFieldStyle + isCorrection: isCorrection, + resolvedFieldStyle: resolvedFieldStyle, + hostTextMetrics: hostTextMetrics, + isWebContentField: isWebContentField, + hasTrailingContent: hasTrailingContent, + elementFrameRect: elementFrameRect ) } } diff --git a/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift b/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift index ab8c5b051..a3b5d9504 100644 --- a/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift +++ b/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift @@ -253,12 +253,19 @@ protocol SuggestionOverlayControlling: AnyObject { /// safely slide (hidden, mirror mode, RTL, multi-line, or nothing rendered to measure /// against); callers then fall back to a caret-anchored present. func advanceInline(to remainingText: String, insertedText: String) -> Bool + + /// Called when generation starts for `context`, so the controller can do the slow parts of an + /// inline presentation (measuring the host's painted baseline) before the suggestion arrives. + func prepareInlinePresentation(for context: FocusedInputContext) } extension SuggestionOverlayControlling { /// Default: not supported, so conformers that do not render an inline panel (e.g. test doubles) /// transparently fall back to the caret-anchored present path. func advanceInline(to remainingText: String, insertedText: String) -> Bool { false } + + /// Default: nothing to prepare. + func prepareInlinePresentation(for context: FocusedInputContext) {} } @MainActor diff --git a/Cotabby/Services/Focus/Caching/FieldStyleCache.swift b/Cotabby/Services/Focus/Caching/FieldStyleCache.swift index 2895db1b0..9477d2626 100644 --- a/Cotabby/Services/Focus/Caching/FieldStyleCache.swift +++ b/Cotabby/Services/Focus/Caching/FieldStyleCache.swift @@ -1,35 +1,103 @@ import Foundation /// Caches the resolved field text style per focused element so the `AXAttributedStringForRange` -/// read happens once per field, not on every focus poll. +/// read happens once per style run, not on every focus poll. /// /// Reading per-character font/color is a synchronous cross-process Accessibility call. The focus /// resolver runs many times per second while focus stays on one field, so re-reading every poll -/// would add avoidable main-thread latency on the hot path. Keying on element identity collapses -/// that to one read per field. A nil result (host exposes no style) is cached too, so a plain field -/// is not re-probed on every poll. +/// would add avoidable main-thread latency on the hot path. Keying on element identity alone is not +/// enough, though: rich-text hosts (Notes, Mail, TextEdit, Pages) change font at the caret when the +/// user moves between runs or restyles text, and a style frozen at first focus then renders the +/// ghost in the wrong face. The cache therefore also tracks the *style run* the caret sits in +/// (`AXStyleRangeForIndex`, one extra call only when the caret leaves the known run) and the caret +/// box height (a free signal that the line's font changed on hosts without style ranges). +/// +/// Chromium builds inline text boxes lazily, so its first attributed-string answer after focus is +/// empty and only later polls carry the font; caching that first nil forever left web fields with +/// no size. Empty answers are retried a bounded number of times, spaced over successive polls. /// /// A reference type so it can carry state across the value-typed `FocusSnapshotResolver`'s /// non-mutating `resolveSnapshot`, mirroring `DeepGeometryWalkThrottle`. The resolver is constructed /// once and retained by `FocusTracker`. @MainActor final class FieldStyleCache { - private var key: String? + static let maximumAttempts = 6 + static let retryInterval: TimeInterval = 0.25 + + /// What one cached answer was resolved for. A poll whose caret is still inside `styleRun` (or + /// whose run lookup returns the same run) with an unchanged caret height reuses the answer. + struct Probe: Equatable { + let fieldKey: String + /// Style run containing the character before the caret, in the host's own text + /// coordinates; nil when the host exposes no style ranges. + let styleRun: NSRange? + /// The caret box height rounded to whole points. + let caretHeightBucket: Int + } + + private var probe: Probe? private var style: ResolvedFieldStyle? + private var attempts = 0 + private var lastAttemptAt: Date? - /// Returns the cached style when `key` matches the last resolution, otherwise resolves once and - /// caches the result (including nil). - func style(forKey key: String, resolve: () -> ResolvedFieldStyle?) -> ResolvedFieldStyle? { - if key == self.key { - return style + /// Returns the cached style for the caret's current style run, otherwise resolves and caches. + /// + /// - `caretLocation` is the caret offset in the host's coordinates; a cached style whose + /// `styleRun` contains the character before it is reused without any AX call. + /// - `styleRun` is invoked only when the cached run does not cover the caret (or nothing is + /// cached); it may return nil for hosts without style ranges. + /// - A nil result is retried on later calls (spaced by `retryInterval`) until + /// `maximumAttempts`, after which nil is cached for the probe. + func style( + forKey fieldKey: String, + caretLocation: Int, + caretHeight: CGFloat, + now: Date = Date(), + styleRun: () -> NSRange?, + resolve: () -> ResolvedFieldStyle? + ) -> ResolvedFieldStyle? { + let heightBucket = Int(caretHeight.rounded()) + if let probe, probe.fieldKey == fieldKey, probe.caretHeightBucket == heightBucket, + let cached = style, let run = probe.styleRun, Self.run(run, containsCaret: caretLocation) { + return cached } + let nextProbe = Probe(fieldKey: fieldKey, styleRun: styleRun(), caretHeightBucket: heightBucket) + if nextProbe != probe { + let sameFieldSameRun = probe?.fieldKey == fieldKey + && probe?.caretHeightBucket == heightBucket + && probe?.styleRun != nil + && probe?.styleRun?.location == nextProbe.styleRun?.location + probe = nextProbe + if !sameFieldSameRun { + style = nil + attempts = 0 + lastAttemptAt = nil + } + } + if let style { + return style + } + guard attempts < Self.maximumAttempts else { + return nil + } + if let lastAttemptAt, now.timeIntervalSince(lastAttemptAt) < Self.retryInterval { + return nil + } + attempts += 1 + lastAttemptAt = now let resolved = resolve() - self.key = key style = resolved return resolved } + /// The character before the caret is the one the style was read from, so the caret sits inside + /// the run when that character does. + private static func run(_ run: NSRange, containsCaret caret: Int) -> Bool { + let probedIndex = max(caret - 1, 0) + return probedIndex >= run.location && probedIndex < run.location + max(run.length, 1) + } + // Stored state is plain value types, safe to release anywhere. The nonisolated deinit keeps // deallocation off the back-deployment main-actor executor shim, whose StopLookupScope // double-frees on macOS 26 (see InputSuppressionController). Production's single long-lived diff --git a/Cotabby/Services/Focus/Caching/HostTextMetricsCache.swift b/Cotabby/Services/Focus/Caching/HostTextMetricsCache.swift new file mode 100644 index 000000000..cf8ad2053 --- /dev/null +++ b/Cotabby/Services/Focus/Caching/HostTextMetricsCache.swift @@ -0,0 +1,89 @@ +import Foundation + +/// Caches `HostTextMetrics` per focused element so the handful of bounds queries behind them run +/// once per field, not on every focus poll. +/// +/// Two retry paths exist because hosts fail in two different ways: +/// - Chromium loads inline text boxes lazily: the first bounds query after focus returns an empty +/// rect and only later queries answer. An empty answer is retried a bounded number of times, +/// spaced out over successive polls, then given up on so a host that never answers costs +/// nothing more. +/// - A field focused while (nearly) empty has no text to measure a width sample from, yet its +/// line geometry may already be known. Caching that answer as final would pin the field to +/// "no sample" for the whole session, and the typeface match would never improve. A missing +/// sample is therefore re-measured when the caret has moved to a new offset where a sample +/// could exist, again a bounded number of times. +/// +/// Same lifetime shape as `FieldStyleCache`: a reference type retained by the value-typed resolver. +@MainActor +final class HostTextMetricsCache { + static let maximumAttempts = 6 + static let maximumSampleAttempts = 6 + static let retryInterval: TimeInterval = 0.25 + /// Shortest prefix the probe can measure a width from (see `HostTextMetricsProbe.widthSample`). + static let minimumSampleCaret = 2 + + private var key: String? + private var metrics: HostTextMetrics? + private var attempts = 0 + private var sampleAttempts = 0 + private var lastSampleCaret: Int? + private var lastAttemptAt: Date? + + func metrics( + forKey key: String, + caretLocation: Int, + now: Date = Date(), + measure: () -> HostTextMetrics? + ) -> HostTextMetrics? { + if key != self.key { + self.key = key + metrics = nil + attempts = 0 + sampleAttempts = 0 + lastSampleCaret = nil + lastAttemptAt = nil + } + if let metrics { + guard metrics.sampleText == nil, shouldRetrySample(caretLocation: caretLocation, now: now) else { + return metrics + } + sampleAttempts += 1 + lastSampleCaret = caretLocation + lastAttemptAt = now + if let remeasured = measure(), remeasured.sampleText != nil { + self.metrics = remeasured + return remeasured + } + return metrics + } + guard attempts < Self.maximumAttempts else { + return nil + } + if let lastAttemptAt, now.timeIntervalSince(lastAttemptAt) < Self.retryInterval { + return nil + } + attempts += 1 + lastAttemptAt = now + lastSampleCaret = caretLocation + let measured = measure() + metrics = measured + return measured + } + + private func shouldRetrySample(caretLocation: Int, now: Date) -> Bool { + guard sampleAttempts < Self.maximumSampleAttempts, + caretLocation >= Self.minimumSampleCaret, + caretLocation != lastSampleCaret + else { + return false + } + if let lastAttemptAt, now.timeIntervalSince(lastAttemptAt) < Self.retryInterval { + return false + } + return true + } + + // Value-typed storage only; see `FieldStyleCache` for why the deinit is nonisolated. + nonisolated deinit {} +} diff --git a/Cotabby/Services/Focus/Resolution/AXTextGeometryResolver.swift b/Cotabby/Services/Focus/Resolution/AXTextGeometryResolver.swift index df47c0d27..e96160acf 100644 --- a/Cotabby/Services/Focus/Resolution/AXTextGeometryResolver.swift +++ b/Cotabby/Services/Focus/Resolution/AXTextGeometryResolver.swift @@ -81,67 +81,90 @@ struct AXTextGeometryResolver { textValue: String? = nil, textSelection: NSRange? = nil, staticRunThrottle: StaticTextRunWalkThrottle? = nil, - focusChangeSequence: UInt64 = 0 + focusChangeSequence: UInt64 = 0, + supportsLineQueries: Bool = false ) -> CaretGeometryResult? { let selectionInTextValue = textSelection ?? selection - // Branch 1: Zero-length BoundsForRange at the caret position — ideal case. - // Gated on `supportsBoundsForRange` because the API is a synchronous cross-process - // call into the focused app's AX implementation. In Chrome that's a round-trip into - // the renderer, and the deep-tree walker can touch many leaves per focus poll; calling - // BoundsForRange on nodes that don't advertise support stalled the main thread badly - // enough to freeze typing. The `rectIsNearAnchor` validator stays as a correctness - // guard for supporters that return rects belonging to an unrelated range. + // Branch 1 (previous-character trailing edge). Ask the host for the bounds of the character + // before the caret and take its trailing edge: for left-to-right text that IS the insertion + // point, and its box is the real rendered line. Measured against TextEdit and Chrome, this + // answer coincides with the zero-length caret query on x while the zero-length query is the + // less trustworthy one: TextKit reports the end-of-document caret one line too high, and + // Chrome reports a caret after a trailing newline at the end of the previous line. A previous + // character that is itself a line break describes the previous line, so that case defers to + // the zero-length query below. + // Gated on `supportsBoundsForRange` because the API is a synchronous cross-process call into + // the focused app's AX implementation; the `rectIsNearAnchor` validator stays as a + // correctness guard for supporters that return rects belonging to an unrelated range. + let previousCharacter = Self.character(before: selectionInTextValue.location, in: textValue) if supportsBoundsForRange, + selection.location > 0, + let previousCharacter, !previousCharacter.isNewline, let rect = AXHelper.parameterizedRectValue( for: kAXBoundsForRangeParameterizedAttribute as CFString, - range: NSRange(location: selection.location, length: 0), + range: NSRange(location: selection.location - 1, length: 1), on: element - ), !rect.isEmpty { + ), rect.width > 0, rect.height > 0, AXHelper.rectHasFiniteComponents(rect) { let cocoaRect = AXHelper.validatedCocoaTextRect( fromAccessibilityRect: rect, anchorFrame: cocoaAnchorFrame ) if rectIsNearAnchor(cocoaRect, anchor: cocoaAnchorFrame) { + let isRightToLeft = textValue.map(TextDirectionDetector.isRightToLeft) ?? false return CaretGeometryResult( - rect: normalizedCaretRect(fromZeroLengthRangeRect: cocoaRect), - quality: .exact + rect: Self.caretRect(afterCharacterFrame: cocoaRect, rightToLeft: isRightToLeft), + quality: .exact, + sourceDetail: "previous-character" ) } } - // Branch 1.5: Chromium / WebKit AXTextMarker fallback. - // Apps like Discord/Chrome fail NSRange queries but return a correct bounding box - // when we ask for the caret via their internal AXTextMarkerRange objects. - if let markerRect = AXHelper.textMarkerCaretRect(on: element), !markerRect.isEmpty { - let cocoaRect = AXHelper.validatedCocoaTextRect( - fromAccessibilityRect: markerRect, - anchorFrame: cocoaAnchorFrame - ) - return CaretGeometryResult( - rect: normalizedCaretRect(fromZeroLengthRangeRect: cocoaRect), - quality: .exact - ) - } - - // Branch 2: BoundsForRange on the character before the caret, then shift to its trailing edge. - // Same gate and anchor validation as Branch 1. + // Branch 1.2: zero-length BoundsForRange at the caret. Reached at the start of a field or + // right after a line break. Hosts answer this with a zero-WIDTH rect, so the check is on + // height, not `isEmpty` (which is true for any zero-width rect and used to discard every + // legitimate caret box here). if supportsBoundsForRange, - selection.location > 0, let rect = AXHelper.parameterizedRectValue( for: kAXBoundsForRangeParameterizedAttribute as CFString, - range: NSRange(location: selection.location - 1, length: 1), + range: NSRange(location: selection.location, length: 0), on: element - ), !rect.isEmpty { + ), rect.height > 0, AXHelper.rectHasFiniteComponents(rect) { let cocoaRect = AXHelper.validatedCocoaTextRect( fromAccessibilityRect: rect, anchorFrame: cocoaAnchorFrame ) + if rectIsNearAnchor(cocoaRect, anchor: cocoaAnchorFrame) { + let normalized = normalizedCaretRect(fromZeroLengthRangeRect: cocoaRect) + return zeroLengthCaretResult( + normalized, + context: ZeroLengthCaretContext( + element: element, + caretLocation: selection.location, + supportsLineQueries: supportsLineQueries, + anchorFrame: cocoaAnchorFrame, + isAtTextEndAfterNewline: previousCharacter?.isNewline == true + && selectionInTextValue.location >= ((textValue ?? "") as NSString).length + ) + ) + } + } + + // Branch 1.5: Chromium / WebKit AXTextMarker fallback. + // Apps like Discord/Chrome fail NSRange queries but return a correct bounding box + // when we ask for the caret via their internal AXTextMarkerRange objects. The caret box is + // zero-width, so only its height is checked. + if let markerRect = AXHelper.textMarkerCaretRect(on: element), + markerRect.height > 0, AXHelper.rectHasFiniteComponents(markerRect) { + let cocoaRect = AXHelper.validatedCocoaTextRect( + fromAccessibilityRect: markerRect, + anchorFrame: cocoaAnchorFrame + ) if rectIsNearAnchor(cocoaRect, anchor: cocoaAnchorFrame) { return CaretGeometryResult( - rect: CGRect( - x: cocoaRect.maxX, y: cocoaRect.minY, width: 2, height: cocoaRect.height), - quality: .derived + rect: normalizedCaretRect(fromZeroLengthRangeRect: cocoaRect), + quality: .exact, + sourceDetail: "text-marker" ) } } @@ -457,9 +480,91 @@ struct AXTextGeometryResolver { } /// Converts the measured character immediately before the selection into Cotabby's normalized - /// caret shape. The trailing edge—not the character origin—is the insertion point. - static func caretRect(afterCharacterFrame frame: CGRect) -> CGRect { - CGRect(x: frame.maxX, y: frame.minY, width: 2, height: frame.height) + /// caret shape. The trailing edge—not the character origin—is the insertion point: the right + /// edge for left-to-right text, the left edge for right-to-left text. + static func caretRect(afterCharacterFrame frame: CGRect, rightToLeft: Bool = false) -> CGRect { + CGRect(x: rightToLeft ? frame.minX : frame.maxX, y: frame.minY, width: 2, height: frame.height) + } + + /// The character immediately before `caretLocation` (a UTF-16 offset into `text`), or nil. + static func character(before caretLocation: Int, in text: String?) -> Character? { + guard let text, caretLocation > 0 else { return nil } + let nsText = text as NSString + guard caretLocation <= nsText.length else { return nil } + let clusterRange = nsText.rangeOfComposedCharacterSequence(at: caretLocation - 1) + return nsText.substring(with: clusterRange).first + } + + /// Finishes a zero-length caret answer. Its x is trustworthy in every measured host, but its + /// line differs: TextKit reports the caret one line too high at every position (measured in + /// TextEdit at line starts, mid-line, and at the end of the document), while Chromium reports a + /// caret that follows a trailing line break at the end of the previous line. + /// + /// TextKit hosts expose `AXLineForIndex`/`AXRangeForLine`, so the caret's real line box is read + /// from them: the line containing the caret, or the (still empty) line below it when the caret + /// sits past that line's trailing break. Hosts without line queries keep the rect as answered, + /// except the trailing-break-at-end case, which is demoted to `.estimated` so the card shows + /// rather than an inline ghost on the wrong line. + /// What the zero-length caret answer needs to be finished (see `zeroLengthCaretResult`). + private struct ZeroLengthCaretContext { + let element: AXUIElement + let caretLocation: Int + let supportsLineQueries: Bool + let anchorFrame: CGRect? + let isAtTextEndAfterNewline: Bool + } + + private func zeroLengthCaretResult(_ rect: CGRect, context: ZeroLengthCaretContext) -> CaretGeometryResult { + if context.supportsLineQueries, + let lineBox = lineBoxForCaret( + element: context.element, + caretLocation: context.caretLocation, + cocoaAnchorFrame: context.anchorFrame + ) { + return CaretGeometryResult( + rect: CGRect(x: rect.minX, y: lineBox.minY, width: rect.width, height: lineBox.height), + quality: .exact, + sourceDetail: "zero-length+line" + ) + } + if context.isAtTextEndAfterNewline { + return CaretGeometryResult(rect: rect, quality: .estimated, sourceDetail: "zero-length-after-break") + } + return CaretGeometryResult(rect: rect, quality: .exact, sourceDetail: "zero-length") + } + + /// The Cocoa box of the visual line the caret is on, from the host's own line queries. When the + /// caret sits after the trailing break of the reported line, the caret is on the next (empty) + /// line, which the host does not enumerate; that box is the reported one moved down by its + /// own height. + private func lineBoxForCaret(element: AXUIElement, caretLocation: Int, cocoaAnchorFrame: CGRect?) -> CGRect? { + guard let lineIndex = AXHelper.parameterizedIntValue( + for: kAXLineForIndexParameterizedAttribute as CFString, + parameter: caretLocation, + on: element + ), lineIndex >= 0, lineIndex < 100_000, + let lineRange = AXHelper.parameterizedRangeValue( + for: kAXRangeForLineParameterizedAttribute as CFString, + parameter: lineIndex, + on: element + ), lineRange.length > 0, + let raw = AXHelper.parameterizedRectValue( + for: kAXBoundsForRangeParameterizedAttribute as CFString, + range: lineRange, + on: element + ), raw.height > 0, AXHelper.rectHasFiniteComponents(raw) else { + return nil + } + let lineBox = AXHelper.validatedCocoaTextRect(fromAccessibilityRect: raw, anchorFrame: cocoaAnchorFrame) + guard rectIsNearAnchor(lineBox, anchor: cocoaAnchorFrame) else { return nil } + let lineText = AXHelper.parameterizedStringValue( + for: kAXStringForRangeParameterizedAttribute as CFString, + range: lineRange, + on: element + ) ?? "" + let caretPastTrailingBreak = caretLocation >= lineRange.location + lineRange.length + && (lineText.last?.isNewline ?? false) + return caretPastTrailingBreak ? lineBox.offsetBy(dx: 0, dy: -lineBox.height) : lineBox } struct CaretRunPlacement: Equatable { diff --git a/Cotabby/Services/Focus/Resolution/FocusSnapshotResolver.swift b/Cotabby/Services/Focus/Resolution/FocusSnapshotResolver.swift index c58adbafc..2b4c8a6b5 100644 --- a/Cotabby/Services/Focus/Resolution/FocusSnapshotResolver.swift +++ b/Cotabby/Services/Focus/Resolution/FocusSnapshotResolver.swift @@ -49,6 +49,10 @@ struct FocusSnapshotResolver { /// `SurfaceContextCache` for why the value is frozen for the whole field session. private let surfaceContextCache = SurfaceContextCache() + /// Caches the measured host text geometry (width sample, line box, line pitch) per field, with + /// bounded retries for hosts that answer empty until their text boxes load. + private let hostTextMetricsCache = HostTextMetricsCache() + init(geometryResolver: AXTextGeometryResolver? = nil) { self.geometryResolver = geometryResolver ?? AXTextGeometryResolver() } @@ -233,22 +237,22 @@ struct FocusSnapshotResolver { } } let focusedURLString = capturedSurface.urlString - // Resolve the host field's own font/color so ghost text can match it. Cached by element - // identity (this is a synchronous AX read and the resolver runs on the focus poll), and - // skipped for secure fields, which are never styled or assisted. - let resolvedFieldStyle: ResolvedFieldStyle? - if resolvedCandidate.isSecure { - resolvedFieldStyle = nil - } else { - let styleKey = "\(application.processIdentifier):\(resolvedCandidate.elementIdentifier)" - resolvedFieldStyle = fieldStyleCache.style(forKey: styleKey) { - AXHelper.resolveFieldStyle( - for: resolvedCandidate.element, - caretLocation: selection.location, - textLength: value.utf16.count - ) - } - } + // Resolve the host field's own font/color so ghost text can match it. Cached per style run + // (see `FieldStyleCache`) and skipped for secure fields, which are never styled or assisted. + let resolvedFieldStyle = resolveFieldStyle( + for: resolvedCandidate, + processIdentifier: application.processIdentifier, + selection: selection, + textLength: value.utf16.count, + caretHeight: caretRect.height + ) + let hostTextMetrics = resolveHostTextMetrics( + for: resolvedCandidate, + processIdentifier: application.processIdentifier, + text: value, + selection: selection, + caretHeight: caretRect.height + ) // Recognize an xterm.js integrated terminal (VS Code / Cursor / web terminal) from the // focused element's DOM classes. The terminal, code editor, and Copilot chat all live in one // process, so this surface-level signal is the only way to suppress ghost text in the @@ -297,7 +301,10 @@ struct FocusSnapshotResolver { focusedURLString: focusedURLString, resolvedFieldStyle: resolvedFieldStyle, windowTitle: capturedSurface.windowTitle, - fieldPlaceholder: capturedSurface.fieldPlaceholder + fieldPlaceholder: capturedSurface.fieldPlaceholder, + hostTextMetrics: hostTextMetrics, + elementFrameRect: resolvedCandidate.elementFrameRect, + hostMarkedTextRange: resolvedCandidate.markedTextRange ) if resolvedCandidate.isSecure { @@ -326,6 +333,83 @@ struct FocusSnapshotResolver { ) } + /// Reads the host's font/color at the caret through the style cache: one attributed-string read + /// per style run, plus one `AXStyleRangeForIndex` call only when the caret leaves the known run. + private func resolveFieldStyle( + for candidate: AXFocusCandidate, + processIdentifier: pid_t, + selection: NSRange, + textLength: Int, + caretHeight: CGFloat + ) -> ResolvedFieldStyle? { + guard !candidate.isSecure else { + return nil + } + let styleKey = "\(processIdentifier):\(candidate.elementIdentifier)" + let documentCaret = candidate.documentCaretLocation ?? selection.location + let supportsStyleRuns = candidate.supportedParameterizedAttributes.contains( + kAXStyleRangeForIndexParameterizedAttribute as String + ) + return fieldStyleCache.style( + forKey: styleKey, + caretLocation: documentCaret, + caretHeight: caretHeight, + styleRun: { + // Hosts without style ranges (Chromium) return nil and fall back to per-field caching + // plus the caret-height signal. + guard supportsStyleRuns else { return nil } + return AXHelper.parameterizedRangeValue( + for: kAXStyleRangeForIndexParameterizedAttribute as CFString, + parameter: max(documentCaret - 1, 0), + on: candidate.element + ) + }, + resolve: { + AXHelper.resolveFieldStyle( + for: candidate.element, + caretLocation: selection.location, + textLength: textLength + ) + } + ) + } + + /// Measures the host's rendered text geometry for the resolved field, once per field and with + /// bounded retries (see `HostTextMetricsCache`). Skipped for secure fields and for + /// marker-synthesized selections, whose window-relative offsets the NSRange bounds API would + /// misread. + private func resolveHostTextMetrics( + for candidate: AXFocusCandidate, + processIdentifier: pid_t, + text: String, + selection: NSRange, + caretHeight: CGFloat + ) -> HostTextMetrics? { + guard !candidate.isSecure, !candidate.usesMarkerSelection else { + return nil + } + // The caret box height changes when the line's font changes, and the measured line box + // moves with the element, so a new height or a moved/resized frame re-measures. Without the + // frame in the key, a window dragged after focus kept vending the old line edge. + let frameKey = candidate.elementFrameRect.map { + "\(Int($0.minX.rounded())),\(Int($0.minY.rounded())),\(Int($0.width.rounded())),\(Int($0.height.rounded()))" + } ?? "-" + let metricsKey = "\(processIdentifier):\(candidate.elementIdentifier):\(Int(caretHeight.rounded())):\(frameKey)" + return hostTextMetricsCache.metrics(forKey: metricsKey, caretLocation: selection.location) { + HostTextMetricsProbe.measure( + HostTextMetricsProbe.Input( + element: candidate.element, + caretLocation: candidate.documentCaretLocation ?? selection.location, + text: text, + caretLocationInText: selection.location, + caretHeight: caretHeight, + supportedParameterizedAttributes: candidate.supportedParameterizedAttributes, + anchorFrame: candidate.elementFrameRect ?? candidate.inputFrameRect + ) + ) + } + } + /// Resolves candidate elements lazily and stops as soon as the first fully capable editable /// target is found. /// @@ -723,13 +807,23 @@ struct FocusSnapshotResolver { on: element, parameterizedAttributes: supportedParameterizedAttributes) : nil - let nativeTextSelection = nativeSelection.flatMap { + // Uncommitted host text (system inline prediction, IME composition). Read only from fields + // that vend the attribute (NSTextView-backed), one call per poll. + let markedTextRange = + canBeEditableTarget && supportedAttributes.contains(HostMarkedTextPolicy.markedRangeAttribute) + ? AXHelper.rangeValue(for: HostMarkedTextPolicy.markedRangeAttribute as CFString, on: element) + .flatMap { $0.length > 0 ? $0 : nil } + : nil + + let nativeTextSelection = nativeSelection.flatMap { documentSelection in nativeTextWindow( on: element, - selection: $0, + selection: documentSelection, supportedAttributes: supportedAttributes, supportedParameterizedAttributes: supportedParameterizedAttributes - ) + ).map { + Self.withoutHostPrediction($0, documentSelection: documentSelection, markedTextRange: markedTextRange) + } } // Prefer the marker-windowed text when we synthesized one so `selection` (window-relative) // and `textValue` stay consistent; otherwise use a bounded native text window when the host @@ -753,6 +847,7 @@ struct FocusSnapshotResolver { supportedAttributes.contains("AXFrame") ? geometryResolver.resolveInputFrameRect(for: element) : nil + let elementFrameRect = inputFrameRect if let currentFrame = inputFrameRect { var finalWidth = currentFrame.width @@ -802,7 +897,10 @@ struct FocusSnapshotResolver { staticRunThrottle: CFEqual(element, focusedReading.element) ? staticRunWalkThrottle : nil, - focusChangeSequence: focusChangeSequence + focusChangeSequence: focusChangeSequence, + supportsLineQueries: markerSelection == nil + && supportedParameterizedAttributes.contains(kAXLineForIndexParameterizedAttribute as String) + && supportedParameterizedAttributes.contains(kAXRangeForLineParameterizedAttribute as String) ) } let caretRect = caretResult?.rect @@ -849,12 +947,37 @@ struct FocusSnapshotResolver { caretSourceDetail: caretResult?.sourceDetail, caretAllowsDeepSearch: caretResult?.allowsDeepSearch ?? true, inputFrameRect: inputFrameRect, + elementFrameRect: elementFrameRect, + markedTextRange: markedTextRange, isSecure: isSecure, vendsDOMAttributes: vendsDOMAttributes, + usesMarkerSelection: markerSelection != nil, + documentCaretLocation: nativeSelection?.location, + supportedParameterizedAttributes: supportedParameterizedAttributes, resolverCandidate: resolverCandidate ) } + /// Drops the host's own inline prediction (marked text after the caret) from a windowed text + /// read, translating the document-coordinate marked range into the window's coordinates. + private static func withoutHostPrediction( + _ window: AXTextSelection, + documentSelection: NSRange, + markedTextRange: NSRange? + ) -> AXTextSelection { + guard let markedTextRange else { return window } + let documentOffset = documentSelection.location - window.selection.location + let windowMarkedRange = NSRange(location: markedTextRange.location - documentOffset, length: markedTextRange.length) + guard windowMarkedRange.location >= 0 else { return window } + let stripped = HostMarkedTextPolicy.strippingPredictionAfterCaret( + text: window.text, + selection: window.selection, + markedRange: windowMarkedRange + ) + guard stripped != window.text else { return window } + return AXTextSelection(text: stripped, selection: window.selection) + } + /// Reads the smallest native text window the host can provide around the current selection. /// /// `AXStringForRange` is the important fast path for large Chrome and WebKit fields: instead of @@ -992,9 +1115,19 @@ private struct AXFocusCandidate { let caretSourceDetail: String? let caretAllowsDeepSearch: Bool let inputFrameRect: CGRect? + /// The element's own `AXFrame` before any widening (see `FocusedInputSnapshot.elementFrameRect`). + let elementFrameRect: CGRect? + /// The host's uncommitted text range in document coordinates (see `HostMarkedTextPolicy`). + let markedTextRange: NSRange? let isSecure: Bool /// Whether the element advertises DOM-reflection attributes, marking it as web-engine /// content (see `WebContentFieldDetector`). let vendsDOMAttributes: Bool + /// True when the selection was synthesized from text markers, so `selection` is window-relative + /// and the NSRange geometry APIs cannot be used against it. + let usesMarkerSelection: Bool + /// The caret offset in the host's document coordinates when a native selection range exists. + let documentCaretLocation: Int? + let supportedParameterizedAttributes: Set let resolverCandidate: FocusCapabilityCandidate } diff --git a/Cotabby/Services/Focus/Resolution/HostTextMetricsProbe.swift b/Cotabby/Services/Focus/Resolution/HostTextMetricsProbe.swift new file mode 100644 index 000000000..9651d459b --- /dev/null +++ b/Cotabby/Services/Focus/Resolution/HostTextMetricsProbe.swift @@ -0,0 +1,179 @@ +import AppKit +import ApplicationServices +import Foundation +import Logging + +/// File overview: +/// Measures how the focused host renders text next to the caret, using the host's own bounds +/// answers rather than font tables: the rendered width of the text just before the caret, the box +/// of the caret's visual line, and the distance to the previous line. `GhostFontResolver` uses the +/// width to pick the typeface a web host names only by size, and `GhostTextLayout` uses the line +/// box and pitch to start wrapped rows exactly where the host starts its next line. +/// +/// Native TextKit hosts answer every query (`AXLineForIndex`, `AXRangeForLine`, `AXBoundsForRange`). +/// Chromium answers `AXBoundsForRange` for `