Skip to content

Key ghost font stabilizer on field identity, not focus sequence#432

Merged
FuJacob merged 1 commit into
mainfrom
fix/ghost-stabilizer-survives-field-grow
May 30, 2026
Merged

Key ghost font stabilizer on field identity, not focus sequence#432
FuJacob merged 1 commit into
mainfrom
fix/ghost-stabilizer-survives-field-grow

Conversation

@FuJacob

@FuJacob FuJacob commented May 30, 2026

Copy link
Copy Markdown
Owner

Summary

Ghost text was doubling in size in chat composers like Slack and iMessage when the input field grew to a second line. The GhostFontSizeStabilizer already clamps caret height to the smallest reading observed during a focus session, but it was keyed on focusChangeSequence, which FocusTracker bumps whenever the focused-input polling signature changes — and that signature includes the field's inputFrameRect. So as soon as the field grew taller (text wrapping onto line 2), the stabilizer treated it as a brand-new focus session, lost its minimum, and the first post-wrap caret reading (often the coarse field-height fallback) became the new baseline.

Add a stable focusedInputIdentityKey derived from (bundleIdentifier, processIdentifier, elementIdentifier) on FocusedInputContext, plumb it through SuggestionOverlayGeometry, and key the stabilizer on it instead. The key survives self-resizing inputs but still resets on genuine field switches. focusChangeSequence keeps its existing meaning for other consumers (e.g., VisualContextCoordinator).

Validation

  • xcodebuild -project Cotabby.xcodeproj -scheme Cotabby -destination 'platform=macOS' build (** BUILD SUCCEEDED **)
  • xcodebuild test ... -only-testing:CotabbyTests/GhostFontSizeStabilizerTests -only-testing:CotabbyTests/SuggestionOverlayStabilityGateTests (16 tests, 0 failures)
  • swiftlint lint --quiet (exit 0)

Manual verification (chat composer wrap) is best done with -cotabby-debug running on real hardware; I haven't done that in this session.

Linked issues

None.

Risk / rollout notes

  • The hash uses Swift's Hasher, which is randomized per process. That's fine here — the key is only ever compared within one process's lifetime.
  • elementIdentifier is CFHash-derived and can theoretically collide across genuinely different fields in the same process. In that pathological case the stabilizer would carry the previous field's minimum into the new field, capping the new field's ghost font slightly small. The downstream minimumGhostFontSize floor bounds how small it can get, so the failure mode is "ghost text a touch smaller than ideal" rather than the current "ghost text doubles." Net trade is strongly positive.
  • focusChangeSequence is unchanged; other consumers that depend on it (notably VisualContextCoordinator) are unaffected.

Greptile Summary

Fixes ghost-text doubling in self-resizing chat composers (Slack, iMessage) by keying GhostFontSizeStabilizer on a new focusedInputIdentityKey — derived from (bundleIdentifier, processIdentifier, elementIdentifier) — instead of focusChangeSequence, which was inadvertently reset whenever a growing input field changed its frame rect.

  • FocusedInputContext gains a computed focusedInputIdentityKey: UInt64 that omits the input frame from its hash, so the key is stable across field growth but distinct across genuine field switches.
  • SuggestionOverlayGeometry carries the new key as a stored property (defaulting to 0 for tests), and SuggestionCoordinator+Acceptance.swift threads it through to the geometry at construction time.
  • Note: the class-level doc comment inside GhostFontSizeStabilizer.swift (not in this diff) still describes the session key as "FocusTracker's focusChangeSequence" — it should be updated to reference focusedInputIdentityKey to stay accurate.

Confidence Score: 4/5

The change is targeted and well-reasoned; the only loose end is a stale doc comment.

The logic change is confined to a single call site in showInline, the new identity key correctly excludes the frame rect that was causing spurious session resets, and the fallback behavior (a slightly-too-small ghost font on hash collision) is clearly bounded. The doc comment on focusChangeSequence in SuggestionOverlayGeometry now incorrectly claims OverlayController keys stabilization on it, which could mislead future maintainers. The parallel stale comment in GhostFontSizeStabilizer.swift (outside the diff) compounds this slightly.

The focusChangeSequence doc comment in SuggestionModels.swift needs updating; GhostFontSizeStabilizer.swift (not in this diff) has a matching stale comment worth cleaning up in a follow-up.

Important Files Changed

Filename Overview
Cotabby/Models/SuggestionModels.swift Adds focusedInputIdentityKey computed property on FocusedInputContext (hashing bundle ID, PID, element ID) and a matching stored property on SuggestionOverlayGeometry; doc comment on the existing focusChangeSequence field is now stale.
Cotabby/Services/UI/OverlayController.swift Single-line change in showInline: passes focusedInputIdentityKey instead of focusChangeSequence as the focusSessionKey to the ghost-font stabilizer; change is isolated and well-commented.
Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift Mechanical plumbing change: passes the new focusedInputIdentityKey from FocusedInputContext into SuggestionOverlayGeometry; no logic change.

Sequence Diagram

sequenceDiagram
    participant FT as FocusTracker
    participant FIC as FocusedInputContext
    participant SC as SuggestionCoordinator
    participant SOG as SuggestionOverlayGeometry
    participant OC as OverlayController
    participant GFSS as GhostFontSizeStabilizer

    FT->>FIC: snapshot (bumps focusChangeSequence on frame change)
    FIC->>FIC: compute focusedInputIdentityKey\n(bundleId + pid + elementId, no frame)
    FIC->>SC: context with focusedInputIdentityKey
    SC->>SOG: build geometry(focusChangeSequence, focusedInputIdentityKey)
    SOG->>OC: geometry
    OC->>GFSS: stabilizedCaretHeight(caretHeight, focusSessionKey: focusedInputIdentityKey)
    GFSS-->>OC: clamped height (session min survives field growth)
    OC->>OC: resolvedGhostFontSize → render inline ghost text
Loading

Comments Outside Diff (1)

  1. Cotabby/Models/SuggestionModels.swift, line 445-448 (link)

    P2 The focusChangeSequence doc comment still says "OverlayController keys its per-session font-size stabilization on this value" — that claim is now false. After this PR, OverlayController drives stabilization from focusedInputIdentityKey; focusChangeSequence is consumed by other paths (e.g., VisualContextCoordinator). Leaving the stale claim will mislead the next reader about which key actually governs ghost-font session scoping.

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

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Reviews (1): Last reviewed commit: "Key ghost font stabilizer on field ident..." | Re-trigger Greptile

@FuJacob FuJacob merged commit efa86ee into main May 30, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant