From 72219babd9bf0a4ce6fc26af91ee969a8705f053 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 11 Jun 2026 19:50:27 +0800
Subject: [PATCH] Refine Quick Switch Space filter interactions

---
 C1.source/Sources/Aligner/SpaceActivationService.swift       |   39 ++++
 C3.tools/round1-space-filter-fixture-qa.sh                   |  163 +++++++++++++++++++
 C1.source/Sources/Aligner/QuickSwitchRootView.swift          |   79 ++++++++-
 C1.source/Resources/Aligner-Info.plist                       |    4 
 C1.source/Sources/Aligner/AlignerApplicationDelegate.swift   |   10 +
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |  179 ++++++++++++++++++++++
 6 files changed, 450 insertions(+), 24 deletions(-)

diff --git a/C1.source/Resources/Aligner-Info.plist b/C1.source/Resources/Aligner-Info.plist
index 19cd854..391051b 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.53</string>
+    <string>0.0.54</string>
 	<key>CFBundleVersion</key>
-    <string>20260611.1607</string>
+    <string>20260611.1947</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 b2d0fa9..4fcad5f 100644
--- a/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
+++ b/C1.source/Sources/Aligner/AlignerApplicationDelegate.swift
@@ -354,6 +354,7 @@
             let controller = QuickSwitchSessionController(
                 snapshotLoader: quickSwitchSnapshotLoader(),
                 windowActivationService: quickSwitchWindowActivationService(),
+                spaceActivationService: quickSwitchSpaceActivationService(),
                 windowCloseService: quickSwitchWindowCloseService(),
                 disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
                 closeConfirmationRequired: quickSwitchConfirmBeforeClose,
@@ -428,6 +429,7 @@
             let controller = QuickSwitchSessionController(
                 snapshotLoader: quickSwitchSnapshotLoader(),
                 windowActivationService: quickSwitchWindowActivationService(),
+                spaceActivationService: quickSwitchSpaceActivationService(),
                 windowCloseService: quickSwitchWindowCloseService(),
                 disableScreenshotRefresh: round1QuickSwitchOptions.disableScreenshotRefresh,
                 closeConfirmationRequired: quickSwitchConfirmBeforeClose,
@@ -546,6 +548,14 @@
         return CGWindowAXWindowService(spaceIDsByWindowIDProvider: { _ in [:] })
     }
 
+    private func quickSwitchSpaceActivationService() -> any SpaceActivationServiceProtocol {
+        if round1QuickSwitchOptions.debugWindowActivation {
+            return DebugSpaceActivationService()
+        }
+
+        return PrivateSpaceActivationService()
+    }
+
     private func quickSwitchWindowCloseService() -> any WindowCloseServiceProtocol {
         if round1QuickSwitchOptions.debugWindowClose {
             return DebugWindowCloseService()
diff --git a/C1.source/Sources/Aligner/QuickSwitchRootView.swift b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
index dba70d2..3ed74c0 100644
--- a/C1.source/Sources/Aligner/QuickSwitchRootView.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchRootView.swift
@@ -82,9 +82,12 @@
     private var lastProjectionTransitionFadeInLayerCount = 0
     private var lastProjectionTransitionFadeOutLayerCount = 0
     private var lastProjectionTransitionDurationMilliseconds: Double = 0
+    private var suppressEventInput = false
+    private var suppressEventBackgroundClicks = false
     var onEscape: (() -> Void)?
     var onCommitSelection: ((QuickSwitchSelection, QuickSwitchCommitSource) -> Void)?
-    var onSpaceLaneClick: ((UInt64) -> Void)?
+    var onSpaceLaneClick: ((UInt64, Int) -> Void)?
+    var onBackgroundClick: (() -> Bool)?
     var onRequestClose: ((QuickSwitchCloseRequest) -> Void)?
     var onCloseConfirmationDisabled: (() -> Void)?
 
@@ -379,6 +382,13 @@
     }
 
     override func keyDown(with event: NSEvent) {
+        if suppressEventInput {
+            DevelopmentDiagnostics.log("quickSwitch.view.keyDown.ignoredForDebugSequence", [
+                "keyCode": event.keyCode
+            ])
+            return
+        }
+
         switch event.keyCode {
         case TriggerKeyCode.escape where Self.shouldDismissOnEscape(event):
             onEscape?()
@@ -462,6 +472,15 @@
     }
 
     override func mouseDown(with event: NSEvent) {
+        if suppressEventInput {
+            DevelopmentDiagnostics.log("quickSwitch.view.mouseDown.ignoredForDebugSequence", [
+                "x": event.locationInWindow.x,
+                "y": event.locationInWindow.y,
+                "bounds": NSStringFromRect(bounds)
+            ])
+            return
+        }
+
         if handleCloseConfirmationMouseDown(at: event.locationInWindow) {
             return
         }
@@ -477,7 +496,7 @@
         }
 
         if let segment = spaceLaneSegment(at: event.locationInWindow) {
-            clickSpaceLaneSegment(segment)
+            clickSpaceLaneSegment(segment, clickCount: max(1, event.clickCount))
             return
         }
 
@@ -491,6 +510,9 @@
             "y": event.locationInWindow.y,
             "bounds": NSStringFromRect(bounds)
         ])
+        if !suppressEventBackgroundClicks, onBackgroundClick?() == true {
+            return
+        }
         super.mouseDown(with: event)
     }
 
@@ -532,6 +554,15 @@
                 break
             }
         }
+    }
+
+    func setSuppressEventBackgroundClicks(_ suppressed: Bool) {
+        suppressEventBackgroundClicks = suppressed
+    }
+
+    func setSuppressEventInput(_ suppressed: Bool) {
+        suppressEventInput = suppressed
+        suppressEventBackgroundClicks = suppressed
     }
 
     func beginPerformanceFirstFrameMeasurement() {
@@ -1207,8 +1238,21 @@
             }
             recordMouseCommand("click-space:\(spaceID)")
             if let segment = spaceLaneSegment(spaceID: spaceID) {
-                clickSpaceLaneSegment(segment)
+                clickSpaceLaneSegment(segment, clickCount: 1)
             }
+        case "double-click-space" where parts.count == 2:
+            guard let spaceID = UInt64(parts[1]) else {
+                recordMouseCommand("invalid:\(command)")
+                return
+            }
+            recordMouseCommand("double-click-space:\(spaceID)")
+            if let segment = spaceLaneSegment(spaceID: spaceID) {
+                clickSpaceLaneSegment(segment, clickCount: 1)
+                clickSpaceLaneSegment(segment, clickCount: 2)
+            }
+        case "click-background":
+            recordMouseCommand("click-background")
+            _ = onBackgroundClick?()
         case "hover-card" where parts.count == 3:
             guard let appGroupIndex = Int(parts[1]), let windowIndex = Int(parts[2]) else {
                 recordMouseCommand("invalid:\(command)")
@@ -1324,13 +1368,14 @@
         focusSpace(segment.space.id, persistent: false, source: source)
     }
 
-    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers) {
+    private func clickSpaceLaneSegment(_ segment: SpaceLaneSegmentLayers, clickCount: Int = 1) {
         DevelopmentDiagnostics.log("quickSwitch.view.clickSpaceLane", [
             "spaceID": segment.space.id,
-            "label": segment.space.label
+            "label": segment.space.label,
+            "clickCount": clickCount
         ])
         lastSpaceLaneClickSpaceID = segment.space.id
-        onSpaceLaneClick?(segment.space.id)
+        onSpaceLaneClick?(segment.space.id, clickCount)
         lastSpaceLaneClickSpaceID = segment.space.id
     }
 
@@ -2443,9 +2488,11 @@
         let isActive = isHovered || isFocused || isLocked
         let isAssociated = (isAppAssociated || isWindowAssociated) && !isActive
 
-        segment.containerLayer.borderWidth = (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
+        segment.containerLayer.borderWidth = isLocked
+            ? 2.75
+            : (hasWindows || isCurrent || isActive || isAssociated) ? 1.5 : 1
         segment.containerLayer.borderColor = (isActive
-            ? spaceLaneOccupiedColor.withAlphaComponent(0.62)
+            ? spaceLaneOccupiedColor.withAlphaComponent(isLocked ? 0.92 : 0.62)
             : isAssociated
                 ? spaceLaneOccupiedColor.withAlphaComponent(0.50)
                 : hasWindows
@@ -2472,17 +2519,20 @@
                 colors: spaceLaneIdleOccupiedGradientColors()
             )
         }
+        segment.containerLayer.shadowColor = isLocked
+            ? NSColor.white.withAlphaComponent(0.98).cgColor
+            : NSColor.black.cgColor
         segment.containerLayer.shadowOpacity = (hasWindows || isLocked)
-            ? (isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
+            ? (isLocked ? 0.72 : isActive ? 0.26 : isAssociated ? 0.18 : isCurrent ? 0.14 : 0)
             : 0
         segment.containerLayer.shadowRadius = (hasWindows || isLocked)
-            ? (isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
+            ? (isLocked ? 30 : isActive ? 24 : isAssociated ? 18 : isCurrent ? 12 : 0)
             : 0
         segment.containerLayer.shadowOffset = CGSize(
             width: 0,
-            height: (hasWindows || isLocked) ? (isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
+            height: (hasWindows || isLocked) ? (isLocked ? 0 : isActive ? -6 : isAssociated ? -4 : isCurrent ? -2.5 : 0) : 0
         )
-        segment.containerLayer.zPosition = isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
+        segment.containerLayer.zPosition = isLocked ? 42 : isActive ? 30 : isAssociated ? 18 : isCurrent ? 10 : 0
         segment.containerLayer.transform = (isActive || isAssociated)
             ? CATransform3DMakeScale(isActive ? 1.03 : 1.015, isActive ? 1.03 : 1.015, 1)
             : CATransform3DIdentity
@@ -4426,8 +4476,13 @@
                     "gradientColors": gradientColorDictionaries(from: segment.containerLayer),
                     "borderColor": colorDictionary(from: segment.containerLayer.borderColor),
                     "borderWidth": Double(segment.containerLayer.borderWidth),
+                    "shadowColor": colorDictionary(from: segment.containerLayer.shadowColor),
                     "shadowOpacity": Double(segment.containerLayer.shadowOpacity),
                     "shadowRadius": Double(segment.containerLayer.shadowRadius),
+                    "shadowOffset": [
+                        "width": Double(segment.containerLayer.shadowOffset.width),
+                        "height": Double(segment.containerLayer.shadowOffset.height)
+                    ],
                     "appFrame": dictionary(from: segment.appLayer.frame),
                     "frame": dictionary(from: segment.containerLayer.frame),
                     "visibleFrame": dictionary(from: visibleFrame),
diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index 5a36765..03e9501 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -15,6 +15,7 @@
     private let snapshotLoader: any QuickSwitchSnapshotLoading
     private let screenshotProvider: any ScreenshotProviderProtocol
     private let windowActivationService: any WindowActivationServiceProtocol
+    private let spaceActivationService: any SpaceActivationServiceProtocol
     private let windowCloseService: any WindowCloseServiceProtocol
     private let systemCriticalWindowDetector: any SystemCriticalWindowDetecting
     private let disableScreenshotRefresh: Bool
@@ -35,6 +36,8 @@
     private var lockedSpaceFilterID: UInt64?
     private var selectionBeforeSpaceFilter: QuickSwitchSelection?
     private var lastSpaceFilterAction: String?
+    private var pendingLockedSpaceUnlockSpaceID: UInt64?
+    private var pendingLockedSpaceUnlockTask: Task<Void, Never>?
     private var lastSnapshotError: String?
     private var snapshotTask: Task<Void, Never>?
     private var screenshotTask: Task<Void, Never>?
@@ -52,6 +55,11 @@
     private var lastActivationWindowID: UInt32?
     private var lastActivationResult: WindowActivationResult?
     private var lastActivationError: String?
+    private var lastSpaceActivationSpaceID: UInt64?
+    private var lastSpaceActivationDidRequestFocus: Bool?
+    private var lastSpaceActivationDisplayIdentifier: String?
+    private var lastSpaceActivationPreviousCurrentSpaceID: UInt64?
+    private var lastSpaceActivationError: String?
     private var lastCloseTargetKind: String?
     private var lastCloseAppGroupIndex: Int?
     private var lastCloseAppName: String?
@@ -106,6 +114,7 @@
         windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService(
             spaceIDsByWindowIDProvider: { _ in [:] }
         ),
+        spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(),
         windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService(
             spaceIDsByWindowIDProvider: { _ in [:] }
         ),
@@ -122,6 +131,7 @@
         self.snapshotLoader = snapshotLoader
         self.screenshotProvider = screenshotProvider
         self.windowActivationService = windowActivationService
+        self.spaceActivationService = spaceActivationService
         self.windowCloseService = windowCloseService
         self.systemCriticalWindowDetector = systemCriticalWindowDetector
         self.disableScreenshotRefresh = disableScreenshotRefresh
@@ -155,6 +165,11 @@
         lastActivationWindowID = nil
         lastActivationResult = nil
         lastActivationError = nil
+        lastSpaceActivationSpaceID = nil
+        lastSpaceActivationDidRequestFocus = nil
+        lastSpaceActivationDisplayIdentifier = nil
+        lastSpaceActivationPreviousCurrentSpaceID = nil
+        lastSpaceActivationError = nil
         lastCloseTargetKind = nil
         lastCloseAppGroupIndex = nil
         lastCloseAppName = nil
@@ -163,6 +178,7 @@
         lastCloseError = nil
         pendingCloseVerification = nil
         suppressedCloseTargets = []
+        cancelPendingLockedSpaceUnlock()
         lockedSpaceFilterID = nil
         selectionBeforeSpaceFilter = nil
         lastSpaceFilterAction = nil
@@ -187,9 +203,13 @@
         view.onCommitSelection = { [weak self] selection, source in
             self?.commitSelection(selection, source: source)
         }
-        view.onSpaceLaneClick = { [weak self] spaceID in
-            self?.handleSpaceLaneClick(spaceID)
+        view.onSpaceLaneClick = { [weak self] spaceID, clickCount in
+            self?.handleSpaceLaneClick(spaceID, clickCount: clickCount)
         }
+        view.onBackgroundClick = { [weak self] in
+            self?.handleBackgroundClick() ?? false
+        }
+        view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
         view.onRequestClose = { [weak self] request in
             self?.requestClose(request)
         }
@@ -228,6 +248,7 @@
         snapshotTask = nil
         screenshotTask?.cancel()
         screenshotTask = nil
+        cancelPendingLockedSpaceUnlock()
         pendingCloseVerification = nil
         view.clearCloseFeedback()
         invalidateSystemCriticalMonitoring()
@@ -288,6 +309,11 @@
             "lastActivationWindowID": lastActivationWindowID ?? NSNull(),
             "lastActivationResult": lastActivationResult.map(Self.activationResultString) ?? NSNull(),
             "lastActivationError": lastActivationError ?? NSNull(),
+            "lastSpaceActivationSpaceID": lastSpaceActivationSpaceID ?? NSNull(),
+            "lastSpaceActivationDidRequestFocus": lastSpaceActivationDidRequestFocus ?? NSNull(),
+            "lastSpaceActivationDisplayIdentifier": lastSpaceActivationDisplayIdentifier ?? NSNull(),
+            "lastSpaceActivationPreviousCurrentSpaceID": lastSpaceActivationPreviousCurrentSpaceID ?? NSNull(),
+            "lastSpaceActivationError": lastSpaceActivationError ?? NSNull(),
             "lastCloseTargetKind": lastCloseTargetKind ?? NSNull(),
             "lastCloseAppGroupIndex": lastCloseAppGroupIndex ?? NSNull(),
             "lastCloseAppName": lastCloseAppName ?? NSNull(),
@@ -534,7 +560,31 @@
         onSnapshotUpdated?()
     }
 
-    private func handleSpaceLaneClick(_ spaceID: UInt64) {
+    private func handleSpaceLaneClick(_ spaceID: UInt64, clickCount: Int) {
+        if clickCount >= 2 {
+            if pendingLockedSpaceUnlockSpaceID == spaceID {
+                cancelPendingLockedSpaceUnlock()
+                activateLockedSpace(spaceID)
+            } else {
+                DevelopmentDiagnostics.log("quickSwitch.spaceFilter.doubleClick.ignored", [
+                    "spaceID": spaceID,
+                    "lockedSpaceID": lockedSpaceFilterID ?? NSNull(),
+                    "pendingUnlockSpaceID": pendingLockedSpaceUnlockSpaceID ?? NSNull()
+                ])
+            }
+            return
+        }
+
+        cancelPendingLockedSpaceUnlock()
+        if lockedSpaceFilterID == spaceID {
+            scheduleLockedSpaceUnlock(spaceID)
+            return
+        }
+
+        applySpaceLaneSingleClick(spaceID)
+    }
+
+    private func applySpaceLaneSingleClick(_ spaceID: UInt64) {
         guard let sourceViewModel else {
             lastSpaceFilterAction = "blockedNoSourceViewModel"
             DevelopmentDiagnostics.log("quickSwitch.spaceFilter.click.blocked", [
@@ -605,6 +655,129 @@
         onSnapshotUpdated?()
     }
 
+    @discardableResult
+    private func handleBackgroundClick() -> Bool {
+        guard lockedSpaceFilterID != nil else { return false }
+
+        cancelPendingLockedSpaceUnlock()
+        let unlocked = unlockSpaceFilter(action: "unlockBackground")
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.backgroundUnlock", [
+            "unlocked": unlocked
+        ])
+        return unlocked
+    }
+
+    private func scheduleLockedSpaceUnlock(_ spaceID: UInt64) {
+        pendingLockedSpaceUnlockSpaceID = spaceID
+        let delay = max(0.12, NSEvent.doubleClickInterval + 0.03)
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.unlock.pending", [
+            "spaceID": spaceID,
+            "delay": delay
+        ])
+
+        pendingLockedSpaceUnlockTask = Task { @MainActor [weak self] in
+            try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+            guard !Task.isCancelled,
+                  let self,
+                  self.pendingLockedSpaceUnlockSpaceID == spaceID
+            else {
+                return
+            }
+
+            self.pendingLockedSpaceUnlockSpaceID = nil
+            self.pendingLockedSpaceUnlockTask = nil
+            self.applySpaceLaneSingleClick(spaceID)
+        }
+    }
+
+    private func cancelPendingLockedSpaceUnlock() {
+        pendingLockedSpaceUnlockTask?.cancel()
+        pendingLockedSpaceUnlockTask = nil
+        pendingLockedSpaceUnlockSpaceID = nil
+    }
+
+    @discardableResult
+    private func unlockSpaceFilter(action: String) -> Bool {
+        guard let sourceViewModel, lockedSpaceFilterID != nil else {
+            return false
+        }
+
+        lockedSpaceFilterID = nil
+        let preferredSelection = selectionBeforeSpaceFilter
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = action
+
+        let visibleViewModel = projectedViewModel(
+            from: sourceViewModel,
+            preferredSelection: preferredSelection
+        )
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+
+        if disableScreenshotRefresh {
+            view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
+            screenshotTask?.cancel()
+            screenshotTask = nil
+        } else {
+            scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
+        }
+
+        onSnapshotUpdated?()
+        return true
+    }
+
+    private func activateLockedSpace(_ spaceID: UInt64) {
+        guard lockedSpaceFilterID == spaceID else {
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.ignored", [
+                "spaceID": spaceID,
+                "lockedSpaceID": lockedSpaceFilterID ?? NSNull()
+            ])
+            return
+        }
+
+        guard let sourceViewModel else {
+            lastSpaceFilterAction = "blockedNoSourceViewModel"
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.blocked", [
+                "spaceID": spaceID,
+                "reason": "noSourceViewModel"
+            ])
+            onSnapshotUpdated?()
+            return
+        }
+
+        lockedSpaceFilterID = nil
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = "activateSpace"
+        lastSpaceActivationSpaceID = spaceID
+        lastSpaceActivationDidRequestFocus = nil
+        lastSpaceActivationDisplayIdentifier = nil
+        lastSpaceActivationPreviousCurrentSpaceID = nil
+        lastSpaceActivationError = nil
+
+        let visibleViewModel = projectedViewModel(from: sourceViewModel)
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.start", [
+            "spaceID": spaceID
+        ])
+        hide(reason: .userClosed)
+        let outcome = spaceActivationService.activate(spaceID: spaceID)
+        lastSpaceActivationSpaceID = outcome.targetSpaceID
+        lastSpaceActivationDidRequestFocus = outcome.didRequestFocus
+        lastSpaceActivationDisplayIdentifier = outcome.displayIdentifier
+        lastSpaceActivationPreviousCurrentSpaceID = outcome.previousCurrentSpaceID
+        lastSpaceActivationError = outcome.error
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.activateSpace.result", [
+            "spaceID": outcome.targetSpaceID,
+            "displayIdentifier": outcome.displayIdentifier ?? NSNull(),
+            "previousCurrentSpaceID": outcome.previousCurrentSpaceID ?? NSNull(),
+            "didRequestFocus": outcome.didRequestFocus,
+            "error": outcome.error ?? NSNull()
+        ])
+        onSnapshotUpdated?()
+    }
+
     private func requestClose(_ request: QuickSwitchCloseRequest) {
         DevelopmentDiagnostics.log("quickSwitch.close.request.start", [
             "targetKind": request.kindDescription,
diff --git a/C1.source/Sources/Aligner/SpaceActivationService.swift b/C1.source/Sources/Aligner/SpaceActivationService.swift
new file mode 100644
index 0000000..4fa14fe
--- /dev/null
+++ b/C1.source/Sources/Aligner/SpaceActivationService.swift
@@ -0,0 +1,39 @@
+import Foundation
+
+protocol SpaceActivationServiceProtocol: AnyObject {
+    func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome
+}
+
+final class PrivateSpaceActivationService: SpaceActivationServiceProtocol {
+    private let bridge: PrivateSpaceActivationBridge?
+
+    init(bridge: PrivateSpaceActivationBridge? = .shared) {
+        self.bridge = bridge
+    }
+
+    func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome {
+        guard let outcome = bridge?.focus(spaceIDs: [spaceID]) else {
+            return PrivateSpaceFocusOutcome(
+                targetSpaceID: spaceID,
+                displayIdentifier: nil,
+                previousCurrentSpaceID: nil,
+                didRequestFocus: false,
+                error: "spaceActivationBridgeUnavailable"
+            )
+        }
+
+        return outcome
+    }
+}
+
+final class DebugSpaceActivationService: SpaceActivationServiceProtocol {
+    func activate(spaceID: UInt64) -> PrivateSpaceFocusOutcome {
+        PrivateSpaceFocusOutcome(
+            targetSpaceID: spaceID,
+            displayIdentifier: "debug",
+            previousCurrentSpaceID: nil,
+            didRequestFocus: true,
+            error: nil
+        )
+    }
+}
diff --git a/C3.tools/round1-space-filter-fixture-qa.sh b/C3.tools/round1-space-filter-fixture-qa.sh
index cc1b1a2..bf0dec4 100755
--- a/C3.tools/round1-space-filter-fixture-qa.sh
+++ b/C3.tools/round1-space-filter-fixture-qa.sh
@@ -69,8 +69,9 @@
 wait_for_visible_report() {
   local report="$1"
   local expected_last_mouse="$2"
+  local expected_action="${3:-}"
 
-  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" <<'PY'
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" "$expected_action" <<'PY'
 import json
 import sys
 import time
@@ -78,6 +79,7 @@
 path = sys.argv[1]
 timeout = float(sys.argv[2])
 expected_last_mouse = sys.argv[3]
+expected_action = sys.argv[4]
 deadline = time.monotonic() + timeout
 last_report = None
 
@@ -91,6 +93,7 @@
             report.get("snapshotLoaded") is True
             and report.get("quickSwitchVisible") is True
             and root.get("lastMouseCommand") == expected_last_mouse
+            and (expected_action == "" or report.get("lastSpaceFilterAction") == expected_action)
         ):
             sys.exit(0)
     except FileNotFoundError:
@@ -101,7 +104,11 @@
 
 if last_report is not None:
     print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr)
-print(f"visible report did not reach mouse command {expected_last_mouse!r} within {timeout:.1f}s", file=sys.stderr)
+print(
+    f"visible report did not reach mouse command {expected_last_mouse!r}"
+    f" and action {expected_action!r} within {timeout:.1f}s",
+    file=sys.stderr
+)
 sys.exit(1)
 PY
 }
@@ -148,11 +155,54 @@
 PY
 }
 
+wait_for_space_activation_report() {
+  local report="$1"
+  local expected_last_mouse="$2"
+
+  /usr/bin/python3 - "$report" "$REPORT_WAIT" "$expected_last_mouse" <<'PY'
+import json
+import sys
+import time
+
+path = sys.argv[1]
+timeout = float(sys.argv[2])
+expected_last_mouse = sys.argv[3]
+deadline = time.monotonic() + timeout
+last_report = None
+
+while time.monotonic() < deadline:
+    try:
+        with open(path, "r", encoding="utf-8") as file:
+            report = json.load(file)
+        last_report = report
+        root = report.get("rootView", {})
+        if (
+            report.get("snapshotLoaded") is True
+            and report.get("quickSwitchVisible") is False
+            and report.get("lastSpaceFilterAction") == "activateSpace"
+            and report.get("lastSpaceActivationSpaceID") is not None
+            and root.get("lastMouseCommand") == expected_last_mouse
+        ):
+            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"space activation report did not reach mouse command {expected_last_mouse!r} within {timeout:.1f}s", file=sys.stderr)
+sys.exit(1)
+PY
+}
+
 run_report() {
   local report="$1"
   local sequence="$2"
   local expected_last_mouse="$3"
   local quick_switch_expectation="${4:-visible}"
+  local expected_action="${5:-}"
 
   rm -f "$report"
   "$APP/Contents/MacOS/Aligner" \
@@ -169,8 +219,11 @@
   if [ "$quick_switch_expectation" = "hidden" ]; then
     wait_for_activation_report "$report" "$expected_last_mouse"
     swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
+  elif [ "$quick_switch_expectation" = "space-hidden" ]; then
+    wait_for_space_activation_report "$report" "$expected_last_mouse"
+    swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-no-quick-switch >&2
   elif [ "$quick_switch_expectation" = "visible" ]; then
-    wait_for_visible_report "$report" "$expected_last_mouse"
+    wait_for_visible_report "$report" "$expected_last_mouse" "$expected_action"
     swift "$SCRIPT_DIR/window-logic-qa.swift" --expect-quick-switch >&2
   else
     fail "unknown quick switch expectation: $quick_switch_expectation"
@@ -228,6 +281,11 @@
 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) >= 2.5, "locked Space must use a stronger blue border")
+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")
+require(segment_by_id[1].get("shadowRadius", 0) >= 26, "locked Space glow radius must be strong enough")
 require("locked" not in segment_by_id[5].get("visualStates", []), "single fullscreen Space must not be marked locked")
 require(root.get("projectionTransitionFadeOutLayerCount", 0) >= 1, "filter transition must fade out removed visual layers")
 require(1 <= root.get("projectionTransitionDurationMilliseconds", 0) <= 220, "filter transition duration must stay within P0 native-feel budget")
@@ -238,6 +296,9 @@
     "columns": root.get("waterfallColumnNames"),
     "cardWindowIDs": [card.get("windowID") for card in cards],
     "lockedSpace": root.get("spaceFilterLockedSpaceID"),
+    "lockedBorderWidth": segment_by_id[1].get("borderWidth"),
+    "lockedShadowOpacity": segment_by_id[1].get("shadowOpacity"),
+    "lockedShadowRadius": segment_by_id[1].get("shadowRadius"),
     "fadeOutLayerCount": root.get("projectionTransitionFadeOutLayerCount"),
     "transitionDurationMilliseconds": root.get("projectionTransitionDurationMilliseconds")
 }, indent=2, ensure_ascii=False))
@@ -316,6 +377,41 @@
 PY
 }
 
+assert_background_unlock_report() {
+  /usr/bin/python3 - "$1" <<'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", {})
+cards = [card for column in root.get("waterfallColumns", []) for card in column.get("cards", [])]
+
+require(report.get("spaceFilterActive") is False, "background click must unlock session filter")
+require(root.get("spaceFilterActive") is False, "background click must unlock root filter")
+require(report.get("lastSpaceFilterAction") == "unlockBackground", "background click must record unlockBackground")
+require(root.get("lastMouseCommand") == "click-background", "last mouse command must be click-background")
+require(report.get("appShelfNames") == ["Alpha Space App", "Beta Space App", "Fullscreen Solo App", "No Space App"], "background unlock must restore all fixture apps")
+require(report.get("windowCount") == 6, "background unlock must restore all six fixture windows")
+require(50901 in [card.get("windowID") for card in cards], "no-space window should be visible again after background unlock")
+
+print(json.dumps({
+    "case": "background-unlock",
+    "apps": report.get("appShelfNames"),
+    "windowCount": report.get("windowCount"),
+    "lastSpaceFilterAction": report.get("lastSpaceFilterAction")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
 assert_empty_a3_report() {
   /usr/bin/python3 - "$1" <<'PY'
 import json
@@ -345,6 +441,10 @@
 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) >= 2.5, "empty locked Space must still use strong locked border")
+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")
 
@@ -355,6 +455,47 @@
     "columnCount": report.get("columnCount"),
     "a3BackgroundKind": segment_by_id[3].get("backgroundKind"),
     "b1BackgroundKind": segment_by_id[4].get("backgroundKind")
+}, indent=2, ensure_ascii=False))
+PY
+}
+
+assert_locked_space_double_click_report() {
+  /usr/bin/python3 - "$1" <<'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", {})
+
+require(report.get("quickSwitchVisible") is False, "double-click locked Space must close Quick Switch")
+require(report.get("spaceFilterActive") is False, "double-click locked Space must release filter")
+require(root.get("spaceFilterActive") is False, "root must show no filter after Space activation")
+require(report.get("lastSpaceFilterAction") == "activateSpace", "double-click locked Space must record activateSpace")
+require(report.get("lastCommittedWindowID") is None, "double-click Space must not commit a window")
+require(report.get("lastActivationWindowID") is None, "double-click Space must not target a window")
+require(report.get("lastActivationResult") is None, "double-click Space must not run window activation")
+require(report.get("lastCommitSource") is None, "double-click Space must not have a commit source")
+require(report.get("lastSpaceActivationSpaceID") == 1, "double-click locked A1 must request Space 1 activation")
+require(report.get("lastSpaceActivationDidRequestFocus") is True, "debug Space activation must request focus")
+require(report.get("lastSpaceActivationDisplayIdentifier") == "debug", "fixture must use debug Space activation service")
+require(report.get("lastSpaceActivationError") is None, "debug Space activation must not fail")
+require(root.get("lastMouseCommand") == "double-click-space:1", "last mouse command must be double-click-space:1")
+
+print(json.dumps({
+    "case": "locked-space-double-click",
+    "quickSwitchVisible": report.get("quickSwitchVisible"),
+    "lastSpaceFilterAction": report.get("lastSpaceFilterAction"),
+    "lastSpaceActivationSpaceID": report.get("lastSpaceActivationSpaceID"),
+    "lastSpaceActivationDisplayIdentifier": report.get("lastSpaceActivationDisplayIdentifier")
 }, indent=2, ensure_ascii=False))
 PY
 }
@@ -408,20 +549,25 @@
 LOCK_A1_REPORT="$REPORT_PREFIX-lock-a1-report.json"
 SWITCH_A2_REPORT="$REPORT_PREFIX-switch-a2-report.json"
 UNLOCK_REPORT="$REPORT_PREFIX-unlock-report.json"
+BACKGROUND_UNLOCK_REPORT="$REPORT_PREFIX-background-unlock-report.json"
 EMPTY_A3_REPORT="$REPORT_PREFIX-empty-a3-report.json"
 FULLSCREEN_REPORT="$REPORT_PREFIX-fullscreen-report.json"
 LOCK_THEN_FULLSCREEN_REPORT="$REPORT_PREFIX-lock-then-fullscreen-report.json"
+LOCKED_DOUBLE_CLICK_REPORT="$REPORT_PREFIX-locked-double-click-report.json"
 
-run_report "$LOCK_A1_REPORT" "click-space:1" "click-space:1" "visible"
+run_report "$LOCK_A1_REPORT" "click-space:1" "click-space:1" "visible" "lock"
 assert_lock_a1_report "$LOCK_A1_REPORT"
 
-run_report "$SWITCH_A2_REPORT" "click-space:1,click-space:2" "click-space:2" "visible"
+run_report "$SWITCH_A2_REPORT" "click-space:1,click-space:2" "click-space:2" "visible" "switch"
 assert_switch_a2_report "$SWITCH_A2_REPORT"
 
-run_report "$UNLOCK_REPORT" "click-space:1,click-space:1" "click-space:1" "visible"
+run_report "$UNLOCK_REPORT" "click-space:1,click-space:1" "click-space:1" "visible" "unlock"
 assert_unlock_report "$UNLOCK_REPORT"
 
-run_report "$EMPTY_A3_REPORT" "click-space:3" "click-space:3" "visible"
+run_report "$BACKGROUND_UNLOCK_REPORT" "click-space:1,click-background" "click-background" "visible" "unlockBackground"
+assert_background_unlock_report "$BACKGROUND_UNLOCK_REPORT"
+
+run_report "$EMPTY_A3_REPORT" "click-space:3" "click-space:3" "visible" "lock"
 assert_empty_a3_report "$EMPTY_A3_REPORT"
 
 run_report "$FULLSCREEN_REPORT" "click-space:5" "click-space:5" "hidden"
@@ -430,6 +576,9 @@
 run_report "$LOCK_THEN_FULLSCREEN_REPORT" "click-space:1,click-space:5" "click-space:5" "hidden"
 assert_single_fullscreen_report "$LOCK_THEN_FULLSCREEN_REPORT" "lock-then-single-fullscreen"
 
+run_report "$LOCKED_DOUBLE_CLICK_REPORT" "click-space:1,double-click-space:1" "double-click-space:1" "space-hidden"
+assert_locked_space_double_click_report "$LOCKED_DOUBLE_CLICK_REPORT"
+
 trap - EXIT
 cleanup
 

--
Gitblit v1.9.3