From 9c898b0ca61c090e5e327f750f6c73e28475e10b Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 12 Jun 2026 19:30:19 +0800
Subject: [PATCH] Fix space filter visual and close verification

---
 C3.tools/round1-close-fixture-qa.sh                          |   24 +++++--
 C3.tools/round1-space-filter-fixture-qa.sh                   |   54 +++++++++++++++++-
 C1.source/Sources/Aligner/QuickSwitchRootView.swift          |   59 +++++++++++++++++--
 C1.source/Resources/Aligner-Info.plist                       |    4 
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |    4 
 5 files changed, 124 insertions(+), 21 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index cd0538d..6a82359 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.60</string>
+    <string>0.0.61</string>
 	<key>CFBundleVersion</key>
-    <string>20260612.1713</string>
+    <string>20260612.1926</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>26.0</string>
 	<key>NSHighResolutionCapable</key>
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index d7f02fc..d3f89ae 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -2848,10 +2848,10 @@
         let isActive = isHovered || isFocused || isLocked
         let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
 
-        segment.containerLayer.borderWidth = isLocked
-            ? 6
-            : (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
-        segment.containerLayer.borderColor = (isActive
+        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
+        segment.containerLayer.borderColor = (isLocked
+            ? NSColor.white.withAlphaComponent(0.86)
+            : isActive
             ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 0.62)
             : isAssociated
                 ? spaceLaneOccupiedColor.withAlphaComponent(0.50)
@@ -2861,7 +2861,12 @@
                     ? NSColor.labelColor.withAlphaComponent(0.34)
                 : NSColor.separatorColor.withAlphaComponent(0.34)
         ).cgColor
-        if !hasWindows {
+        if isLocked {
+            applySpaceLaneSolidFill(
+                to: segment.containerLayer,
+                color: spaceLaneLockedFillColor
+            )
+        } else if !hasWindows {
             applySpaceLaneSolidFill(to: segment.containerLayer, color: .clear)
         } else if isActive {
             applySpaceLaneSolidFill(
@@ -2896,7 +2901,7 @@
         segment.containerLayer.transform = (isActive || isAssociated)
             ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1)
             : CATransform3DIdentity
-        segment.containerLayer.shadowPath = hasWindows
+        segment.containerLayer.shadowPath = (hasWindows || isLocked)
             ? CGPath(
                 roundedRect: segment.containerLayer.bounds,
                 cornerWidth: segment.containerLayer.cornerRadius,
@@ -2904,6 +2909,34 @@
                 transform: nil
             )
             : nil
+
+        applySpaceLaneContentVisualState(segment, isLocked: isLocked)
+    }
+
+    private func applySpaceLaneContentVisualState(_ segment: SpaceLaneSegmentLayers, isLocked: Bool) {
+        let primaryTextColor: NSColor = isLocked ? .white : .labelColor
+        let secondaryTextColor: NSColor = isLocked
+            ? .white
+            : .secondaryLabelColor
+
+        segment.labelLayer.foregroundColor = primaryTextColor.cgColor
+        segment.countLayer.foregroundColor = (isLocked ? NSColor.white : .secondaryLabelColor).cgColor
+        segment.appLayer.foregroundColor = (segment.space.type == .fullscreen
+            ? primaryTextColor
+            : secondaryTextColor
+        ).cgColor
+
+        for block in segment.windowBlocks {
+            block.backgroundColor = (isLocked
+                ? NSColor.white
+                : spaceLaneOccupiedColor.withAlphaComponent(segment.space.isCurrent ? 0.72 : 0.58)
+            ).cgColor
+        }
+
+        (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor = (isLocked
+            ? NSColor.white
+            : NSColor.secondaryLabelColor.withAlphaComponent(segment.space.isCurrent ? 0.42 : 0.28)
+        ).cgColor
     }
 
     private func shouldScrollSpaceLane(for event: NSEvent) -> Bool {
@@ -4395,6 +4428,14 @@
         return NSColor(calibratedRed: 0, green: 122 / 255, blue: 1, alpha: 1)
     }
 
+    private var spaceLaneLockedFillColor: NSColor {
+        if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
+            return NSColor(calibratedRed: 58 / 255, green: 148 / 255, blue: 1, alpha: 0.86)
+        }
+
+        return NSColor(calibratedRed: 90 / 255, green: 171 / 255, blue: 1, alpha: 0.82)
+    }
+
     private func spaceLaneIdleOccupiedGradientColors() -> [NSColor] {
         if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
             let blue = NSColor(calibratedRed: 36 / 255, green: 54 / 255, blue: 72 / 255, alpha: 1)
@@ -5059,6 +5100,12 @@
                     "windowBlockCount": segment.windowBlocks.count,
                     "windowBlockColors": segment.windowBlocks.map { colorDictionary(from: $0.backgroundColor) },
                     "windowBlockFrames": segment.windowBlocks.map { dictionary(from: $0.frame) },
+                    "labelColor": colorDictionary(from: segment.labelLayer.foregroundColor),
+                    "countColor": colorDictionary(from: segment.countLayer.foregroundColor),
+                    "appColor": colorDictionary(from: segment.appLayer.foregroundColor),
+                    "fullscreenMarkerStrokeColor": colorDictionary(
+                        from: (segment.fullscreenMarkerLayer as? CAShapeLayer)?.strokeColor
+                    ),
                     "backgroundKind": spaceLaneFillKind(for: segment.containerLayer),
                     "backgroundColor": colorDictionary(from: segment.containerLayer.backgroundColor),
                     "gradientColors": gradientColorDictionaries(from: segment.containerLayer),
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 13457a2..947b96e 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -844,12 +844,12 @@
                 "result": Self.closeResultString(result)
             ])
             if result == .requested {
-                suppressCloseTarget(request)
                 DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [
                     "targetKind": request.kindDescription,
                     "appGroupIndex": request.appGroupIndex,
                     "appName": request.appName,
-                    "windowID": request.windowID
+                    "windowID": request.windowID,
+                    "optimisticRemove": false
                 ])
                 pendingCloseVerification = PendingCloseVerification(
                     request: request,
diff --git a/C3.tools/round1-close-fixture-qa.sh b/C3.tools/round1-close-fixture-qa.sh
index facf709..e266c67 100755
--- a/C3.tools/round1-close-fixture-qa.sh
+++ b/C3.tools/round1-close-fixture-qa.sh
@@ -297,8 +297,8 @@
 require(report.get("lastCloseTargetKind") == "window", "confirmed window close must record window target")
 require(report.get("lastCloseWindowID") is not None, "confirmed window close must record target window")
 require(report.get("lastCloseResult") == "requested", "debug window close service must return requested")
-require(report.get("lastCloseWindowID") not in visible_window_ids, "confirmed window close must optimistically remove the stale card from Waterfall")
-require(report.get("suppressedCloseTargetCount") == 1, "confirmed window close must expose one suppressed close target")
+require(report.get("lastCloseWindowID") in visible_window_ids, "requested-but-still-present window close must keep the card visible")
+require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present window close must not suppress the card")
 require(root.get("closeConfirmationVisible") is False, "confirmed window close must hide confirmation")
 require(root.get("closeFeedbackVisible") is True, "confirmed window close must show failure feedback when target remains")
 require(root.get("closeFeedbackKind") == "failure", "confirmed window close feedback must be failure")
@@ -310,6 +310,8 @@
     "lastCloseTargetKind": report.get("lastCloseTargetKind"),
     "lastCloseResult": report.get("lastCloseResult"),
     "lastDismissReason": report.get("lastDismissReason"),
+    "stillVisible": report.get("lastCloseWindowID") in visible_window_ids,
+    "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"),
     "closeFeedbackKind": root.get("closeFeedbackKind"),
     "closeFeedbackMessage": root.get("closeFeedbackMessage")
 }, indent=2, ensure_ascii=False))
@@ -415,8 +417,8 @@
 require(report.get("quickSwitchVisible") is True, "opt-out confirmed close must keep Quick Switch visible")
 require(report.get("lastCloseTargetKind") == "window", "opt-out confirmed close must record window target")
 require(report.get("lastCloseResult") == "requested", "opt-out confirmed close must request close")
-require(report.get("lastCloseWindowID") not in visible_window_ids, "opt-out confirmed close must optimistically remove the stale card")
-require(report.get("suppressedCloseTargetCount") == 1, "opt-out confirmed close must expose one suppressed close target")
+require(report.get("lastCloseWindowID") in visible_window_ids, "opt-out requested-but-still-present close must keep the card visible")
+require(report.get("suppressedCloseTargetCount") == 0, "opt-out requested-but-still-present close must not suppress the card")
 require(root.get("closeConfirmationVisible") is False, "opt-out confirmed close must hide confirmation")
 require(root.get("pendingCloseOptOut") is False, "opt-out confirmed close must clear temporary checkbox state")
 require(root.get("closeFeedbackKind") == "failure", "opt-out confirmed close must still verify close result")
@@ -424,6 +426,8 @@
     "case": "cardOptOutConfirmed",
     "lastCloseTargetKind": report.get("lastCloseTargetKind"),
     "lastCloseResult": report.get("lastCloseResult"),
+    "stillVisible": report.get("lastCloseWindowID") in visible_window_ids,
+    "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"),
     "pendingCloseOptOut": root.get("pendingCloseOptOut"),
     "closeConfirmationVisible": root.get("closeConfirmationVisible")
 }, indent=2, ensure_ascii=False))
@@ -454,8 +458,8 @@
 require(report.get("quickSwitchVisible") is True, "second close after opt-out must keep Quick Switch visible")
 require(report.get("lastCloseTargetKind") == "window", "second close after opt-out must directly request window close")
 require(report.get("lastCloseResult") == "requested", "second close after opt-out must call close service")
-require(report.get("lastCloseWindowID") not in visible_window_ids, "second close after opt-out must optimistically remove the stale card")
-require(report.get("suppressedCloseTargetCount") == 1, "second close after opt-out must expose one suppressed close target")
+require(report.get("lastCloseWindowID") in visible_window_ids, "second requested-but-still-present close must keep the card visible")
+require(report.get("suppressedCloseTargetCount") == 0, "second requested-but-still-present close must not suppress the card")
 require(root.get("pendingCloseTargetKind") is None, "second close after opt-out must not create pending close target")
 require(root.get("closeConfirmationVisible") is False, "second close after opt-out must not show confirmation")
 require(root.get("closeFeedbackKind") == "failure", "second close after opt-out must still verify close result")
@@ -463,6 +467,8 @@
     "case": "cardOptOutSecondClose",
     "lastCloseTargetKind": report.get("lastCloseTargetKind"),
     "lastCloseResult": report.get("lastCloseResult"),
+    "stillVisible": report.get("lastCloseWindowID") in visible_window_ids,
+    "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"),
     "pendingCloseTargetKind": root.get("pendingCloseTargetKind"),
     "closeConfirmationVisible": root.get("closeConfirmationVisible")
 }, indent=2, ensure_ascii=False))
@@ -490,8 +496,8 @@
 require(report.get("lastCloseTargetKind") == "app", "confirmed App close must record app target")
 require(report.get("lastCloseAppGroupIndex") == 1, "confirmed App close must record target app group")
 require(report.get("lastCloseResult") == "requested", "debug App close service must return requested")
-require(report.get("suppressedCloseTargetCount") == 1, "confirmed App close must expose one suppressed close target")
-require(1 not in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])], "confirmed App close must optimistically remove the target column")
+require(report.get("suppressedCloseTargetCount") == 0, "requested-but-still-present App close must not suppress the column")
+require(1 in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])], "requested-but-still-present App close must keep the target column visible")
 require(root.get("closeConfirmationVisible") is False, "confirmed App close must hide confirmation")
 require(root.get("closeFeedbackVisible") is True, "confirmed App close must show failure feedback when target remains")
 require(root.get("closeFeedbackKind") == "failure", "confirmed App close feedback must be failure")
@@ -503,6 +509,8 @@
     "lastCloseTargetKind": report.get("lastCloseTargetKind"),
     "lastCloseResult": report.get("lastCloseResult"),
     "lastDismissReason": report.get("lastDismissReason"),
+    "targetColumnStillVisible": 1 in [column.get("appGroupIndex") for column in root.get("waterfallColumns", [])],
+    "suppressedCloseTargetCount": report.get("suppressedCloseTargetCount"),
     "closeFeedbackKind": root.get("closeFeedbackKind"),
     "closeFeedbackMessage": root.get("closeFeedbackMessage")
 }, indent=2, ensure_ascii=False))
diff --git a/C3.tools/round1-space-filter-fixture-qa.sh b/C3.tools/round1-space-filter-fixture-qa.sh
index 9ca4e13..ca911b5 100755
--- a/C3.tools/round1-space-filter-fixture-qa.sh
+++ b/C3.tools/round1-space-filter-fixture-qa.sh
@@ -251,6 +251,22 @@
         print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
         sys.exit(1)
 
+def is_locked_fill(color):
+    return (
+        color.get("blue", 0) >= 0.85
+        and color.get("green", 0) >= 0.55
+        and color.get("red", 1) <= 0.58
+        and color.get("alpha", 0) >= 0.70
+    )
+
+def is_white(color):
+    return (
+        color.get("red", 0) >= 0.94
+        and color.get("green", 0) >= 0.94
+        and color.get("blue", 0) >= 0.94
+        and color.get("alpha", 0) >= 0.88
+    )
+
 root = report.get("rootView", {})
 items = root.get("appShelfItems", [])
 columns = root.get("waterfallColumns", [])
@@ -287,7 +303,13 @@
 require(segment_by_id[4].get("windowCount") == 0, "B1 must remain visible as empty Space")
 require(segment_by_id[5].get("windowCount") == 1, "B2 fullscreen Space must remain visible")
 require("locked" in segment_by_id[1].get("visualStates", []), "locked Space must expose locked visual state")
-require(segment_by_id[1].get("borderWidth", 0) >= 5.5, "locked Space must use a heavy blue border")
+require(segment_by_id[1].get("borderWidth", 99) <= 2.0, "locked Space must not use the old heavy border")
+require(segment_by_id[1].get("backgroundKind") == "solid", "locked Space must use a solid light-blue fill")
+require(is_locked_fill(segment_by_id[1].get("backgroundColor", {})), "locked Space fill must be clean light blue")
+require(is_white(segment_by_id[1].get("labelColor", {})), "locked Space label text must be pure white")
+require(is_white(segment_by_id[1].get("countColor", {})), "locked Space count text must be pure white")
+require(is_white(segment_by_id[1].get("appColor", {})), "locked Space app text must be pure white")
+require(all(is_white(color) for color in segment_by_id[1].get("windowBlockColors", [])), "locked Space window blocks must be pure white")
 lock_shadow = segment_by_id[1].get("shadowColor", {})
 require(lock_shadow.get("red", 0) >= 0.90 and lock_shadow.get("green", 0) >= 0.90 and lock_shadow.get("blue", 0) >= 0.90, "locked Space must use a white glow")
 require(segment_by_id[1].get("shadowOpacity", 0) >= 0.60, "locked Space glow must be visibly stronger than hover")
@@ -303,6 +325,9 @@
     "cardWindowIDs": [card.get("windowID") for card in cards],
     "lockedSpace": root.get("spaceFilterLockedSpaceID"),
     "lockedBorderWidth": segment_by_id[1].get("borderWidth"),
+    "lockedBackgroundColor": segment_by_id[1].get("backgroundColor"),
+    "lockedLabelColor": segment_by_id[1].get("labelColor"),
+    "lockedWindowBlockColors": segment_by_id[1].get("windowBlockColors"),
     "lockedShadowOpacity": segment_by_id[1].get("shadowOpacity"),
     "lockedShadowRadius": segment_by_id[1].get("shadowRadius"),
     "waterfallLeftBlankWidth": left_blank_width,
@@ -435,6 +460,22 @@
         print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr)
         sys.exit(1)
 
+def is_locked_fill(color):
+    return (
+        color.get("blue", 0) >= 0.85
+        and color.get("green", 0) >= 0.55
+        and color.get("red", 1) <= 0.58
+        and color.get("alpha", 0) >= 0.70
+    )
+
+def is_white(color):
+    return (
+        color.get("red", 0) >= 0.94
+        and color.get("green", 0) >= 0.94
+        and color.get("blue", 0) >= 0.94
+        and color.get("alpha", 0) >= 0.88
+    )
+
 root = report.get("rootView", {})
 segments = root.get("spaceLaneSegments", [])
 segment_by_id = {segment.get("spaceID"): segment for segment in segments}
@@ -449,11 +490,15 @@
 require(root.get("waterfallColumnCount") == 0, "root Waterfall must be empty")
 require(root.get("spaceLaneLabels") == ["A1", "A2", "A3", "B1", "B2"], "Space Lane must stay global while empty Space is locked")
 require("locked" in segment_by_id[3].get("visualStates", []), "empty locked Space must expose locked visual state")
-require(segment_by_id[3].get("borderWidth", 0) >= 5.5, "empty locked Space must still use a heavy locked border")
+require(segment_by_id[3].get("borderWidth", 99) <= 2.0, "empty locked Space must not use the old heavy border")
+require(segment_by_id[3].get("backgroundKind") == "solid", "empty locked Space must use the same light-blue fill")
+require(is_locked_fill(segment_by_id[3].get("backgroundColor", {})), "empty locked Space fill must be clean light blue")
+require(is_white(segment_by_id[3].get("labelColor", {})), "empty locked Space label text must be pure white")
+require(is_white(segment_by_id[3].get("countColor", {})), "empty locked Space count text must be pure white")
+require(is_white(segment_by_id[3].get("appColor", {})), "empty locked Space app text must be pure white")
 empty_shadow = segment_by_id[3].get("shadowColor", {})
 require(empty_shadow.get("red", 0) >= 0.90 and empty_shadow.get("green", 0) >= 0.90 and empty_shadow.get("blue", 0) >= 0.90, "empty locked Space must use white glow")
 require(segment_by_id[3].get("shadowOpacity", 0) >= 0.60, "empty locked Space glow must be visible")
-require(segment_by_id[3].get("backgroundKind") == "clear", "empty locked Space must not use gray/gradient fill")
 require(segment_by_id[4].get("backgroundKind") == "clear", "empty sibling Space must not inherit gray/gradient fill")
 
 print(json.dumps({
@@ -461,6 +506,9 @@
     "lockedSpace": report.get("spaceFilterLockedSpaceID"),
     "appCount": report.get("appCount"),
     "columnCount": report.get("columnCount"),
+    "a3BorderWidth": segment_by_id[3].get("borderWidth"),
+    "a3BackgroundColor": segment_by_id[3].get("backgroundColor"),
+    "a3LabelColor": segment_by_id[3].get("labelColor"),
     "a3BackgroundKind": segment_by_id[3].get("backgroundKind"),
     "b1BackgroundKind": segment_by_id[4].get("backgroundKind")
 }, indent=2, ensure_ascii=False))

--
Gitblit v1.9.3