From 6983a4ed2137bf1a8377e5d1cf82183e02e617f8 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sat, 13 Jun 2026 14:08:17 +0800
Subject: [PATCH] Fix multi-page targeting and split view tile QA
---
C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift | 227 +++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 205 insertions(+), 22 deletions(-)
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index bba0912..87b6c01 100644
--- a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
+++ b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
@@ -162,16 +162,12 @@
let privateActivationOutcome = activateViaPrivateWindowServerAPI(window)
let didPrivatelyActivate = privateActivationOutcome?.succeeded == true
- let axWindow = AXWindowMetadataReader
- .metadata(appCategorizer: appCategorizer)
- .first { metadata in
- if let windowID = metadata.windowID {
- return windowID == window.id
- }
-
- return metadata.processIdentifier == window.app.processIdentifier
- && metadata.title == window.title
- }
+ let axWindow = axWindow(
+ for: window,
+ in: AXWindowMetadataReader.metadata(appCategorizer: appCategorizer),
+ operation: "windowActivation",
+ matchAttempt: "initial"
+ )
DevelopmentDiagnostics.log("windowActivation.activate.axLookup", [
"windowID": window.id,
@@ -273,6 +269,15 @@
var matchedAXWindow = axWindow(for: window, in: axMetadata)
var finalAXMetadata = axMetadata
+ let shouldPreferFocusedPageClose = matchedAXWindow?.windowID != window.id
+ if shouldPreferFocusedPageClose,
+ let focusedPageCloseResult = closeFocusedPageViaCommandW(
+ window: window,
+ reason: matchedAXWindow == nil ? "axMissing" : "nonDirectAXMatch"
+ ) {
+ return focusedPageCloseResult
+ }
+
if matchedAXWindow == nil {
Thread.sleep(forTimeInterval: 0.12)
finalAXMetadata = AXWindowMetadataReader.metadata(appCategorizer: appCategorizer)
@@ -289,6 +294,14 @@
"matchingSizeCandidateCount": matchingAXSizeCandidateCount(for: window, in: finalAXMetadata)
])
return .windowNotFound
+ }
+
+ if matchedAXWindow.windowID != window.id,
+ let focusedPageCloseResult = closeFocusedPageViaCommandW(
+ window: window,
+ reason: "retryNonDirectAXMatch"
+ ) {
+ return focusedPageCloseResult
}
guard let closeButton = closeButton(for: matchedAXWindow) else {
@@ -364,13 +377,82 @@
}
}
+ private func closeFocusedPageViaCommandW(
+ window: AlignerWindow,
+ reason: String
+ ) -> WindowCloseResult? {
+ guard window.identifierSource == .cgWindow,
+ let processIdentifier = window.app.processIdentifier
+ else {
+ DevelopmentDiagnostics.log("windowClose.commandW.skipped", [
+ "windowID": window.id,
+ "reason": reason,
+ "identifierSource": String(describing: window.identifierSource),
+ "hasPID": window.app.processIdentifier != nil
+ ])
+ return nil
+ }
+
+ 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)
+ guard postCommandW(to: processIdentifier) else {
+ DevelopmentDiagnostics.log("windowClose.commandW.postFailed", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .failed("commandWPostFailed")
+ }
+
+ DevelopmentDiagnostics.log("windowClose.commandW.requested", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "reason": reason
+ ])
+ return .requested
+ }
+
+ private func postCommandW(to processIdentifier: Int32) -> Bool {
+ let source = CGEventSource(stateID: .combinedSessionState)
+ guard let keyDown = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: Self.commandWVirtualKeyCode,
+ keyDown: true
+ ),
+ let keyUp = CGEvent(
+ keyboardEventSource: source,
+ virtualKey: Self.commandWVirtualKeyCode,
+ keyDown: false
+ )
+ else {
+ return false
+ }
+
+ keyDown.flags = .maskCommand
+ keyUp.flags = .maskCommand
+ keyDown.postToPid(processIdentifier)
+ keyUp.postToPid(processIdentifier)
+ return true
+ }
+
private func axWindow(
for window: AlignerWindow,
in axMetadata: [AXWindowMetadata],
+ operation: String = "windowClose",
matchAttempt: String = "initial"
) -> AXWindowMetadata? {
if let directMatch = axMetadata.first(where: { $0.windowID == window.id }) {
- DevelopmentDiagnostics.log("windowClose.axMatch.directWindowID", [
+ DevelopmentDiagnostics.log("\(operation).axMatch.directWindowID", [
"windowID": window.id,
"pid": window.app.processIdentifier,
"attempt": matchAttempt
@@ -382,12 +464,35 @@
return nil
}
+ if let geometryFingerprint = axWindowGeometryFingerprint(for: window) {
+ let geometryMatches = axMetadata.filter { metadata in
+ AXWindowGeometryFingerprint(metadata) == geometryFingerprint
+ }
+ if geometryMatches.count == 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprint", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "attempt": matchAttempt
+ ])
+ return geometryMatches[0]
+ }
+
+ if geometryMatches.count > 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.geometryFingerprintAmbiguous", [
+ "windowID": window.id,
+ "pid": processIdentifier,
+ "candidateCount": geometryMatches.count,
+ "attempt": matchAttempt
+ ])
+ }
+ }
+
if let cgFingerprint = cgWindowFingerprint(for: window) {
let fingerprintMatches = axMetadata.filter { metadata in
AXWindowFingerprint(metadata) == cgFingerprint
}
if fingerprintMatches.count == 1 {
- DevelopmentDiagnostics.log("windowClose.axMatch.fingerprint", [
+ DevelopmentDiagnostics.log("\(operation).axMatch.fingerprint", [
"windowID": window.id,
"pid": processIdentifier,
"attempt": matchAttempt
@@ -396,10 +501,11 @@
}
if fingerprintMatches.count > 1 {
- DevelopmentDiagnostics.log("windowClose.axMatch.fingerprintAmbiguous", [
+ DevelopmentDiagnostics.log("\(operation).axMatch.fingerprintAmbiguous", [
"windowID": window.id,
"pid": processIdentifier,
- "candidateCount": fingerprintMatches.count
+ "candidateCount": fingerprintMatches.count,
+ "attempt": matchAttempt
])
}
}
@@ -409,8 +515,8 @@
metadata.processIdentifier == processIdentifier
&& normalizedWindowTitle(metadata.title ?? "") == normalizedTitle
}
- if titleMatches.count == 1 {
- DevelopmentDiagnostics.log("windowClose.axMatch.normalizedTitle", [
+ if !normalizedTitle.isEmpty, titleMatches.count == 1 {
+ DevelopmentDiagnostics.log("\(operation).axMatch.normalizedTitle", [
"windowID": window.id,
"pid": processIdentifier,
"attempt": matchAttempt
@@ -424,7 +530,7 @@
processIdentifier: processIdentifier,
in: axMetadata
) {
- DevelopmentDiagnostics.log("windowClose.axMatch.finderFallback", [
+ DevelopmentDiagnostics.log("\(operation).axMatch.finderFallback", [
"windowID": window.id,
"pid": processIdentifier,
"attempt": matchAttempt
@@ -433,10 +539,11 @@
}
if titleMatches.count > 1 {
- DevelopmentDiagnostics.log("windowClose.axMatch.titleAmbiguous", [
+ DevelopmentDiagnostics.log("\(operation).axMatch.titleAmbiguous", [
"windowID": window.id,
"pid": processIdentifier,
- "candidateCount": titleMatches.count
+ "candidateCount": titleMatches.count,
+ "attempt": matchAttempt
])
}
@@ -475,6 +582,29 @@
title: normalizedWindowTitle(title(rawWindow: rawWindow, axWindow: nil)),
size: bounds(rawWindow[kCGWindowBounds as String]).size
)
+ }
+
+ private func axWindowGeometryFingerprint(for window: AlignerWindow) -> AXWindowGeometryFingerprint? {
+ guard let processIdentifier = window.app.processIdentifier else { return nil }
+ let frame = cgWindowFrame(for: window) ?? window.frame
+ return AXWindowGeometryFingerprint(
+ processIdentifier: processIdentifier,
+ title: normalizedWindowTitle(window.title),
+ frame: frame
+ )
+ }
+
+ private func cgWindowFrame(for window: AlignerWindow) -> CGRect? {
+ guard let rawWindows = CGWindowListCopyWindowInfo(
+ [.optionIncludingWindow],
+ CGWindowID(window.id)
+ ) as? [[String: Any]],
+ let rawWindow = rawWindows.first
+ else {
+ return nil
+ }
+
+ return bounds(rawWindow[kCGWindowBounds as String])
}
private func matchingAXTitleCandidateCount(
@@ -642,13 +772,14 @@
return nil
}
+ let rawBounds = bounds(rawWindow[kCGWindowBounds as String])
let ownerPID = int32Value(rawWindow[kCGWindowOwnerPID as String])
let runningApplication = ownerPID.flatMap(NSRunningApplication.init(processIdentifier:))
let rawFingerprint = ownerPID.map {
AXWindowFingerprint(
processIdentifier: $0,
title: title(rawWindow: rawWindow, axWindow: nil),
- size: bounds(rawWindow[kCGWindowBounds as String]).size
+ size: rawBounds.size
)
}
let directAXWindow = axMetadata[windowID]
@@ -668,7 +799,8 @@
app: app,
activationPolicy: activationPolicy(from: runningApplication),
title: title(rawWindow: rawWindow, axWindow: axWindow),
- size: bounds(rawWindow[kCGWindowBounds as String]).size,
+ size: rawBounds.size,
+ frame: rawBounds,
subrole: axWindow?.subrole ?? .standard,
isMinimized: axWindow?.isMinimized ?? false,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -748,6 +880,7 @@
title: axWindow.title ?? "",
identifierSource: identifierSource,
size: axWindow.size,
+ frame: axWindow.frame,
subrole: axWindow.subrole,
isMinimized: axWindow.isMinimized,
isFullscreen: !Set(spaceIDs).isDisjoint(with: fullscreenSpaceIDs),
@@ -981,6 +1114,7 @@
}
}
+ private static let commandWVirtualKeyCode: CGKeyCode = 13
}
private struct AXWindowMetadata {
@@ -990,6 +1124,7 @@
let title: String?
let isMinimized: Bool
let size: CGSize
+ let frame: CGRect?
let subrole: AlignerWindowSubrole
let hasExplicitSubrole: Bool
let isInteractable: Bool
@@ -1049,6 +1184,34 @@
}
}
+private struct AXWindowGeometryFingerprint: Hashable {
+ let processIdentifier: Int32
+ let title: String
+ let x: Int
+ let y: Int
+ let width: Int
+ let height: Int
+
+ init?(processIdentifier: Int32, title: String, frame: CGRect?) {
+ guard let frame else { return nil }
+
+ self.processIdentifier = processIdentifier
+ self.title = title
+ self.x = Int(frame.origin.x.rounded())
+ self.y = Int(frame.origin.y.rounded())
+ self.width = Int(frame.size.width.rounded())
+ self.height = Int(frame.size.height.rounded())
+ }
+
+ init?(_ metadata: AXWindowMetadata) {
+ self.init(
+ processIdentifier: metadata.processIdentifier,
+ title: normalizedWindowTitle(metadata.title ?? ""),
+ frame: metadata.frame
+ )
+ }
+}
+
private struct RestorableAXWindow {
let processIdentifier: Int32
let element: AXUIElement
@@ -1081,13 +1244,16 @@
for (windowIndex, window) in windows.enumerated() {
let subrole = subrole(for: window)
+ let size = size(for: window)
+ let position = position(for: window)
result.append(AXWindowMetadata(
windowID: windowID(for: window),
app: app,
activationPolicy: activationPolicy(from: application),
title: title(for: window),
isMinimized: boolAttribute(kAXMinimizedAttribute as String, for: window) ?? false,
- size: size(for: window),
+ size: size,
+ frame: position.map { CGRect(origin: $0, size: size) },
subrole: subrole.value,
hasExplicitSubrole: subrole.isExplicit,
isInteractable: boolAttribute(kAXEnabledAttribute as String, for: window) ?? true,
@@ -1214,6 +1380,23 @@
return size
}
+ private static func position(for window: AXUIElement) -> CGPoint? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(window, kAXPositionAttribute as CFString, &value) == .success,
+ let axValue = value,
+ CFGetTypeID(axValue) == AXValueGetTypeID()
+ else {
+ return nil
+ }
+
+ var position = CGPoint.zero
+ guard AXValueGetValue(axValue as! AXValue, .cgPoint, &position) else {
+ return nil
+ }
+
+ return position
+ }
+
private static func subrole(for window: AXUIElement) -> (value: AlignerWindowSubrole, isExplicit: Bool) {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(window, kAXSubroleAttribute as CFString, &value) == .success,
--
Gitblit v1.9.3