Ariver
2026-06-27 369c8f03db2f6d507697ab2296db5e6f81c8cd1e
src/Apptag/ContentView.swift
@@ -16,7 +16,10 @@
enum SettingsTabTarget {
    static let userInfoKey = "tab"
    static let theme = "theme"
    static let tags = "tags"
    static let data = "data"
    static let about = "about"
}
// MARK: - Edit Phase
@@ -326,8 +329,11 @@
    var editingBubble: AppBubbleContext? = nil
    var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil
    var pendingTagRemovalDrop: PendingTagRemovalDrop? = nil
    var proPrompt: PendingProPrompt? = nil
    var uncategorizedDropSuppressFuturePrompt = false
    var tagRemovalDropSuppressFuturePrompt = false
    var usageTipsCloseReminderVisible = false
    var usageTipsCloseSuppressFuturePrompt = false
    var appDragResetToken = 0
    var bubbleDraftNote = ""
    var tagNavigationHoveredGroupName: String? = nil
@@ -355,11 +361,18 @@
    let tagName: String
}
private struct PendingProPrompt: Identifiable {
    let id = UUID()
    let feature: ProFeature
    let noteQuotaStatus: ProNoteQuotaStatus?
}
struct ContentView: View {
    let hideOverlay: () -> Void
    private let initialQuickSearchSource: String?
    @Environment(\.colorScheme) private var colorScheme
    @ObservedObject private var proEntitlement = ProEntitlementCenter.shared
    @State private var allApps: [AppInfo] = []
    @State private var displayGroups: [TagGroup] = []
    @State private var tagColors: [String: Int] = [:]
@@ -438,6 +451,7 @@
    @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
    @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
    @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
    @AppStorage("skipUsageTipsCloseReminder") private var skipUsageTipsCloseReminder = false
    private let editSidebarWidth: CGFloat = 188
    private let editSidebarHorizontalInset: CGFloat = 12
@@ -449,6 +463,8 @@
            || appGridInteraction.appDragModeActive
            || appGridInteraction.pendingUncategorizedDrop != nil
            || appGridInteraction.pendingTagRemovalDrop != nil
            || appGridInteraction.usageTipsCloseReminderVisible
            || appGridInteraction.proPrompt != nil
    }
    private var appGridHighlightedGroupName: String? {
        appGridInteraction.tagNavigationHoveredGroupName
@@ -468,7 +484,9 @@
    }
    private var renderedAppGridTheme: AppGridTheme {
        editPhase == .none ? appGridTheme : .defaultLight
        editPhase == .none
            ? ProEntitlementPolicy.effectiveTheme(for: appGridTheme)
            : .defaultLight
    }
    private var isSideLayout: Bool {
@@ -508,176 +526,174 @@
    }
    var body: some View {
        contentStack
            .onAppear(perform: handleContentAppear)
            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow), perform: handleOverlayDidShow)
            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide), perform: handleOverlayDidHide)
            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested), perform: handleQuickSearchRequested)
            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested), perform: handleQuickSearchDismissRequested)
            .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification), perform: handleApplicationDidBecomeActive)
            .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange), perform: handleDataDidChange)
            .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange), perform: handleAppLanguageDidChange)
            .onChange(of: editPhase) { _, newPhase in
                let active = newPhase != .none
                dismissAppBubble()
                NotificationCenter.default.post(
                    name: .tagLauncherEditModeChanged,
                    object: nil,
                    userInfo: ["active": active]
                )
            }
            .onChange(of: bubbleNoteFocused) { _, focused in
                if appGridInteraction.editingBubble != nil && !focused {
                    commitBubbleNote()
                }
            }
            .onChange(of: draggedTagNames) { _, newOrder in
                rebuildDisplayGroups(apps: allApps, tagOrder: newOrder)
            }
            .onChange(of: defaultGroupName) { _, _ in
                rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
            }
            .onChange(of: quickSearchQuery) { _, _ in
                quickSearchErrorMessage = nil
                refreshQuickSearchResults()
            }
            .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in
                publishModalInteractionState()
            }
            .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
                publishModalInteractionState()
            }
            .onChange(of: appGridInteraction.proPrompt != nil) { _, _ in
                publishModalInteractionState()
            }
            .onChange(of: smartStartNotice != nil) { _, _ in
                publishModalInteractionState()
            }
            .onChange(of: proEntitlement.accessState) { _, newState in
                handleProAccessStateChange(newState)
            }
            .onDisappear {
                NotificationCenter.default.post(
                    name: .tagLauncherModalInteractionChanged,
                    object: nil,
                    userInfo: ["active": false]
                )
            }
    }
    @ViewBuilder
    private var contentStack: some View {
        ZStack {
            if shouldRenderAppGridBehindQuickSearch {
                appGridBackground
            }
            if shouldRenderAppGridBehindQuickSearch {
                if notchHeight > 0 {
                    VStack {
                        Rectangle().fill(.black)
                            .frame(height: notchHeight)
                            .ignoresSafeArea(edges: .top)
                        Spacer()
                    }
                    .allowsHitTesting(false)
                }
                switch editPhase {
                case .none:
                    normalContent
                case .editingTags:
                    editTagsView
                case .editingApps:
                    editAppsView
                }
                uncommonAppBubbleOverlay
                smartStartNoticeOverlay
                editActionFeedbackOverlay
                uncategorizedDropConfirmOverlay
                tagRemovalDropConfirmOverlay
                appGridInteractionLayer
            }
            quickSearchOverlay
            if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
                Text(message)
                    .font(.system(size: 16, weight: .semibold))
                    .foregroundStyle(.primary)
                    .padding(.horizontal, 22)
                    .padding(.vertical, 12)
                    .background(
                        RoundedRectangle(cornerRadius: 10)
                            .fill(.ultraThickMaterial)
                            .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
                    )
                    .transition(.scale(scale: 0.96).combined(with: .opacity))
                    .allowsHitTesting(false)
            }
            if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
                Color.black.opacity(0.08)
                    .ignoresSafeArea()
                    .transition(.opacity)
                    .allowsHitTesting(false)
                ProgressView()
                    .progressViewStyle(.circular)
                    .controlSize(.large)
                    .scaleEffect(1.15)
                    .padding(24)
                    .background(
                        RoundedRectangle(cornerRadius: 12)
                            .fill(.ultraThickMaterial)
                            .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
                    )
                    .transition(.scale(scale: 0.92).combined(with: .opacity))
                    .allowsHitTesting(false)
            }
            dropWarningToastOverlay
            dropRefreshOverlay
        }
        .onAppear {
            refreshNotchHeight()
            refreshAppsForOverlay()
            if let initialQuickSearchSource, !initialQuickSearchConsumed {
                initialQuickSearchConsumed = true
                if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
                    quickSearchCloseHidesOverlay = true
                    quickSearchOnlySession = true
                }
                if !quickSearchVisible {
                    quickSearchVisible = true
                    quickSearchFocusToken &+= 1
                }
                quickSearchOpenedAt = Date()
                refreshQuickSearchResults()
                refreshAppsForQuickSearch()
                NotificationCenter.default.post(
                    name: .tagLauncherQuickSearchVisibilityChanged,
                    object: nil,
                    userInfo: [
                        "active": true,
                        "hideOverlayOnClose": quickSearchCloseHidesOverlay
                    ]
    }
    @ViewBuilder
    private var appGridInteractionLayer: some View {
        if notchHeight > 0 {
            VStack {
                Rectangle().fill(.black)
                    .frame(height: notchHeight)
                    .ignoresSafeArea(edges: .top)
                Spacer()
            }
            .allowsHitTesting(false)
        }
        switch editPhase {
        case .none:
            normalContent
        case .editingTags:
            editTagsView
        case .editingApps:
            editAppsView
        }
        uncommonAppBubbleOverlay
        smartStartNoticeOverlay
        editActionFeedbackOverlay
        uncategorizedDropConfirmOverlay
        tagRemovalDropConfirmOverlay
        usageTipsCloseReminderOverlay
        proAccessPromptOverlay
    }
    @ViewBuilder
    private var dropWarningToastOverlay: some View {
        if shouldRenderAppGridBehindQuickSearch, let message = appGridInteraction.dropWarningToast {
            Text(message)
                .font(.system(size: 16, weight: .semibold))
                .foregroundStyle(.primary)
                .padding(.horizontal, 22)
                .padding(.vertical, 12)
                .background(
                    RoundedRectangle(cornerRadius: 10)
                        .fill(.ultraThickMaterial)
                        .shadow(color: .black.opacity(0.22), radius: 18, y: 10)
                )
                .transition(.scale(scale: 0.96).combined(with: .opacity))
                .allowsHitTesting(false)
        }
    }
    @ViewBuilder
    private var proAccessPromptOverlay: some View {
        if let prompt = appGridInteraction.proPrompt {
            ZStack {
                Color.black.opacity(0.16)
                    .ignoresSafeArea()
                ProUpgradePromptView(
                    title: tr(prompt.feature.titleKey),
                    message: proPromptMessage(for: prompt),
                    benefitText: tr(prompt.feature.benefitKey),
                    operationText: proEntitlement.operationState.statusMessageKey.map(tr),
                    unlockTitle: tr("pro.card.unlock"),
                    restoreTitle: tr("pro.card.restore"),
                    isBusy: proEntitlement.operationState.isBusy,
                    onClose: dismissProPrompt,
                    onUnlock: { proEntitlement.purchasePro() },
                    onRestore: { proEntitlement.restorePurchases() }
                )
            }
            .transition(.opacity)
            .zIndex(920)
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
            resetTransientDragState()
            refreshNotchHeight()
            refreshAppsForOverlay()
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
            resetTransientDragState()
            closeQuickSearch(notify: true, hideOverlayIfNeeded: false)
            quickSearchOnlySession = false
            quickSearchCloseHidesOverlay = false
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested)) { notification in
            let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
            openQuickSearch(source: source)
        }
        .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 }
            refreshApps()
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherDataDidChange)) { _ in
            refreshApps(forceLayoutRefresh: true)
        }
        .onReceive(NotificationCenter.default.publisher(for: .appLanguageDidChange)) { _ in
            let appsSnapshot = allApps
            DispatchQueue.global(qos: .userInitiated).async {
                _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
                _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
                DispatchQueue.main.async {
                    refreshApps(forceLayoutRefresh: true)
                }
            }
        }
        .onChange(of: editPhase) { _, newPhase in
            let active = newPhase != .none
            dismissAppBubble()
            NotificationCenter.default.post(
                name: .tagLauncherEditModeChanged,
                object: nil,
                userInfo: ["active": active]
            )
        }
        .onChange(of: bubbleNoteFocused) { _, focused in
            if appGridInteraction.editingBubble != nil && !focused {
                commitBubbleNote()
            }
        }
        .onChange(of: draggedTagNames) { _, newOrder in
            rebuildDisplayGroups(apps: allApps, tagOrder: newOrder)
        }
        .onChange(of: defaultGroupName) { _, _ in
            rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
        }
        .onChange(of: quickSearchQuery) { _, _ in
            quickSearchErrorMessage = nil
            refreshQuickSearchResults()
        }
        .onChange(of: appGridInteraction.pendingUncategorizedDrop != nil) { _, _ in
            publishModalInteractionState()
        }
        .onChange(of: appGridInteraction.pendingTagRemovalDrop != nil) { _, _ in
            publishModalInteractionState()
        }
        .onChange(of: smartStartNotice != nil) { _, _ in
            publishModalInteractionState()
        }
        .onDisappear {
            NotificationCenter.default.post(
                name: .tagLauncherModalInteractionChanged,
                object: nil,
                userInfo: ["active": false]
            )
    }
    @ViewBuilder
    private var dropRefreshOverlay: some View {
        if shouldRenderAppGridBehindQuickSearch && appGridInteraction.dropRefreshVisible {
            Color.black.opacity(0.08)
                .ignoresSafeArea()
                .transition(.opacity)
                .allowsHitTesting(false)
            ProgressView()
                .progressViewStyle(.circular)
                .controlSize(.large)
                .scaleEffect(1.15)
                .padding(24)
                .background(
                    RoundedRectangle(cornerRadius: 12)
                        .fill(.ultraThickMaterial)
                        .shadow(color: .black.opacity(0.26), radius: 24, y: 12)
                )
                .transition(.scale(scale: 0.92).combined(with: .opacity))
                .allowsHitTesting(false)
        }
    }
@@ -689,6 +705,8 @@
                "active": smartStartNotice != nil
                    || appGridInteraction.pendingUncategorizedDrop != nil
                    || appGridInteraction.pendingTagRemovalDrop != nil
                    || appGridInteraction.usageTipsCloseReminderVisible
                    || appGridInteraction.proPrompt != nil
            ]
        )
    }
@@ -1242,7 +1260,7 @@
                    },
                    onScrollActivity: handleAppGridScrollActivity,
                    onDragModeChange: { setAppDragMode($0) },
                    onHideUsageTips: { hideUsageTips = true },
                    onHideUsageTips: requestHideUsageTips,
                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                )
            }
@@ -1271,6 +1289,8 @@
                    isEditing: editing,
                    placement: placement,
                    arrowOffset: metrics.arrowOffset,
                    noteQuotaText: editing ? noteQuotaHintText(for: context.app) : nil,
                    noteQuotaWarning: editing && isNoteQuotaWarning(for: context.app),
                    draftNote: $appGridInteraction.bubbleDraftNote,
                    noteFocused: $bubbleNoteFocused,
                    onCommit: commitBubbleNote,
@@ -1691,6 +1711,34 @@
        .allowsHitTesting(appGridInteraction.pendingTagRemovalDrop != nil)
    }
    private var usageTipsCloseReminderOverlay: some View {
        GeometryReader { proxy in
            if appGridInteraction.usageTipsCloseReminderVisible {
                ZStack {
                    Color.black.opacity(0.14)
                        .ignoresSafeArea()
                    UsageTipsCloseReminderBubble(
                        title: tr("usageTips.closeReminder.title"),
                        message: tr("usageTips.closeReminder.message"),
                        previewCallout: tr("usageTips.closeReminder.previewCallout"),
                        doNotRemindTitle: tr("usageTips.closeReminder.doNotRemind"),
                        doNotRemind: $appGridInteraction.usageTipsCloseSuppressFuturePrompt,
                        confirmTitle: tr("edit.confirm"),
                        onCancel: dismissUsageTipsCloseReminder,
                        onConfirm: confirmUsageTipsCloseReminder
                    )
                    .frame(width: min(760, max(420, proxy.size.width - 120)))
                    .position(x: proxy.size.width / 2, y: proxy.size.height / 2)
                    .transition(.scale(scale: 0.94).combined(with: .opacity))
                }
                .zIndex(712)
            }
        }
        .ignoresSafeArea()
        .allowsHitTesting(appGridInteraction.usageTipsCloseReminderVisible)
    }
    private func buildEditActionFeedback(for selectedApps: [AppInfo], tags: [String]) -> EditActionFeedback {
        let appNames = selectedApps.map(\.name)
            .joined(separator: localizedListSeparator)
@@ -2040,10 +2088,18 @@
    private func commitBubbleNote() {
        guard let context = appGridInteraction.editingBubble else { return }
        guard appGridInteraction.proPrompt == nil else { return }
        let limited = String(appGridInteraction.bubbleDraftNote.prefix(TagDatabase.maxAppNoteLength))
            .trimmingCharacters(in: .whitespacesAndNewlines)
        TagEditor.setAppNote(limited, for: context.app.path.path)
        let decision = TagEditor.setAppNote(limited, for: context.app.path.path)
        appGridInteraction.bubbleDraftNote = limited
        guard case .allow = decision else {
            if case .blocked(let status) = decision {
                presentProPrompt(for: .unlimitedNotes, noteQuotaStatus: status)
            }
            bubbleNoteFocused = false
            return
        }
        appGridInteraction.editingBubble = nil
        bubbleNoteFocused = false
        notifyAppNoteEditing(active: false)
@@ -2087,6 +2143,159 @@
        return app.note ?? ""
    }
    private func noteQuotaStatus(for app: AppInfo) -> ProNoteQuotaStatus? {
        guard !proEntitlement.isUnlocked else { return nil }
        let decision = ProEntitlementPolicy.noteSaveDecision(
            note: appGridInteraction.bubbleDraftNote,
            for: app.path.path
        )
        switch decision {
        case .allow(let status), .blocked(let status):
            return status
        }
    }
    private func noteQuotaHintText(for app: AppInfo) -> String? {
        guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return nil }
        if status.remaining <= 0 {
            return tr("pro.notes.limitReached")
        }
        return tr(
            "pro.notes.remaining",
            replacements: [
                "%remaining%": "\(status.remaining)",
                "%limit%": "\(status.limit)"
            ]
        )
    }
    private func isNoteQuotaWarning(for app: AppInfo) -> Bool {
        guard let status = noteQuotaStatus(for: app), !status.isUnlimited else { return false }
        return status.remaining <= 0
    }
    private func proPromptMessage(for prompt: PendingProPrompt) -> String {
        if prompt.feature == .unlimitedNotes,
           let status = prompt.noteQuotaStatus,
           !status.isUnlimited,
           status.remaining <= 0 {
            return tr("pro.notes.limitReached")
        }
        return tr(prompt.feature.promptMessageKey)
    }
    private func presentProPrompt(for feature: ProFeature, noteQuotaStatus: ProNoteQuotaStatus? = nil) {
        proEntitlement.clearTransientOperationState()
        withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
            appGridInteraction.proPrompt = PendingProPrompt(
                feature: feature,
                noteQuotaStatus: noteQuotaStatus
            )
        }
    }
    private func dismissProPrompt() {
        proEntitlement.clearTransientOperationState()
        withAnimation(.easeOut(duration: 0.18)) {
            appGridInteraction.proPrompt = nil
        }
    }
    private func handleProAccessStateChange(_ newState: ProAccessState) {
        guard newState.isUnlocked else { return }
        if let preview = proEntitlement.themePreviewState {
            appGridThemeID = preview.theme.rawValue
            proEntitlement.stopThemePreview()
        }
        let hadPrompt = appGridInteraction.proPrompt != nil
        if hadPrompt {
            dismissProPrompt()
        }
        if appGridInteraction.editingBubble != nil {
            DispatchQueue.main.async {
                bubbleNoteFocused = true
            }
        }
    }
    private func handleContentAppear() {
        refreshNotchHeight()
        refreshAppsForOverlay()
        guard let initialQuickSearchSource, !initialQuickSearchConsumed else { return }
        initialQuickSearchConsumed = true
        if initialQuickSearchSource == QuickSearchOpenSource.globalHidden {
            quickSearchCloseHidesOverlay = true
            quickSearchOnlySession = true
        }
        if !quickSearchVisible {
            quickSearchVisible = true
            quickSearchFocusToken &+= 1
        }
        quickSearchOpenedAt = Date()
        refreshQuickSearchResults()
        refreshAppsForQuickSearch()
        NotificationCenter.default.post(
            name: .tagLauncherQuickSearchVisibilityChanged,
            object: nil,
            userInfo: [
                "active": true,
                "hideOverlayOnClose": quickSearchCloseHidesOverlay
            ]
        )
    }
    private func handleOverlayDidShow(_ notification: Notification) {
        _ = notification
        resetTransientDragState()
        refreshNotchHeight()
        refreshAppsForOverlay()
    }
    private func handleOverlayDidHide(_ notification: Notification) {
        _ = notification
        resetTransientDragState()
        closeQuickSearch(notify: true, hideOverlayIfNeeded: false)
        quickSearchOnlySession = false
        quickSearchCloseHidesOverlay = false
    }
    private func handleQuickSearchRequested(_ notification: Notification) {
        let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
        openQuickSearch(source: source)
    }
    private func handleQuickSearchDismissRequested(_ notification: Notification) {
        let source = notification.userInfo?["source"] as? String
        closeQuickSearch(dismissalSource: source)
    }
    private func handleApplicationDidBecomeActive(_ notification: Notification) {
        _ = notification
        guard allApps.isEmpty, !refreshInProgress else { return }
        refreshApps()
    }
    private func handleDataDidChange(_ notification: Notification) {
        _ = notification
        refreshApps(forceLayoutRefresh: true)
    }
    private func handleAppLanguageDidChange(_ notification: Notification) {
        _ = notification
        let appsSnapshot = allApps
        DispatchQueue.global(qos: .userInitiated).async {
            _ = AppleDefaultAppCatalog.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
            _ = SmartStartService.relocalizeDefaultNotesForCurrentLanguage(apps: appsSnapshot)
            DispatchQueue.main.async {
                refreshApps(forceLayoutRefresh: true)
            }
        }
    }
    private func bubblePlacement(for frame: CGRect, rootFrame: CGRect) -> BubblePlacement {
        frame.minY < 170 ? .below : .above
    }
@@ -2122,7 +2331,8 @@
    private func estimatedBubbleHeight(for app: AppInfo, width: CGFloat, isEditing: Bool) -> CGFloat {
        if isEditing {
            return 120
            let hint = noteQuotaHintText(for: app)
            return hint == nil ? 124 : 148
        }
        let note = currentNote(for: app).trimmingCharacters(in: .whitespacesAndNewlines)
        guard !note.isEmpty else { return 68 }
@@ -2347,6 +2557,10 @@
        containerAppOrder[key] = paths
        rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
        guard proEntitlement.isUnlocked else {
            showDropWarning(message: tr("pro.sorting.previewToast"))
            return
        }
        TagEditor.reorderApps(inContainer: key, orderedPaths: paths)
        refreshApps(forceLayoutRefresh: true)
    }
@@ -2496,6 +2710,40 @@
        }
    }
    private func requestHideUsageTips() {
        guard !hideUsageTips else { return }
        guard !skipUsageTipsCloseReminder else {
            hideUsageTips = true
            return
        }
        clearAppBubbleState()
        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
        withAnimation(.spring(response: 0.24, dampingFraction: 0.84)) {
            appGridInteraction.usageTipsCloseReminderVisible = true
        }
        publishModalInteractionState()
    }
    private func dismissUsageTipsCloseReminder() {
        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
        withAnimation(.easeOut(duration: 0.18)) {
            appGridInteraction.usageTipsCloseReminderVisible = false
        }
        publishModalInteractionState()
    }
    private func confirmUsageTipsCloseReminder() {
        if appGridInteraction.usageTipsCloseSuppressFuturePrompt {
            skipUsageTipsCloseReminder = true
        }
        appGridInteraction.usageTipsCloseSuppressFuturePrompt = false
        withAnimation(.easeOut(duration: 0.16)) {
            appGridInteraction.usageTipsCloseReminderVisible = false
        }
        hideUsageTips = true
        publishModalInteractionState()
    }
    private func removeTagFromDroppedApp(app: AppInfo, tagName: String) {
        guard isRemovableRegularTag(tagName),
              appHasTag(app, tagName: tagName)
@@ -2533,9 +2781,9 @@
        return !protectedNames.contains(tag) && tagColors[tag] != nil
    }
    private func showDropWarning() {
    private func showDropWarning(message: String = tr("drop.systemDefaultWarning")) {
        withAnimation(.spring(response: 0.24, dampingFraction: 0.82)) {
            appGridInteraction.dropWarningToast = tr("drop.systemDefaultWarning")
            appGridInteraction.dropWarningToast = message
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) {
            withAnimation(.easeOut(duration: 0.18)) {
@@ -2706,6 +2954,15 @@
            closeQuickSearch(hideOverlayIfNeeded: true)
            hideOverlay()
        }
        if isCurrentApp(app) {
            if closeQuickSearchOnSuccess && !closeQuickSearchAndOverlayBeforeOpening {
                closeQuickSearch(hideOverlayIfNeeded: closeOverlayOnSuccess)
            }
            if closeOverlayOnSuccess && !closeQuickSearchAndOverlayBeforeOpening {
                hideOverlay()
            }
            return
        }
        let configuration = NSWorkspace.OpenConfiguration()
        NSWorkspace.shared.openApplication(at: app.path, configuration: configuration) { _, error in
            DispatchQueue.main.async {
@@ -2727,6 +2984,16 @@
        }
    }
    private func isCurrentApp(_ app: AppInfo) -> Bool {
        if app.bundleIdentifier?.caseInsensitiveCompare(AppIdentity.bundleIdentifier) == .orderedSame {
            return true
        }
        let currentURL = Bundle.main.bundleURL.standardizedFileURL.resolvingSymlinksInPath()
        let appURL = app.path.standardizedFileURL.resolvingSymlinksInPath()
        return currentURL == appURL
    }
    func openApp(_ app: AppInfo) {
        hideOverlay()
        launchApp(app, closeOverlayOnSuccess: false)