Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
cda19c5
Add browser omnibar stale-focus regression test
lawrencecchen Mar 19, 2026
2a11cfe
Clear stale browser omnibar focus on web view focus
lawrencecchen Mar 19, 2026
70221f4
Add browser arrow key XCUITest after Cmd+L
lawrencecchen Mar 21, 2026
d1a1d91
Fix test-e2e workflow shell quoting
lawrencecchen Mar 21, 2026
0a1342b
Stabilize browser arrow key XCUITest setup
lawrencecchen Mar 21, 2026
e52458a
Broaden UI test cmux CLI discovery
lawrencecchen Mar 21, 2026
c1a5dab
Use browser web area as XCUITest click target
lawrencecchen Mar 21, 2026
04c0ac5
Wait for browser readiness before DOM harness injection
lawrencecchen Mar 21, 2026
ec96bdc
Stabilize browser keybind UI test launch
lawrencecchen Mar 21, 2026
0fb8150
Log browser UI test launch diagnostics
lawrencecchen Mar 21, 2026
ddb0159
Retry UI test startup activation
lawrencecchen Mar 21, 2026
ccce76a
Harden UI test startup activation
lawrencecchen Mar 21, 2026
b75a69c
Retry alternate UI test CLI paths
lawrencecchen Mar 21, 2026
6a479d1
Use control socket for browser arrow XCUITest
lawrencecchen Mar 21, 2026
aeddd83
Allow arrow XCUITest socket access
lawrencecchen Mar 21, 2026
4003142
Wait longer for browser socket replies
lawrencecchen Mar 21, 2026
d59f6e8
Poll browser eval for DOM readiness
lawrencecchen Mar 21, 2026
4029cf0
Add browser arrow omnibar XCUITest path
lawrencecchen Mar 21, 2026
d04bbf4
Retry browser arrow UI test page-input setup
lawrencecchen Mar 21, 2026
0c409de
Use raw HID arrow keys in browser XCUITest
lawrencecchen Mar 21, 2026
b3cb3b5
Route browser XCUITest arrows via simulate_shortcut
lawrencecchen Mar 21, 2026
b936e9c
Use in-process socket for browser XCUITest shortcuts
lawrencecchen Mar 21, 2026
73b29cd
Use raw socket shortcut command in browser XCUITest
lawrencecchen Mar 21, 2026
7f23a1e
Allow browser XCUITests to use the control socket
lawrencecchen Mar 21, 2026
0c92a89
Use notifications for browser XCUITest shortcuts
lawrencecchen Mar 21, 2026
de529f3
Use native key events in browser XCUITest
lawrencecchen Mar 21, 2026
5b805b0
Record real primary click for browser XCUITest
lawrencecchen Mar 21, 2026
61822eb
Record browser responder shape in arrow XCUITest
lawrencecchen Mar 21, 2026
b902e4d
Route browser arrows before AppKit key handling
lawrencecchen Mar 21, 2026
393da6c
Record omnibar focus after actual focus
lawrencecchen Mar 21, 2026
e286479
Generalize browser key routing
lawrencecchen Mar 22, 2026
a0b1e9c
Stabilize browser arrow UI test
lawrencecchen Mar 22, 2026
bbdcf5e
Add contenteditable browser arrow regression test
lawrencecchen Mar 22, 2026
106dc36
Stabilize browser contenteditable XCUITest launch
lawrencecchen Mar 23, 2026
e03ac68
Keep browser XCUITest socket override with launch tag
lawrencecchen Mar 23, 2026
0fd8c45
Wait for contenteditable fixture prerequisites
lawrencecchen Mar 23, 2026
23d793b
Capture browser eval failures in contenteditable XCUITest
lawrencecchen Mar 23, 2026
010edb8
Move contenteditable arrow XCUITest to app-side harness
lawrencecchen Mar 23, 2026
94c59af
Merge remote-tracking branch 'origin/main' into task-browser-up-down-…
lawrencecchen Mar 24, 2026
d0d4298
Add browser textarea and omnibar arrow regressions
lawrencecchen Mar 24, 2026
eca313f
Clear stale omnibar focus on browser clicks
lawrencecchen Mar 24, 2026
cbe375c
Route browser content keys around performKeyEquivalent
lawrencecchen Mar 24, 2026
e259eae
Preserve webview click blur intent for omnibar
lawrencecchen Mar 24, 2026
233a91c
Add browser webview click focus regressions
lawrencecchen Mar 24, 2026
c45e7ea
Use authoritative omnibar state for webview click handoff
lawrencecchen Mar 24, 2026
499729f
Fix browser arrow XCUITest counter baselines
lawrencecchen Mar 24, 2026
6d4d57f
Merge remote-tracking branch 'origin/main' into task-browser-up-down-…
lawrencecchen Mar 24, 2026
07b36b9
Fix modified browser arrow XCUITest expectations
lawrencecchen Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,110 changes: 952 additions & 158 deletions Sources/AppDelegate.swift

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions Sources/Panels/BrowserPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4908,6 +4908,28 @@ extension BrowserPanel {
#endif
}

func syncWebViewFirstResponderPolicy(isPanelFocused: Bool, reason: String) {
guard let cmuxWebView = webView as? CmuxWebView else { return }
let next = isPanelFocused && !shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy.resync panel=\(id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) reason=\(reason) " +
"panelFocusedUsed=\(isPanelFocused ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
}

func prepareForExplicitWebViewFocus(isPanelFocused: Bool, reason: String) {
endSuppressWebViewFocusForAddressBar()
clearWebViewFocusSuppression()
syncWebViewFirstResponderPolicy(isPanelFocused: isPanelFocused, reason: reason)
}

func shouldSuppressOmnibarAutofocus() -> Bool {
if let until = suppressOmnibarAutofocusUntil {
return Date() < until
Expand Down
141 changes: 103 additions & 38 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -510,17 +510,54 @@ struct BrowserPanelView: View {
guard let webView = note.object as? CmuxWebView else { return false }
return webView === panel?.webView
}) { _ in
let shouldDismissOmnibar = browserWebViewClickShouldDismissOmnibar(
localAddressBarFocused: addressBarFocused,
focusedAddressBarPanelId: AppDelegate.shared?.focusedBrowserAddressBarPanelId(),
pendingAddressBarFocusRequestId: panel.pendingAddressBarFocusRequestId,
panelId: panel.id
)
let shouldPromoteWebViewFocus = browserWebViewClickShouldPromoteWebViewFocus(
isPanelFocused: isFocused,
shouldDismissOmnibar: shouldDismissOmnibar
)
#if DEBUG
dlog(
"browser.focus.clickIntent panel=\(panel.id.uuidString.prefix(5)) " +
"isFocused=\(isFocused ? 1 : 0) " +
"addressFocused=\(addressBarFocused ? 1 : 0)"
"addressFocused=\(addressBarFocused ? 1 : 0) " +
"dismissOmnibar=\(shouldDismissOmnibar ? 1 : 0) " +
"promoteWeb=\(shouldPromoteWebViewFocus ? 1 : 0)"
)
#endif
if addressBarFocused {
if shouldDismissOmnibar {
#if DEBUG
logBrowserFocusState(event: "addressBarFocus.webViewClickBlur")
#endif
NotificationCenter.default.post(
name: .browserWillBlurAddressBarForWebViewClick,
object: panel.id
)
}
if shouldPromoteWebViewFocus {
panel.prepareForExplicitWebViewFocus(
isPanelFocused: true,
reason: "webView.clickIntent"
)
if let window = panel.webView.window,
!panel.webView.isHiddenOrHasHiddenAncestor {
let focusedWebView = window.makeFirstResponder(panel.webView)
if focusedWebView {
panel.noteWebViewFocused()
}
#if DEBUG
logBrowserFocusState(
event: "addressBarFocus.webViewClickFocus",
detail: "focusedWebView=\(focusedWebView ? 1 : 0)"
)
#endif
}
}
if shouldDismissOmnibar {
setAddressBarFocused(false, reason: "webView.clickIntent")
}
if !isFocused {
Expand Down Expand Up @@ -1033,6 +1070,7 @@ struct BrowserPanelView: View {
}

OmnibarTextFieldRepresentable(
panelId: panel.id,
text: Binding(
get: { omnibarState.buffer },
set: { newValue in
Expand Down Expand Up @@ -1239,20 +1277,8 @@ struct BrowserPanelView: View {
reason: String,
isPanelFocusedOverride: Bool? = nil
) {
guard let cmuxWebView = panel.webView as? CmuxWebView else { return }
let isPanelFocused = isPanelFocusedOverride ?? isFocused
let next = isPanelFocused && !panel.shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy.resync panel=\(panel.id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) reason=\(reason) " +
"panelFocusedUsed=\(isPanelFocused ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
panel.syncWebViewFirstResponderPolicy(isPanelFocused: isPanelFocused, reason: reason)
}

private func setAddressBarFocused(_ focused: Bool, reason: String) {
Expand Down Expand Up @@ -3181,9 +3207,28 @@ struct OmnibarSuggestion: Identifiable, Hashable {

func browserOmnibarShouldReacquireFocusAfterEndEditing(
desiredOmnibarFocus: Bool,
nextResponderIsOtherTextField: Bool
nextResponderIsOtherTextField: Bool,
explicitPointerBlurIntent: Bool = false
) -> Bool {
desiredOmnibarFocus && !nextResponderIsOtherTextField
desiredOmnibarFocus && !nextResponderIsOtherTextField && !explicitPointerBlurIntent
}

func browserWebViewClickShouldDismissOmnibar(
localAddressBarFocused: Bool,
focusedAddressBarPanelId: UUID?,
pendingAddressBarFocusRequestId: UUID?,
panelId: UUID
) -> Bool {
localAddressBarFocused ||
focusedAddressBarPanelId == panelId ||
pendingAddressBarFocusRequestId != nil
}

func browserWebViewClickShouldPromoteWebViewFocus(
isPanelFocused: Bool,
shouldDismissOmnibar: Bool
) -> Bool {
isPanelFocused || shouldDismissOmnibar
}

private final class OmnibarNativeTextField: NSTextField {
Expand Down Expand Up @@ -3349,6 +3394,7 @@ private final class OmnibarNativeTextField: NSTextField {
}

private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
let panelId: UUID
@Binding var text: String
@Binding var isFocused: Bool
let inlineCompletion: OmnibarInlineCompletion?
Expand All @@ -3368,15 +3414,33 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
var parent: OmnibarTextFieldRepresentable
var isProgrammaticMutation: Bool = false
var selectionObserver: NSObjectProtocol?
var explicitPointerBlurObserver: NSObjectProtocol?
weak var observedEditor: NSTextView?
var appliedInlineCompletion: OmnibarInlineCompletion?
var lastPublishedSelection: NSRange = NSRange(location: NSNotFound, length: 0)
var lastPublishedHasMarkedText: Bool = false
/// Guards against infinite focus loops: `true` = focus requested, `false` = blur requested, `nil` = idle.
var pendingFocusRequest: Bool?
var pendingExplicitPointerBlurIntent: Bool = false

init(parent: OmnibarTextFieldRepresentable) {
self.parent = parent
super.init()
explicitPointerBlurObserver = NotificationCenter.default.addObserver(
forName: .browserWillBlurAddressBarForWebViewClick,
object: nil,
queue: .main
) { [weak self] notification in
guard let self,
let panelId = notification.object as? UUID,
panelId == self.parent.panelId else {
return
}
self.pendingExplicitPointerBlurIntent = true
#if DEBUG
self.logFocusEvent("webViewClickBlurIntent")
#endif
}
}

#if DEBUG
Expand Down Expand Up @@ -3412,6 +3476,9 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
if let selectionObserver {
NotificationCenter.default.removeObserver(selectionObserver)
}
if let explicitPointerBlurObserver {
NotificationCenter.default.removeObserver(explicitPointerBlurObserver)
}
}

private func nextResponderIsOtherTextField(window: NSWindow?) -> Bool {
Expand Down Expand Up @@ -3483,19 +3550,18 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
}

private func shouldReacquireFocusAfterEndEditing(window: NSWindow?) -> Bool {
if pointerDownBlurIntent(window: window) {
return false
}
return browserOmnibarShouldReacquireFocusAfterEndEditing(
desiredOmnibarFocus: parent.isFocused,
nextResponderIsOtherTextField: nextResponderIsOtherTextField(window: window)
nextResponderIsOtherTextField: nextResponderIsOtherTextField(window: window),
explicitPointerBlurIntent: pointerDownBlurIntent(window: window) || pendingExplicitPointerBlurIntent
)
}

func controlTextDidBeginEditing(_ obj: Notification) {
#if DEBUG
logFocusEvent("controlTextDidBeginEditing")
#endif
pendingExplicitPointerBlurIntent = false
if !parent.isFocused {
DispatchQueue.main.async {
#if DEBUG
Expand All @@ -3509,16 +3575,23 @@ private struct OmnibarTextFieldRepresentable: NSViewRepresentable {
}

func controlTextDidEndEditing(_ obj: Notification) {
#if DEBUG
let explicitPointerBlurIntent =
pointerDownBlurIntent(window: parentField?.window) || pendingExplicitPointerBlurIntent
pendingExplicitPointerBlurIntent = false
let nextOther = nextResponderIsOtherTextField(window: parentField?.window)
let pointerBlur = pointerDownBlurIntent(window: parentField?.window)
let shouldReacquire = browserOmnibarShouldReacquireFocusAfterEndEditing(
desiredOmnibarFocus: parent.isFocused,
nextResponderIsOtherTextField: nextOther,
explicitPointerBlurIntent: explicitPointerBlurIntent
)
#if DEBUG
logFocusEvent(
"controlTextDidEndEditing",
detail: "nextOther=\(nextOther ? 1 : 0) pointerBlur=\(pointerBlur ? 1 : 0) shouldReacquire=\(shouldReacquireFocusAfterEndEditing(window: parentField?.window) ? 1 : 0)"
detail: "nextOther=\(nextOther ? 1 : 0) pointerBlur=\(explicitPointerBlurIntent ? 1 : 0) shouldReacquire=\(shouldReacquire ? 1 : 0)"
)
#endif
if parent.isFocused {
if shouldReacquireFocusAfterEndEditing(window: parentField?.window) {
if shouldReacquire {
#if DEBUG
logFocusEvent("controlTextDidEndEditing.reacquire.begin")
#endif
Expand Down Expand Up @@ -6336,19 +6409,11 @@ struct WebViewRepresentable: NSViewRepresentable {
webView: WKWebView,
isPanelFocused: Bool
) {
guard let cmuxWebView = webView as? CmuxWebView else { return }
let next = isPanelFocused && !panel.shouldSuppressWebViewFocus()
if cmuxWebView.allowsFirstResponderAcquisition != next {
#if DEBUG
dlog(
"browser.focus.policy panel=\(panel.id.uuidString.prefix(5)) " +
"web=\(ObjectIdentifier(cmuxWebView)) old=\(cmuxWebView.allowsFirstResponderAcquisition ? 1 : 0) " +
"new=\(next ? 1 : 0) isPanelFocused=\(isPanelFocused ? 1 : 0) " +
"suppress=\(panel.shouldSuppressWebViewFocus() ? 1 : 0)"
)
#endif
}
cmuxWebView.allowsFirstResponderAcquisition = next
guard webView is CmuxWebView else { return }
panel.syncWebViewFirstResponderPolicy(
isPanelFocused: isPanelFocused,
reason: "applyWebViewFirstResponderPolicy"
)
}

static func dismantleNSView(_ nsView: NSView, coordinator: Coordinator) {
Expand Down
1 change: 1 addition & 0 deletions Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5182,6 +5182,7 @@ extension Notification.Name {
static let browserDidExitAddressBar = Notification.Name("browserDidExitAddressBar")
static let browserDidFocusAddressBar = Notification.Name("browserDidFocusAddressBar")
static let browserDidBlurAddressBar = Notification.Name("browserDidBlurAddressBar")
static let browserWillBlurAddressBarForWebViewClick = Notification.Name("browserWillBlurAddressBarForWebViewClick")
static let webViewDidReceiveClick = Notification.Name("webViewDidReceiveClick")
static let terminalPortalVisibilityDidChange = Notification.Name("cmux.terminalPortalVisibilityDidChange")
static let browserPortalRegistryDidChange = Notification.Name("cmux.browserPortalRegistryDidChange")
Expand Down
20 changes: 14 additions & 6 deletions Sources/TerminalController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8635,6 +8635,12 @@ class TerminalController {
tabManager.selectWorkspace(ws)
}

browserPanel.prepareForExplicitWebViewFocus(
isPanelFocused: true,
reason: "socket.browser.focus_webview"
)
NotificationCenter.default.post(name: .browserDidBlurAddressBar, object: surfaceId)

// Prevent omnibar auto-focus from immediately stealing first responder back.
browserPanel.suppressOmnibarAutofocus(for: 1.0)

Expand Down Expand Up @@ -11804,19 +11810,19 @@ class TerminalController {
case "left":
storedKey = "←"
keyCode = 123
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSLeftArrowFunctionKey)!)
case "right":
storedKey = "→"
keyCode = 124
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSRightArrowFunctionKey)!)
case "down":
storedKey = "↓"
keyCode = 125
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSDownArrowFunctionKey)!)
case "up":
storedKey = "↑"
keyCode = 126
charactersIgnoringModifiers = storedKey
charactersIgnoringModifiers = String(UnicodeScalar(NSUpArrowFunctionKey)!)
case "enter", "return":
storedKey = "\r"
keyCode = UInt16(kVK_Return)
Expand Down Expand Up @@ -13587,8 +13593,10 @@ class TerminalController {

// Programmatic WebView focus should win over stale omnibar focus state, especially
// after workspace switches where the blank-page omnibar auto-focus can re-trigger.
browserPanel.endSuppressWebViewFocusForAddressBar()
browserPanel.clearWebViewFocusSuppression()
browserPanel.prepareForExplicitWebViewFocus(
isPanelFocused: true,
reason: "socket.focus_webview"
)
NotificationCenter.default.post(name: .browserDidBlurAddressBar, object: panelId)

// Prevent omnibar auto-focus from immediately stealing first responder back.
Expand Down
Loading
Loading