Ariver
2026-06-07 0f1f2221235caab7f7d3ede783c24373c7ed3dd1
Apptag/ContentView.swift
@@ -376,6 +376,7 @@
    @State private var refreshInProgress = false
    @State private var refreshAgainAfterCurrent = false
    @State private var refreshAgainForceLayout = false
    @State private var refreshAgainUseCache = true
    @FocusState private var bubbleNoteFocused: Bool
    // Quick Search
@@ -389,6 +390,7 @@
    @State private var quickSearchSelectionScrollToken = 0
    @State private var quickSearchCloseHidesOverlay = false
    @State private var quickSearchOnlySession = false
    @State private var quickSearchOpenedAt: Date? = nil
    @State private var quickSearchErrorMessage: String? = nil
    @State private var initialQuickSearchConsumed = false
@@ -531,7 +533,7 @@
        }
        .onAppear {
            refreshNotchHeight()
            refreshAppsIfNeeded()
            refreshAppsForOverlay()
            if let initialQuickSearchSource, !initialQuickSearchConsumed {
                initialQuickSearchConsumed = true
                if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
@@ -542,7 +544,9 @@
                    quickSearchVisible = true
                    quickSearchFocusToken &+= 1
                }
                quickSearchOpenedAt = Date()
                refreshQuickSearchResults()
                refreshAppsForQuickSearch()
                NotificationCenter.default.post(
                    name: .tagLauncherQuickSearchVisibilityChanged,
                    object: nil,
@@ -556,7 +560,7 @@
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
            resetTransientDragState()
            refreshNotchHeight()
            refreshAppsIfNeeded()
            refreshAppsForOverlay()
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
            resetTransientDragState()
@@ -568,8 +572,9 @@
            let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
            openQuickSearch(source: source)
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { _ in
            closeQuickSearch()
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { notification in
            let source = notification.userInfo?["source"] as? String
            closeQuickSearch(dismissalSource: source)
        }
        .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
            guard allApps.isEmpty, !refreshInProgress else { return }
@@ -663,7 +668,7 @@
                    .ignoresSafeArea()
                    .contentShape(Rectangle())
                    .onTapGesture {
                        closeQuickSearch()
                        closeQuickSearch(dismissalSource: QuickSearchDismissSource.backdrop)
                    }
                    .zIndex(899)
@@ -673,7 +678,7 @@
                    selectedID: quickSearchSelectedID,
                    focusToken: quickSearchFocusToken,
                    selectionScrollToken: quickSearchSelectionScrollToken,
                    isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
                    isLoading: quickSearchShouldShowLoading,
                    maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
                    panelTopY: quickSearchPanelTopY(in: proxy.size),
                    panelHeight: quickSearchPanelContentHeight(in: proxy.size),
@@ -692,7 +697,7 @@
    }
    private func quickSearchPanelContentHeight(in size: CGSize) -> CGFloat {
        let hasResultList = !(quickSearchDocuments.isEmpty && refreshInProgress)
        let hasResultList = !quickSearchShouldShowLoading
            && quickSearchErrorMessage == nil
            && !quickSearchResults.isEmpty
        let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
@@ -726,11 +731,13 @@
        quickSearchCloseHidesOverlay = quickSearchCloseHidesOverlay
            || source == QuickSearchOpenSource.globalHidden
        quickSearchVisible = true
        quickSearchOpenedAt = Date()
        quickSearchQuery = ""
        quickSearchManualSelection = false
        quickSearchErrorMessage = nil
        quickSearchFocusToken &+= 1
        refreshQuickSearchResults()
        refreshAppsForQuickSearch()
        NotificationCenter.default.post(
            name: .tagLauncherQuickSearchVisibilityChanged,
            object: nil,
@@ -749,10 +756,30 @@
            && !quickSearchVisible
    }
    private func closeQuickSearch(notify: Bool = true, hideOverlayIfNeeded: Bool = true) {
    private var quickSearchShouldShowLoading: Bool {
        quickSearchVisible && refreshInProgress && quickSearchResults.isEmpty
    }
    private func closeQuickSearch(
        notify: Bool = true,
        hideOverlayIfNeeded: Bool = true,
        dismissalSource: String? = nil
    ) {
        Diagnostics.log("content.quickSearch.closeRequest", [
            "source": dismissalSource,
            "notify": notify,
            "hideOverlayIfNeeded": hideOverlayIfNeeded,
            "quickSearchVisible": quickSearchVisible,
            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
            "quickSearchOnlySession": quickSearchOnlySession
        ])
        if shouldIgnoreEarlyQuickSearchDismiss(from: dismissalSource) {
            return
        }
        let shouldHideOverlay = quickSearchVisible && quickSearchCloseHidesOverlay && hideOverlayIfNeeded
        guard quickSearchVisible else { return }
        quickSearchVisible = false
        quickSearchOpenedAt = nil
        quickSearchQuery = ""
        quickSearchResults = []
        quickSearchSelectedID = nil
@@ -775,9 +802,29 @@
        }
    }
    private func shouldIgnoreEarlyQuickSearchDismiss(from source: String?) -> Bool {
        guard quickSearchVisible,
              let quickSearchOpenedAt,
              source == QuickSearchDismissSource.mouse || source == QuickSearchDismissSource.backdrop
        else { return false }
        let age = Date().timeIntervalSince(quickSearchOpenedAt)
        let gracePeriod: TimeInterval = quickSearchOnlySession ? 1.25 : 0.30
        guard age < gracePeriod else { return false }
        Diagnostics.log("content.quickSearch.ignoredEarlyDismiss", [
            "source": source,
            "age": String(format: "%.3f", age),
            "gracePeriod": String(format: "%.2f", gracePeriod)
        ])
        return true
    }
    private func refreshQuickSearchResults() {
        let previousSelection = quickSearchSelectedID
        quickSearchResults = QuickSearchEngine.search(quickSearchQuery, documents: quickSearchDocuments)
        quickSearchResults = QuickSearchEngine.search(
            quickSearchQuery,
            documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
        )
        if quickSearchResults.isEmpty {
            quickSearchSelectedID = nil
@@ -796,6 +843,11 @@
    }
    private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
        Diagnostics.log("content.quickSearch.command", [
            "command": String(describing: command),
            "quickSearchCloseHidesOverlay": quickSearchCloseHidesOverlay,
            "quickSearchOnlySession": quickSearchOnlySession
        ])
        switch command {
        case .moveUp:
            moveQuickSearchSelection(by: -1)
@@ -1761,6 +1813,22 @@
        refreshApps()
    }
    private func refreshAppsForOverlay() {
        // Each overlay show creates a fresh ContentView with empty `allApps`. The indexer
        // cache may still be warm from a prior overlay/settings scan, so path-signature
        // alone must not skip the first hydrate or the grid spinner never clears.
        guard allApps.isEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return }
        refreshApps()
    }
    private func refreshAppsForQuickSearch() {
        guard allApps.isEmpty
            || quickSearchDocuments.isEmpty
            || AppIndexer.shouldRefreshForSearchPathChanges()
        else { return }
        refreshApps()
    }
    private func refreshNotchHeight() {
        let mousePoint = NSEvent.mouseLocation
        let activeScreen = NSScreen.screens.first(where: {
@@ -2369,18 +2437,17 @@
        }
    }
    func refreshApps(forceLayoutRefresh: Bool = false) {
    func refreshApps(forceLayoutRefresh: Bool = false, useCache: Bool = true) {
        guard !refreshInProgress else {
            if forceLayoutRefresh {
                refreshAgainAfterCurrent = true
                refreshAgainForceLayout = true
            }
            refreshAgainAfterCurrent = true
            refreshAgainForceLayout = refreshAgainForceLayout || forceLayoutRefresh
            refreshAgainUseCache = refreshAgainUseCache && useCache
            return
        }
        refreshInProgress = true
        DispatchQueue.global(qos: .userInitiated).async {
            let result = AppLibraryController.refresh()
            let result = AppLibraryController.refresh(useCache: useCache)
            DispatchQueue.main.async {
                applyAppLibrarySnapshot(result.snapshot)
                handleSmartStartRunResult(result.smartStartResult)
@@ -2390,9 +2457,11 @@
                refreshInProgress = false
                if refreshAgainAfterCurrent {
                    let shouldForceLayout = refreshAgainForceLayout
                    let shouldUseCache = refreshAgainUseCache
                    refreshAgainAfterCurrent = false
                    refreshAgainForceLayout = false
                    refreshApps(forceLayoutRefresh: shouldForceLayout)
                    refreshAgainUseCache = true
                    refreshApps(forceLayoutRefresh: shouldForceLayout, useCache: shouldUseCache)
                }
            }
        }
@@ -2417,6 +2486,13 @@
    ) {
        appGridInteraction.appDragModeActive = false
        endTagNavReorder()
        guard FileManager.default.fileExists(atPath: app.path.path) else {
            AppIndexer.invalidateScanCache()
            refreshApps()
            onFailure?()
            return
        }
        let closeQuickSearchAndOverlayBeforeOpening = closeQuickSearchOnSuccess
            && closeOverlayOnSuccess
            && (quickSearchCloseHidesOverlay || quickSearchOnlySession)