From c5bdc7f1eb3ff4287beeeebb17d4f02f376a8d38 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 28 Aug 2026 17:34:19 +0800
Subject: [PATCH] feat: add quick search tag filtering

---
 C1.source/Apptag/Localization/no.json           |    4 
 C1.source/Scripts/quick_search_tag_filter_qa.sh |  308 ++++++++++++++++++++
 C1.source/Apptag/Localization/nl.json           |    4 
 C1.source/CHANGELOG.md                          |    8 
 C1.source/Apptag/Localization/es.json           |    4 
 C1.source/Apptag/Localization/pl.json           |    4 
 C1.source/Apptag/Localization/sr-Cyrl.json      |    4 
 C1.source/Apptag/Localization/zh-Hans.json      |    4 
 C1.source/Apptag/Localization/ja.json           |    4 
 C1.source/Apptag/Localization/ms.json           |    4 
 C1.source/Apptag/Localization/ar-Najdi.json     |    4 
 C1.source/Apptag/Localization/zh-Hant.json      |    4 
 C1.source/Apptag/Localization/th.json           |    4 
 C1.source/Apptag/Localization/da.json           |    4 
 C1.source/Apptag/Localization/id.json           |    4 
 C1.source/Apptag/Localization/ro.json           |    4 
 C1.source/Apptag/Localization/cs.json           |    4 
 C1.source/Apptag/Localization/it.json           |    4 
 C1.source/Apptag/ContentView.swift              |   71 ++++
 C1.source/Apptag/Localization/tr.json           |    4 
 C1.source/Apptag/Info.plist                     |    4 
 C1.source/Apptag/Localization/nn.json           |    4 
 C1.source/Apptag/Localization/ru.json           |    4 
 C1.source/Apptag/Localization/en.json           |    4 
 C1.source/Apptag/Localization/sv.json           |    4 
 C1.source/Apptag/Localization/pt-BR.json        |    4 
 CODEGRAPH.md                                    |   13 
 C1.source/Apptag/Localization/ko.json           |    4 
 C1.source/Apptag/DataLayer.swift                |   29 +
 C1.source/Apptag/Localization/de.json           |    4 
 C1.source/Apptag/TagNavigationView.swift        |   11 
 C1.source/Apptag/Localization/uk.json           |    4 
 C1.source/Apptag/QuickSearch.swift              |  320 ++++++++++++++++++++-
 C1.source/Apptag/Localization/vi.json           |    4 
 C1.source/Apptag/Localization/fr.json           |    4 
 C1.source/Apptag/Localization/ar.json           |    4 
 C1.source/Apptag/Localization/nb.json           |    4 
 37 files changed, 849 insertions(+), 31 deletions(-)

diff --git a/C1.source/Apptag/ContentView.swift b/C1.source/Apptag/ContentView.swift
index b1ca582..d9ae06f 100644
--- a/C1.source/Apptag/ContentView.swift
+++ b/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(
diff --git a/C1.source/Apptag/DataLayer.swift b/C1.source/Apptag/DataLayer.swift
index b949e7b..f39f906 100644
--- a/C1.source/Apptag/DataLayer.swift
+++ b/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(
diff --git a/C1.source/Apptag/Info.plist b/C1.source/Apptag/Info.plist
index 7885d89..39009ee 100644
--- a/C1.source/Apptag/Info.plist
+++ b/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>
diff --git a/C1.source/Apptag/Localization/ar-Najdi.json b/C1.source/Apptag/Localization/ar-Najdi.json
index 567f86f..0dc94c8 100644
--- a/C1.source/Apptag/Localization/ar-Najdi.json
+++ b/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": "اختصارات لوحة المفاتيح",
diff --git a/C1.source/Apptag/Localization/ar.json b/C1.source/Apptag/Localization/ar.json
index e1f14ef..3247b0a 100644
--- a/C1.source/Apptag/Localization/ar.json
+++ b/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": "اختصارات لوحة المفاتيح",
diff --git a/C1.source/Apptag/Localization/cs.json b/C1.source/Apptag/Localization/cs.json
index 8f09d1e..c632ad8 100644
--- a/C1.source/Apptag/Localization/cs.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/da.json b/C1.source/Apptag/Localization/da.json
index 2c1cc90..3557250 100644
--- a/C1.source/Apptag/Localization/da.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/de.json b/C1.source/Apptag/Localization/de.json
index a1ee613..78bb775 100644
--- a/C1.source/Apptag/Localization/de.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/en.json b/C1.source/Apptag/Localization/en.json
index 83e8723..8b976fb 100644
--- a/C1.source/Apptag/Localization/en.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/es.json b/C1.source/Apptag/Localization/es.json
index 013bd9c..94076f6 100644
--- a/C1.source/Apptag/Localization/es.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/fr.json b/C1.source/Apptag/Localization/fr.json
index 757cd64..4f23cec 100644
--- a/C1.source/Apptag/Localization/fr.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/id.json b/C1.source/Apptag/Localization/id.json
index a510814..8be6c5c 100644
--- a/C1.source/Apptag/Localization/id.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/it.json b/C1.source/Apptag/Localization/it.json
index 6399d25..03eb6ec 100644
--- a/C1.source/Apptag/Localization/it.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/ja.json b/C1.source/Apptag/Localization/ja.json
index ee5e996..fc64737 100644
--- a/C1.source/Apptag/Localization/ja.json
+++ b/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": "キーボードショートカット",
diff --git a/C1.source/Apptag/Localization/ko.json b/C1.source/Apptag/Localization/ko.json
index 7b67c10..45f9911 100644
--- a/C1.source/Apptag/Localization/ko.json
+++ b/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": "키보드 단축키",
diff --git a/C1.source/Apptag/Localization/ms.json b/C1.source/Apptag/Localization/ms.json
index b16ac1b..090aaea 100644
--- a/C1.source/Apptag/Localization/ms.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/nb.json b/C1.source/Apptag/Localization/nb.json
index 4b53453..f904085 100644
--- a/C1.source/Apptag/Localization/nb.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/nl.json b/C1.source/Apptag/Localization/nl.json
index 9bc018d..b9083ec 100644
--- a/C1.source/Apptag/Localization/nl.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/nn.json b/C1.source/Apptag/Localization/nn.json
index 990634f..10fb631 100644
--- a/C1.source/Apptag/Localization/nn.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/no.json b/C1.source/Apptag/Localization/no.json
index 4b53453..f904085 100644
--- a/C1.source/Apptag/Localization/no.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/pl.json b/C1.source/Apptag/Localization/pl.json
index 6b44813..5115844 100644
--- a/C1.source/Apptag/Localization/pl.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/pt-BR.json b/C1.source/Apptag/Localization/pt-BR.json
index 7fff595..5d64984 100644
--- a/C1.source/Apptag/Localization/pt-BR.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/ro.json b/C1.source/Apptag/Localization/ro.json
index c324790..3b1f546 100644
--- a/C1.source/Apptag/Localization/ro.json
+++ b/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ă",
diff --git a/C1.source/Apptag/Localization/ru.json b/C1.source/Apptag/Localization/ru.json
index 941c049..4e657f8 100644
--- a/C1.source/Apptag/Localization/ru.json
+++ b/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": "Сочетания клавиш",
diff --git a/C1.source/Apptag/Localization/sr-Cyrl.json b/C1.source/Apptag/Localization/sr-Cyrl.json
index 820e8ae..2ce10e3 100644
--- a/C1.source/Apptag/Localization/sr-Cyrl.json
+++ b/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": "Пречице на тастатури",
diff --git a/C1.source/Apptag/Localization/sv.json b/C1.source/Apptag/Localization/sv.json
index 5d46075..eba12be 100644
--- a/C1.source/Apptag/Localization/sv.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/th.json b/C1.source/Apptag/Localization/th.json
index 8265015..28ee2bb 100644
--- a/C1.source/Apptag/Localization/th.json
+++ b/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": "แป้นพิมพ์ลัด",
diff --git a/C1.source/Apptag/Localization/tr.json b/C1.source/Apptag/Localization/tr.json
index 8364404..c89788e 100644
--- a/C1.source/Apptag/Localization/tr.json
+++ b/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ı",
diff --git a/C1.source/Apptag/Localization/uk.json b/C1.source/Apptag/Localization/uk.json
index f16feb0..ed8a799 100644
--- a/C1.source/Apptag/Localization/uk.json
+++ b/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": "Комбінації клавіш",
diff --git a/C1.source/Apptag/Localization/vi.json b/C1.source/Apptag/Localization/vi.json
index 8ae8be4..dd46d2c 100644
--- a/C1.source/Apptag/Localization/vi.json
+++ b/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",
diff --git a/C1.source/Apptag/Localization/zh-Hans.json b/C1.source/Apptag/Localization/zh-Hans.json
index e0b1b3f..7f50b3c 100644
--- a/C1.source/Apptag/Localization/zh-Hans.json
+++ b/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": "快捷键",
diff --git a/C1.source/Apptag/Localization/zh-Hant.json b/C1.source/Apptag/Localization/zh-Hant.json
index ef7bad4..29493cd 100644
--- a/C1.source/Apptag/Localization/zh-Hant.json
+++ b/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": "快速鍵",
diff --git a/C1.source/Apptag/QuickSearch.swift b/C1.source/Apptag/QuickSearch.swift
index f4365c4..1c0c33e 100644
--- a/C1.source/Apptag/QuickSearch.swift
+++ b/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?
diff --git a/C1.source/Apptag/TagNavigationView.swift b/C1.source/Apptag/TagNavigationView.swift
index 551eaa1..5cc3871 100644
--- a/C1.source/Apptag/TagNavigationView.swift
+++ b/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
diff --git a/C1.source/CHANGELOG.md b/C1.source/CHANGELOG.md
index 2b922e5..d69fb3a 100644
--- a/C1.source/CHANGELOG.md
+++ b/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 备注气泡输入框的默认提示文字颜色,在深色浮层上保持次级提示感但不再近似不可见
diff --git a/C1.source/Scripts/quick_search_tag_filter_qa.sh b/C1.source/Scripts/quick_search_tag_filter_qa.sh
new file mode 100755
index 0000000..aa6bc86
--- /dev/null
+++ b/C1.source/Scripts/quick_search_tag_filter_qa.sh
@@ -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
diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index 2c01bc6..fa29b4f 100644
--- a/CODEGRAPH.md
+++ b/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` 的系统标签,不能长期保留旧英文初始化标签。

--
Gitblit v1.9.3