Skip to content

fix(ai-chat): let the composer's focus highlight be switched off, and draw a focus ring when it is - #3010

Merged
datlechin merged 3 commits into
mainfrom
fix/2995-composer-highlight
Sep 20, 2026
Merged

datlechin merged 3 commits into
mainfrom
fix/2995-composer-highlight

Conversation

@datlechin

Copy link
Copy Markdown
Member

Fixes #2995.

What the reporter asked for

Right now, every input for AI ask your database has fancy, colorful input select. It is distracting for me. I wish I could at least switch that effect off in settings.

The switch ships as a context menu item on the composer, Highlight When Focused, rather than a Settings row, so it sits where the annoyance is. The highlight itself is unchanged and stays the default.

Why the "off" state needed more than deleting the highlight

Measured on macOS 27 with a compiled swiftc probe in a real key window, counting drawFocusRingMask calls:

focusRingType mask called while the window is key
unset (what ships today) 0
.exterior on the NSTextView 0
.exterior on the NSScrollView 1

So the composer drew no native focus ring at all, and the highlight was the only thing marking the field as focused. A toggle that merely removed it would have left a text field with nothing to say it holds the keyboard.

The off state therefore adopts the system focus ring, using the same three overrides ShortcutRecorderNSView already uses (ShortcutRecorderView.swift:59-67): focusRingType = .exterior, drawFocusRingMask(), focusRingMaskBounds. Per the table, the ring has to sit on the scroll view, not the text view. A second probe confirmed no ancestor in the real SwiftUI hosting tree clips it (masksToBounds is false from the scroll view up to NSThemeFrame), so the exterior ring is not cut off.

ComposerHighlightPreference is the single answer to "which affordance", so the two can never both paint and never both go missing. ChatComposerMetrics.cornerRadius gives the SwiftUI shape and the ring mask one owner, the way QuickSwitcherMetrics.cornerRadius already does for the Quick Switcher panel.

Two bugs in the highlight, fixed while it stays

Both measured twice with probes. Both made the highlight appear when the field was not focused, which is the same complaint the issue is about.

  • It painted on a window that is not key. resignFirstResponder is never sent when the window resigns key, so the SwiftUI isFocused mirror latched true and the full-strength highlight kept painting on an inactive window, where macOS withdraws every other focus affordance. It now also requires controlActiveState == .key.
  • It survived a pane switch. resignFirstResponder is not sent on unparenting either: measured, after removeFromSuperview the window's first responder has moved away but no resign was delivered. viewDidMoveToWindow now re-reads focus from the window instead of waiting for a callback that never comes. This is the hazard class CLAUDE.md's "Appearance is not lifetime" invariant covers.

The highlight now also steps aside for Reduce Transparency and Increase Contrast, which is what SolidSurfacePreference already does for every other translucent surface in the app. Reduce Motion is deliberately not a gate: the gradient is static (stops is written once and never mutated), and the only motion is the crossfade, which moved off a raw .animation onto the app's own motionAnimation gate.

Folded in: the composer had no accessible name

setAccessibilityPlaceholderValue sat behind if textView.placeholder != placeholder, but makeNSView had already stored the same string, so the comparison was never true again and the call has not run since #2097. The placeholder is painted in draw(_:) and never reaches the accessibility tree, so VoiceOver announced an unlabelled text area. Moving the call into the property's didSet (with its own equality guard, so it does not fire per keystroke) means no assignment path can leave the field nameless.

Included because this change is about the composer's focus affordances, and shipping those while the field stays nameless to VoiceOver would be half a fix.

Verification

All through .claude/skills/fix-issue/scripts/verify.sh.

  • generate PASS, build PASS
  • test PASS: 36 executed, 36 passed, 0 failed over ComposerHighlightPreferenceTests, ChatComposerScrollViewTests, ChatComposerTextViewAccessibilityTests, AISettingsTests, AppSettingsManagerMigrationTests
  • lint 0 violations
  • docs PASS
  • The new tests were mutation-checked: reverting focusRingType to always .none and dropping the accessibility call each turn their suites red, so they assert rather than decorate.

No UI automation. The composer only renders when an AI provider is configured (AIChatPanelView.swift:54 gates the whole input area on hasConfiguredProvider) and the UI-test sandbox has none, so there is no deterministic way to reach it from TableProUITests.

Tests ran in a separate worktree: a concurrent session had the Kafka plugin mid-refactor in the shared checkout, and TableProTests compiles plugin sources, so their in-progress edit broke the test target's build. Nothing in this branch touches Plugins/.

Screenshots are pending. The "before" state was captured and matches the reporter's screenshot exactly. The machine's screen locked partway through the session, so the "after" states (highlight off showing the system focus ring, and the context menu itself) could not be captured. They need a run on an unlocked screen before merge.

Why not remove the highlight outright

Recorded because the evidence points that way and the decision went the other way deliberately. Apple's Intelligence glow means an assistant is working, not this field is focused: Siri's glow appears "when Siri is active", and nm over Xcode.app/Contents/PlugIns/IDEIntelligenceChat.framework puts every glow symbol on UVBanner, none on its prompt input. The HIG's Focus and selection page says to rely on system-provided focus effects and to use a focus ring for a text field. There is also no public glow API: grep -ci glow over the macOS 27 SDK's SwiftUI interface returns 0. Keeping the highlight as the default and making it optional was chosen over removing it.

@mintlify

mintlify Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 20, 2026, 12:12 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit bef1ed5 into main Sep 20, 2026
5 checks passed
@datlechin
datlechin deleted the fix/2995-composer-highlight branch September 20, 2026 12:13

This branch was successfully deployed

1 active deployment
staging - docs — bf84dcac Deployed Sep 20, 2026 by mintlify[bot]
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.

Optional simple input for "Ask about database"

1 participant