Ariver
2026-06-12 9c898b0ca61c090e5e327f750f6c73e28475e10b
Fix space filter visual and close verification
5 files modified
145 ■■■■ changed files
C1.source/Resources/Aligner-Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchRootView.swift 59 ●●●● patch | view | raw | blame | history
C1.source/Sources/Aligner/QuickSwitchSessionController.swift 4 ●●●● patch | view | raw | blame | history
C3.tools/round1-close-fixture-qa.sh 24 ●●●●● patch | view | raw | blame | history
C3.tools/round1-space-filter-fixture-qa.sh 54 ●●●●● patch | view | raw | blame | history
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>
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),
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,
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))
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))