From bedb145ee1413482763707fe73787b622cb52cbf Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 16:08:40 +0800
Subject: [PATCH] Fix Finder tab space activation
---
C1.source/Sources/Aligner/Diagnostics/Round1SnapshotDump.swift | 12
C1.source/Sources/AlignerCore/Windows/FinderTabSpaceAttributionPolicy.swift | 143 +++++++
C3.tools/round1-finder-tabs-live-qa.sh | 452 +++++++++++++++++++++++
C1.source/Resources/Aligner-Info.plist | 4
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 342 +++++++++++++++++
C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift | 156 ++++++++
6 files changed, 1,096 insertions(+), 13 deletions(-)
diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index dd38074..fdeb485 100644
--- a/C1.source/Resources/Aligner-Info.plist
+++ b/C1.source/Resources/Aligner-Info.plist
@@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>0.0.63</string>
+ <string>0.0.64</string>
<key>CFBundleVersion</key>
- <string>20260613.1401</string>
+ <string>20260613.1606</string>
<key>LSMinimumSystemVersion</key>
<string>26.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/Diagnostics/Round1SnapshotDump.swift b/C1.source/Sources/Aligner/Diagnostics/Round1SnapshotDump.swift
index bb70050..c8e3b03 100644
--- a/C1.source/Sources/Aligner/Diagnostics/Round1SnapshotDump.swift
+++ b/C1.source/Sources/Aligner/Diagnostics/Round1SnapshotDump.swift
@@ -89,6 +89,7 @@
"isMinimized": window.isMinimized,
"isFullscreen": window.isFullscreen,
"isGhost": window.isGhost,
+ "frame": frameDictionary(window.frame) as Any,
"spaceIDs": window.spaceIDs,
"app": appDictionary(window.app)
] as [String: Any]
@@ -98,6 +99,17 @@
return result
}
+ private static func frameDictionary(_ frame: CGRect?) -> Any {
+ guard let frame else { return NSNull() }
+
+ return [
+ "x": frame.origin.x,
+ "y": frame.origin.y,
+ "width": frame.size.width,
+ "height": frame.size.height
+ ]
+ }
+
private static func appDictionary(_ app: AlignerApp) -> [String: Any] {
let pathSummary = DevelopmentDiagnostics.pathSummary(app.executablePath)
var result = [
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 87b6c01..b1b1cd3 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -162,9 +162,10 @@
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
+ let currentAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
let axWindow = axWindow(
for: window,
- in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
+ in: currentAXMetadata,
operation: "windowActivation",
matchAttempt: "initial"
)
@@ -188,6 +189,20 @@
"spaceFocusError": spaceFocusOutcome?.error
])
guard let axWindow else {
+ if let finderTabActivated = activateFinderTabPageIfNeeded(
+ window: window,
+ axMetadata: currentAXMetadata,
+ operation: "windowActivation",
+ reason: "axMissing"
+ ) {
+ DevelopmentDiagnostics.log("windowActivation.activate.finderTabResult", [
+ "windowID": window.id,
+ "pid": window.app.processIdentifier,
+ "activated": finderTabActivated
+ ])
+ return finderTabActivated ? .activated : .activationFailed
+ }
+
let activated = activateApplication(for: window)
DevelopmentDiagnostics.log("windowActivation.activate.axMissing", [
"windowID": window.id,
@@ -393,15 +408,31 @@
return nil
}
- guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
- activationOutcome.succeeded
- else {
- DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
- "windowID": window.id,
- "pid": processIdentifier,
- "reason": reason
- ])
- return nil
+ if let finderTabActivated = activateFinderTabPageIfNeeded(
+ window: window,
+ axMetadata: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
+ operation: "windowClose",
+ reason: reason
+ ) {
+ guard finderTabActivated else {
+ DevelopmentDiagnostics.log("windowClose.commandW.finderTabActivationFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .failed("finderTabActivationFailed")
+ }
+ } else {
+ guard let activationOutcome = activateViaPrivateWindowServerAPI(window),
+ activationOutcome.succeeded
+ else {
+ DevelopmentDiagnostics.log("windowClose.commandW.activationFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return nil
+ }
}
Thread.sleep(forTimeInterval: 0.08)
@@ -564,6 +595,271 @@
&& metadata.subrole == .standard
}
return finderCandidates.count == 1 ? finderCandidates[0] : nil
+ }
+
+ private func activateFinderTabPageIfNeeded(
+ window: AlignerWindow,
+ axMetadata: [AXWindowMetadata],
+ operation: String,
+ reason: String
+ ) -> Bool? {
+ guard isFinderCGOnlyPageCandidate(window),
+ let processIdentifier = window.app.processIdentifier
+ else {
+ return nil
+ }
+
+ guard let host = finderTabHostAXWindow(for: window, in: axMetadata) else {
+ DevelopmentDiagnostics.log("\(operation).finderTab.hostMissing", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason,
+ "spaceIDs": window.spaceIDs
+ ])
+ return false
+ }
+
+ let hostPrivateActivationSucceeded: Bool
+ if let hostWindowID = host.windowID,
+ let privateActivationBridge {
+ hostPrivateActivationSucceeded = privateActivationBridge.activate(
+ processIdentifier: processIdentifier,
+ windowID: hostWindowID
+ ).succeeded
+ } else {
+ hostPrivateActivationSucceeded = false
+ }
+
+ let applicationActivated = activateApplication(processIdentifier: processIdentifier)
+ let focusBeforeResult = focusWindowResult(host.element, processIdentifier: processIdentifier)
+ let raiseBeforeResult = raiseWindowResult(host.element)
+ let selectedTab = selectFinderTab(window, in: host)
+ Thread.sleep(forTimeInterval: 0.05)
+ let focusAfterResult = focusWindowResult(host.element, processIdentifier: processIdentifier)
+ let raiseAfterResult = raiseWindowResult(host.element)
+
+ let didFocusOrRaise = focusBeforeResult.success
+ || raiseBeforeResult.success
+ || focusAfterResult.success
+ || raiseAfterResult.success
+ let activated = selectedTab
+ && (hostPrivateActivationSucceeded || applicationActivated || didFocusOrRaise)
+
+ DevelopmentDiagnostics.log("\(operation).finderTab.result", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason,
+ "hostWindowID": host.windowID,
+ "hostTitleHash": DevelopmentDiagnostics.stableFingerprint(host.title),
+ "hostTitleLength": host.title?.count,
+ "hostPrivateActivated": hostPrivateActivationSucceeded,
+ "applicationActivated": applicationActivated,
+ "selectedTab": selectedTab,
+ "focusBeforeAXError": String(describing: focusBeforeResult.error),
+ "raiseBeforeAXError": String(describing: raiseBeforeResult.error),
+ "focusAfterAXError": String(describing: focusAfterResult.error),
+ "raiseAfterAXError": String(describing: raiseAfterResult.error),
+ "activated": activated
+ ])
+
+ return activated
+ }
+
+ private func isFinderCGOnlyPageCandidate(_ window: AlignerWindow) -> Bool {
+ window.identifierSource == .cgWindow
+ && window.app.bundleIdentifier == "com.apple.finder"
+ && !window.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ }
+
+ private func finderTabHostAXWindow(
+ for window: AlignerWindow,
+ in axMetadata: [AXWindowMetadata]
+ ) -> AXWindowMetadata? {
+ guard let processIdentifier = window.app.processIdentifier else { return nil }
+ let candidates = axMetadata.filter { metadata in
+ metadata.processIdentifier == processIdentifier
+ && metadata.subrole == .standard
+ && metadata.frame?.isEmpty == false
+ }
+ guard !candidates.isEmpty else { return nil }
+
+ let targetSpaceIDs = Set(window.spaceIDs)
+ let candidateIDs = candidates.compactMap(\.windowID)
+ let candidateSpacesByID = targetSpaceIDs.isEmpty ? [:] : spaceIDsByWindowIDProvider(candidateIDs)
+ let spaceCompatibleCandidates = targetSpaceIDs.isEmpty
+ ? candidates
+ : candidates.filter { metadata in
+ guard let windowID = metadata.windowID else { return false }
+ return !Set(candidateSpacesByID[windowID] ?? []).isDisjoint(with: targetSpaceIDs)
+ }
+ let hostCandidates = spaceCompatibleCandidates.isEmpty ? candidates : spaceCompatibleCandidates
+ let targetFrame = cgWindowFrame(for: window) ?? window.frame
+ let scoredHosts = hostCandidates.compactMap { metadata -> (metadata: AXWindowMetadata, score: CGFloat)? in
+ guard let score = frameOverlapScore(targetFrame, metadata.frame),
+ score >= 0.85
+ else {
+ return nil
+ }
+
+ return (metadata, score)
+ }
+ .sorted { lhs, rhs in
+ if lhs.score != rhs.score {
+ return lhs.score > rhs.score
+ }
+ return (lhs.metadata.windowID ?? UInt32.max) < (rhs.metadata.windowID ?? UInt32.max)
+ }
+
+ if let best = scoredHosts.first {
+ if scoredHosts.count == 1 {
+ return best.metadata
+ }
+
+ let secondBest = scoredHosts[1]
+ if best.score - secondBest.score >= 0.05 {
+ return best.metadata
+ }
+ }
+
+ return hostCandidates.count == 1 ? hostCandidates[0] : nil
+ }
+
+ private func selectFinderTab(
+ _ window: AlignerWindow,
+ in host: AXWindowMetadata
+ ) -> Bool {
+ let normalizedTargetTitle = normalizedWindowTitle(window.title)
+ guard !normalizedTargetTitle.isEmpty else { return false }
+
+ if normalizedWindowTitle(host.title ?? "") == normalizedTargetTitle {
+ return true
+ }
+
+ guard let tabButton = finderTabButton(
+ matchingNormalizedTitle: normalizedTargetTitle,
+ in: host.element
+ ) else {
+ DevelopmentDiagnostics.log("windowActivation.finderTab.tabButtonMissing", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count
+ ])
+ return false
+ }
+
+ let tabButtonRole = axStringAttribute(kAXRoleAttribute as String, for: tabButton)
+ let tabButtonSubrole = axStringAttribute(kAXSubroleAttribute as String, for: tabButton)
+ let pressResult = AXUIElementPerformAction(tabButton, kAXPressAction as CFString)
+ DevelopmentDiagnostics.log("windowActivation.finderTab.pressTab", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "tabButtonRole": tabButtonRole,
+ "tabButtonSubrole": tabButtonSubrole,
+ "axError": String(describing: pressResult)
+ ])
+ guard pressResult == .success else { return false }
+
+ for _ in 0..<10 {
+ Thread.sleep(forTimeInterval: 0.04)
+ let selectedTitle = normalizedWindowTitle(
+ axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? ""
+ )
+ if selectedTitle == normalizedTargetTitle {
+ return true
+ }
+ }
+
+ let currentHostTitle = axStringAttribute(kAXTitleAttribute as String, for: host.element) ?? ""
+ DevelopmentDiagnostics.log("windowActivation.finderTab.selectionVerificationFailed", [
+ "windowID": window.id,
+ "hostWindowID": host.windowID,
+ "targetTitleHash": DevelopmentDiagnostics.stableFingerprint(window.title),
+ "targetTitleLength": window.title.count,
+ "currentHostTitleHash": DevelopmentDiagnostics.stableFingerprint(currentHostTitle),
+ "currentHostTitleLength": currentHostTitle.count
+ ])
+ return false
+ }
+
+ private func finderTabButton(
+ matchingNormalizedTitle targetTitle: String,
+ in root: AXUIElement
+ ) -> AXUIElement? {
+ var visitedCount = 0
+
+ func search(_ element: AXUIElement, depth: Int, insideTabGroup: Bool) -> AXUIElement? {
+ visitedCount += 1
+ guard visitedCount <= 240, depth <= 9 else { return nil }
+
+ let role = axStringAttribute(kAXRoleAttribute as String, for: element)
+ let subrole = axStringAttribute(kAXSubroleAttribute as String, for: element)
+ let title = axStringAttribute(kAXTitleAttribute as String, for: element)
+ ?? axStringAttribute(kAXDescriptionAttribute as String, for: element)
+ ?? axStringAttribute(kAXValueAttribute as String, for: element)
+ let isTabGroup = role == "AXTabGroup"
+ let isTabControl = subrole == "AXTabButton"
+ || (insideTabGroup && (role == kAXRadioButtonRole as String || role == kAXButtonRole as String))
+
+ if normalizedWindowTitle(title ?? "") == targetTitle,
+ isTabControl {
+ return element
+ }
+
+ guard isTabGroup || insideTabGroup || depth < 7 else { return nil }
+ for child in axChildren(of: element) {
+ if let match = search(child, depth: depth + 1, insideTabGroup: insideTabGroup || isTabGroup) {
+ return match
+ }
+ }
+
+ return nil
+ }
+
+ return search(root, depth: 0, insideTabGroup: false)
+ }
+
+ private func axChildren(of element: AXUIElement) -> [AXUIElement] {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) == .success,
+ let children = value as? [AXUIElement]
+ else {
+ return []
+ }
+
+ return children
+ }
+
+ private func axStringAttribute(_ attribute: String, for element: AXUIElement) -> String? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let value
+ else {
+ return nil
+ }
+
+ return value as? String
+ }
+
+ private func frameOverlapScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
+ guard let lhs,
+ let rhs,
+ !lhs.isEmpty,
+ !rhs.isEmpty
+ else {
+ return nil
+ }
+
+ let intersection = lhs.intersection(rhs)
+ guard !intersection.isNull,
+ !intersection.isEmpty
+ else {
+ return nil
+ }
+
+ let denominator = min(lhs.width * lhs.height, rhs.width * rhs.height)
+ guard denominator > 0 else { return nil }
+ return (intersection.width * intersection.height) / denominator
}
private func cgWindowFingerprint(for window: AlignerWindow) -> AXWindowFingerprint? {
@@ -895,7 +1191,31 @@
}
restorableAXWindowsByID = nextRestorableAXWindowsByID
- return cgRecords + axOnlyRecords
+ let records = cgRecords + axOnlyRecords
+ let attributedRecords = FinderTabSpaceAttributionPolicy.attributedRecords(records)
+ DevelopmentDiagnostics.log("windowEnumeration.finderTabSpaceAttribution", [
+ "finderNoSpaceCandidateCount": records.filter { record in
+ record.app.bundleIdentifier == "com.apple.finder"
+ && record.identifierSource == .cgWindow
+ && record.spaceIDs.isEmpty
+ && !record.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ && record.frame?.isEmpty == false
+ }.count,
+ "finderFullscreenHostCount": records.filter { record in
+ record.app.bundleIdentifier == "com.apple.finder"
+ && record.isFullscreen
+ && !record.spaceIDs.isEmpty
+ && record.subrole == .standard
+ && record.frame?.isEmpty == false
+ }.count,
+ "attributedCount": zip(records, attributedRecords).filter { before, after in
+ before.spaceIDs.isEmpty && !after.spaceIDs.isEmpty
+ }.count,
+ "attributedWindowIDs": zip(records, attributedRecords).compactMap { before, after in
+ before.spaceIDs.isEmpty && !after.spaceIDs.isEmpty ? after.id : nil
+ }
+ ])
+ return attributedRecords
}
private func app(
diff --git a/C1.source/Sources/AlignerCore/Windows/FinderTabSpaceAttributionPolicy.swift b/C1.source/Sources/AlignerCore/Windows/FinderTabSpaceAttributionPolicy.swift
new file mode 100644
index 0000000..0de281d
--- /dev/null
+++ b/C1.source/Sources/AlignerCore/Windows/FinderTabSpaceAttributionPolicy.swift
@@ -0,0 +1,143 @@
+import CoreGraphics
+import Foundation
+
+public enum FinderTabSpaceAttributionPolicy {
+ private static let finderBundleIdentifier = "com.apple.finder"
+ private static let minimumOverlapRatio: CGFloat = 0.85
+ private static let minimumBestScoreGap: CGFloat = 0.05
+
+ public static func attributedRecords(_ records: [WindowEnumerationRecord]) -> [WindowEnumerationRecord] {
+ let hosts = records.filter(isFinderFullscreenHost)
+ guard !hosts.isEmpty else { return records }
+
+ return records.map { record in
+ guard isFinderTabCandidate(record),
+ let host = bestHost(for: record, in: hosts)
+ else {
+ return record
+ }
+
+ return copy(
+ record,
+ isFullscreen: true,
+ spaceIDs: host.spaceIDs
+ )
+ }
+ }
+
+ private static func isFinderTabCandidate(_ record: WindowEnumerationRecord) -> Bool {
+ WindowEnumerationPolicy.shouldInclude(record)
+ && record.identifierSource == .cgWindow
+ && record.app.bundleIdentifier == finderBundleIdentifier
+ && record.spaceIDs.isEmpty
+ && !record.isMinimized
+ && !record.title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ && record.frame?.isEmpty == false
+ }
+
+ private static func isFinderFullscreenHost(_ record: WindowEnumerationRecord) -> Bool {
+ WindowEnumerationPolicy.shouldInclude(record)
+ && record.app.bundleIdentifier == finderBundleIdentifier
+ && record.isFullscreen
+ && !record.spaceIDs.isEmpty
+ && record.subrole == .standard
+ && record.frame?.isEmpty == false
+ }
+
+ private static func bestHost(
+ for candidate: WindowEnumerationRecord,
+ in hosts: [WindowEnumerationRecord]
+ ) -> WindowEnumerationRecord? {
+ let compatibleHosts = hosts.filter { host in
+ sameProcess(candidate, host)
+ }
+ let scoredHosts = compatibleHosts.compactMap { host -> (record: WindowEnumerationRecord, score: CGFloat)? in
+ guard let score = overlapScore(candidate.frame, host.frame),
+ score >= minimumOverlapRatio
+ else {
+ return nil
+ }
+
+ return (host, score)
+ }
+ .sorted { lhs, rhs in
+ if lhs.score != rhs.score {
+ return lhs.score > rhs.score
+ }
+ return lhs.record.id < rhs.record.id
+ }
+
+ guard let best = scoredHosts.first else { return nil }
+ if scoredHosts.count == 1 {
+ return best.record
+ }
+
+ let sameSpaceCompetitors = scoredHosts.filter { $0.record.spaceIDs == best.record.spaceIDs }
+ if sameSpaceCompetitors.count == scoredHosts.count {
+ return best.record
+ }
+
+ let secondBest = scoredHosts[1]
+ return best.score - secondBest.score >= minimumBestScoreGap ? best.record : nil
+ }
+
+ private static func sameProcess(
+ _ lhs: WindowEnumerationRecord,
+ _ rhs: WindowEnumerationRecord
+ ) -> Bool {
+ guard let lhsPID = lhs.app.processIdentifier,
+ let rhsPID = rhs.app.processIdentifier
+ else {
+ return false
+ }
+
+ return lhsPID == rhsPID
+ }
+
+ private static func overlapScore(_ lhs: CGRect?, _ rhs: CGRect?) -> CGFloat? {
+ guard let lhs,
+ let rhs,
+ !lhs.isEmpty,
+ !rhs.isEmpty
+ else {
+ return nil
+ }
+
+ let intersection = lhs.intersection(rhs)
+ guard !intersection.isNull,
+ !intersection.isEmpty
+ else {
+ return nil
+ }
+
+ let denominator = min(lhs.width * lhs.height, rhs.width * rhs.height)
+ guard denominator > 0 else { return nil }
+ return (intersection.width * intersection.height) / denominator
+ }
+
+ private static func copy(
+ _ record: WindowEnumerationRecord,
+ isFullscreen: Bool,
+ spaceIDs: [UInt64]
+ ) -> WindowEnumerationRecord {
+ WindowEnumerationRecord(
+ id: record.id,
+ app: record.app,
+ activationPolicy: record.activationPolicy,
+ title: record.title,
+ identifierSource: record.identifierSource,
+ size: record.size,
+ frame: record.frame,
+ subrole: record.subrole,
+ isMinimized: record.isMinimized,
+ isFullscreen: isFullscreen,
+ isGhost: record.isGhost,
+ isDesktopElement: record.isDesktopElement,
+ isSystemCritical: record.isSystemCritical,
+ isInteractable: record.isInteractable,
+ hasAXBacking: record.hasAXBacking,
+ isOnscreen: record.isOnscreen,
+ spaceIDs: spaceIDs
+ )
+ }
+}
diff --git a/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift b/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
index b83efb7..042e94c 100644
--- a/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
+++ b/C1.source/Tests/AlignerCoreTests/AlignerCoreTests.swift
@@ -377,6 +377,162 @@
XCTAssertNil(WindowEnumerationPolicy.window(from: tinyRecord))
}
+ func testFinderTabSpaceAttributionInheritsFullscreenHostSpaceForMatchingFrame() {
+ let finderApp = AlignerApp(
+ bundleIdentifier: "com.apple.finder",
+ name: "访达",
+ category: .finder,
+ processIdentifier: 74678
+ )
+ let frame = CGRect(x: 0, y: 0, width: 960, height: 1080)
+ let host = WindowEnumerationRecord(
+ id: 33112,
+ app: finderApp,
+ title: "下载",
+ size: frame.size,
+ frame: frame,
+ subrole: .standard,
+ isFullscreen: true,
+ hasAXBacking: true,
+ spaceIDs: [843]
+ )
+ let inactiveTab = WindowEnumerationRecord(
+ id: 42286,
+ app: finderApp,
+ title: "Appcache",
+ size: frame.size,
+ frame: frame,
+ subrole: .standard,
+ hasAXBacking: false,
+ isOnscreen: false
+ )
+
+ let attributed = FinderTabSpaceAttributionPolicy.attributedRecords([inactiveTab, host])
+
+ XCTAssertEqual(attributed[0].spaceIDs, [843])
+ XCTAssertTrue(attributed[0].isFullscreen)
+ }
+
+ func testFinderTabSpaceAttributionDoesNotGuessNoSpaceWindowsWithoutHostOverlap() {
+ let finderApp = AlignerApp(
+ bundleIdentifier: "com.apple.finder",
+ name: "访达",
+ category: .finder,
+ processIdentifier: 74678
+ )
+ let otherApp = AlignerApp(
+ bundleIdentifier: "com.example.Other",
+ name: "Other",
+ category: .generic,
+ processIdentifier: 74678
+ )
+ let hostFrame = CGRect(x: 0, y: 0, width: 960, height: 1080)
+ let farFrame = CGRect(x: 1200, y: 0, width: 500, height: 600)
+ let host = WindowEnumerationRecord(
+ id: 33112,
+ app: finderApp,
+ title: "下载",
+ size: hostFrame.size,
+ frame: hostFrame,
+ subrole: .standard,
+ isFullscreen: true,
+ hasAXBacking: true,
+ spaceIDs: [843]
+ )
+ let farFinderTab = WindowEnumerationRecord(
+ id: 42286,
+ app: finderApp,
+ title: "Appcache",
+ size: farFrame.size,
+ frame: farFrame,
+ subrole: .standard,
+ hasAXBacking: false,
+ isOnscreen: false
+ )
+ let nonFinderWindow = WindowEnumerationRecord(
+ id: 50222,
+ app: otherApp,
+ title: "Appcache",
+ size: hostFrame.size,
+ frame: hostFrame,
+ subrole: .standard,
+ hasAXBacking: false,
+ isOnscreen: false
+ )
+
+ let attributed = FinderTabSpaceAttributionPolicy.attributedRecords([farFinderTab, nonFinderWindow, host])
+
+ XCTAssertEqual(attributed[0].spaceIDs, [])
+ XCTAssertFalse(attributed[0].isFullscreen)
+ XCTAssertEqual(attributed[1].spaceIDs, [])
+ XCTAssertFalse(attributed[1].isFullscreen)
+ }
+
+ func testFinderTabSpaceAttributionHandlesFinderSplitViewHostsByFrame() {
+ let finderApp = AlignerApp(
+ bundleIdentifier: "com.apple.finder",
+ name: "访达",
+ category: .finder,
+ processIdentifier: 74678
+ )
+ let leftFrame = CGRect(x: 0, y: 0, width: 960, height: 1080)
+ let rightFrame = CGRect(x: 960, y: 0, width: 960, height: 1080)
+ let leftHost = WindowEnumerationRecord(
+ id: 33112,
+ app: finderApp,
+ title: "下载",
+ size: leftFrame.size,
+ frame: leftFrame,
+ subrole: .standard,
+ isFullscreen: true,
+ hasAXBacking: true,
+ spaceIDs: [843]
+ )
+ let rightHost = WindowEnumerationRecord(
+ id: 41557,
+ app: finderApp,
+ title: "应用程序",
+ size: rightFrame.size,
+ frame: rightFrame,
+ subrole: .standard,
+ isFullscreen: true,
+ hasAXBacking: true,
+ spaceIDs: [843]
+ )
+ let leftInactiveTab = WindowEnumerationRecord(
+ id: 42286,
+ app: finderApp,
+ title: "Appcache",
+ size: leftFrame.size,
+ frame: leftFrame,
+ subrole: .standard,
+ hasAXBacking: false,
+ isOnscreen: false
+ )
+ let rightInactiveTab = WindowEnumerationRecord(
+ id: 61385,
+ app: finderApp,
+ title: "build",
+ size: rightFrame.size,
+ frame: rightFrame,
+ subrole: .standard,
+ hasAXBacking: false,
+ isOnscreen: false
+ )
+
+ let attributed = FinderTabSpaceAttributionPolicy.attributedRecords([
+ leftInactiveTab,
+ rightInactiveTab,
+ leftHost,
+ rightHost
+ ])
+
+ XCTAssertEqual(attributed[0].spaceIDs, [843])
+ XCTAssertEqual(attributed[1].spaceIDs, [843])
+ XCTAssertTrue(attributed[0].isFullscreen)
+ XCTAssertTrue(attributed[1].isFullscreen)
+ }
+
func testWindowEnumerationPolicyKeepsMinimizedAndFullscreenCGOnlyOffscreenWindows() {
let minimized = makeWindowRecord(
title: "Minimized Document.md",
diff --git a/C3.tools/round1-finder-tabs-live-qa.sh b/C3.tools/round1-finder-tabs-live-qa.sh
new file mode 100755
index 0000000..99fc856
--- /dev/null
+++ b/C3.tools/round1-finder-tabs-live-qa.sh
@@ -0,0 +1,452 @@
+#!/bin/bash
+# Round01 real-desktop QA for Finder tabs/pages. It verifies that CG-only
+# Finder pages inherit the high-confidence fullscreen Space from their visible
+# host, and that clicking such a card actually selects the matching Finder tab
+# instead of only reporting a WindowServer activation success.
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+# shellcheck source=build-output-paths.sh
+source "$SCRIPT_DIR/build-output-paths.sh"
+APP="$BUILD_CURRENT_APP"
+RUN_ID="$(date +%Y%m%d_%H%M%S)"
+REPORT_DIR="$BUILD_REPORT_ROOT/round01-finder-tabs-live-$RUN_ID"
+DEV_LOG="$HOME/Library/Logs/Aligner/aligner-dev.log"
+REPORT_WAIT="${ALIGNER_ROUND1_FINDER_TABS_REPORT_WAIT:-7.0}"
+
+fail() {
+ echo "Round01 Finder tabs live QA failed: $*" >&2
+ exit 1
+}
+
+aligner_pids_for_current_app() {
+ ps -axo pid=,args= | while read -r pid command; do
+ case "$command" in
+ "$APP/Contents/MacOS/Aligner"*) echo "$pid" ;;
+ esac
+ done
+}
+
+stop_current_aligner() {
+ for pid in $(aligner_pids_for_current_app); do
+ kill "$pid" 2>/dev/null || true
+ done
+
+ for _ in {1..30}; do
+ [ -z "$(aligner_pids_for_current_app)" ] && return
+ sleep 0.1
+ done
+
+ fail "current Aligner app did not exit before QA"
+}
+
+dev_log_size() {
+ if [ -f "$DEV_LOG" ]; then
+ stat -f%z "$DEV_LOG"
+ else
+ echo 0
+ fi
+}
+
+extract_dev_log_since() {
+ local offset="$1"
+ local output="$2"
+ if [ ! -f "$DEV_LOG" ]; then
+ : >"$output"
+ return
+ fi
+
+ dd if="$DEV_LOG" bs=1 skip="$offset" 2>/dev/null >"$output" || : >"$output"
+}
+
+wait_for_report() {
+ local report="$1"
+ local mode="$2"
+ /usr/bin/python3 - "$report" "$REPORT_WAIT" "$mode" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+mode = sys.argv[3]
+deadline = time.monotonic() + timeout
+last_report = None
+
+def ready(report):
+ if report.get("snapshotLoaded") is not True:
+ return False
+ if mode == "loaded":
+ return True
+ if mode == "activated":
+ return (
+ report.get("quickSwitchVisible") is False
+ and report.get("lastActivationResult") is not None
+ )
+ return False
+
+while time.monotonic() < deadline:
+ try:
+ with open(path, "r", encoding="utf-8") as file:
+ report = json.load(file)
+ last_report = report
+ if ready(report):
+ sys.exit(0)
+ except FileNotFoundError:
+ pass
+ except json.JSONDecodeError:
+ pass
+ time.sleep(0.2)
+
+if last_report is not None:
+ print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
+print(f"Finder tabs report {path} did not reach mode={mode} within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+run_snapshot_dump() {
+ local snapshot="$1"
+ local stderr_log="$2"
+ stop_current_aligner
+ ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE=1 \
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-dump-window-snapshot \
+ --round01-dump-window-snapshot-pretty >"$snapshot" 2>"$stderr_log"
+}
+
+run_report() {
+ local report="$1"
+ local app_log="$2"
+ stop_current_aligner
+ rm -f "$report"
+ ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE=1 \
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-open-quick-switch \
+ --round01-disable-screenshot-refresh \
+ --round01-quick-switch-auto-hide-after=0.8 \
+ --round01-quick-switch-quit-after=1.3 \
+ --round01-quick-switch-report="$report" >"$app_log" 2>&1
+ wait_for_report "$report" "loaded"
+}
+
+run_click() {
+ local app_group_index="$1"
+ local window_index="$2"
+ local report="$3"
+ local app_log="$4"
+ stop_current_aligner
+ rm -f "$report"
+ ALIGNER_DIAGNOSTICS_INCLUDE_SENSITIVE=1 \
+ "$APP/Contents/MacOS/Aligner" \
+ --round0-skip-permissions \
+ --round01-open-quick-switch \
+ --round01-disable-screenshot-refresh \
+ --round01-debug-mouse-sequence="click-card:$app_group_index:$window_index" \
+ --round01-quick-switch-quit-after=1.8 \
+ --round01-quick-switch-report="$report" >"$app_log" 2>&1
+ wait_for_report "$report" "activated"
+}
+
+focused_finder_window_report() {
+ local output="$1"
+ swift - "$output" <<'SWIFT'
+import AppKit
+import ApplicationServices
+import Foundation
+
+func fail(_ message: String, code: Int32 = 2) -> Never {
+ fputs(message + "\n", stderr)
+ exit(code)
+}
+
+func jsonString(_ value: String) -> String {
+ let data = try! JSONSerialization.data(withJSONObject: ["value": value], options: [])
+ let text = String(data: data, encoding: .utf8)!
+ let prefix = "{\"value\":\""
+ let suffix = "\"}"
+ return String(text.dropFirst(prefix.count).dropLast(suffix.count))
+}
+
+func stableFingerprint(_ text: String) -> String {
+ var hash: UInt64 = 0xcbf29ce484222325
+ for byte in text.utf8 {
+ hash ^= UInt64(byte)
+ hash = hash &* 0x100000001b3
+ }
+ return String(format: "%016llx", hash)
+}
+
+func title(of element: AXUIElement) -> String? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &value) == .success else {
+ return nil
+ }
+ return value as? String
+}
+
+func focusedWindowID(_ element: AXUIElement) -> UInt32? {
+ var value: CFTypeRef?
+ if AXUIElementCopyAttributeValue(element, "AXWindowNumber" as CFString, &value) == .success,
+ let number = value as? NSNumber {
+ return number.uint32Value
+ }
+ return nil
+}
+
+let outputPath = CommandLine.arguments[1]
+guard let finder = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == "com.apple.finder" }) else {
+ fail("Finder is not running")
+}
+
+let appElement = AXUIElementCreateApplication(finder.processIdentifier)
+var windowValue: CFTypeRef?
+var axError = AXUIElementCopyAttributeValue(appElement, kAXFocusedWindowAttribute as CFString, &windowValue)
+if axError != .success || windowValue == nil || CFGetTypeID(windowValue!) != AXUIElementGetTypeID() {
+ axError = AXUIElementCopyAttributeValue(appElement, kAXMainWindowAttribute as CFString, &windowValue)
+}
+guard axError == .success,
+ let rawWindow = windowValue,
+ CFGetTypeID(rawWindow) == AXUIElementGetTypeID()
+else {
+ fail("Finder focused/main window is unavailable: \(axError)")
+}
+
+let window = rawWindow as! AXUIElement
+let windowTitle = title(of: window) ?? ""
+let output = """
+{
+ "bundleIdentifier": "com.apple.finder",
+ "pid": \(finder.processIdentifier),
+ "focusedWindowID": \(focusedWindowID(window).map(String.init) ?? "null"),
+ "title": "\(jsonString(windowTitle))",
+ "titleLength": \(windowTitle.count),
+ "titleHash": "\(stableFingerprint(windowTitle))"
+}
+"""
+try output.write(toFile: outputPath, atomically: true, encoding: .utf8)
+SWIFT
+}
+
+extract_attributed_ids() {
+ local log_file="$1"
+ /usr/bin/python3 - "$log_file" <<'PY'
+import re
+import sys
+
+ids = []
+for line in open(sys.argv[1], "r", encoding="utf-8", errors="replace"):
+ if "event=windowEnumeration.finderTabSpaceAttribution" not in line:
+ continue
+ match = re.search(r'attributedWindowIDs="?(\[[^\]"]*\])"?', line)
+ if not match:
+ continue
+ ids = [int(value) for value in re.findall(r"\d+", match.group(1))]
+
+if not ids:
+ print("No attributed Finder tab/page windowIDs found in this QA run", file=sys.stderr)
+ sys.exit(3)
+
+print(",".join(str(value) for value in sorted(set(ids))))
+PY
+}
+
+select_target() {
+ local snapshot="$1"
+ local report="$2"
+ local attributed_ids_csv="$3"
+ local output="$4"
+ /usr/bin/python3 - "$snapshot" "$report" "$attributed_ids_csv" "$output" <<'PY'
+import json
+import sys
+
+snapshot_path, report_path, ids_csv, output_path = sys.argv[1:5]
+attributed_ids = {int(value) for value in ids_csv.split(",") if value}
+with open(snapshot_path, "r", encoding="utf-8") as file:
+ snapshot = json.load(file)
+with open(report_path, "r", encoding="utf-8") as file:
+ report = json.load(file)
+
+def require(condition, message):
+ if not condition:
+ print(message, file=sys.stderr)
+ sys.exit(4)
+
+snapshot_windows = {window.get("id"): window for window in snapshot.get("windows", [])}
+finder_attributed = [
+ window for window_id, window in snapshot_windows.items()
+ if window_id in attributed_ids
+ and window.get("app", {}).get("bundleIdentifier") == "com.apple.finder"
+ and window.get("identifierSource") == "cgWindow"
+ and window.get("spaceIDs")
+ and window.get("title")
+]
+require(finder_attributed, "snapshot must contain at least one attributed Finder CG-only tab/page")
+
+root = report.get("rootView", {})
+cards = []
+for column in root.get("waterfallColumns", []):
+ if column.get("bundleIdentifier") != "com.apple.finder":
+ continue
+ for card in column.get("cards", []):
+ if card.get("windowID") not in attributed_ids:
+ continue
+ visible = card.get("visibleFrame") or {}
+ if visible.get("width", 0) <= 1 or visible.get("height", 0) <= 1:
+ continue
+ title = card.get("title") or snapshot_windows.get(card.get("windowID"), {}).get("title") or ""
+ if not title.strip():
+ continue
+ cards.append((column, card, title))
+
+require(cards, "Quick Switch report must expose at least one visible attributed Finder tab/page card")
+
+title_counts = {}
+for _, _, title in cards:
+ title_counts[title] = title_counts.get(title, 0) + 1
+
+cards.sort(key=lambda item: (
+ 0 if title_counts[item[2]] == 1 else 1,
+ item[1].get("globalIndex", 10**9),
+ item[1].get("windowID", 10**9),
+))
+column, card, title = cards[0]
+target = {
+ "windowID": card["windowID"],
+ "appGroupIndex": column["appGroupIndex"],
+ "windowIndex": card["windowIndex"],
+ "primarySpaceID": card.get("primarySpaceID"),
+ "title": title,
+ "titleLength": len(title),
+ "titleHash": card.get("titleHash") or snapshot_windows.get(card.get("windowID"), {}).get("titleHash"),
+ "visibleFrame": card.get("visibleFrame"),
+}
+
+with open(output_path, "w", encoding="utf-8") as file:
+ json.dump(target, file, indent=2, ensure_ascii=False)
+
+print(f"TARGET_WINDOW_ID={target['windowID']}")
+print(f"APP_GROUP_INDEX={target['appGroupIndex']}")
+print(f"WINDOW_INDEX={target['windowIndex']}")
+print(f"TARGET_SPACE_ID={target['primarySpaceID']}")
+PY
+}
+
+assert_click_report() {
+ local report="$1"
+ local target="$2"
+ /usr/bin/python3 - "$report" "$target" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+ report = json.load(file)
+with open(sys.argv[2], "r", encoding="utf-8") as file:
+ target = json.load(file)
+
+def require(condition, message):
+ if not condition:
+ print(message, file=sys.stderr)
+ print(json.dumps({
+ "targetWindowID": target.get("windowID"),
+ "lastCommittedWindowID": report.get("lastCommittedWindowID"),
+ "lastActivationWindowID": report.get("lastActivationWindowID"),
+ "lastActivationResult": report.get("lastActivationResult"),
+ "lastActivationError": report.get("lastActivationError"),
+ "quickSwitchVisible": report.get("quickSwitchVisible"),
+ }, indent=2, ensure_ascii=False), file=sys.stderr)
+ sys.exit(5)
+
+require(report.get("lastCommittedWindowID") == target["windowID"], "commit must target the clicked Finder card")
+require(report.get("lastActivationWindowID") == target["windowID"], "activation must receive exact Finder tab/page windowID")
+require(report.get("lastActivationResult") == "activated", "activation result must be activated")
+require(report.get("quickSwitchVisible") is False, "Quick Switch must close after click activation")
+PY
+}
+
+assert_focused_finder_title() {
+ local target="$1"
+ local focused="$2"
+ /usr/bin/python3 - "$target" "$focused" <<'PY'
+import json
+import sys
+
+with open(sys.argv[1], "r", encoding="utf-8") as file:
+ target = json.load(file)
+with open(sys.argv[2], "r", encoding="utf-8") as file:
+ focused = json.load(file)
+
+target_title = target.get("title", "").strip()
+focused_title = focused.get("title", "").strip()
+if target_title != focused_title:
+ print("Focused Finder window title must match clicked tab/page title", file=sys.stderr)
+ print(json.dumps({
+ "targetWindowID": target.get("windowID"),
+ "targetTitleHash": target.get("titleHash"),
+ "targetTitleLength": target.get("titleLength"),
+ "focusedWindowID": focused.get("focusedWindowID"),
+ "focusedTitleHash": focused.get("titleHash"),
+ "focusedTitleLength": focused.get("titleLength"),
+ }, indent=2, ensure_ascii=False), file=sys.stderr)
+ sys.exit(6)
+
+print(json.dumps({
+ "case": "finderTabRealActivation",
+ "targetWindowID": target.get("windowID"),
+ "targetSpaceID": target.get("primarySpaceID"),
+ "targetTitleHash": target.get("titleHash"),
+ "targetTitleLength": target.get("titleLength"),
+ "focusedWindowID": focused.get("focusedWindowID"),
+ "focusedTitleHash": focused.get("titleHash"),
+ "focusedTitleLength": focused.get("titleLength"),
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+mkdir -p "$REPORT_DIR"
+stop_current_aligner
+"$SCRIPT_DIR/package-app.sh" >&2
+
+SNAPSHOT_JSON="$REPORT_DIR/snapshot.json"
+SNAPSHOT_STDERR="$REPORT_DIR/snapshot.stderr.log"
+SNAPSHOT_DEV_LOG="$REPORT_DIR/snapshot-dev.log"
+INITIAL_REPORT="$REPORT_DIR/initial-report.json"
+INITIAL_APP_LOG="$REPORT_DIR/initial-app.log"
+CLICK_REPORT="$REPORT_DIR/click-report.json"
+CLICK_APP_LOG="$REPORT_DIR/click-app.log"
+CLICK_DEV_LOG="$REPORT_DIR/click-dev.log"
+TARGET_JSON="$REPORT_DIR/target.json"
+FOCUSED_JSON="$REPORT_DIR/focused-finder.json"
+
+SNAPSHOT_LOG_OFFSET="$(dev_log_size)"
+run_snapshot_dump "$SNAPSHOT_JSON" "$SNAPSHOT_STDERR"
+sleep 0.4
+extract_dev_log_since "$SNAPSHOT_LOG_OFFSET" "$SNAPSHOT_DEV_LOG"
+ATTRIBUTED_IDS="$(extract_attributed_ids "$SNAPSHOT_DEV_LOG")"
+
+run_report "$INITIAL_REPORT" "$INITIAL_APP_LOG"
+eval "$(select_target "$SNAPSHOT_JSON" "$INITIAL_REPORT" "$ATTRIBUTED_IDS" "$TARGET_JSON")"
+
+CLICK_LOG_OFFSET="$(dev_log_size)"
+run_click "$APP_GROUP_INDEX" "$WINDOW_INDEX" "$CLICK_REPORT" "$CLICK_APP_LOG"
+sleep 0.8
+extract_dev_log_since "$CLICK_LOG_OFFSET" "$CLICK_DEV_LOG"
+focused_finder_window_report "$FOCUSED_JSON"
+
+assert_click_report "$CLICK_REPORT" "$TARGET_JSON"
+assert_focused_finder_title "$TARGET_JSON" "$FOCUSED_JSON"
+
+stop_current_aligner
+
+cat <<EOF
+Round01 Finder tabs live QA passed
+Log directory: $REPORT_DIR
+Covered:
+- Finder CG-only tab/page records inherit a high-confidence fullscreen Space.
+- Quick Switch click targets the exact Finder tab/page windowID.
+- Finder focused window title matches the clicked card after activation.
+EOF
--
Gitblit v1.9.3