Ariver
2026-08-28 c5bdc7f1eb3ff4287beeeebb17d4f02f376a8d38
feat: add quick search tag filtering
36 files modified
1 files added
880 ■■■■■ changed files
C1.source/Apptag/ContentView.swift 71 ●●●●● patch | view | raw | blame | history
C1.source/Apptag/DataLayer.swift 29 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ar-Najdi.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ar.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/cs.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/da.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/de.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/en.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/es.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/fr.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/id.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/it.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ja.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ko.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ms.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/nb.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/nl.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/nn.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/no.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/pl.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/pt-BR.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ro.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/ru.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/sr-Cyrl.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/sv.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/th.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/tr.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/uk.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/vi.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/zh-Hans.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/Localization/zh-Hant.json 4 ●●●● patch | view | raw | blame | history
C1.source/Apptag/QuickSearch.swift 320 ●●●●● patch | view | raw | blame | history
C1.source/Apptag/TagNavigationView.swift 11 ●●●● patch | view | raw | blame | history
C1.source/CHANGELOG.md 8 ●●●●● patch | view | raw | blame | history
C1.source/Scripts/quick_search_tag_filter_qa.sh 308 ●●●●● patch | view | raw | blame | history
CODEGRAPH.md 13 ●●●●● patch | view | raw | blame | history
C1.source/Apptag/ContentView.swift
@@ -447,6 +447,7 @@
    @State private var quickSearchQuery = ""
    @State private var quickSearchDocuments: [QuickSearchDocument] = []
    @State private var quickSearchResults: [QuickSearchResult] = []
    @State private var quickSearchSelectedTagName: String? = nil
    @State private var quickSearchSelectedID: URL? = nil
    @State private var quickSearchManualSelection = false
    @State private var quickSearchFocusToken = 0
@@ -620,6 +621,9 @@
            }
            .onChange(of: defaultGroupName) { _, _ in
                rebuildDisplayGroups(apps: allApps, tagOrder: draggedTagNames)
                if quickSearchVisible {
                    refreshQuickSearchResults()
                }
            }
            .onChange(of: quickSearchQuery) { _, _ in
                quickSearchErrorMessage = nil
@@ -816,6 +820,8 @@
                QuickSearchPanelPresentationView(
                    query: $quickSearchQuery,
                    results: quickSearchResults,
                    tagFilters: quickSearchTagFilters,
                    selectedTagName: quickSearchSelectedTagName,
                    selectedID: quickSearchSelectedID,
                    focusToken: quickSearchFocusToken,
                    selectionScrollToken: quickSearchSelectionScrollToken,
@@ -826,7 +832,8 @@
                    errorMessage: quickSearchErrorMessage,
                    onCommand: handleQuickSearchCommand,
                    onHover: selectQuickSearchResult,
                    onLaunch: launchQuickSearchResult
                    onLaunch: launchQuickSearchResult,
                    onTagFilterChange: selectQuickSearchTagFilter
                )
                .frame(width: 0, height: 0)
                .allowsHitTesting(false)
@@ -843,6 +850,7 @@
            && !quickSearchResults.isEmpty
        let visibleRows = min(max(1, quickSearchResults.count), quickSearchMaxVisibleRows(in: size))
        return QuickSearchPanelMetrics.contentHeight(
            hasTagFilters: !quickSearchTagFilters.isEmpty,
            hasResultList: hasResultList,
            visibleRows: visibleRows
        )
@@ -855,6 +863,7 @@
    private func quickSearchMaxVisibleRows(in size: CGSize) -> Int {
        let bottomClearance: CGFloat = 84
        let chromeHeight = QuickSearchPanelMetrics.headerHeight
            + (quickSearchTagFilters.isEmpty ? 0 : QuickSearchPanelMetrics.tagFilterBarHeight)
            + QuickSearchPanelMetrics.dividerHeight
            + QuickSearchPanelMetrics.resultListVerticalInset * 2
        let rowHeightWithSpacing = QuickSearchPanelMetrics.rowHeight
@@ -874,6 +883,7 @@
        quickSearchVisible = true
        quickSearchOpenedAt = Date()
        quickSearchQuery = ""
        quickSearchSelectedTagName = nil
        quickSearchCompositionActive = false
        quickSearchManualSelection = false
        quickSearchErrorMessage = nil
@@ -928,6 +938,7 @@
        quickSearchCompositionActive = false
        quickSearchQuery = ""
        quickSearchResults = []
        quickSearchSelectedTagName = nil
        quickSearchSelectedID = nil
        quickSearchManualSelection = false
        quickSearchErrorMessage = nil
@@ -977,9 +988,12 @@
    private func refreshQuickSearchResults() {
        let previousSelection = quickSearchSelectedID
        let documents = quickSearchVisibleDocuments
        let selectedTagName = validateQuickSearchSelectedTagName(against: quickSearchTagFilters)
        quickSearchResults = QuickSearchEngine.search(
            quickSearchQuery,
            documents: quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
            documents: documents,
            selectedTagName: selectedTagName
        )
        if quickSearchResults.isEmpty {
@@ -996,6 +1010,58 @@
            quickSearchSelectedID = quickSearchResults.first?.id
            quickSearchManualSelection = false
        }
    }
    private var quickSearchVisibleDocuments: [QuickSearchDocument] {
        quickSearchDocuments.filter { FileManager.default.fileExists(atPath: $0.app.path.path) }
    }
    private var quickSearchTagFilters: [QuickSearchTagFilterOption] {
        let availableTagNames = Set(
            quickSearchVisibleDocuments
                .flatMap(\.tagNames)
                .filter(isQuickSearchFilterableTag)
        )
        guard !availableTagNames.isEmpty else { return [] }
        let ordered = draggedTagNames
            .filter { availableTagNames.contains($0) && isQuickSearchFilterableTag($0) }
        let orderedSet = Set(ordered)
        let remaining = availableTagNames
            .filter { !orderedSet.contains($0) }
            .sorted { $0.localizedStandardCompare($1) == .orderedAscending }
        return (ordered + remaining).map { tagName in
            let definition = tagDefinitions[tagName]
            return QuickSearchTagFilterOption(
                name: tagName,
                colorIndex: definition?.color ?? tagColors[tagName] ?? 0,
                customColor: definition?.customColor ?? tagCustomColors[tagName]
            )
        }
    }
    private func isQuickSearchFilterableTag(_ tagName: String) -> Bool {
        let name = tagName.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !name.isEmpty, tagDefinitions[name] != nil else { return false }
        return !AppContainerID.isReservedGroupName(name, defaultGroupName: defaultGroupName)
    }
    private func validateQuickSearchSelectedTagName(against filters: [QuickSearchTagFilterOption]) -> String? {
        guard let quickSearchSelectedTagName else { return nil }
        guard filters.contains(where: { $0.name == quickSearchSelectedTagName }) else {
            self.quickSearchSelectedTagName = nil
            return nil
        }
        return quickSearchSelectedTagName
    }
    private func selectQuickSearchTagFilter(_ tagName: String?) {
        quickSearchSelectedTagName = tagName
        quickSearchErrorMessage = nil
        quickSearchManualSelection = false
        quickSearchFocusToken &+= 1
        refreshQuickSearchResults()
    }
    private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
@@ -2429,6 +2495,7 @@
            quickSearchFocusToken &+= 1
        }
        quickSearchOpenedAt = Date()
        quickSearchSelectedTagName = nil
        refreshQuickSearchResults()
        refreshAppsForQuickSearch()
        NotificationCenter.default.post(
C1.source/Apptag/DataLayer.swift
@@ -70,12 +70,31 @@
        return tag(name)
    }
    static func isReservedGroupName(_ name: String, defaultGroupName: String = "Other") -> Bool {
        reservedGroupNames(defaultGroupName: defaultGroupName).contains(
            name.trimmingCharacters(in: .whitespacesAndNewlines)
        )
    }
    private static func isUncategorizedDisplayName(_ name: String) -> Bool {
        name == "Other" || name == tr("group.uncategorized")
    }
    private static func isAppleBuiltInDisplayName(_ name: String) -> Bool {
        name == "Mac自带" || name == tr("group.appleBuiltIn")
    }
    private static func reservedGroupNames(defaultGroupName: String) -> Set<String> {
        let names = [
            "Other",
            defaultGroupName,
            tr("group.uncategorized"),
            "Mac自带",
            tr("group.appleBuiltIn"),
            TagDatabase.uncommonTagKey,
            tr("group.uncommon")
        ]
        return Set(names.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }.filter { !$0.isEmpty })
    }
}
@@ -868,7 +887,9 @@
            } else {
                for tag in uniqueOrdered(app.tags) {
                    let displayName = nameOverrides[tag] ?? tag
                    guard displayName != macCategory else { continue }
                    guard !AppContainerID.isReservedGroupName(displayName, defaultGroupName: defaultGroupName) else {
                        continue
                    }
                    appendGroupedApp(
                        app,
                        to: displayName,
@@ -886,9 +907,9 @@
            tagOrder: tagOrder
        ) {
            let displayName = nameOverrides[tagName] ?? tagName
            guard displayName != macCategory,
                  displayName != defaultGroupName
            else { continue }
            guard !AppContainerID.isReservedGroupName(displayName, defaultGroupName: defaultGroupName) else {
                continue
            }
            if containerIDsByGroupName[displayName] == nil {
                containerIDsByGroupName[displayName] = AppContainerID.forTag(
C1.source/Apptag/Info.plist
@@ -19,7 +19,7 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>8.3.7</string>
    <string>8.4.0</string>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
@@ -32,7 +32,7 @@
        </dict>
    </array>
    <key>CFBundleVersion</key>
    <string>20260822.2207</string>
    <string>20260828.1732</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
C1.source/Apptag/Localization/ar-Najdi.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "حقل البحث السريع",
  "quickSearch.emptyPrompt": "ابدأ بالكتابة عشان تبحث في التطبيقات والوسوم والملاحظات.",
  "quickSearch.noResults": "ما لقينا تطبيقات.",
  "quickSearch.tagFilter.all": "الكل",
  "quickSearch.tagFilter.noResults": "ما لقينا تطبيقات في %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "إظهار كل نتائج البحث السريع",
  "quickSearch.tagFilter.tagAccessibility": "تصفية البحث السريع حسب الوسم %tagName%",
  "quickSearch.launchFailed": "تعذر فتح هالتطبيق. جرّب مرة ثانية أو اختر تطبيق ثاني.",
  "quickSearch.tagPrefix": "وسم",
  "quickSearch.hotkeys": "اختصارات لوحة المفاتيح",
C1.source/Apptag/Localization/ar.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "حقل البحث السريع",
  "quickSearch.emptyPrompt": "ابدأ بالكتابة للبحث في التطبيقات والوسوم والملاحظات.",
  "quickSearch.noResults": "لم يتم العثور على تطبيقات.",
  "quickSearch.tagFilter.all": "الكل",
  "quickSearch.tagFilter.noResults": "لم يتم العثور على تطبيقات في %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "إظهار كل نتائج البحث السريع",
  "quickSearch.tagFilter.tagAccessibility": "تصفية البحث السريع حسب الوسم %tagName%",
  "quickSearch.launchFailed": "تعذر فتح هذا التطبيق. حاول مرة أخرى أو اختر تطبيقًا آخر.",
  "quickSearch.tagPrefix": "وسم",
  "quickSearch.hotkeys": "اختصارات لوحة المفاتيح",
C1.source/Apptag/Localization/cs.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Vstup rychlého hledání",
  "quickSearch.emptyPrompt": "Začněte psát a hledejte aplikace, štítky a poznámky.",
  "quickSearch.noResults": "Nebyly nalezeny žádné aplikace.",
  "quickSearch.tagFilter.all": "Vše",
  "quickSearch.tagFilter.noResults": "V %tagName% nebyly nalezeny žádné aplikace.",
  "quickSearch.tagFilter.allAccessibility": "Zobrazit všechny výsledky rychlého hledání",
  "quickSearch.tagFilter.tagAccessibility": "Filtrovat rychlé hledání podle štítku %tagName%",
  "quickSearch.launchFailed": "Tuto aplikaci nelze otevřít. Zkuste to znovu nebo vyberte jinou aplikaci.",
  "quickSearch.tagPrefix": "Štítek",
  "quickSearch.hotkeys": "Klávesové zkratky",
C1.source/Apptag/Localization/da.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Input til hurtigsøgning",
  "quickSearch.emptyPrompt": "Begynd at skrive for at søge efter apps, mærker og noter.",
  "quickSearch.noResults": "Ingen apps fundet.",
  "quickSearch.tagFilter.all": "Alle",
  "quickSearch.tagFilter.noResults": "Ingen apps fundet i %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Vis alle resultater i Hurtig søgning",
  "quickSearch.tagFilter.tagAccessibility": "Filtrer Hurtig søgning efter tagget %tagName%",
  "quickSearch.launchFailed": "Denne app kunne ikke åbnes. Prøv igen, eller vælg en anden app.",
  "quickSearch.tagPrefix": "Mærke",
  "quickSearch.hotkeys": "Tastaturgenveje",
C1.source/Apptag/Localization/de.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Eingabefeld für Schnellsuche",
  "quickSearch.emptyPrompt": "Tippen Sie, um Apps, Tags und Notizen zu suchen.",
  "quickSearch.noResults": "Keine Apps gefunden.",
  "quickSearch.tagFilter.all": "Alle",
  "quickSearch.tagFilter.noResults": "Keine Apps in %tagName% gefunden.",
  "quickSearch.tagFilter.allAccessibility": "Alle Quick Search-Ergebnisse anzeigen",
  "quickSearch.tagFilter.tagAccessibility": "Quick Search nach Tag %tagName% filtern",
  "quickSearch.launchFailed": "Diese App konnte nicht geöffnet werden. Versuchen Sie es erneut oder wählen Sie eine andere App.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Tastaturkurzbefehle",
C1.source/Apptag/Localization/en.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Quick Search input",
  "quickSearch.emptyPrompt": "Start typing to search apps, tags, and notes.",
  "quickSearch.noResults": "No apps found.",
  "quickSearch.tagFilter.all": "All",
  "quickSearch.tagFilter.noResults": "No apps found in %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Show all Quick Search results",
  "quickSearch.tagFilter.tagAccessibility": "Filter Quick Search by tag %tagName%",
  "quickSearch.launchFailed": "Unable to open this app. Please try again or choose another app.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Keyboard shortcuts",
C1.source/Apptag/Localization/es.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Campo de búsqueda rápida",
  "quickSearch.emptyPrompt": "Empieza a escribir para buscar apps, etiquetas y notas.",
  "quickSearch.noResults": "No se encontraron apps.",
  "quickSearch.tagFilter.all": "Todo",
  "quickSearch.tagFilter.noResults": "No se encontraron apps en %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Mostrar todos los resultados de búsqueda rápida",
  "quickSearch.tagFilter.tagAccessibility": "Filtrar búsqueda rápida por la etiqueta %tagName%",
  "quickSearch.launchFailed": "No se pudo abrir esta app. Inténtalo de nuevo o elige otra app.",
  "quickSearch.tagPrefix": "Etiqueta",
  "quickSearch.hotkeys": "Atajos de teclado",
C1.source/Apptag/Localization/fr.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Champ de recherche rapide",
  "quickSearch.emptyPrompt": "Commencez à saisir pour rechercher des apps, des étiquettes et des notes.",
  "quickSearch.noResults": "Aucune app trouvée.",
  "quickSearch.tagFilter.all": "Tout",
  "quickSearch.tagFilter.noResults": "Aucune app trouvée dans %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Afficher tous les résultats de Recherche rapide",
  "quickSearch.tagFilter.tagAccessibility": "Filtrer Recherche rapide par le tag %tagName%",
  "quickSearch.launchFailed": "Impossible d’ouvrir cette app. Réessayez ou choisissez une autre app.",
  "quickSearch.tagPrefix": "Étiquette",
  "quickSearch.hotkeys": "Raccourcis clavier",
C1.source/Apptag/Localization/id.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Input pencarian cepat",
  "quickSearch.emptyPrompt": "Mulai mengetik untuk mencari app, tag, dan catatan.",
  "quickSearch.noResults": "Tidak ada app ditemukan.",
  "quickSearch.tagFilter.all": "Semua",
  "quickSearch.tagFilter.noResults": "Tidak ada app yang ditemukan di %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Tampilkan semua hasil Pencarian Cepat",
  "quickSearch.tagFilter.tagAccessibility": "Filter Pencarian Cepat menurut tag %tagName%",
  "quickSearch.launchFailed": "App ini tidak dapat dibuka. Coba lagi atau pilih app lain.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Pintasan keyboard",
C1.source/Apptag/Localization/it.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Campo di ricerca rapida",
  "quickSearch.emptyPrompt": "Inizia a digitare per cercare app, tag e note.",
  "quickSearch.noResults": "Nessuna app trovata.",
  "quickSearch.tagFilter.all": "Tutte",
  "quickSearch.tagFilter.noResults": "Nessuna app trovata in %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Mostra tutti i risultati di Ricerca rapida",
  "quickSearch.tagFilter.tagAccessibility": "Filtra Ricerca rapida per tag %tagName%",
  "quickSearch.launchFailed": "Impossibile aprire questa app. Riprova o scegli un’altra app.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Scorciatoie da tastiera",
C1.source/Apptag/Localization/ja.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "クイック検索入力欄",
  "quickSearch.emptyPrompt": "入力を開始してアプリ、タグ、メモを検索します。",
  "quickSearch.noResults": "アプリが見つかりません。",
  "quickSearch.tagFilter.all": "すべて",
  "quickSearch.tagFilter.noResults": "%tagName% にアプリが見つかりません。",
  "quickSearch.tagFilter.allAccessibility": "クイック検索のすべての結果を表示",
  "quickSearch.tagFilter.tagAccessibility": "%tagName% タグでクイック検索を絞り込む",
  "quickSearch.launchFailed": "このアプリを開けません。もう一度試すか、別のアプリを選択してください。",
  "quickSearch.tagPrefix": "タグ",
  "quickSearch.hotkeys": "キーボードショートカット",
C1.source/Apptag/Localization/ko.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "빠른 검색 입력란",
  "quickSearch.emptyPrompt": "앱, 태그, 메모를 검색하려면 입력을 시작하세요.",
  "quickSearch.noResults": "앱을 찾을 수 없습니다.",
  "quickSearch.tagFilter.all": "전체",
  "quickSearch.tagFilter.noResults": "%tagName%에서 앱을 찾을 수 없습니다.",
  "quickSearch.tagFilter.allAccessibility": "모든 빠른 검색 결과 보기",
  "quickSearch.tagFilter.tagAccessibility": "%tagName% 태그로 빠른 검색 필터링",
  "quickSearch.launchFailed": "이 앱을 열 수 없습니다. 다시 시도하거나 다른 앱을 선택하세요.",
  "quickSearch.tagPrefix": "태그",
  "quickSearch.hotkeys": "키보드 단축키",
C1.source/Apptag/Localization/ms.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Input carian pantas",
  "quickSearch.emptyPrompt": "Mula menaip untuk mencari app, tag dan nota.",
  "quickSearch.noResults": "Tiada app ditemui.",
  "quickSearch.tagFilter.all": "Semua",
  "quickSearch.tagFilter.noResults": "Tiada app ditemui dalam %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Tunjukkan semua hasil Carian Pantas",
  "quickSearch.tagFilter.tagAccessibility": "Tapis Carian Pantas mengikut tag %tagName%",
  "quickSearch.launchFailed": "App ini tidak dapat dibuka. Cuba lagi atau pilih app lain.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Pintasan papan kekunci",
C1.source/Apptag/Localization/nb.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Inndata for hurtigsøk",
  "quickSearch.emptyPrompt": "Begynn å skrive for å søke etter apper, tagger og notater.",
  "quickSearch.noResults": "Fant ingen apper.",
  "quickSearch.tagFilter.all": "Alle",
  "quickSearch.tagFilter.noResults": "Ingen apper funnet i %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Vis alle hurtigsøk-resultater",
  "quickSearch.tagFilter.tagAccessibility": "Filtrer hurtigsøk etter taggen %tagName%",
  "quickSearch.launchFailed": "Kunne ikke åpne denne appen. Prøv igjen eller velg en annen app.",
  "quickSearch.tagPrefix": "Tagg",
  "quickSearch.hotkeys": "Tastatursnarveier",
C1.source/Apptag/Localization/nl.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Invoer voor snel zoeken",
  "quickSearch.emptyPrompt": "Begin met typen om apps, tags en notities te zoeken.",
  "quickSearch.noResults": "Geen apps gevonden.",
  "quickSearch.tagFilter.all": "Alles",
  "quickSearch.tagFilter.noResults": "Geen apps gevonden in %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Alle resultaten van Snel zoeken tonen",
  "quickSearch.tagFilter.tagAccessibility": "Snel zoeken filteren op tag %tagName%",
  "quickSearch.launchFailed": "Deze app kan niet worden geopend. Probeer opnieuw of kies een andere app.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Toetscombinaties",
C1.source/Apptag/Localization/nn.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Inndata for hurtigsøk",
  "quickSearch.emptyPrompt": "Begynn å skrive for å søke etter apper, tagger og notater.",
  "quickSearch.noResults": "Fant ingen apper.",
  "quickSearch.tagFilter.all": "Alle",
  "quickSearch.tagFilter.noResults": "Fann ingen appar i %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Vis alle hurtigsøk-resultat",
  "quickSearch.tagFilter.tagAccessibility": "Filtrer hurtigsøk etter taggen %tagName%",
  "quickSearch.launchFailed": "Kunne ikke åpne denne appen. Prøv igjen eller velg en annen app.",
  "quickSearch.tagPrefix": "Tagg",
  "quickSearch.hotkeys": "Tastatursnarvegar",
C1.source/Apptag/Localization/no.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Inndata for hurtigsøk",
  "quickSearch.emptyPrompt": "Begynn å skrive for å søke etter apper, tagger og notater.",
  "quickSearch.noResults": "Fant ingen apper.",
  "quickSearch.tagFilter.all": "Alle",
  "quickSearch.tagFilter.noResults": "Ingen apper funnet i %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Vis alle hurtigsøk-resultater",
  "quickSearch.tagFilter.tagAccessibility": "Filtrer hurtigsøk etter taggen %tagName%",
  "quickSearch.launchFailed": "Kunne ikke åpne denne appen. Prøv igjen eller velg en annen app.",
  "quickSearch.tagPrefix": "Tagg",
  "quickSearch.hotkeys": "Tastatursnarveier",
C1.source/Apptag/Localization/pl.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Pole szybkiego wyszukiwania",
  "quickSearch.emptyPrompt": "Zacznij pisać, aby szukać aplikacji, tagów i notatek.",
  "quickSearch.noResults": "Nie znaleziono aplikacji.",
  "quickSearch.tagFilter.all": "Wszystkie",
  "quickSearch.tagFilter.noResults": "Nie znaleziono aplikacji w %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Pokaż wszystkie wyniki szybkiego wyszukiwania",
  "quickSearch.tagFilter.tagAccessibility": "Filtruj szybkie wyszukiwanie według tagu %tagName%",
  "quickSearch.launchFailed": "Nie można otworzyć tej aplikacji. Spróbuj ponownie lub wybierz inną aplikację.",
  "quickSearch.tagPrefix": "Tag",
  "quickSearch.hotkeys": "Skróty klawiaturowe",
C1.source/Apptag/Localization/pt-BR.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Campo de busca rápida",
  "quickSearch.emptyPrompt": "Comece a digitar para buscar apps, etiquetas e notas.",
  "quickSearch.noResults": "Nenhum app encontrado.",
  "quickSearch.tagFilter.all": "Tudo",
  "quickSearch.tagFilter.noResults": "Nenhum app encontrado em %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Mostrar todos os resultados da busca rápida",
  "quickSearch.tagFilter.tagAccessibility": "Filtrar busca rápida pela tag %tagName%",
  "quickSearch.launchFailed": "Não foi possível abrir este app. Tente novamente ou escolha outro app.",
  "quickSearch.tagPrefix": "Etiqueta",
  "quickSearch.hotkeys": "Atalhos de teclado",
C1.source/Apptag/Localization/ro.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Câmp de căutare rapidă",
  "quickSearch.emptyPrompt": "Începe să tastezi pentru a căuta aplicații, etichete și note.",
  "quickSearch.noResults": "Nu s-au găsit aplicații.",
  "quickSearch.tagFilter.all": "Toate",
  "quickSearch.tagFilter.noResults": "Nu s-au găsit aplicații în %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Afișează toate rezultatele Căutării rapide",
  "quickSearch.tagFilter.tagAccessibility": "Filtrează Căutarea rapidă după eticheta %tagName%",
  "quickSearch.launchFailed": "Această aplicație nu poate fi deschisă. Încearcă din nou sau alege altă aplicație.",
  "quickSearch.tagPrefix": "Etichetă",
  "quickSearch.hotkeys": "Scurtături de la tastatură",
C1.source/Apptag/Localization/ru.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Поле быстрого поиска",
  "quickSearch.emptyPrompt": "Начните вводить, чтобы искать приложения, теги и заметки.",
  "quickSearch.noResults": "Приложения не найдены.",
  "quickSearch.tagFilter.all": "Все",
  "quickSearch.tagFilter.noResults": "В %tagName% приложения не найдены.",
  "quickSearch.tagFilter.allAccessibility": "Показать все результаты быстрого поиска",
  "quickSearch.tagFilter.tagAccessibility": "Фильтровать быстрый поиск по тегу %tagName%",
  "quickSearch.launchFailed": "Не удалось открыть это приложение. Повторите попытку или выберите другое приложение.",
  "quickSearch.tagPrefix": "Тег",
  "quickSearch.hotkeys": "Сочетания клавиш",
C1.source/Apptag/Localization/sr-Cyrl.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Поље брзе претраге",
  "quickSearch.emptyPrompt": "Почните да куцате да претражите апликације, ознаке и белешке.",
  "quickSearch.noResults": "Нема пронађених апликација.",
  "quickSearch.tagFilter.all": "Све",
  "quickSearch.tagFilter.noResults": "Нема апликација у %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Прикажи све резултате брзе претраге",
  "quickSearch.tagFilter.tagAccessibility": "Филтрирај брзу претрагу по ознаци %tagName%",
  "quickSearch.launchFailed": "Ову апликацију није могуће отворити. Покушајте поново или изаберите другу апликацију.",
  "quickSearch.tagPrefix": "Ознака",
  "quickSearch.hotkeys": "Пречице на тастатури",
C1.source/Apptag/Localization/sv.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Inmatning för snabbsökning",
  "quickSearch.emptyPrompt": "Börja skriva för att söka appar, taggar och anteckningar.",
  "quickSearch.noResults": "Inga appar hittades.",
  "quickSearch.tagFilter.all": "Alla",
  "quickSearch.tagFilter.noResults": "Inga appar hittades i %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Visa alla resultat i Snabbsökning",
  "quickSearch.tagFilter.tagAccessibility": "Filtrera Snabbsökning efter taggen %tagName%",
  "quickSearch.launchFailed": "Det gick inte att öppna appen. Försök igen eller välj en annan app.",
  "quickSearch.tagPrefix": "Tagg",
  "quickSearch.hotkeys": "Kortkommandon",
C1.source/Apptag/Localization/th.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "ช่องค้นหาอย่างรวดเร็ว",
  "quickSearch.emptyPrompt": "เริ่มพิมพ์เพื่อค้นหาแอป แท็ก และบันทึก",
  "quickSearch.noResults": "ไม่พบแอป",
  "quickSearch.tagFilter.all": "ทั้งหมด",
  "quickSearch.tagFilter.noResults": "ไม่พบแอปใน %tagName%",
  "quickSearch.tagFilter.allAccessibility": "แสดงผลลัพธ์ Quick Search ทั้งหมด",
  "quickSearch.tagFilter.tagAccessibility": "กรอง Quick Search ด้วยแท็ก %tagName%",
  "quickSearch.launchFailed": "ไม่สามารถเปิดแอปนี้ได้ ลองอีกครั้งหรือเลือกแอปอื่น",
  "quickSearch.tagPrefix": "แท็ก",
  "quickSearch.hotkeys": "แป้นพิมพ์ลัด",
C1.source/Apptag/Localization/tr.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Hızlı arama girişi",
  "quickSearch.emptyPrompt": "Uygulamaları, etiketleri ve notları aramak için yazmaya başlayın.",
  "quickSearch.noResults": "Uygulama bulunamadı.",
  "quickSearch.tagFilter.all": "Tümü",
  "quickSearch.tagFilter.noResults": "%tagName% içinde uygulama bulunamadı.",
  "quickSearch.tagFilter.allAccessibility": "Tüm Hızlı Arama sonuçlarını göster",
  "quickSearch.tagFilter.tagAccessibility": "Hızlı Aramayı %tagName% etiketiyle filtrele",
  "quickSearch.launchFailed": "Bu uygulama açılamadı. Yeniden deneyin veya başka bir uygulama seçin.",
  "quickSearch.tagPrefix": "Etiket",
  "quickSearch.hotkeys": "Klavye kısayolları",
C1.source/Apptag/Localization/uk.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Поле швидкого пошуку",
  "quickSearch.emptyPrompt": "Почніть вводити, щоб шукати програми, теги й нотатки.",
  "quickSearch.noResults": "Програми не знайдено.",
  "quickSearch.tagFilter.all": "Усі",
  "quickSearch.tagFilter.noResults": "У %tagName% не знайдено програм.",
  "quickSearch.tagFilter.allAccessibility": "Показати всі результати швидкого пошуку",
  "quickSearch.tagFilter.tagAccessibility": "Фільтрувати швидкий пошук за тегом %tagName%",
  "quickSearch.launchFailed": "Не вдалося відкрити цю програму. Спробуйте ще раз або виберіть іншу програму.",
  "quickSearch.tagPrefix": "Тег",
  "quickSearch.hotkeys": "Комбінації клавіш",
C1.source/Apptag/Localization/vi.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "Ô nhập tìm kiếm nhanh",
  "quickSearch.emptyPrompt": "Bắt đầu nhập để tìm ứng dụng, thẻ và ghi chú.",
  "quickSearch.noResults": "Không tìm thấy ứng dụng.",
  "quickSearch.tagFilter.all": "Tất cả",
  "quickSearch.tagFilter.noResults": "Không tìm thấy ứng dụng trong %tagName%.",
  "quickSearch.tagFilter.allAccessibility": "Hiển thị tất cả kết quả Tìm kiếm nhanh",
  "quickSearch.tagFilter.tagAccessibility": "Lọc Tìm kiếm nhanh theo thẻ %tagName%",
  "quickSearch.launchFailed": "Không thể mở ứng dụng này. Hãy thử lại hoặc chọn ứng dụng khác.",
  "quickSearch.tagPrefix": "Thẻ",
  "quickSearch.hotkeys": "Phím tắt bàn phím",
C1.source/Apptag/Localization/zh-Hans.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "快捷搜索输入框",
  "quickSearch.emptyPrompt": "开始输入以搜索 App、标签和备注。",
  "quickSearch.noResults": "未找到应用。",
  "quickSearch.tagFilter.all": "全部",
  "quickSearch.tagFilter.noResults": "“%tagName%”标签下未找到应用。",
  "quickSearch.tagFilter.allAccessibility": "显示全部快捷搜索结果",
  "quickSearch.tagFilter.tagAccessibility": "按“%tagName%”标签筛选快捷搜索",
  "quickSearch.launchFailed": "无法打开这个应用。请重试或选择其他应用。",
  "quickSearch.tagPrefix": "标签",
  "quickSearch.hotkeys": "快捷键",
C1.source/Apptag/Localization/zh-Hant.json
@@ -170,6 +170,10 @@
  "quickSearch.inputAccessibility": "快速搜尋輸入框",
  "quickSearch.emptyPrompt": "開始輸入以搜尋 App、標籤和備註。",
  "quickSearch.noResults": "找不到 App。",
  "quickSearch.tagFilter.all": "全部",
  "quickSearch.tagFilter.noResults": "「%tagName%」標籤下找不到 App。",
  "quickSearch.tagFilter.allAccessibility": "顯示全部快速搜尋結果",
  "quickSearch.tagFilter.tagAccessibility": "依「%tagName%」標籤篩選快速搜尋",
  "quickSearch.launchFailed": "無法開啟這個 App。請重試或選擇其他 App。",
  "quickSearch.tagPrefix": "標籤",
  "quickSearch.hotkeys": "快速鍵",
C1.source/Apptag/QuickSearch.swift
@@ -298,6 +298,13 @@
    var app: AppInfo { document.app }
}
struct QuickSearchTagFilterOption: Identifiable, Equatable {
    var id: String { name }
    let name: String
    let colorIndex: Int
    let customColor: TagCustomColor?
}
enum QuickSearchEngine {
    static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
        apps
@@ -329,14 +336,34 @@
            }
    }
    static func search(_ query: String, documents: [QuickSearchDocument], limit: Int = 50) -> [QuickSearchResult] {
    static func search(
        _ query: String,
        documents: [QuickSearchDocument],
        selectedTagName: String? = nil,
        limit: Int = 50
    ) -> [QuickSearchResult] {
        let activeTagName = normalizedSelectedTagName(selectedTagName)
        let scopedDocuments = activeTagName.map { tagName in
            documents.filter { document in
                document.tagNames.contains(tagName)
            }
        } ?? documents
        let normalizedQuery = normalizeQuery(query)
        guard !normalizedQuery.isEmpty else {
            return emptyQueryResults(documents: documents, limit: min(limit, 6))
            if let activeTagName {
                return tagFilteredEmptyQueryResults(
                    documents: scopedDocuments,
                    selectedTagName: activeTagName,
                    limit: limit
                )
            }
            return emptyQueryResults(documents: scopedDocuments, limit: min(limit, 6))
        }
        let tokens = normalizedQuery.split(separator: " ").map(String.init)
        let results = documents.compactMap { result(for: $0, tokens: tokens) }
        let results = scopedDocuments.compactMap {
            result(for: $0, tokens: tokens, selectedTagName: activeTagName)
        }
        return results.sorted(by: rank).prefix(limit).map { $0 }
    }
@@ -350,7 +377,11 @@
            .lowercased()
    }
    private static func result(for document: QuickSearchDocument, tokens: [String]) -> QuickSearchResult? {
    private static func result(
        for document: QuickSearchDocument,
        tokens: [String],
        selectedTagName: String?
    ) -> QuickSearchResult? {
        let fields = document.searchableFields
        var textScore: Double = 0
        var bestFieldRank = Int.max
@@ -379,7 +410,7 @@
            finalScore: finalScore,
            textScore: textScore,
            bestFieldRank: bestFieldRank,
            matchedTagName: matchedTagName,
            matchedTagName: matchedTagName ?? selectedTagName,
            noteSnippet: noteSnippet
        )
    }
@@ -549,6 +580,27 @@
        return nil
    }
    private static func tagFilteredEmptyQueryResults(
        documents: [QuickSearchDocument],
        selectedTagName: String,
        limit: Int
    ) -> [QuickSearchResult] {
        documents
            .map {
                QuickSearchResult(
                    document: $0,
                    finalScore: behaviorBoost(for: $0),
                    textScore: 0,
                    bestFieldRank: Int.max,
                    matchedTagName: selectedTagName,
                    noteSnippet: nil
                )
            }
            .sorted(by: rank)
            .prefix(limit)
            .map { $0 }
    }
    private static func emptyQueryResults(documents: [QuickSearchDocument], limit: Int) -> [QuickSearchResult] {
        var used = Set<URL>()
        let recent = documents
@@ -698,6 +750,12 @@
        return normalizeField(String(parts))
    }
    private static func normalizedSelectedTagName(_ value: String?) -> String? {
        guard let value else { return nil }
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }
    private static func internalBundleNames(for app: AppInfo) -> [String] {
        guard let bundle = Bundle(url: app.path) else { return [] }
        let values = [
@@ -738,23 +796,28 @@
    static let rowSpacing: CGFloat = 2
    static let resultListVerticalInset: CGFloat = 10
    static let headerHeight: CGFloat = 84
    static let tagFilterBarHeight: CGFloat = 40
    static let dividerHeight: CGFloat = 1
    static let messageRowHeight: CGFloat = 86
    static func contentHeight(hasResultList: Bool, visibleRows: Int) -> CGFloat {
    static func contentHeight(hasTagFilters: Bool, hasResultList: Bool, visibleRows: Int) -> CGFloat {
        let tagFilterHeight = hasTagFilters ? tagFilterBarHeight : 0
        if hasResultList {
            return headerHeight
                + tagFilterHeight
                + dividerHeight
                + CGFloat(max(1, visibleRows)) * (rowHeight + rowSpacing)
                + resultListVerticalInset * 2
        }
        return headerHeight + dividerHeight + messageRowHeight
        return headerHeight + tagFilterHeight + dividerHeight + messageRowHeight
    }
}
struct QuickSearchPanelPresentationView: NSViewRepresentable {
    @Binding var query: String
    let results: [QuickSearchResult]
    let tagFilters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let selectedID: URL?
    let focusToken: Int
    let selectionScrollToken: Int
@@ -766,6 +829,7 @@
    let onCommand: (QuickSearchCommand) -> Void
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void
    let onTagFilterChange: (String?) -> Void
    func makeNSView(context: Context) -> QuickSearchPanelAnchorView {
        QuickSearchPanelAnchorView()
@@ -784,6 +848,8 @@
                QuickSearchOverlayView(
                    query: $query,
                    results: results,
                    tagFilters: tagFilters,
                    selectedTagName: selectedTagName,
                    selectedID: selectedID,
                    focusToken: focusToken,
                    selectionScrollToken: selectionScrollToken,
@@ -792,7 +858,8 @@
                    errorMessage: errorMessage,
                    onCommand: onCommand,
                    onHover: onHover,
                    onLaunch: onLaunch
                    onLaunch: onLaunch,
                    onTagFilterChange: onTagFilterChange
                )
            )
        )
@@ -957,10 +1024,15 @@
    override var canBecomeMain: Bool { false }
    override func sendEvent(_ event: NSEvent) {
        if event.type == .leftMouseDown,
           let row = quickSearchResultRow(at: event.locationInWindow) {
            row.launchFromMouseClick()
            return
        if event.type == .leftMouseDown {
            if let tagChip = quickSearchTagFilterClickTarget(at: event.locationInWindow),
               tagChip.activateFromMouseClick() {
                return
            }
            if let row = quickSearchResultRow(at: event.locationInWindow) {
                row.launchFromMouseClick()
                return
            }
        }
        super.sendEvent(event)
    }
@@ -976,9 +1048,42 @@
        }
        return contentView.quickSearchResultRowDescendant(containingWindowPoint: windowPoint)
    }
    private func quickSearchTagFilterClickTarget(at windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        guard let contentView else { return nil }
        if let chip = contentView.quickSearchTagFilterClickTargetFromHitTest(atWindowPoint: windowPoint) {
            return chip
        }
        return contentView.quickSearchTagFilterClickTargetDescendant(containingWindowPoint: windowPoint)
    }
}
private extension NSView {
    func quickSearchTagFilterClickTargetFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        let localPoint = convert(windowPoint, from: nil)
        var hitView = hitTest(localPoint)
        while let candidate = hitView {
            if let chip = candidate as? QuickSearchTagFilterClickTargetView {
                return chip
            }
            hitView = candidate.superview
        }
        return nil
    }
    func quickSearchTagFilterClickTargetDescendant(containingWindowPoint windowPoint: NSPoint) -> QuickSearchTagFilterClickTargetView? {
        for subview in subviews.reversed() {
            if let chip = subview as? QuickSearchTagFilterClickTargetView,
               chip.containsWindowPointForActivation(windowPoint) {
                return chip
            }
            if let chip = subview.quickSearchTagFilterClickTargetDescendant(containingWindowPoint: windowPoint) {
                return chip
            }
        }
        return nil
    }
    func quickSearchResultRowFromHitTest(atWindowPoint windowPoint: NSPoint) -> QuickSearchResultRowView? {
        let localPoint = convert(windowPoint, from: nil)
        var hitView = hitTest(localPoint)
@@ -1051,6 +1156,8 @@
struct QuickSearchOverlayView: View {
    @Binding var query: String
    let results: [QuickSearchResult]
    let tagFilters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let selectedID: URL?
    let focusToken: Int
    let selectionScrollToken: Int
@@ -1060,6 +1167,7 @@
    let onCommand: (QuickSearchCommand) -> Void
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void
    let onTagFilterChange: (String?) -> Void
    @Environment(\.colorScheme) private var colorScheme
@@ -1074,6 +1182,26 @@
    private var panelShape: RoundedRectangle {
        RoundedRectangle(cornerRadius: 34, style: .continuous)
    }
    private var hasTagFilters: Bool {
        !tagFilters.isEmpty
    }
    private var emptyResultsSystemImage: String {
        if selectedTagName != nil {
            return "tag"
        }
        return query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "keyboard" : "magnifyingglass"
    }
    private var emptyResultsMessage: String {
        if let selectedTagName {
            return tr("quickSearch.tagFilter.noResults", replacements: ["%tagName%": selectedTagName])
        }
        return query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
            ? tr("quickSearch.emptyPrompt")
            : tr("quickSearch.noResults")
    }
    var body: some View {
@@ -1096,6 +1224,15 @@
            .padding(.top, 22)
            .padding(.bottom, 18)
            if hasTagFilters {
                QuickSearchTagFilterBar(
                    filters: tagFilters,
                    selectedTagName: selectedTagName,
                    onSelect: onTagFilterChange
                )
                .frame(height: QuickSearchPanelMetrics.tagFilterBarHeight)
            }
            Divider().opacity(0.35)
            if isLoading {
@@ -1112,10 +1249,8 @@
                )
            } else if results.isEmpty {
                QuickSearchMessageRow(
                    systemImage: query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "keyboard" : "magnifyingglass",
                    message: query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                        ? tr("quickSearch.emptyPrompt")
                        : tr("quickSearch.noResults"),
                    systemImage: emptyResultsSystemImage,
                    message: emptyResultsMessage,
                    tint: .secondary
                )
            } else {
@@ -1152,6 +1287,159 @@
    }
}
private struct QuickSearchTagFilterBar: View {
    let filters: [QuickSearchTagFilterOption]
    let selectedTagName: String?
    let onSelect: (String?) -> Void
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 8) {
                QuickSearchTagFilterChip(
                    title: tr("quickSearch.tagFilter.all"),
                    colorIndex: nil,
                    customColor: nil,
                    isSelected: selectedTagName == nil,
                    accessibilityLabel: tr("quickSearch.tagFilter.allAccessibility")
                ) {
                    onSelect(nil)
                }
                ForEach(filters) { filter in
                    QuickSearchTagFilterChip(
                        title: filter.name,
                        colorIndex: filter.colorIndex,
                        customColor: filter.customColor,
                        isSelected: selectedTagName == filter.name,
                        accessibilityLabel: tr(
                            "quickSearch.tagFilter.tagAccessibility",
                            replacements: ["%tagName%": filter.name]
                        )
                    ) {
                        onSelect(selectedTagName == filter.name ? nil : filter.name)
                    }
                }
            }
            .padding(.horizontal, 28)
            .padding(.vertical, 5)
        }
        .accessibilityElement(children: .contain)
    }
}
private struct QuickSearchTagFilterChip: View {
    let title: String
    let colorIndex: Int?
    let customColor: TagCustomColor?
    let isSelected: Bool
    let accessibilityLabel: String
    let action: () -> Void
    @Environment(\.colorScheme) private var colorScheme
    @State private var isHovered = false
    private var tagDotColor: Color? {
        guard let colorIndex else { return nil }
        return Color(nsColor: TagColor.nsColor(for: colorIndex, customColor: customColor))
    }
    private var backgroundColor: Color {
        if isSelected {
            return Color.primary.opacity(colorScheme == .dark ? 0.18 : 0.10)
        }
        return Color.primary.opacity(isHovered ? (colorScheme == .dark ? 0.11 : 0.075) : (colorScheme == .dark ? 0.08 : 0.055))
    }
    private var strokeColor: Color {
        Color.primary.opacity(isSelected ? 0.34 : 0.11)
    }
    var body: some View {
        Button(action: action) {
            HStack(spacing: 7) {
                if let tagDotColor {
                    Circle()
                        .fill(tagDotColor)
                        .frame(width: 7, height: 7)
                }
                Text(title)
                    .font(.system(size: 13, weight: .semibold))
                    .lineLimit(1)
                    .truncationMode(.tail)
                    .foregroundStyle(.primary)
                    .layoutPriority(1)
            }
            .padding(.horizontal, 11)
            .frame(maxWidth: 160, minHeight: 30, maxHeight: 30, alignment: .center)
            .background(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .fill(backgroundColor)
            )
            .overlay(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .stroke(strokeColor, lineWidth: isSelected ? 1.2 : 1)
            )
            .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
        }
        .buttonStyle(.plain)
        .help(title)
        .onHover { isHovered = $0 }
        .overlay(
            QuickSearchTagFilterClickTarget(action: action)
                .accessibilityHidden(true)
        )
        .accessibilityLabel(accessibilityLabel)
        .accessibilityAddTraits(isSelected ? .isSelected : [])
    }
}
private struct QuickSearchTagFilterClickTarget: NSViewRepresentable {
    let action: () -> Void
    func makeNSView(context: Context) -> QuickSearchTagFilterClickTargetView {
        let view = QuickSearchTagFilterClickTargetView()
        view.onActivate = action
        return view
    }
    func updateNSView(_ view: QuickSearchTagFilterClickTargetView, context: Context) {
        view.onActivate = action
    }
}
private final class QuickSearchTagFilterClickTargetView: NSView {
    var onActivate: (() -> Void)?
    override var isFlipped: Bool { true }
    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
        true
    }
    override func mouseDown(with event: NSEvent) {
        guard activateFromMouseClick() else {
            super.mouseDown(with: event)
            return
        }
    }
    @discardableResult
    func activateFromMouseClick() -> Bool {
        guard let onActivate else { return false }
        DispatchQueue.main.async { [onActivate] in
            onActivate()
        }
        return true
    }
    func containsWindowPointForActivation(_ windowPoint: NSPoint) -> Bool {
        guard !isHidden, window != nil else { return false }
        let localPoint = convert(windowPoint, from: nil)
        return bounds.contains(localPoint) && visibleRect.contains(localPoint)
    }
}
private struct QuickSearchResultListView: NSViewRepresentable {
    let results: [QuickSearchResult]
    let selectedID: URL?
C1.source/Apptag/TagNavigationView.swift
@@ -209,7 +209,7 @@
    ) {
        let oldNames = self.items.map(\.name)
        let newNames = items.map(\.name)
        let needsRebuild = Set(oldNames) != Set(newNames) || self.orientation != orientation
        let needsRebuild = oldNames.count != newNames.count || Set(oldNames) != Set(newNames) || self.orientation != orientation
        self.items = items
        self.orientation = orientation
        self.contentInsets = contentInsets
@@ -292,9 +292,14 @@
    }
    private func reuseButtonsInCurrentOrder() {
        let existing = Dictionary(uniqueKeysWithValues: buttons.map { ($0.itemName, $0) })
        var existing: [String: [TagNavigationButton]] = [:]
        for button in buttons {
            existing[button.itemName, default: []].append(button)
        }
        buttons = items.compactMap { item in
            guard let button = existing[item.name] else { return nil }
            guard var candidates = existing[item.name], !candidates.isEmpty else { return nil }
            let button = candidates.removeFirst()
            existing[item.name] = candidates
            button.configure(item: item, orientation: orientation)
            button.refreshAppDropRegistration()
            return button
C1.source/CHANGELOG.md
@@ -2,6 +2,14 @@
## [Unreleased]
## [8.4.0] — 2026-08-28
- Quick Search 增加标签筛选条,可点击 `全部` 清除筛选,也可点击普通标签在该标签内继续搜索 App、标签和备注
- 标签筛选保持运行时单选状态,不持久化,不改变 App Grid、Pro 权益、数据结构或发布链路
- 修复 `全部` 标签在真实 UI 下不能稳定清除筛选的问题,并防止保护容器名触发 duplicate-key 崩溃
- 补齐 29 个语种的 Quick Search 标签筛选文案,新增专项 QA 门禁与真实 UI 复测证据
- 版本号更新为 `8.4.0`,Build 更新为 `20260828.1732`
## [8.3.7] — 2026-08-22
- 优化 App Grid 备注气泡输入框的默认提示文字颜色,在深色浮层上保持次级提示感但不再近似不可见
C1.source/Scripts/quick_search_tag_filter_qa.sh
New file
@@ -0,0 +1,308 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
QUICK_SEARCH="$ROOT_DIR/Apptag/QuickSearch.swift"
CONTENT_VIEW="$ROOT_DIR/Apptag/ContentView.swift"
DATA_LAYER="$ROOT_DIR/Apptag/DataLayer.swift"
TAG_NAVIGATION="$ROOT_DIR/Apptag/TagNavigationView.swift"
L10N_SWIFT="$ROOT_DIR/Apptag/L10n.swift"
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
python3 - "$QUICK_SEARCH" "$CONTENT_VIEW" "$DATA_LAYER" "$TAG_NAVIGATION" "$L10N_SWIFT" "$LOCALIZATION_DIR" <<'PY'
import json
import re
import sys
from pathlib import Path
quick_search = Path(sys.argv[1]).read_text(encoding="utf-8")
content_view = Path(sys.argv[2]).read_text(encoding="utf-8")
data_layer = Path(sys.argv[3]).read_text(encoding="utf-8")
tag_navigation = Path(sys.argv[4]).read_text(encoding="utf-8")
l10n_swift = Path(sys.argv[5]).read_text(encoding="utf-8")
localization_dir = Path(sys.argv[6])
def fail(message: str) -> None:
    print(f"FAIL: {message}", file=sys.stderr)
    sys.exit(1)
def require(condition: bool, message: str) -> None:
    if not condition:
        fail(message)
def require_contains(source: str, needle: str, message: str) -> None:
    require(needle in source, message)
def block(source: str, pattern: str, message: str) -> str:
    match = re.search(pattern, source, re.S)
    require(match is not None, message)
    return match.group(0)
search_block = block(
    quick_search,
    r"static func search\(\n        _ query: String,.*?static func normalizeQuery",
    "QuickSearchEngine.search block is missing",
)
for needle, message in [
    ("selectedTagName: String? = nil", "search must accept a runtime selected tag filter"),
    ("let activeTagName = normalizedSelectedTagName(selectedTagName)", "search must normalize the selected tag name without persisting it"),
    ("documents.filter { document in", "search must scope documents before text matching"),
    ("document.tagNames.contains(tagName)", "search tag filter must use exact tag membership"),
    ("if let activeTagName", "empty query must branch when a tag filter is active"),
    ("tagFilteredEmptyQueryResults(", "empty query with selected tag must list that tag's apps"),
    ("emptyQueryResults(documents: scopedDocuments, limit: min(limit, 6))", "empty query without selected tag must keep the existing recent/frequent cap"),
    ("result(for: $0, tokens: tokens, selectedTagName: activeTagName)", "text query must combine keyword matching with the selected tag scope"),
]:
    require_contains(search_block, needle, message)
require_contains(
    quick_search,
    "matchedTagName: matchedTagName ?? selectedTagName",
    "filtered results should prefer the selected tag in the right-side row badge",
)
require_contains(
    quick_search,
    "struct QuickSearchTagFilterOption: Identifiable, Equatable",
    "Quick Search must expose tag filter options for the panel",
)
metrics_block = block(
    quick_search,
    r"enum QuickSearchPanelMetrics.*?struct QuickSearchPanelPresentationView",
    "Quick Search panel metrics block is missing",
)
for needle, message in [
    ("static let tagFilterBarHeight: CGFloat = 40", "tag filter bar height must follow the V1 design contract"),
    ("contentHeight(hasTagFilters: Bool", "panel height must account for the tag filter bar"),
    ("let tagFilterHeight = hasTagFilters ? tagFilterBarHeight : 0", "panel height must only grow when real filters exist"),
]:
    require_contains(metrics_block, needle, message)
presentation_block = block(
    quick_search,
    r"struct QuickSearchPanelPresentationView: NSViewRepresentable.*?private final class QuickSearchPanel",
    "Quick Search presentation view block is missing",
)
for needle, message in [
    ("let tagFilters: [QuickSearchTagFilterOption]", "panel presentation must receive available tag filters"),
    ("let selectedTagName: String?", "panel presentation must receive the selected tag"),
    ("let onTagFilterChange: (String?) -> Void", "panel presentation must report tag changes"),
    ("tagFilters: tagFilters", "panel presentation must pass filters into the overlay"),
    ("selectedTagName: selectedTagName", "panel presentation must pass selected tag into the overlay"),
    ("onTagFilterChange: onTagFilterChange", "panel presentation must pass tag change handler into the overlay"),
]:
    require_contains(presentation_block, needle, message)
overlay_block = block(
    quick_search,
    r"struct QuickSearchOverlayView: View.*?private struct QuickSearchResultListView",
    "Quick Search overlay block is missing",
)
for needle, message in [
    ("QuickSearchTagFilterBar(", "overlay must render the tag filter bar"),
    ("ScrollView(.horizontal, showsIndicators: false)", "tag filter bar must be single-line horizontal scrolling"),
    ("QuickSearchTagFilterChip(", "tag filter bar must render tappable chips"),
    ("title: tr(\"quickSearch.tagFilter.all\")", "tag filter bar must include the localized All chip"),
    ("onSelect(nil)", "tapping the All chip must clear the selected tag filter"),
    ("onSelect(selectedTagName == filter.name ? nil : filter.name)", "tapping the selected tag must clear the filter"),
    ("tr(\"quickSearch.tagFilter.noResults\"", "selected tag no-results copy must be localized"),
    (".frame(maxWidth: 160, minHeight: 30, maxHeight: 30", "tag chips must cap long labels and keep a stable hit target"),
    (".help(title)", "long or truncated tag chips must expose the full tag name"),
]:
    require_contains(overlay_block, needle, message)
tag_bar_index = overlay_block.find("QuickSearchTagFilterBar(")
divider_index = overlay_block.find("Divider().opacity")
result_list_index = overlay_block.find("QuickSearchResultListView(")
require(
    0 <= tag_bar_index < divider_index < result_list_index,
    "tag filter bar must sit between the search field and the result list divider",
)
require(
    "LazyVGrid" not in overlay_block and "GridItem" not in overlay_block,
    "Quick Search results must remain long-list rows, not a square grid",
)
require_contains(
    overlay_block,
    "QuickSearchResultListView(",
    "Quick Search result list must continue using the existing long-row host",
)
panel_class = block(
    quick_search,
    r"private final class QuickSearchPanel: NSPanel.*?final class QuickSearchPanelAnchorView",
    "QuickSearchPanel class block is missing",
)
for needle, message in [
    ("quickSearchTagFilterClickTarget(at: event.locationInWindow)", "panel mouse fallback must resolve tag filter chips from the event point"),
    ("tagChip.activateFromMouseClick()", "panel mouse fallback must invoke the resolved tag chip action"),
    ("quickSearchTagFilterClickTargetFromHitTest(atWindowPoint: windowPoint)", "panel tag fallback should keep the direct hitTest path"),
    ("quickSearchTagFilterClickTargetDescendant(containingWindowPoint: windowPoint)", "panel tag fallback must keep a recursive geometry path"),
    ("quickSearchResultRow(at: event.locationInWindow)", "panel mouse fallback must preserve result-row launch handling"),
]:
    require_contains(panel_class, needle, message)
tag_fallback_index = panel_class.find("quickSearchTagFilterClickTarget(at: event.locationInWindow)")
row_fallback_index = panel_class.find("quickSearchResultRow(at: event.locationInWindow)")
require(
    0 <= tag_fallback_index < row_fallback_index,
    "panel mouse fallback must process tag chips before result rows",
)
chip_block = block(
    quick_search,
    r"private struct QuickSearchTagFilterChip: View.*?private struct QuickSearchResultListView",
    "QuickSearchTagFilterChip block is missing",
)
for needle, message in [
    ("QuickSearchTagFilterClickTarget(action: action)", "tag chips must install a native mouse click target"),
    ("private struct QuickSearchTagFilterClickTarget: NSViewRepresentable", "tag chips must use an AppKit-backed click target"),
    ("private final class QuickSearchTagFilterClickTargetView: NSView", "tag chip native click target view is missing"),
    ("override func acceptsFirstMouse(for event: NSEvent?) -> Bool", "tag chip native click target must accept first mouse"),
    ("override func mouseDown(with event: NSEvent)", "tag chip native click target must handle real mouse down"),
    ("func activateFromMouseClick() -> Bool", "tag chip native click target must expose a panel-level activation fallback"),
    ("func containsWindowPointForActivation(_ windowPoint: NSPoint) -> Bool", "tag chip native click target must expose geometry containment"),
    ("bounds.contains(localPoint) && visibleRect.contains(localPoint)", "tag chip geometry fallback must reject hidden or clipped chip areas"),
]:
    require_contains(chip_block, needle, message)
state_block = block(
    content_view,
    r"// Quick Search\n.*?init\(hideOverlay",
    "ContentView Quick Search state block is missing",
)
require_contains(
    state_block,
    "@State private var quickSearchSelectedTagName: String? = nil",
    "ContentView must keep selected tag as runtime state only",
)
require(
    "AppStorage(\"quickSearchSelectedTagName\")" not in content_view
    and "UserDefaults.standard.set" not in block(content_view, r"private func selectQuickSearchTagFilter.*?private func handleQuickSearchCommand", "tag selection handler block is missing"),
    "Quick Search tag filter must not be persisted",
)
content_panel_block = block(
    content_view,
    r"private var quickSearchOverlay: some View.*?private func openQuickSearch",
    "ContentView Quick Search overlay block is missing",
)
for needle, message in [
    ("tagFilters: quickSearchTagFilters", "ContentView must pass real available tag filters into the panel"),
    ("selectedTagName: quickSearchSelectedTagName", "ContentView must pass selected tag into the panel"),
    ("onTagFilterChange: selectQuickSearchTagFilter", "ContentView must handle tag chip clicks"),
    ("hasTagFilters: !quickSearchTagFilters.isEmpty", "panel height must account for visible filters"),
    ("quickSearchTagFilters.isEmpty ? 0 : QuickSearchPanelMetrics.tagFilterBarHeight", "max visible rows must reserve height for the filter bar"),
]:
    require_contains(content_panel_block, needle, message)
refresh_block = block(
    content_view,
    r"private func refreshQuickSearchResults\(\).*?private func handleQuickSearchCommand",
    "ContentView Quick Search refresh block is missing",
)
for needle, message in [
    ("let documents = quickSearchVisibleDocuments", "refresh must filter out missing app paths once before searching"),
    ("let selectedTagName = validateQuickSearchSelectedTagName(against: quickSearchTagFilters)", "refresh must clear deleted/renamed tag filters before searching"),
    ("selectedTagName: selectedTagName", "refresh must pass the validated selected tag to search"),
    ("private var quickSearchTagFilters: [QuickSearchTagFilterOption]", "ContentView must derive real tag filters"),
    (".flatMap(\\.tagNames)", "available filters must come from indexed app tags"),
    (".filter(isQuickSearchFilterableTag)", "available filters must exclude protected/system-only names"),
    ("draggedTagNames", "available filters must follow user tag order"),
    ("definition?.customColor", "available filters must preserve custom tag colors"),
    ("guard !name.isEmpty, tagDefinitions[name] != nil else { return false }", "filter chips must only include real tag definitions"),
    ("AppContainerID.isReservedGroupName(name, defaultGroupName: defaultGroupName)", "filter chips must use the shared reserved group-name guard"),
    ("private func validateQuickSearchSelectedTagName(against filters: [QuickSearchTagFilterOption]) -> String?", "selected tag validation must return the active filter or clear it"),
    ("quickSearchFocusToken &+= 1", "tag clicks must return focus to the search input"),
]:
    require_contains(refresh_block, needle, message)
container_id_block = block(
    data_layer,
    r"enum AppContainerID.*?enum AppDisplayNameResolver",
    "AppContainerID block is missing",
)
for needle, message in [
    ("static func isReservedGroupName(_ name: String, defaultGroupName: String = \"Other\") -> Bool", "AppContainerID must expose a shared reserved group-name guard"),
    ("\"Other\"", "reserved group-name guard must include the neutral uncategorized key"),
    ("defaultGroupName", "reserved group-name guard must include the configured uncategorized key"),
    ("tr(\"group.uncategorized\")", "reserved group-name guard must include localized uncategorized names"),
    ("\"Mac自带\"", "reserved group-name guard must include the Apple built-in container"),
    ("tr(\"group.appleBuiltIn\")", "reserved group-name guard must include localized Apple built-in names"),
    ("TagDatabase.uncommonTagKey", "reserved group-name guard must include the system uncommon key"),
    ("tr(\"group.uncommon\")", "reserved group-name guard must include localized uncommon names"),
    ("trimmingCharacters(in: .whitespacesAndNewlines)", "reserved group-name guard must trim names before comparison"),
]:
    require_contains(container_id_block, needle, message)
group_block = block(
    data_layer,
    r"static func group\(\n        apps: \[AppInfo\],.*?static func orderedApps",
    "AppIndexer.group block is missing",
)
require_contains(
    group_block,
    "!AppContainerID.isReservedGroupName(displayName, defaultGroupName: defaultGroupName)",
    "AppIndexer.group must skip protected container/system tag names before building display groups",
)
require(
    group_block.count("!AppContainerID.isReservedGroupName(displayName, defaultGroupName: defaultGroupName)") >= 2,
    "AppIndexer.group must skip protected names in both app tag grouping and empty tag-definition groups",
)
reuse_block = block(
    tag_navigation,
    r"private func reuseButtonsInCurrentOrder\(\).*?private func updateButtonRuntimeState",
    "TagNavigationView button reuse block is missing",
)
require(
    "Dictionary(uniqueKeysWithValues: buttons.map { ($0.itemName, $0) })" not in reuse_block,
    "TagNavigationView must not fatal when duplicate display names reach button reuse",
)
require_contains(
    tag_navigation,
    "oldNames.count != newNames.count",
    "TagNavigationView must rebuild when duplicate-name counts change",
)
for needle, message in [
    ("var existing: [String: [TagNavigationButton]] = [:]", "TagNavigationView must build a duplicate-tolerant reuse map"),
    ("existing[button.itemName, default: []].append(button)", "TagNavigationView must keep every reusable button for duplicate names"),
    ("let button = candidates.removeFirst()", "TagNavigationView must consume one reusable button per item"),
]:
    require_contains(reuse_block, needle, message)
for pattern, message in [
    (r"private func openQuickSearch\(source: String\).*?private var canOpenQuickSearch", "openQuickSearch block is missing"),
    (r"private func closeQuickSearch\(.*?private func shouldIgnoreEarlyQuickSearchDismiss", "closeQuickSearch block is missing"),
    (r"private func handleContentAppear\(\).*?private func handleOverlayDidShow", "initial Quick Search block is missing"),
]:
    require_contains(block(content_view, pattern, message), "quickSearchSelectedTagName = nil", f"{message}; selected tag must reset on lifecycle transition")
supported_match = re.search(r"static let supported: \[\(code: String, name: String\)\] = \[(.*?)\n    \]", l10n_swift, re.S)
require(supported_match is not None, "L10n.supported block is missing")
supported_codes = re.findall(r'\("([^"]+)"\s*,', supported_match.group(1))
required_l10n_keys = [
    "quickSearch.tagFilter.all",
    "quickSearch.tagFilter.noResults",
    "quickSearch.tagFilter.allAccessibility",
    "quickSearch.tagFilter.tagAccessibility",
]
for code in supported_codes:
    file = localization_dir / f"{code}.json"
    require(file.exists(), f"missing localization file for {code}")
    try:
        data = json.loads(file.read_text(encoding="utf-8"))
    except json.JSONDecodeError as exc:
        fail(f"invalid JSON in {file.name}: {exc}")
    for key in required_l10n_keys:
        value = data.get(key)
        require(isinstance(value, str) and value.strip(), f"{file.name} missing non-empty {key}")
    require("%tagName%" in data["quickSearch.tagFilter.noResults"], f"{file.name} no-results copy must include %tagName%")
    require("%tagName%" in data["quickSearch.tagFilter.tagAccessibility"], f"{file.name} tag accessibility copy must include %tagName%")
print("PASS quick search tag filter QA")
PY
CODEGRAPH.md
@@ -33,6 +33,7 @@
  - 编辑模式使用 runtime theme override:任何主题下进入编辑都临时显示默认浅色毛玻璃。
  - 使用最近一次完整 `AppLibrarySnapshot` 先渲染 AppGrid,再后台刷新,避免启动/重开时立刻显示转圈。
  - 使用技巧关闭提醒由 `ContentView` 统一弹 modal 并写入 `hideUsageTips` / `skipUsageTipsCloseReminder`。
  - Quick Search 标签筛选状态只存在于当前面板生命周期;`ContentView` 从当前索引和 `tagDefinitions` 生成可选真实标签,并通过 `AppContainerID.isReservedGroupName` 排除未分类 / Mac 自带 / 不常用等保护容器名,打开、关闭、初始 Quick Search、标签删除/重命名或刷新后失效时都清空筛选。
  - Quick Search 点击 TagLauncher 自身结果时只关闭当前浮层,不走外部自启动,避免 AppGrid 被重新拉起。
- `C1.source/Apptag/PreferencesView.swift`
  - `PreferencesView`:设置窗口、语言、通用、主题、快捷键、标签、数据、Pro、关于等设置页。
@@ -83,7 +84,7 @@
- `C1.source/Apptag/DataLayer.swift`
  - `AppInfo`:应用模型。
  - `AppDisplayNameResolver`:多语言应用显示名解析。
  - `AppIndexer`:扫描 `/Applications` 等应用来源,处理 bundle / wrapper / localized display name。
  - `AppIndexer`:扫描 `/Applications` 等应用来源,处理 bundle / wrapper / localized display name;主分组生成时通过 `AppContainerID.isReservedGroupName` 跳过被错误写成普通标签的未分类 / Mac 自带 / 不常用等保护容器名,避免本地化显示名重复导致标签导航 fatal。
  - `TagGroup` / `TagColor` / `TagCustomColor`:标签分组、基础色和可选 sRGB 自定义色;`TagDef.customColor` 为可选字段,旧 JSON 缺字段时保持兼容。
  - `TagDatabase`:标签、备注、隐藏状态、分类方案、导入导出、备份和持久化。
  - `TagEditor`:把数据库中的标签/备注/隐藏状态标注回扫描到的 App 列表;`setCustomColor` 是写入自定义标签颜色的唯一数据层入口,必须先过 Pro gate,基础色切换会清除自定义色。
@@ -104,9 +105,10 @@
- `C1.source/Apptag/QuickSearch.swift`
  - `LauncherHotkey` / `LauncherHotkeyKind`:主面板和 Quick Search 快捷键定义。
  - `LauncherHotkeyRegistrationStore`:快捷键注册状态持久化。
  - `QuickSearchDocument` / `QuickSearchEngine`:搜索索引与排序。
  - `QuickSearchDocument` / `QuickSearchEngine`:搜索索引与排序;`selectedTagName` 是运行时单选筛选条件,先按真实标签精确收窄文档,再与文本 token 做交集。无文本但已选标签时列出该标签下 App;无标签筛选的空搜索继续保持最近/常用候选。
  - `QuickSearchTagFilterOption` / `QuickSearchTagFilterBar` / `QuickSearchTagFilterChip`:搜索框下方、结果列表上方的单行横向标签筛选条;只在存在真实可用标签时显示,包含本地化 `全部` 和真实标签胶囊,不改结果长条列表形态。
  - `QuickSearchPanelPresentationView` / `QuickSearchOverlayView` / `QuickSearchResultListHostView`:Quick Search 浮层 UI 与结果列表;Quick Search 面板必须是可成为 key 的激活面板,不能使用 `.nonactivatingPanel`,否则 Return 可能落到前一个前台 App。
  - `QuickSearchPanel.sendEvent` 对左键点击做 AppKit 层兜底:先通过 `hitTest` 解析 `QuickSearchResultRowView`,失败时按窗口坐标递归扫描结果行几何命中,并走同一启动路径,避免 SwiftUI / NSHostingView / NSScrollView 层级吞掉候选行点击。
  - `QuickSearchPanel.sendEvent` 对左键点击做 AppKit 层兜底:先解析 `QuickSearchTagFilterClickTargetView` 并触发标签筛选 / 清除;再通过 `hitTest` 和窗口坐标递归几何命中解析 `QuickSearchResultRowView` 并走同一启动路径,避免 SwiftUI / NSHostingView / NSScrollView 层级吞掉标签 chip 或候选行点击。
  - `QuickSearchResultRowView`:结果行内部文字、图标、标签区域统一命中到整行,保证点击任意可见区域都能启动并关闭浮层。
  - `QuickSearchTextField` 同时保留 field editor command delegate 和 `NSControl.target/action` submit fallback,保证 Return / Enter 都能进入 `.submit`。
- `C1.source/Apptag/LauncherHotkeySettings.swift`
@@ -184,6 +186,8 @@
  - Quick Search 应用显示名回归。
- `C1.source/Scripts/quick_search_system_app_qa.sh`
  - Quick Search 系统应用回归。
- `C1.source/Scripts/quick_search_tag_filter_qa.sh`
  - Quick Search 标签筛选静态门禁;检查运行时单选标签筛选、`全部` 清除、tag chip 原生鼠标命中兜底、空搜索 + 标签列出该标签 App、标签 + 关键词交集、保护容器名从 Quick Search / 主分组排除且导航复用不因重复显示名 fatal、标签条横向胶囊、结果仍走长条候选项和 29 语种本地化 key 完整。
- `C1.source/Scripts/quick_search_launch_contract_qa.sh`
  - Quick Search 启动路径静态门禁;检查 in-app mouse monitor 不吞结果行点击、面板是激活 key panel、panel-level `sendEvent` 点击兜底存在、Return submit fallback 存在、点击 / submit 都接到 `NSWorkspace.openApplication` 启动路径。
- `C1.source/Scripts/windowserver_event_safety_qa.sh`
@@ -242,9 +246,10 @@
- 旧用户 `useDarkAppGrid=true` 必须迁移到 `deepBlue` 主题。
- AppGrid 启动或重开不应因为新 `ContentView` 初始 `allApps.isEmpty` 立刻显示转圈;应先复用最近一次完整快照,并延迟显示 loading。
- Quick Search 显示标题应优先使用解析后的 `AppInfo.displayName`,内部 bundle 名只作为搜索字段。
- Quick Search 标签筛选条只做 V1 单选运行时过滤:显示 `全部` + 当前索引中真实存在的标签;不得显示未分类 / Mac 自带 / 不常用等保护容器;点击已选标签或 `全部` 清除,并由原生鼠标命中兜底保持面板稳定打开;选中标签后输入关键词必须取交集;不得改成方块矩阵、不得持久化筛选状态。
- Quick Search 结果行内部文字 / 图标区域不得吞掉点击;点中行内任意可见区域都应按结果启动路径处理。
- Quick Search 打开后必须可靠接收键盘;面板不得是 non-activating panel,展示时必须激活 App 并成为 key window,Return / Enter 必须通过 command delegate 或 target/action fallback 进入 submit。
- Quick Search 打开期间,本地 mouse monitor 不得吞掉 TagLauncher overlayWindow 内部事件;外部点击可关闭 Quick Search,结果行点击必须通过 `QuickSearchResultRowView.mouseDown` 或 `QuickSearchPanel.sendEvent` 兜底进入同一启动路径;panel 级兜底不得只依赖 `hitTest`,必须保留窗口坐标递归几何命中结果行的路径。
- Quick Search 打开期间,本地 mouse monitor 不得吞掉 TagLauncher overlayWindow 内部事件;外部点击可关闭 Quick Search,标签 chip 和结果行点击必须通过对应 native view mouseDown 或 `QuickSearchPanel.sendEvent` 兜底进入同一筛选 / 启动路径;panel 级兜底不得只依赖 `hitTest`,必须保留窗口坐标递归几何命中路径。
- Quick Search-only 点击 TagLauncher 自身结果必须关闭浮层,不得重新打开 AppGrid。
- Apple 默认应用备注必须按当前语言显示,不允许 A 语言出现 B 语言备注。
- SmartStart/default system tag 初始化标签必须按当前语言显示;升级用户启动后应重刷带 `systemCategoryID` 的系统标签,不能长期保留旧英文初始化标签。