From 6f2856fa70722dad7f7c69a3addaa05ea9fd6e94 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 19 Jun 2026 22:36:43 +0800
Subject: [PATCH] Add two-key window shortcut codes

---
 C1.source/Sources/Aligner/QuickSwitchSessionController.swift |  810 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 795 insertions(+), 15 deletions(-)

diff --git a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
index fa79190..18d84f0 100644
--- a/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
+++ b/C1.source/Sources/Aligner/QuickSwitchSessionController.swift
@@ -5,13 +5,23 @@
 @MainActor
 final class QuickSwitchSessionController {
     static let overlayTitle = "Aligner Quick Switch"
+    private static let closeRequestStillPresentMessage = "已发送关闭请求,但窗口仍在列表中。"
+    private static let closeTargetNotFoundMessage = "没有找到这个窗口的关闭控件,已刷新列表。"
+    private static let closeUnsupportedMessage = "这个窗口没有可用的关闭按钮。"
+    private static let closeFailedMessage = "关闭请求没有被系统接受。"
+    private static let closeVerificationRetryDelays: [TimeInterval] = [0.35, 0.70, 1.20]
 
     private let view = QuickSwitchRootView()
     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
+    private var waterfallViewMode: QuickSwitchWaterfallViewMode
+    private var closeConfirmationRequired: Bool
+    private let onCloseConfirmationDisabled: (() -> Void)?
     private let debugOverlayWidth: CGFloat?
     private let debugKeySequence: [String]
     private let debugMouseSequence: [String]
@@ -22,7 +32,13 @@
         debugOverlayWidth: debugOverlayWidth
     )
     private var currentSnapshot: QuickSwitchSnapshot?
+    private var sourceViewModel: QuickSwitchViewModel?
     private var currentViewModel: QuickSwitchViewModel?
+    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>?
@@ -40,6 +56,19 @@
     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?
+    private var lastCloseWindowID: UInt32?
+    private var lastCloseResult: WindowCloseResult?
+    private var lastCloseError: String?
+    private var pendingCloseVerification: PendingCloseVerification?
+    private var suppressedCloseTargets: [SuppressedCloseTarget] = []
     private var lastDismissReason: DismissReason?
     private var lastSystemCriticalAction: String?
     private var lastSystemCriticalMonitorSource: String?
@@ -57,6 +86,27 @@
     private var lifecycleResidualOverlayWindows = 0
     private var lifecycleResidualVisibleOverlayWindows = 0
     var onSnapshotUpdated: (() -> Void)?
+    var onToggleWaterfallViewMode: (() -> Void)?
+
+    private struct PendingCloseVerification {
+        let request: QuickSwitchCloseRequest
+        let generation: Int
+        let attempt: Int
+    }
+
+    private enum SuppressedCloseTarget: Equatable {
+        case app(AlignerApp)
+        case window(UInt32)
+
+        init(_ request: QuickSwitchCloseRequest) {
+            switch request {
+            case .app:
+                self = .app(request.app)
+            case .window:
+                self = .window(request.windowID ?? 0)
+            }
+        }
+    }
 
     init(
         snapshotLoader: any QuickSwitchSnapshotLoading = LiveQuickSwitchSnapshotLoader(),
@@ -66,8 +116,15 @@
         windowActivationService: any WindowActivationServiceProtocol = CGWindowAXWindowService(
             spaceIDsByWindowIDProvider: { _ in [:] }
         ),
+        spaceActivationService: any SpaceActivationServiceProtocol = PrivateSpaceActivationService(),
+        windowCloseService: any WindowCloseServiceProtocol = CGWindowAXWindowService(
+            spaceIDsByWindowIDProvider: { _ in [:] }
+        ),
         systemCriticalWindowDetector: any SystemCriticalWindowDetecting = CGWindowSystemCriticalWindowDetector(),
         disableScreenshotRefresh: Bool = false,
+        waterfallViewMode: QuickSwitchWaterfallViewMode = .verticalColumns,
+        closeConfirmationRequired: Bool = true,
+        onCloseConfirmationDisabled: (() -> Void)? = nil,
         debugHoveredAppGroupIndex: Int? = nil,
         debugOverlayWidth: CGFloat? = nil,
         debugKeySequence: [String] = [],
@@ -77,8 +134,13 @@
         self.snapshotLoader = snapshotLoader
         self.screenshotProvider = screenshotProvider
         self.windowActivationService = windowActivationService
+        self.spaceActivationService = spaceActivationService
+        self.windowCloseService = windowCloseService
         self.systemCriticalWindowDetector = systemCriticalWindowDetector
         self.disableScreenshotRefresh = disableScreenshotRefresh
+        self.waterfallViewMode = waterfallViewMode
+        self.closeConfirmationRequired = closeConfirmationRequired
+        self.onCloseConfirmationDisabled = onCloseConfirmationDisabled
         self.debugOverlayWidth = debugOverlayWidth
         self.debugKeySequence = debugKeySequence
         self.debugMouseSequence = debugMouseSequence
@@ -107,6 +169,23 @@
         lastActivationWindowID = nil
         lastActivationResult = nil
         lastActivationError = nil
+        lastSpaceActivationSpaceID = nil
+        lastSpaceActivationDidRequestFocus = nil
+        lastSpaceActivationDisplayIdentifier = nil
+        lastSpaceActivationPreviousCurrentSpaceID = nil
+        lastSpaceActivationError = nil
+        lastCloseTargetKind = nil
+        lastCloseAppGroupIndex = nil
+        lastCloseAppName = nil
+        lastCloseWindowID = nil
+        lastCloseResult = nil
+        lastCloseError = nil
+        pendingCloseVerification = nil
+        suppressedCloseTargets = []
+        cancelPendingLockedSpaceUnlock()
+        lockedSpaceFilterID = nil
+        selectionBeforeSpaceFilter = nil
+        lastSpaceFilterAction = nil
         lastDismissReason = nil
         lastSystemCriticalAction = nil
         lastSystemCriticalMonitorSource = nil
@@ -128,6 +207,26 @@
         view.onCommitSelection = { [weak self] selection, source in
             self?.commitSelection(selection, source: source)
         }
+        view.onSpaceLaneClick = { [weak self] spaceID, clickCount in
+            self?.handleSpaceLaneClick(spaceID, clickCount: clickCount)
+        }
+        view.onBackgroundClick = { [weak self] in
+            self?.handleBackgroundClick() ?? false
+        }
+        view.onToggleWaterfallViewMode = { [weak self] in
+            self?.onToggleWaterfallViewMode?()
+        }
+        view.setSuppressEventInput(!debugMouseSequence.isEmpty || !debugKeySequence.isEmpty)
+        view.onRequestClose = { [weak self] request in
+            self?.requestClose(request)
+        }
+        view.onCloseConfirmationDisabled = { [weak self] in
+            guard let self else { return }
+            self.closeConfirmationRequired = false
+            self.onCloseConfirmationDisabled?()
+        }
+        view.setCloseConfirmationRequired(closeConfirmationRequired)
+        view.setWaterfallViewMode(waterfallViewMode)
         view.apply(viewModel: nil)
         view.beginPerformanceFirstFrameMeasurement()
         coordinator.openQuickSwitch()
@@ -157,6 +256,9 @@
         snapshotTask = nil
         screenshotTask?.cancel()
         screenshotTask = nil
+        cancelPendingLockedSpaceUnlock()
+        pendingCloseVerification = nil
+        view.clearCloseFeedback()
         invalidateSystemCriticalMonitoring()
         coordinator.closeQuickSwitch(reason: reason)
         lifecycleResidualOverlayWindows = overlayWindowCount()
@@ -178,7 +280,44 @@
 
     func retriggerFromShortcut() {
         DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut")
-        show()
+        guard isVisible else {
+            show()
+            return
+        }
+
+        guard currentViewModel != nil else {
+            DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.noSnapshot", [
+                "action": "fallbackShow"
+            ])
+            show()
+            return
+        }
+
+        let didCycle = view.hoverNextAppGroupIndex()
+        DevelopmentDiagnostics.log("quickSwitch.session.retriggerFromShortcut.hoverApp", [
+            "didCycle": didCycle
+        ])
+
+        if !didCycle {
+            show()
+        }
+    }
+
+    func setWaterfallViewMode(_ mode: QuickSwitchWaterfallViewMode) {
+        guard waterfallViewMode != mode else { return }
+
+        waterfallViewMode = mode
+        DevelopmentDiagnostics.log("quickSwitch.session.waterfallViewModeChanged", [
+            "mode": mode.rawValue,
+            "visible": isVisible,
+            "hasViewModel": currentViewModel != nil
+        ])
+
+        view.setWaterfallViewMode(mode)
+        if isVisible, let currentViewModel {
+            view.applyProjected(viewModel: currentViewModel)
+        }
+        onSnapshotUpdated?()
     }
 
     func reportDictionary() -> [String: Any] {
@@ -201,6 +340,9 @@
             "initialSelectionWindowID": currentViewModel?.initialSelection?.windowID ?? NSNull(),
             "appShelfNames": currentViewModel?.appShelf.map(\.app.name) ?? [],
             "spaceLabels": currentViewModel?.displays.flatMap { $0.spaces.map(\.label) } ?? [],
+            "spaceFilterLockedSpaceID": lockedSpaceFilterID ?? NSNull(),
+            "spaceFilterActive": lockedSpaceFilterID != nil,
+            "lastSpaceFilterAction": lastSpaceFilterAction ?? NSNull(),
             "overlayOpenElapsedMilliseconds": overlayOpenElapsedMilliseconds ?? NSNull(),
             "snapshotStartElapsedMilliseconds": snapshotStartElapsedMilliseconds ?? NSNull(),
             "snapshotDurationMilliseconds": snapshotDurationMilliseconds ?? NSNull(),
@@ -212,6 +354,18 @@
             "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(),
+            "lastCloseWindowID": lastCloseWindowID ?? NSNull(),
+            "lastCloseResult": lastCloseResult.map(Self.closeResultString) ?? NSNull(),
+            "lastCloseError": lastCloseError ?? NSNull(),
+            "suppressedCloseTargetCount": suppressedCloseTargets.count,
             "lastDismissReason": lastDismissReason.map(Self.dismissReasonString) ?? NSNull(),
             "overlayLevel": Int(coordinator.currentLevel),
             "overlayMainMenuLevel": Int(CGWindowLevelForKey(.mainMenuWindow)),
@@ -264,16 +418,26 @@
         updateLifecycleWindowHighWaterMark()
     }
 
-    private func scheduleSnapshotRefresh(for generation: Int) {
+    private func scheduleSnapshotRefresh(
+        for generation: Int,
+        replayDebugCommands: Bool = true,
+        delay: TimeInterval = 0
+    ) {
         DevelopmentDiagnostics.log("quickSwitch.snapshot.schedule", [
             "generation": generation,
-            "visible": isVisible
+            "visible": isVisible,
+            "replayDebugCommands": replayDebugCommands,
+            "delayMilliseconds": Int(delay * 1_000)
         ])
         snapshotTask?.cancel()
         let loader = snapshotLoader
         let showStartTime = showStartTime
         snapshotTask = Task { [weak self] in
             guard let self, !Task.isCancelled else { return }
+            if delay > 0 {
+                try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
+                guard !Task.isCancelled else { return }
+            }
 
             let loadTask = Task.detached(priority: .userInitiated) {
                 SnapshotLoadResult.load(using: loader, showStartTime: showStartTime)
@@ -285,12 +449,17 @@
             }
 
             guard !Task.isCancelled else { return }
-            self.applySnapshotResult(result, generation: generation)
+            self.applySnapshotResult(
+                result,
+                generation: generation,
+                replayDebugCommands: replayDebugCommands
+            )
         }
     }
 
     private func clearSnapshotState() {
         currentSnapshot = nil
+        sourceViewModel = nil
         currentViewModel = nil
         lastSnapshotError = nil
     }
@@ -362,7 +531,11 @@
         onSnapshotUpdated?()
     }
 
-    private func commitSelection(_ selection: QuickSwitchSelection, source: QuickSwitchCommitSource) {
+    private func commitSelection(
+        _ selection: QuickSwitchSelection,
+        source: QuickSwitchCommitSource,
+        in viewModel: QuickSwitchViewModel? = nil
+    ) {
         DevelopmentDiagnostics.log("quickSwitch.activation.commit.start", [
             "source": source.rawValue,
             "appGroupIndex": selection.appGroupIndex,
@@ -374,7 +547,7 @@
         lastActivationWindowID = selection.windowID
         lastActivationError = nil
 
-        guard let window = window(for: selection) else {
+        guard let window = window(for: selection, in: viewModel ?? currentViewModel) else {
             lastActivationResult = .windowNotFound
             DevelopmentDiagnostics.log("quickSwitch.activation.commit.windowNotFound", [
                 "source": source.rawValue,
@@ -432,6 +605,455 @@
         onSnapshotUpdated?()
     }
 
+    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", [
+                "spaceID": spaceID,
+                "reason": "noSourceViewModel"
+            ])
+            onSnapshotUpdated?()
+            return
+        }
+
+        if let fullscreenSelection = QuickSwitchSpaceFilterPolicy.singleFullscreenSelection(
+            inSpaceID: spaceID,
+            viewModel: sourceViewModel
+        ) {
+            lockedSpaceFilterID = nil
+            selectionBeforeSpaceFilter = nil
+            lastSpaceFilterAction = "activateSingleFullscreen"
+            let visibleViewModel = projectedViewModel(from: sourceViewModel)
+            currentViewModel = visibleViewModel
+            view.applyProjected(viewModel: visibleViewModel)
+            DevelopmentDiagnostics.log("quickSwitch.spaceFilter.singleFullscreenActivate", [
+                "spaceID": spaceID,
+                "windowID": fullscreenSelection.windowID
+            ])
+            commitSelection(fullscreenSelection, source: .mouse, in: sourceViewModel)
+            return
+        }
+
+        let preferredSelection: QuickSwitchSelection?
+        if lockedSpaceFilterID == spaceID {
+            lockedSpaceFilterID = nil
+            preferredSelection = selectionBeforeSpaceFilter
+            selectionBeforeSpaceFilter = nil
+            lastSpaceFilterAction = "unlock"
+        } else {
+            if lockedSpaceFilterID == nil {
+                selectionBeforeSpaceFilter = view.effectiveSelectionForProjection()
+                lastSpaceFilterAction = "lock"
+            } else {
+                lastSpaceFilterAction = "switch"
+            }
+            lockedSpaceFilterID = spaceID
+            preferredSelection = nil
+        }
+
+        let visibleViewModel = projectedViewModel(
+            from: sourceViewModel,
+            preferredSelection: preferredSelection
+        )
+        currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+        DevelopmentDiagnostics.log("quickSwitch.spaceFilter.apply", [
+            "spaceID": spaceID,
+            "lockedSpaceID": lockedSpaceFilterID,
+            "action": lastSpaceFilterAction,
+            "visibleAppCount": visibleViewModel.appShelf.count,
+            "visibleWindowCount": visibleViewModel.windowCount
+        ])
+
+        if disableScreenshotRefresh {
+            view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
+            screenshotTask?.cancel()
+            screenshotTask = nil
+        } else {
+            scheduleScreenshotRefresh(for: visibleViewModel, generation: sessionGeneration)
+        }
+
+        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,
+            "appGroupIndex": request.appGroupIndex,
+            "appName": request.appName,
+            "windowID": request.windowID,
+            "titleHash": DevelopmentDiagnostics.stableFingerprint(request.title),
+            "titleLength": request.title.count
+        ])
+        lastCloseTargetKind = request.kindDescription
+        lastCloseAppGroupIndex = request.appGroupIndex
+        lastCloseAppName = request.appName
+        lastCloseWindowID = request.windowID
+        lastCloseError = nil
+        pendingCloseVerification = nil
+        view.showCloseProgress(for: request)
+
+        do {
+            let result: WindowCloseResult
+            switch request {
+            case .app:
+                result = try windowCloseService.close(app: request.app)
+            case .window:
+                guard let window = request.window else {
+                    lastCloseResult = .windowNotFound
+                    suppressCloseTarget(request)
+                    view.clearCloseFeedback()
+                    onSnapshotUpdated?()
+                    return
+                }
+                result = try windowCloseService.close(window: window)
+            }
+            lastCloseResult = result
+            DevelopmentDiagnostics.log("quickSwitch.close.request.result", [
+                "targetKind": request.kindDescription,
+                "appGroupIndex": request.appGroupIndex,
+                "appName": request.appName,
+                "windowID": request.windowID,
+                "result": Self.closeResultString(result)
+            ])
+            if result == .requested {
+                DevelopmentDiagnostics.log("quickSwitch.close.request.refreshAfterClose", [
+                    "targetKind": request.kindDescription,
+                    "appGroupIndex": request.appGroupIndex,
+                    "appName": request.appName,
+                    "windowID": request.windowID,
+                    "optimisticRemove": false
+                ])
+                pendingCloseVerification = PendingCloseVerification(
+                    request: request,
+                    generation: sessionGeneration,
+                    attempt: 0
+                )
+                scheduleSnapshotRefresh(
+                    for: sessionGeneration,
+                    replayDebugCommands: false,
+                    delay: Self.closeVerificationRetryDelays[0]
+                )
+            } else if result == .windowNotFound {
+                suppressCloseTarget(request)
+                view.clearCloseFeedback()
+                DevelopmentDiagnostics.log("quickSwitch.close.request.staleTargetRemoved", [
+                    "targetKind": request.kindDescription,
+                    "appGroupIndex": request.appGroupIndex,
+                    "appName": request.appName,
+                    "windowID": request.windowID
+                ])
+                scheduleSnapshotRefresh(
+                    for: sessionGeneration,
+                    replayDebugCommands: false,
+                    delay: 0.10
+                )
+            } else {
+                view.showCloseFailure(message: closeFailureMessage(for: result), for: request)
+            }
+        } catch {
+            let errorSummary = DevelopmentDiagnostics.errorSummaryString(error)
+            lastCloseResult = .failed(errorSummary)
+            lastCloseError = errorSummary
+            pendingCloseVerification = nil
+            view.showCloseFailure(message: Self.closeFailedMessage, for: request)
+            var fields: [String: CustomStringConvertible?] = [
+                "targetKind": request.kindDescription,
+                "appGroupIndex": request.appGroupIndex,
+                "appName": request.appName,
+                "windowID": request.windowID,
+                "titleHash": DevelopmentDiagnostics.stableFingerprint(request.title),
+                "titleLength": request.title.count
+            ]
+            DevelopmentDiagnostics.errorSummaryFields(error).forEach { fields[$0.key] = $0.value }
+            DevelopmentDiagnostics.log("quickSwitch.close.request.error", fields)
+        }
+
+        onSnapshotUpdated?()
+    }
+
+    private func suppressCloseTarget(_ request: QuickSwitchCloseRequest) {
+        let target = SuppressedCloseTarget(request)
+        if !suppressedCloseTargets.contains(target) {
+            suppressedCloseTargets.append(target)
+        }
+
+        guard let sourceViewModel else { return }
+        let visibleViewModel = projectedViewModel(from: sourceViewModel, preferredSelection: view.effectiveSelectionForProjection())
+        self.currentViewModel = visibleViewModel
+        view.applyProjected(viewModel: visibleViewModel)
+        DevelopmentDiagnostics.log("quickSwitch.close.optimisticRemove", [
+            "targetKind": request.kindDescription,
+            "appGroupIndex": request.appGroupIndex,
+            "appName": request.appName,
+            "windowID": request.windowID,
+            "remainingWindowCount": visibleViewModel.windowCount,
+            "suppressedCloseTargetCount": suppressedCloseTargets.count
+        ])
+    }
+
+    private func viewModel(
+        _ viewModel: QuickSwitchViewModel,
+        excluding suppressedTargets: [SuppressedCloseTarget]
+    ) -> QuickSwitchViewModel {
+        guard !suppressedTargets.isEmpty else { return viewModel }
+
+        var nextGlobalIndex = 0
+        let columns = viewModel.waterfallColumns.compactMap { column -> QuickSwitchWaterfallColumnViewModel? in
+            let windows = column.windows.filter { card in
+                !suppressedTargets.contains { target in
+                    suppressedTarget(target, matches: card)
+                }
+            }
+            guard !windows.isEmpty else { return nil }
+
+            let reindexedWindows = windows.enumerated().map { windowIndex, card in
+                defer { nextGlobalIndex += 1 }
+                return QuickSwitchWindowCardViewModel(
+                    appGroupIndex: column.appGroupIndex,
+                    windowIndex: windowIndex,
+                    globalIndex: nextGlobalIndex,
+                    window: card.window,
+                    primarySpaceID: card.primarySpaceID,
+                    primarySpaceLabel: card.primarySpaceLabel
+                )
+            }
+
+            return QuickSwitchWaterfallColumnViewModel(
+                appGroupIndex: column.appGroupIndex,
+                app: column.app,
+                windows: reindexedWindows
+            )
+        }
+
+        let columnsByAppGroupIndex = Dictionary(uniqueKeysWithValues: columns.map { ($0.appGroupIndex, $0) })
+        let appShelf = viewModel.appShelf.compactMap { item -> QuickSwitchAppShelfItemViewModel? in
+            guard let column = columnsByAppGroupIndex[item.appGroupIndex] else { return nil }
+            return QuickSwitchAppShelfItemViewModel(
+                appGroupIndex: item.appGroupIndex,
+                app: item.app,
+                windowCount: column.windows.count,
+                primarySpaceIDs: orderedPrimarySpaceIDs(in: column.windows),
+                hasMinimizedWindows: column.windows.contains { $0.window.isMinimized },
+                hasFullscreenWindows: column.windows.contains { $0.window.isFullscreen }
+            )
+        }
+
+        return QuickSwitchViewModel(
+            displays: viewModel.displays,
+            appShelf: appShelf,
+            waterfallColumns: columns,
+            initialSelection: initialSelection(in: columns, fallback: viewModel.initialSelection),
+            lockedSpaceID: viewModel.lockedSpaceID
+        )
+    }
+
+    private func projectedViewModel(
+        from sourceViewModel: QuickSwitchViewModel,
+        preferredSelection: QuickSwitchSelection? = nil
+    ) -> QuickSwitchViewModel {
+        let closeProjectedViewModel = viewModel(sourceViewModel, excluding: suppressedCloseTargets)
+        return QuickSwitchSpaceFilterPolicy.projectedViewModel(
+            from: closeProjectedViewModel,
+            lockedSpaceID: lockedSpaceFilterID,
+            preferredSelection: preferredSelection
+        )
+    }
+
+    private func orderedPrimarySpaceIDs(in cards: [QuickSwitchWindowCardViewModel]) -> [UInt64] {
+        var seen = Set<UInt64>()
+        var result: [UInt64] = []
+        for card in cards {
+            guard let spaceID = card.primarySpaceID,
+                  !seen.contains(spaceID)
+            else {
+                continue
+            }
+            seen.insert(spaceID)
+            result.append(spaceID)
+        }
+        return result
+    }
+
+    private func suppressedTarget(
+        _ target: SuppressedCloseTarget,
+        matches card: QuickSwitchWindowCardViewModel
+    ) -> Bool {
+        switch target {
+        case .window(let windowID):
+            return card.window.id == windowID
+        case .app(let app):
+            return appMatches(card.window.app, app)
+        }
+    }
+
+    private func initialSelection(
+        in columns: [QuickSwitchWaterfallColumnViewModel],
+        fallback: QuickSwitchSelection?
+    ) -> QuickSwitchSelection? {
+        if let fallback,
+           columns.contains(where: { column in
+               column.appGroupIndex == fallback.appGroupIndex
+                   && column.windows.contains { $0.window.id == fallback.windowID }
+           }) {
+            return fallback
+        }
+
+        guard let firstCard = columns.first?.windows.first else { return nil }
+        return QuickSwitchSelection(
+            appGroupIndex: firstCard.appGroupIndex,
+            windowIndex: firstCard.windowIndex,
+            windowID: firstCard.window.id
+        )
+    }
+
     private func shouldCloseAfterActivation(_ result: WindowActivationResult?) -> Bool {
         switch result {
         case .activated, .restored, .appActivatedOnly:
@@ -441,8 +1063,11 @@
         }
     }
 
-    private func window(for selection: QuickSwitchSelection) -> AlignerWindow? {
-        currentViewModel?
+    private func window(
+        for selection: QuickSwitchSelection,
+        in viewModel: QuickSwitchViewModel?
+    ) -> AlignerWindow? {
+        viewModel?
             .waterfallColumns
             .flatMap(\.windows)
             .first { card in
@@ -453,7 +1078,11 @@
             .window
     }
 
-    private func applySnapshotResult(_ result: SnapshotLoadResult, generation: Int) {
+    private func applySnapshotResult(
+        _ result: SnapshotLoadResult,
+        generation: Int,
+        replayDebugCommands: Bool
+    ) {
         guard generation == sessionGeneration, isVisible else {
             DevelopmentDiagnostics.log("quickSwitch.snapshot.drop", [
                 "resultGeneration": generation,
@@ -466,15 +1095,23 @@
         switch result {
         case .success(let success):
             currentSnapshot = success.snapshot
-            currentViewModel = success.viewModel
+            sourceViewModel = success.viewModel
+            let visibleViewModel = projectedViewModel(
+                from: success.viewModel,
+                preferredSelection: view.effectiveSelectionForProjection()
+            )
+            currentViewModel = visibleViewModel
             lastSnapshotError = nil
             snapshotStartElapsedMilliseconds = success.timing.startElapsedMilliseconds
             snapshotDurationMilliseconds = success.timing.durationMilliseconds
             snapshotRanOnMainThread = success.timing.ranOnMainThread
-            view.apply(viewModel: success.viewModel)
+            view.apply(viewModel: visibleViewModel)
             view.recordPerformanceFirstFrameIfNeeded()
-            view.performDebugKeyboardCommands(debugKeySequence)
-            view.performDebugMouseCommands(debugMouseSequence)
+            if replayDebugCommands {
+                view.performDebugKeyboardCommands(debugKeySequence)
+                view.performDebugMouseCommands(debugMouseSequence)
+            }
+            verifyPendingCloseIfNeeded(in: success.viewModel)
             DevelopmentDiagnostics.log("quickSwitch.snapshot.success", [
                 "generation": generation,
                 "displayCount": success.viewModel.displays.count,
@@ -487,17 +1124,18 @@
             ])
             guard isVisible else { return }
             if disableScreenshotRefresh {
-                view.markScreenshotsNotRequested(for: success.viewModel, reason: "disabledByLaunchOption")
+                view.markScreenshotsNotRequested(for: visibleViewModel, reason: "disabledByLaunchOption")
                 screenshotTask?.cancel()
                 screenshotTask = nil
                 DevelopmentDiagnostics.log("quickSwitch.screenshot.disabledByLaunchOption", [
                     "generation": generation
                 ])
             } else {
-                scheduleScreenshotRefresh(for: success.viewModel, generation: generation)
+                scheduleScreenshotRefresh(for: visibleViewModel, generation: generation)
             }
         case .failure(let failure):
             currentSnapshot = nil
+            sourceViewModel = nil
             currentViewModel = nil
             lastSnapshotError = failure.message
             snapshotStartElapsedMilliseconds = failure.timing.startElapsedMilliseconds
@@ -506,6 +1144,7 @@
             view.apply(viewModel: nil)
             screenshotTask?.cancel()
             screenshotTask = nil
+            failPendingCloseVerificationIfNeeded(reason: "snapshotFailure")
             DevelopmentDiagnostics.log("quickSwitch.snapshot.failure", [
                 "generation": generation,
                 "message": failure.message,
@@ -515,6 +1154,104 @@
         }
 
         onSnapshotUpdated?()
+    }
+
+    private func verifyPendingCloseIfNeeded(in viewModel: QuickSwitchViewModel) {
+        guard let pendingCloseVerification else { return }
+
+        self.pendingCloseVerification = nil
+        let request = pendingCloseVerification.request
+        let stillPresent = closeTargetStillPresent(request, in: viewModel)
+        DevelopmentDiagnostics.log("quickSwitch.close.verify", [
+            "targetKind": request.kindDescription,
+            "appGroupIndex": request.appGroupIndex,
+            "appName": request.appName,
+            "windowID": request.windowID,
+            "requestedGeneration": pendingCloseVerification.generation,
+            "currentGeneration": sessionGeneration,
+            "attempt": pendingCloseVerification.attempt,
+            "stillPresent": stillPresent
+        ])
+
+        if stillPresent {
+            let nextAttempt = pendingCloseVerification.attempt + 1
+            if nextAttempt < Self.closeVerificationRetryDelays.count {
+                self.pendingCloseVerification = PendingCloseVerification(
+                    request: request,
+                    generation: sessionGeneration,
+                    attempt: nextAttempt
+                )
+                scheduleSnapshotRefresh(
+                    for: sessionGeneration,
+                    replayDebugCommands: false,
+                    delay: Self.closeVerificationRetryDelays[nextAttempt]
+                )
+            } else {
+                view.showCloseFailure(message: Self.closeRequestStillPresentMessage, for: request)
+            }
+        } else {
+            view.clearCloseFeedback()
+        }
+    }
+
+    private func failPendingCloseVerificationIfNeeded(reason: String) {
+        guard let pendingCloseVerification else { return }
+
+        self.pendingCloseVerification = nil
+        let request = pendingCloseVerification.request
+        DevelopmentDiagnostics.log("quickSwitch.close.verify.failed", [
+            "targetKind": request.kindDescription,
+            "appGroupIndex": request.appGroupIndex,
+            "appName": request.appName,
+            "windowID": request.windowID,
+            "requestedGeneration": pendingCloseVerification.generation,
+            "currentGeneration": sessionGeneration,
+            "reason": reason
+        ])
+        view.showCloseFailure(message: Self.closeFailedMessage, for: request)
+    }
+
+    private func closeFailureMessage(for result: WindowCloseResult) -> String {
+        switch result {
+        case .requested:
+            return Self.closeRequestStillPresentMessage
+        case .windowNotFound:
+            return Self.closeTargetNotFoundMessage
+        case .unsupported:
+            return Self.closeUnsupportedMessage
+        case .appNotFound, .failed:
+            return Self.closeFailedMessage
+        }
+    }
+
+    private func closeTargetStillPresent(
+        _ request: QuickSwitchCloseRequest,
+        in viewModel: QuickSwitchViewModel
+    ) -> Bool {
+        switch request {
+        case .window:
+            guard let windowID = request.windowID else { return false }
+            return viewModel.waterfallColumns
+                .flatMap(\.windows)
+                .contains { $0.window.id == windowID }
+        case .app:
+            return viewModel.appShelf.contains { appMatches($0.app, request.app) }
+                || viewModel.waterfallColumns.contains { appMatches($0.app, request.app) }
+        }
+    }
+
+    private func appMatches(_ candidate: AlignerApp, _ target: AlignerApp) -> Bool {
+        if let candidatePID = candidate.processIdentifier,
+           let targetPID = target.processIdentifier {
+            return candidatePID == targetPID
+        }
+
+        if !candidate.bundleIdentifier.isEmpty,
+           candidate.bundleIdentifier == target.bundleIdentifier {
+            return true
+        }
+
+        return candidate.name == target.name
     }
 
     private func scheduleScreenshotRefresh(
@@ -650,6 +1387,21 @@
         }
     }
 
+    private static func closeResultString(_ result: WindowCloseResult) -> String {
+        switch result {
+        case .requested:
+            return "requested"
+        case .windowNotFound:
+            return "windowNotFound"
+        case .appNotFound:
+            return "appNotFound"
+        case .unsupported:
+            return "unsupported"
+        case .failed(let message):
+            return "failed(\(message))"
+        }
+    }
+
     private static func screenshotSourceString(_ source: ScreenshotResolutionSource) -> String {
         switch source {
         case .realScreenshot:
@@ -742,15 +1494,21 @@
     let snapshotLoadDelay: TimeInterval?
     let fixtureAppCount: Int?
     let fixtureWindowsPerApp: Int?
+    let fixtureMultiDisplay: Bool
     let fixtureCandidateFiltering: Bool
     let fixtureActivation: Bool
+    let fixtureSpaceFilter: Bool
+    let fixtureSplitView: Bool
+    let fixtureMultiPageIdentity: Bool
     let debugHoveredAppGroupIndex: Int?
     let debugOverlayWidth: CGFloat?
     let debugKeySequence: [String]
     let debugMouseSequence: [String]
     let debugSystemCriticalAfter: TimeInterval?
     let debugWindowActivation: Bool
+    let debugWindowClose: Bool
     let disableScreenshotRefresh: Bool
+    let waterfallViewMode: QuickSwitchWaterfallViewMode?
     let lifecycleCycles: Int?
     let lifecycleInterval: TimeInterval
     let lifecycleVisibleDuration: TimeInterval
@@ -766,15 +1524,21 @@
             snapshotLoadDelay: timeInterval(for: "--round01-snapshot-load-delay", in: arguments),
             fixtureAppCount: intValue(for: "--round01-fixture-app-count", in: arguments),
             fixtureWindowsPerApp: intValue(for: "--round01-fixture-windows-per-app", in: arguments),
+            fixtureMultiDisplay: arguments.contains("--round01-fixture-multi-display"),
             fixtureCandidateFiltering: arguments.contains("--round01-fixture-candidate-filtering"),
             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),
             debugMouseSequence: stringListValue(for: "--round01-debug-mouse-sequence", in: arguments),
             debugSystemCriticalAfter: timeInterval(for: "--round01-debug-system-critical-after", in: arguments),
             debugWindowActivation: arguments.contains("--round01-debug-window-activation"),
+            debugWindowClose: arguments.contains("--round01-debug-window-close"),
             disableScreenshotRefresh: arguments.contains("--round01-disable-screenshot-refresh"),
+            waterfallViewMode: waterfallViewModeValue(for: "--round01-waterfall-view-mode", in: arguments),
             lifecycleCycles: intValue(for: "--round01-quick-switch-lifecycle-cycles", in: arguments),
             lifecycleInterval: timeInterval(for: "--round01-quick-switch-lifecycle-interval", in: arguments) ?? 0.02,
             lifecycleVisibleDuration: timeInterval(for: "--round01-quick-switch-lifecycle-visible-duration", in: arguments) ?? 0.16
@@ -799,6 +1563,22 @@
         return String(argument.dropFirst(prefix.count))
     }
 
+    private static func waterfallViewModeValue(
+        for key: String,
+        in arguments: [String]
+    ) -> QuickSwitchWaterfallViewMode? {
+        guard let value = stringValue(for: key, in: arguments) else { return nil }
+
+        switch value {
+        case "vertical", "vertical-columns", "verticalColumns":
+            return .verticalColumns
+        case "horizontal", "horizontal-masonry", "horizontalMasonry":
+            return .horizontalMasonry
+        default:
+            return nil
+        }
+    }
+
     private static func intValue(for key: String, in arguments: [String]) -> Int? {
         stringValue(for: key, in: arguments).flatMap(Int.init)
     }

--
Gitblit v1.9.3