Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 28 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ jobs:

tests-build-and-lag:
# Build the full cmux scheme and run the lag regression on WarpBuild.
# XCUITests cannot run on WarpBuild (Virtualization.framework limitation:
# XCUIApplication stuck "Running Background", 62s activation timeout per
# test). Interactive UI tests run via test-e2e.yml on GitHub-hosted runners.
# Keep lag validation separate from UI regressions so functional UI failures
# and performance regressions stay isolated. Broader interactive UI suites
# still run via test-e2e.yml on GitHub-hosted runners.
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 20
Expand Down Expand Up @@ -350,6 +350,7 @@ jobs:
VDISPLAY_PID=$!
echo "VDISPLAY_PID=$VDISPLAY_PID" >> "$GITHUB_ENV"
sleep 3
kill -0 "$VDISPLAY_PID"

- name: Run workspace churn typing-lag regression
run: |
Expand Down Expand Up @@ -393,7 +394,16 @@ jobs:
CMUX_LAG_KEY_EVENTS=180 \
python3 tests/test_workspace_churn_up_arrow_lag.py

ui-display-resolution-regression:
- name: Cleanup virtual display
if: always()
run: |
set -euo pipefail
if [ -n "${VDISPLAY_PID:-}" ]; then
kill "$VDISPLAY_PID" >/dev/null 2>&1 || true
fi
rm -f /tmp/create-virtual-display

ui-regressions:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: warp-macos-15-arm64-6x
timeout-minutes: 25
Expand Down Expand Up @@ -445,8 +455,8 @@ jobs:
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
with:
path: .ci-source-packages
key: spm-ui-display-resolution-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-ui-display-resolution-
key: spm-ui-regressions-${{ hashFiles('GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-ui-regressions-

- name: Resolve Swift packages
run: |
Expand Down Expand Up @@ -491,3 +501,15 @@ jobs:
-destination "platform=macOS" \
-only-testing:cmuxUITests/DisplayResolutionRegressionUITests \
test

- name: Run browser find focus UI regression
run: |
set -euo pipefail
SOURCE_PACKAGES_DIR="$PWD/.ci-source-packages"
xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \
-clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \
-disableAutomaticPackageResolution \
-destination "platform=macOS" \
-maximum-test-execution-time-allowance 120 \
-only-testing:cmuxUITests/BrowserPaneNavigationKeybindUITests/testCmdFFocusesBrowserFindFieldAfterCmdDCmdLNavigation \
test
4 changes: 1 addition & 3 deletions .github/workflows/test-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,7 @@ jobs:
clang -framework Foundation -framework CoreGraphics \
-o "$HELPER_PATH" scripts/create-virtual-display.m

cat >"$MANIFEST_PATH" <<EOF
{"helperBinaryPath":"$HELPER_PATH"}
EOF
printf '%s\n' "{\"helperBinaryPath\":\"$HELPER_PATH\"}" > "$MANIFEST_PATH"
fi

# Start recording right before the test (after build/resolve).
Expand Down
7 changes: 7 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12463,6 +12463,13 @@ private extension NSWindow {
in window: NSWindow,
event: NSEvent?
) -> CmuxWebView? {
// Browser find runs in the portal slot alongside the hosted WKWebView.
// Treat its native field editor chain as browser chrome, not as web content,
// so Cmd+F can move first responder into the find field while web focus is suppressed.
if BrowserWindowPortalRegistry.searchOverlayPanelId(for: responder, in: window) != nil {
Comment thread
lawrencecchen marked this conversation as resolved.
return nil
Comment on lines +12469 to +12470

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate search-overlay lookup to field-editor responders

This new early check runs before any responder-type filtering, so every cmuxOwningWebView(for:in:event:) call now performs a portal-wide searchOverlayPanelId scan even when the first responder is unrelated (e.g., terminal/web view). Because this helper is called from cmux_performKeyEquivalent on key events, the change adds avoidable O(number of browser slots) work to a typing hot path and can regress input latency in sessions with many panes; limit the lookup to field-editor responders (the only case that needs special-casing).

Useful? React with 👍 / 👎.

}

if let webView = cmuxOwningWebView(for: responder) {
return webView
}
Expand Down
35 changes: 30 additions & 5 deletions Sources/BrowserWindowPortal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1777,13 +1777,38 @@ final class WindowBrowserSlotView: NSView {
logSearchOverlayEvent("create", panelId: configuration.panelId)
}

private func searchOverlayOwnsFieldEditor(_ fieldEditor: NSTextView, in root: NSView) -> Bool {
guard fieldEditor.isFieldEditor else { return false }

if let textField = root as? NSTextField, textField.currentEditor() === fieldEditor {
return true
}

for subview in root.subviews {
if searchOverlayOwnsFieldEditor(fieldEditor, in: subview) {
return true
}
}

return false
}

func searchOverlayPanelId(for responder: NSResponder) -> UUID? {
guard let overlay = searchOverlayHostingView,
let view = responder.browserPortalOwningView,
view.isDescendant(of: overlay) else {
return nil
guard let overlay = searchOverlayHostingView else { return nil }

let panelId = objc_getAssociatedObject(overlay, &cmuxBrowserSearchOverlayPanelIdAssociationKey) as? UUID

if let view = responder as? NSView,
view === overlay || view.isDescendant(of: overlay) {
return panelId
}
return objc_getAssociatedObject(overlay, &cmuxBrowserSearchOverlayPanelIdAssociationKey) as? UUID

if let fieldEditor = responder as? NSTextView,
searchOverlayOwnsFieldEditor(fieldEditor, in: overlay) {
return panelId
}

return nil
}

@discardableResult
Expand Down
Loading
Loading