Skip to content

fix: exclude spaceless windows from narrowed Space scopes - #192

Open
nikolaskaralis wants to merge 1 commit into
rokartur:mainfrom
nikolaskaralis:fix/spaceless-native-tabs-clean
Open

nikolaskaralis wants to merge 1 commit into
rokartur:mainfrom
nikolaskaralis:fix/spaceless-native-tabs-clean

Conversation

@nikolaskaralis

Copy link
Copy Markdown

Summary

Prevent confirmed-spaceless windows from appearing as ordinary switcher rows when the Space scope is narrowed to Current Space or Visible Spaces.

Native AppKit background tabs (e.g. TextEdit / Preview) can remain real NSWindows while tabbed away, but WindowServer reports them as belonging to no Space. Previously, filterToAllowedSpaces kept these rows because it only rejected windows positively resolved to another Space.

If one of these background-tab windows is then selected by BetterCmdTab, activating it can cause macOS to materialize it on the currently visible Space.

The filter now drops confirmed-spaceless ordinary rows under a narrowed Space scope.

isTabSibling rows are deliberately exempt so "Expand native tabs as windows" continues to work as requested.

Reproduction

  1. Create native TextEdit tab groups on different Spaces.
  2. Set BetterCmdTab to:
    • Show windows from: Current Space
    • Expand native tabs as windows: Off
  3. Switch repeatedly between apps / Spaces with BetterCmdTab.
  4. A tabbed-away TextEdit window can unexpectedly appear on the current Space.

Testing

Manual A/B isolation:

  • resolver change only: bug still reproduced
  • resolver + activation guard: bug still reproduced
  • resolver + activation guard + Space filter: fixed
  • resolver + Space filter: fixed
  • Space filter only: fixed

Regression test:

  • without the fix: [10, 20, 30] is retained, incorrectly keeping the ordinary spaceless row
  • with the fix: [10, 30], while the explicit native-tab sibling remains available

Copilot AI lite review requested due to automatic review settings August 24, 2026 12:30

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f475d994e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

//
// Explicit expanded-tab rows remain eligible because the user asked
// to surface native tabs individually.
if spaces.confirmedSpaceless.contains(row.cgWindowID), !row.isTabSibling {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep Stage Manager windows in narrowed scopes

When Stage Manager is enabled and at least one catalog window resolves normally, WindowServer can report real off-stage titled windows as confirmedSpaceless; WindowEnumerator.resolveTabStacks intentionally leaves those windows as ordinary, non-isTabSibling rows, and the existing phantom filter preserves them via its title safeguard. This unconditional check removes them under Current Space or Visible Spaces, undoing the Stage Manager behavior from #122 and hiding real user windows from the switcher. The predicate needs to distinguish collapsed native-tab artifacts without discarding titled Stage Manager windows (and similarly protected real minimized windows).

Useful? React with 👍 / 👎.

Comment on lines +368 to +370
if spaces.confirmedSpaceless.contains(row.cgWindowID), !row.isTabSibling {
dropOffsets.insert(offset)
continue

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 Handle resolutions containing only spaceless windows

When the Space API succeeds but every queried window is reported with zero memberships, allowedSpaces and confirmedSpaceless are populated while spaceByWindow is empty. The existing guard returns before this new branch executes, so a lone or all-spaceless ordinary row is still retained and activating it can cause exactly the unwanted Space materialization this change targets. Availability should be determined from allowedSpaces, not from whether any window happened to resolve to one Space.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The current early-return guard in filterToAllowedSpaces prevents the new confirmed-spaceless filtering from running when spaceByWindow is empty, leaving the targeted spaceless-row leak unfixed in that scenario.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a Space-scoping edge case where WindowServer-reported “confirmed spaceless” NSWindows (notably native AppKit background tabs) could leak into narrowed Space scopes (Current/Visible), and activating them could cause macOS to materialize them onto the current Space.

Changes:

  • Drop confirmed-spaceless ordinary switcher rows when the Space scope is narrowed, while keeping explicit isTabSibling rows eligible.
  • Add a regression test covering ordinary spaceless rows vs tab-sibling rows under a narrowed Space scope.
File summaries
File Description
BetterCmdTab/Catalog/CatalogFilter.swift Updates Space-scope filtering to exclude confirmed-spaceless non-tab-sibling rows when narrowing to allowed Spaces.
BetterCmdTabTests/SpacelessSpaceFilterTests.swift Adds a new test validating that narrowed Space scopes drop ordinary spaceless rows but keep explicit tab-sibling rows.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +52 to +54
#expect(keptIDs == [10, 30])
}
}
Comment on lines 356 to 358
static func filterToAllowedSpaces(_ rows: [SwitcherRow], _ spaces: SpaceResolution) -> [SwitcherRow] {
guard !spaces.allowedSpaces.isEmpty, !spaces.spaceByWindow.isEmpty else { return rows }
var dropOffsets = Set<Int>()
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.

2 participants