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

---
 C3.tools/round1-space-lane-split-view-fixture-qa.sh                            |    5 
 C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift                      |   53 +++++
 C3.tools/round1-main-ui-qa.sh                                                  |    4 
 C3.tools/round1-app-column-alignment-fixture-qa.sh                             |    8 
 C1.source/Sources/Aligner/QuickSwitchRootView.swift                            |   45 +++-
 C1.source/Resources/Aligner-Info.plist                                         |    4 
 C1.source/Sources/Aligner/AlignerApplicationDelegate.swift                     |    4 
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift                   |    2 
 C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift |  197 +++++++++++++++++-
 C3.tools/round1-multi-page-identity-fixture-qa.sh                              |  233 +++++++++++++++++++++++
 10 files changed, 517 insertions(+), 38 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index ca3405f..dd38074 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.62</string>
+    <string>0.0.63</string>
 	<key>CFBundleVersion</key>
-    <string>20260612.2049</string>
+    <string>20260613.1401</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>26.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
index 3334d05..ec6e5bf 100644
--- a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
+++ b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -666,6 +666,10 @@
             return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .splitView)
         }
 
+        if round1QuickSwitchOptions.fixtureMultiPageIdentity {
+            return FixtureQuickSwitchSnapshotLoader(appCount: 1, mode: .multiPageIdentity)
+        }
+
         if let fixtureAppCount = round1QuickSwitchOptions.fixtureAppCount {
             return FixtureQuickSwitchSnapshotLoader(
                 appCount: fixtureAppCount,
diff --git a/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift b/C1.source/Sources/Aligner/Infrastructure/Windows/CGWindowAXWindowService.swift
index 55b5f44..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(
@@ -984,6 +1114,7 @@
         }
     }
 
+    private static let commandWVirtualKeyCode: CGKeyCode = 13
 }
 
 private struct AXWindowMetadata {
@@ -1053,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
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index 4ea3319..3d16422 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -3087,13 +3087,16 @@
 
     private func layoutSpaceLaneSegmentContents(_ segment: SpaceLaneSegmentLayers) {
         let bounds = segment.containerLayer.bounds
+        let splitAppNames = splitViewAppNames(for: segment.space)
+        let isSplitView = splitAppNames.count >= 2
         segment.labelLayer.frame = CGRect(x: 10, y: bounds.height - 29, width: max(1, bounds.width - 48), height: 20)
         segment.countLayer.frame = CGRect(x: bounds.width - 34, y: bounds.height - 29, width: 24, height: 20)
+        segment.countLayer.string = isSplitView ? "" : "\(segment.space.windowCount)"
+        segment.countLayer.isHidden = isSplitView
         if segment.space.type == .fullscreen {
             let markerFrame = fullscreenMarkerFrame(in: bounds)
-            let splitAppNames = splitViewAppNames(for: segment.space)
-            segment.appLayer.isHidden = splitAppNames.count >= 2
-            if splitAppNames.count >= 2 {
+            segment.appLayer.isHidden = isSplitView
+            if isSplitView {
                 let labelY = markerFrame.midY - 8.5
                 let halfWidth = markerFrame.width / 2
                 for (index, splitAppLayer) in segment.splitAppLayers.enumerated() {
@@ -3128,7 +3131,7 @@
             layoutFullscreenMarkerLayer(
                 fullscreenMarkerLayer,
                 in: bounds,
-                isSplitView: splitViewAppNames(for: segment.space).count >= 2
+                isSplitView: isSplitView
             )
         }
 
@@ -4740,17 +4743,11 @@
 
         let bounds = shapeLayer.bounds
         if isSplitView {
-            let insetBounds = bounds.insetBy(dx: 0.6, dy: 0.6)
-            let radius = min(7, max(5, insetBounds.height * 0.22))
             let path = CGMutablePath()
-            path.addRoundedRect(
-                in: insetBounds,
-                cornerWidth: radius,
-                cornerHeight: radius
-            )
-            path.move(to: CGPoint(x: bounds.midX, y: insetBounds.minY + 1.5))
-            path.addLine(to: CGPoint(x: bounds.midX, y: insetBounds.maxY - 1.5))
+            path.move(to: CGPoint(x: bounds.midX, y: bounds.minY))
+            path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY))
             shapeLayer.path = path
+            shapeLayer.lineCap = .butt
             return
         }
 
@@ -4774,6 +4771,7 @@
         path.addLine(to: CGPoint(x: 0, y: bounds.maxY - cornerLength))
 
         shapeLayer.path = path
+        shapeLayer.lineCap = .round
     }
 
     private func fullscreenMarkerFrame(in segmentBounds: CGRect) -> CGRect {
@@ -5176,6 +5174,10 @@
                         markerLayer: segment.fullscreenMarkerLayer,
                         splitAppNames: splitAppNames
                     ),
+                    "fullscreenMarkerPathStyle": fullscreenMarkerPathStyle(
+                        markerLayer: segment.fullscreenMarkerLayer,
+                        splitAppNames: splitAppNames
+                    ),
                     "fullscreenMarkerFrame": fullscreenMarkerFrame.map { dictionary(from: $0) } ?? NSNull(),
                     "fullscreenMarkerCenterRatio": fullscreenMarkerCenterRatio ?? NSNull(),
                     "splitViewAppNames": splitAppNames,
@@ -5187,6 +5189,8 @@
                     "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) },
                     "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) },
                     "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor),
+                    "countText": string(from: segment.countLayer.string),
+                    "countVisible": !segment.countLayer.isHidden && segment.countLayer.opacity > 0,
                     "countColor": colorDictionary(from: segment.countLayer.foregroundColor),
                     "appColor": colorDictionary(from: segment.appLayer.foregroundColor),
                     "fullscreenMarkerStrokeColor": colorDictionary(
@@ -5229,6 +5233,11 @@
     private func fullscreenMarkerKind(markerLayer: CALayer?, splitAppNames: [String]) -> String {
         guard markerLayer != nil else { return "none" }
         return splitAppNames.count >= 2 ? "splitViewPair" : "cornerBracket"
+    }
+
+    private func fullscreenMarkerPathStyle(markerLayer: CALayer?, splitAppNames: [String]) -> String {
+        guard markerLayer != nil else { return "none" }
+        return splitAppNames.count >= 2 ? "centerDividerOnly" : "cornerBracket"
     }
 
     private func visualStates(for space: QuickSwitchSpaceViewModel) -> [String] {
@@ -5282,6 +5291,16 @@
         ]
     }
 
+    private func string(from textLayerValue: Any?) -> String {
+        if let string = textLayerValue as? String {
+            return string
+        }
+        if let attributedString = textLayerValue as? NSAttributedString {
+            return attributedString.string
+        }
+        return ""
+    }
+
     private func colorDictionary(from color: CGColor?) -> [String: Double] {
         guard let color,
               let nsColor = NSColor(cgColor: color)?.usingColorSpace(.deviceRGB)
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 645a35b..7fa4f39 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -1479,6 +1479,7 @@
     let fixtureActivation: Bool
     let fixtureSpaceFilter: Bool
     let fixtureSplitView: Bool
+    let fixtureMultiPageIdentity: Bool
     let debugHoveredAppGroupIndex: Int?
     let debugOverlayWidth: CGFloat?
     let debugKeySequence: [String]
@@ -1508,6 +1509,7 @@
             fixtureActivation: arguments.contains("--round01-fixture-window-activation"),
             fixtureSpaceFilter: arguments.contains("--round01-fixture-space-filter"),
             fixtureSplitView: arguments.contains("--round01-fixture-split-view"),
+            fixtureMultiPageIdentity: arguments.contains("--round01-fixture-multi-page-identity"),
             debugHoveredAppGroupIndex: intValue(for: "--round01-debug-hover-app-index", in: arguments),
             debugOverlayWidth: cgFloatValue(for: "--round01-debug-overlay-width", in: arguments),
             debugKeySequence: stringListValue(for: "--round01-debug-key-sequence", in: arguments),
diff --git a/C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift b/C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift
index 1cce8b6..121e38f 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSnapshotLoader.swift
@@ -17,6 +17,7 @@
     case activation
     case spaceFilter
     case splitView
+    case multiPageIdentity
 }
 
 final class LiveQuickSwitchSnapshotLoader: QuickSwitchSnapshotLoading {
@@ -91,6 +92,9 @@
         }
         if mode == .splitView {
             return splitViewSnapshot()
+        }
+        if mode == .multiPageIdentity {
+            return multiPageIdentitySnapshot()
         }
 
         return layoutSnapshot()
@@ -578,4 +582,53 @@
             currentSpaceIDs: [1]
         )
     }
+
+    private func multiPageIdentitySnapshot() -> QuickSwitchSnapshotLoad {
+        let displayUUID = "fixture-display-a"
+        let displays = [
+            AlignerDisplay(
+                uuid: displayUUID,
+                physical: true,
+                spaces: [
+                    AlignerSpace(id: 1, type: .user, displayUUID: displayUUID, index: 1)
+                ]
+            )
+        ]
+
+        let app = AlignerApp(
+            bundleIdentifier: "com.example.multipage.identity",
+            name: "Multi Page App",
+            category: .generic,
+            processIdentifier: 70_001
+        )
+
+        let windows = [
+            AlignerWindow(
+                id: 70_101,
+                app: app,
+                title: "Shared Page",
+                frame: CGRect(x: 120, y: 120, width: 920, height: 640),
+                spaceIDs: [1]
+            ),
+            AlignerWindow(
+                id: 70_102,
+                app: app,
+                title: "Shared Page",
+                frame: CGRect(x: 160, y: 140, width: 920, height: 640),
+                spaceIDs: [1]
+            ),
+            AlignerWindow(
+                id: 70_103,
+                app: app,
+                title: "Shared Page",
+                frame: CGRect(x: 200, y: 160, width: 920, height: 640),
+                spaceIDs: [1]
+            )
+        ]
+
+        return QuickSwitchSnapshotLoad(
+            snapshot: QuickSwitchSnapshotBuilder.snapshot(displays: displays, windows: windows),
+            currentSpaceIDs: [1]
+        )
+    }
 }
diff --git a/C3.tools/round1-app-column-alignment-fixture-qa.sh b/C3.tools/round1-app-column-alignment-fixture-qa.sh
index 963fc2b..5e63221 100755
--- a/C3.tools/round1-app-column-alignment-fixture-qa.sh
+++ b/C3.tools/round1-app-column-alignment-fixture-qa.sh
@@ -28,9 +28,11 @@
 
 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
+    if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]] \
+      || [[ "$command" == "/Applications/Aligner.app/Contents/MacOS/Aligner"* ]] \
+      || [[ "$command" == "$HOME/Applications/Aligner.app/Contents/MacOS/Aligner"* ]]; then
+      echo "$pid"
+    fi
   done
 }
 
diff --git a/C3.tools/round1-main-ui-qa.sh b/C3.tools/round1-main-ui-qa.sh
index aadf711..aa5c4d2 100755
--- a/C3.tools/round1-main-ui-qa.sh
+++ b/C3.tools/round1-main-ui-qa.sh
@@ -110,6 +110,7 @@
 run_step screenshot-session "$SCRIPT_DIR/round1-screenshot-session-qa.sh"
 run_step keyboard-navigation "$SCRIPT_DIR/round1-keyboard-navigation-fixture-qa.sh"
 run_step window-activation "$SCRIPT_DIR/round1-window-activation-fixture-qa.sh"
+run_step multi-page-identity "$SCRIPT_DIR/round1-multi-page-identity-fixture-qa.sh"
 run_step mouse-interaction "$SCRIPT_DIR/round1-mouse-interaction-fixture-qa.sh"
 run_step app-column-alignment "$SCRIPT_DIR/round1-app-column-alignment-fixture-qa.sh"
 run_step horizontal-waterfall "$SCRIPT_DIR/round1-horizontal-waterfall-fixture-qa.sh"
@@ -124,5 +125,6 @@
 Log directory: $LOG_DIR
 Covered: trigger, overlay uniqueness, keyboard navigation, candidate filtering,
 screenshots/fallbacks, minimized-window restore/activation, mouse activation,
-App column alignment, Split View Space Lane rendering, and final no-residual check.
+multi-page precise activation/close targeting, App column alignment,
+Split View Space Lane rendering, and final no-residual check.
 EOF
diff --git a/C3.tools/round1-multi-page-identity-fixture-qa.sh b/C3.tools/round1-multi-page-identity-fixture-qa.sh
new file mode 100755
index 0000000..cbf0dbd
--- /dev/null
+++ b/C3.tools/round1-multi-page-identity-fixture-qa.sh
@@ -0,0 +1,233 @@
+#!/bin/bash
+# Round01 multi-page identity fixture QA. It verifies that Quick Switch keeps
+# same-App/same-title pages distinct for activation and close requests.
+
+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"
+BUNDLE_ID="com.ar.Aligner"
+CONFIRM_DEFAULTS_KEY="QuickSwitchConfirmBeforeClose"
+REPORT_WAIT="${ALIGNER_ROUND1_MULTI_PAGE_REPORT_WAIT:-7.0}"
+ACTIVATION_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-activation-report.json"
+CLOSE_REPORT="$BUILD_REPORT_ROOT/round01-multi-page-close-report.json"
+
+fail() {
+  echo "Round01 multi-page identity fixture 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"
+}
+
+reset_close_confirmation_preference() {
+  defaults delete "$BUNDLE_ID" "$CONFIRM_DEFAULTS_KEY" >/dev/null 2>&1 || true
+}
+
+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):
+    root = report.get("rootView", {})
+    if report.get("snapshotLoaded") is not True:
+        return False
+    if mode == "activated":
+        return (
+            report.get("quickSwitchVisible") is False
+            and report.get("lastActivationResult") is not None
+        )
+    if mode == "verifiedFailure":
+        return (
+            report.get("quickSwitchVisible") is True
+            and report.get("lastCloseResult") is not None
+            and root.get("closeFeedbackKind") == "failure"
+        )
+    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"multi-page identity report {path} did not reach mode={mode} within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
+run_case() {
+  local report="$1"
+  local sequence="$2"
+  local mode="$3"
+  local activation_flag="${4:-}"
+  local close_flag="${5:-}"
+
+  stop_current_aligner
+  reset_close_confirmation_preference
+  rm -f "$report"
+
+  local args=(
+    --round0-skip-permissions
+    --round01-open-quick-switch
+    --round01-waterfall-view-mode=vertical
+    --round01-fixture-multi-page-identity
+    --round01-disable-screenshot-refresh
+    --round01-debug-mouse-sequence="$sequence"
+    --round01-quick-switch-report="$report"
+  )
+  if [ -n "$activation_flag" ]; then
+    args+=(--round01-debug-window-activation)
+  fi
+  if [ -n "$close_flag" ]; then
+    args+=(--round01-debug-window-close)
+  fi
+
+  "$APP/Contents/MacOS/Aligner" "${args[@]}" &
+  APP_PID=$!
+
+  cleanup() {
+    kill "$APP_PID" 2>/dev/null || true
+    wait "$APP_PID" 2>/dev/null || true
+    stop_current_aligner
+  }
+  trap cleanup EXIT
+
+  wait_for_report "$report" "$mode"
+  kill "$APP_PID" 2>/dev/null || true
+  wait "$APP_PID" 2>/dev/null || true
+  trap - EXIT
+  stop_current_aligner
+}
+
+assert_activation_report() {
+  /usr/bin/python3 - "$ACTIVATION_REPORT" <<'PY'
+import json
+import sys
+
+path = sys.argv[1]
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(message, file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+root = report.get("rootView", {})
+columns = root.get("waterfallColumns", [])
+cards = columns[0].get("cards", []) if columns else []
+ids = [card.get("windowID") for card in cards]
+
+require(report.get("snapshotLoaded") is True, "activation fixture must load")
+require(report.get("quickSwitchVisible") is False, "activation must close Quick Switch")
+require(report.get("appCount") == 1, "multi-page fixture must expose one App")
+require(report.get("windowCount") == 3, "multi-page fixture must expose three pages")
+require(ids == [70101, 70102, 70103], "same-title pages must keep stable windowID order")
+require(report.get("lastCommittedAppGroupIndex") == 0, "activation must target App group 0")
+require(report.get("lastCommittedWindowIndex") == 1, "activation must target second page index")
+require(report.get("lastCommittedWindowID") == 70102, "activation commit must target page windowID 70102")
+require(report.get("lastActivationWindowID") == 70102, "activation service must receive exact page windowID 70102")
+require(report.get("lastActivationResult") == "activated", "debug activation must report activated")
+require(root.get("selectedWindowID") == 70102, "selectedWindowID must track exact activated page")
+
+print(json.dumps({
+    "case": "multiPageActivation",
+    "windowIDs": ids,
+    "lastActivationWindowID": report.get("lastActivationWindowID")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+assert_close_report() {
+  /usr/bin/python3 - "$CLOSE_REPORT" <<'PY'
+import json
+import sys
+
+path = sys.argv[1]
+with open(path, "r", encoding="utf-8") as file:
+    report = json.load(file)
+
+def require(condition, message):
+    if not condition:
+        print(message, file=sys.stderr)
+        print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
+        sys.exit(1)
+
+root = report.get("rootView", {})
+columns = root.get("waterfallColumns", [])
+cards = columns[0].get("cards", []) if columns else []
+ids = [card.get("windowID") for card in cards]
+
+require(report.get("snapshotLoaded") is True, "close fixture must load")
+require(report.get("quickSwitchVisible") is True, "close verification failure must keep Quick Switch visible")
+require(report.get("appCount") == 1, "multi-page close fixture must expose one App")
+require(report.get("windowCount") == 3, "fixture must still expose all three pages when debug close does not remove them")
+require(ids == [70101, 70102, 70103], "close must not reorder or fake-remove same-title pages")
+require(report.get("lastCloseTargetKind") == "window", "close must target a window/page")
+require(report.get("lastCloseAppGroupIndex") == 0, "close must target App group 0")
+require(report.get("lastCloseWindowID") == 70103, "close service must receive exact page windowID 70103")
+require(report.get("lastCloseResult") == "requested", "debug close must report requested")
+require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present must not suppress the card")
+require(root.get("closeFeedbackKind") == "failure", "still-present debug close must show verification failure")
+
+print(json.dumps({
+    "case": "multiPageClose",
+    "windowIDs": ids,
+    "lastCloseWindowID": report.get("lastCloseWindowID"),
+    "closeFeedbackKind": root.get("closeFeedbackKind")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+stop_current_aligner
+"$SCRIPT_DIR/package-app.sh" >&2
+
+run_case "$ACTIVATION_REPORT" "click-card:0:1" "activated" "activation" ""
+assert_activation_report
+
+run_case "$CLOSE_REPORT" "click-card-close:0:2,confirm-close" "verifiedFailure" "" "close"
+assert_close_report
+
+echo "Round01 multi-page identity fixture QA passed."
diff --git a/C3.tools/round1-space-lane-split-view-fixture-qa.sh b/C3.tools/round1-space-lane-split-view-fixture-qa.sh
index 335fcd6..6a7ce26 100755
--- a/C3.tools/round1-space-lane-split-view-fixture-qa.sh
+++ b/C3.tools/round1-space-lane-split-view-fixture-qa.sh
@@ -116,8 +116,11 @@
 require(split.get("type") == "fullscreen", "Split View tile must still be a fullscreen Space")
 require(split.get("windowCount") == 2, "Split View tile must count both fullscreen windows")
 require(split.get("appCount") == 2, "Split View tile must count both Split View apps")
+require(split.get("countVisible") is False, "Split View tile must hide the top-right count label")
+require(split.get("countText") == "", "Split View tile must clear the top-right count text")
 require(split.get("windowBlockCount") == 0, "Split View tile must not draw normal window blocks")
 require("splitView" in split.get("visualStates", []), "Split View tile must expose splitView visual state")
+require(split.get("fullscreenMarkerPathStyle") == "centerDividerOnly", "Split View marker must draw only the center divider")
 require(
     split.get("splitViewAppNames") == ["左侧应用", "右侧应用"],
     "Split View app names must be ordered by physical window frame from left to right"
@@ -153,6 +156,8 @@
     "splitSpace": split.get("label"),
     "splitNames": split.get("splitViewAppNames"),
     "markerKind": split.get("fullscreenMarkerKind"),
+    "markerPathStyle": split.get("fullscreenMarkerPathStyle"),
+    "countVisible": split.get("countVisible"),
     "singleFullscreenLabels": [segment.get("label") for segment in single_fullscreen],
 }, indent=2, ensure_ascii=False))
 PY

--
Gitblit v1.9.3