40 files modified
14 files added
1171 ■■■■■ changed files
.codebase-memory/artifact.json 14 ●●●● patch | view | raw | blame | history
.codebase-memory/graph.db.zst patch | view | raw | blame | history
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
C2.builds/AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg patch | view | raw | blame | history
C2.builds/AppStore-8.4.0-20260828.2219/SHA256SUMS.txt 2 ●●●●● patch | view | raw | blame | history
C2.builds/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg patch | view | raw | blame | history
C2.builds/LocalTest-8.4.0-20260828.1732/Archive/TagLauncher-8.4.0-build20260828.1732-localtest.dmg patch | view | raw | blame | history
C2.builds/LocalTest-8.4.0-20260828.1732/RELEASE_MANIFEST.md 44 ●●●●● patch | view | raw | blame | history
C2.builds/LocalTest-8.4.0-20260828.1732/SHA256SUMS.txt 1 ●●●● patch | view | raw | blame | history
C2.builds/LocalTest-8.4.0-20260828.1732/Verification/launch-smoke.log 1 ●●●● patch | view | raw | blame | history
C2.builds/README.md 7 ●●●● patch | view | raw | blame | history
CODEGRAPH.md 13 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/PKG_PREFLIGHT_REPORT.md 74 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/QA_RELEASE_EVIDENCE.md 44 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/RELEASE_MANIFEST.md 100 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/SHA256SUMS.txt 2 ●●●●● patch | view | raw | blame | history
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg patch | view | raw | blame | history
K3.运营与发布资料/Release/README.md 2 ●●● patch | view | raw | blame | history
.codebase-memory/artifact.json
@@ -1,11 +1,11 @@
{
    "schema_version": 1,
    "commit": "2309264ee94daca4b9abe3592d84602c98b77690",
    "indexed_at": "2026-07-02T09:00:28Z",
    "commit": "7a94f69bb0dc5206a2e68d82486fd5959d16e664",
    "indexed_at": "2026-08-28T14:20:24Z",
    "project": "Users-ar-Projects-Taglauncher-03-O",
    "nodes": 16612,
    "edges": 25581,
    "original_size": 16973824,
    "compressed_size": 2222538,
    "compression_level": 9
    "nodes": 121768,
    "edges": 130956,
    "original_size": 206569472,
    "compressed_size": 11646142,
    "compression_level": 3
}
.codebase-memory/graph.db.zst
Binary files differ
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.2219</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.2219`
## [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
C2.builds/AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg
Binary files differ
C2.builds/AppStore-8.4.0-20260828.2219/SHA256SUMS.txt
New file
@@ -0,0 +1,2 @@
1dc42dc6577721ef27985243e9f7b40537f3bcac6f340b8e330be7bf8ce4e4d6  Archive/TagLauncher-8.4.0-build20260828.2219.dmg
0d374a03b25c11a4bf9bad19238defd7c3043b72386ee70f7857e01f4f9d6318  Upload/TagLauncher-8.4.0-build20260828.2219.pkg
C2.builds/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg
Binary files differ
C2.builds/LocalTest-8.4.0-20260828.1732/Archive/TagLauncher-8.4.0-build20260828.1732-localtest.dmg
Binary files differ
C2.builds/LocalTest-8.4.0-20260828.1732/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,44 @@
# TagLauncher 8.4.0 Local Test Build
- Type: local installation acceptance DMG
- App version: `8.4.0`
- Build: `20260828.1732`
- Source commit: `c5bdc7f1eb3ff4287beeeebb17d4f02f376a8d38`
- Branch: `codex/slim-migration-taglauncher2`
- Artifact: `Archive/TagLauncher-8.4.0-build20260828.1732-localtest.dmg`
- SHA256: `76647a1133fbb96abdb8dd0531dc83819ac58815057071671204b746b781070f`
## Scope
- Quick Search 增加标签筛选条:`全部` 清除筛选,普通标签单选筛选,搜索框继续支持在标签内搜索 App、标签和备注。
- 标签筛选保持运行时状态,不持久化,不改变 App Grid、Pro 权益、数据结构或发布链路。
- 修复 QA 阻断项:`全部` chip 真实 UI 点击清除不稳定,以及保护容器名导致 duplicate-key 崩溃。
- 补齐 29 个语种文案、专项 QA 脚本、CODEGRAPH 和 8.4.0 CHANGELOG。
## Verification
- `bash Scripts/quick_search_tag_filter_qa.sh`
- `bash Scripts/quick_search_launch_contract_qa.sh`
- `bash Scripts/windowserver_event_safety_qa.sh`
- `bash Scripts/quick_search_system_app_qa.sh`
- `bash Scripts/macos14_availability_typecheck_qa.sh`
- `git diff --check -- C1.source/Apptag/QuickSearch.swift C1.source/Apptag/ContentView.swift C1.source/Apptag/DataLayer.swift C1.source/Apptag/TagNavigationView.swift C1.source/Apptag/Info.plist C1.source/Apptag/Localization C1.source/CHANGELOG.md C1.source/Scripts/quick_search_tag_filter_qa.sh CODEGRAPH.md`
- `plutil -lint C1.source/Apptag/Info.plist`
- `APP_BUILD=20260828.1732 bash build.sh`
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`
- `bash Scripts/macos14_build_metadata_qa.sh build/TagLauncher.app`
- `hdiutil verify C2.builds/LocalTest-8.4.0-20260828.1732/Archive/TagLauncher-8.4.0-build20260828.1732-localtest.dmg`
- DMG mounted read-only; verified `TagLauncher.app`, `Applications` symlink, app version/build, bundle id, code signature, and non-root-readable file permissions.
- Isolated launch smoke passed using `C1.source/build/TagLauncher.app`; launch log did not contain `fatal`, `duplicate`, `crash`, or `Duplicate values`.
## QA Gate
- Development: `QSF-DEV-001` and `QSF-DEV-002`
- Code review: `QSF-CR-001 / CR_PASS_QSF`, `QSF-CR-002 / CR_PASS_QSF_FIX`
- QA retest: `QSF-QA-002 / QA_PASS_QSF_FIX`
- QA report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/09-QA复测报告-8.4.0.md`
- QA evidence: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/QA-Retest-Evidence-8.4.0-20260828-QSF-QA-002`
## Boundary
This is an ad-hoc signed local installation acceptance DMG for Owner experience validation. It is not an App Store Connect upload package, not a TestFlight build, not notarized for public Developer ID distribution, and not a final release tag.
C2.builds/LocalTest-8.4.0-20260828.1732/SHA256SUMS.txt
New file
@@ -0,0 +1 @@
76647a1133fbb96abdb8dd0531dc83819ac58815057071671204b746b781070f  Archive/TagLauncher-8.4.0-build20260828.1732-localtest.dmg
C2.builds/LocalTest-8.4.0-20260828.1732/Verification/launch-smoke.log
New file
@@ -0,0 +1 @@
Boot-out failed: 3: No such process
C2.builds/README.md
@@ -1,6 +1,11 @@
# C2.builds
当前仅保留 8.3.5 迁移包体作为历史恢复证据:
当前最新推荐上传包:
- `AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- `AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg`
历史迁移包体:
- `AppStore-8.3.5-20260702.1046/Archive/TagLauncher-8.3.5-build20260702.1046.dmg`
- `AppStore-8.3.5-20260702.1046/Upload/TagLauncher-8.3.5-build20260702.1046.pkg`
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` 的系统标签,不能长期保留旧英文初始化标签。
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg
Binary files differ
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/PKG_PREFLIGHT_REPORT.md
New file
@@ -0,0 +1,74 @@
# TagLauncher 8.4.0 App Store PKG Preflight
Date: 2026-08-28
Version: 8.4.0
Build: 20260828.2219
## Package
- Upload package: `Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- Local rollback DMG: `Archive/TagLauncher-8.4.0-build20260828.2219.dmg`
- Upload package SHA256: `0d374a03b25c11a4bf9bad19238defd7c3043b72386ee70f7857e01f4f9d6318`
- DMG SHA256: `1dc42dc6577721ef27985243e9f7b40537f3bcac6f340b8e330be7bf8ce4e4d6`
## Signing
- App signing identity: `3rd Party Mac Developer Application: Hainan Wanxing Technology Co., Ltd. (CR3J54M8BQ)`
- Installer signing identity: `3rd Party Mac Developer Installer: Hainan Wanxing Technology Co., Ltd. (CR3J54M8BQ)`
- Embedded provisioning profile: `TagLauncher Mac App Store 20260707`
- Provisioning profile UUID: `3ee49163-ef5c-4467-8971-f93cc1fc6b6d`
## Entitlement Match Gate
Signed app entitlements:
- `com.apple.application-identifier`: `CR3J54M8BQ.com.taglauncher.app`
- `com.apple.developer.team-identifier`: `CR3J54M8BQ`
- `com.apple.security.app-sandbox`: `true`
- `com.apple.security.files.user-selected.read-write`: `true`
Embedded provisioning profile entitlements:
- `com.apple.application-identifier`: `CR3J54M8BQ.com.taglauncher.app`
- `com.apple.developer.team-identifier`: `CR3J54M8BQ`
Result:
- PASS: signed app application identifier matches embedded provisioning profile.
- PASS: signed app Team ID matches embedded provisioning profile.
## Payload Metadata
- `CFBundleShortVersionString`: `8.4.0`
- `CFBundleVersion`: `20260828.2219`
- Bundle ID: `com.taglauncher.app`
- Team ID: `CR3J54M8BQ`
## Permission Gate
Expanded package payload check:
```bash
find "$APP" \( -type d ! -perm -005 -o -type f ! -perm -004 \) -print
```
Result:
- PASS: no app bundle directories are restricted from non-root search/read access.
- PASS: no app bundle files are restricted from non-root read access.
## Verification
Commands completed successfully:
```bash
pkgutil --check-signature Upload/TagLauncher-8.4.0-build20260828.2219.pkg
pkgutil --expand-full Upload/TagLauncher-8.4.0-build20260828.2219.pkg <preflight-tmp>/expanded
codesign --verify --deep --strict --verbose=2 <preflight-tmp>/expanded/com.taglauncher.app.pkg/Payload/TagLauncher.app
hdiutil verify Archive/TagLauncher-8.4.0-build20260828.2219.dmg
shasum -a 256 -c SHA256SUMS.txt
```
## Non-Gate Note
Direct local launch/open smoke is not treated as a blocker for this App Store signed upload package. The user-facing local acceptance build was already verified via local DMG; this package is for Transporter/App Store Connect validation.
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/QA_RELEASE_EVIDENCE.md
New file
@@ -0,0 +1,44 @@
# TagLauncher 8.4.0 QA And Release Evidence
Date: 2026-08-28
Subproject: `quicksearchfilter`
Target version: `8.4.0`
## Gate Results
- Development completed: `DEV_READY_QSF`
- Independent code review completed: `CR_PASS_QSF`
- Initial QA failed and blocked package generation: `QA_FAIL_QSF`
- Minimum fix completed: `DEV_READY_QSF_FIX`
- Fix code review completed: `CR_PASS_QSF_FIX`
- QA retest completed: `QA_PASS_QSF_FIX`
- Local installable package generated: `LOCAL_TEST_READY_QSF`
- Owner installation acceptance: `OWNER_ACCEPTED_QSF`
## Evidence
- Development self-test report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/04-开发自测与回报-8.4.0.md`
- Code review report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/05-代码审核报告-8.4.0.md`
- Initial QA report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/06-QA执行报告-8.4.0.md`
- Fix development report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/07-开发返修与回报-8.4.0.md`
- Fix code review report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/08-返修代码审核报告-8.4.0.md`
- QA retest report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/09-QA复测报告-8.4.0.md`
- Local installable package report: `/Users/ar/Projects/Taglauncher/02-P/Round01-QuickSearchTagFilter/10-本地安装体验包-8.4.0.md`
## Owner Acceptance
Owner confirmed in the PMO conversation on 2026-08-28: the installed local 8.4.0 package had no observed problems.
This acceptance upgrades the local installable package to accepted for user experience. It does not by itself complete App Store Connect processing.
## Release Candidate
- Version: `8.4.0`
- Build: `20260828.2219`
- Upload package: `/Users/ar/Projects/Taglauncher/03-O/K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- Upload package SHA256: `0d374a03b25c11a4bf9bad19238defd7c3043b72386ee70f7857e01f4f9d6318`
- Release tag: `v8.4.0-build20260828.2219`
## Status
Ready for Transporter / App Store Connect upload. After upload, Transporter delivery logs and App Store Connect processing results must be recorded before declaring platform release readiness.
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,100 @@
# TagLauncher 8.4.0 Release Manifest
Date: 2026-08-28
Channel: App Store Connect / TestFlight upload candidate
## Version
- Version: `8.4.0`
- Build: `20260828.2219`
- Branch: `codex/slim-migration-taglauncher2`
- Source freeze commit: `7a94f69bb0dc5206a2e68d82486fd5959d16e664`
- Release tag: `v8.4.0-build20260828.2219`
- Local acceptance build superseded for upload: `8.4.0 (20260828.1732)`
- Local acceptance DMG SHA256: `76647a1133fbb96abdb8dd0531dc83819ac58815057071671204b746b781070f`
## Scope
- Quick Search 增加运行时单选标签筛选条。
- 支持点击 `全部` 清除筛选,点击普通标签在该标签内继续搜索。
- 标签筛选不持久化,不改变 AppGrid、Pro 权益、数据结构或排序策略。
- 修复 QA 发现的两个阻断项:`全部` chip 真实 UI 清除不稳定、保护容器名触发 duplicate-key 崩溃。
## Gate Chain
- Development: `DEV_READY_QSF`
- Code review: `CR_PASS_QSF`
- QA initial: `QA_FAIL_QSF`
- Fix development: `DEV_READY_QSF_FIX`
- Fix code review: `CR_PASS_QSF_FIX`
- QA retest: `QA_PASS_QSF_FIX`
- Local installable acceptance package: `LOCAL_TEST_READY_QSF`
- Owner acceptance: `OWNER_ACCEPTED_QSF`
Owner acceptance source: user confirmed on 2026-08-28 that the installed local 8.4.0 package had no observed problems.
## Artifacts
- App Store upload package: `Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- Local rollback / archive DMG: `Archive/TagLauncher-8.4.0-build20260828.2219.dmg`
- Hash file: `SHA256SUMS.txt`
- PKG preflight report: `PKG_PREFLIGHT_REPORT.md`
- QA / release evidence: `QA_RELEASE_EVIDENCE.md`
Copies are also stored under:
- `C2.builds/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- `C2.builds/AppStore-8.4.0-20260828.2219/Archive/TagLauncher-8.4.0-build20260828.2219.dmg`
## Signing
- App signing identity: `3rd Party Mac Developer Application: Hainan Wanxing Technology Co., Ltd. (CR3J54M8BQ)`
- Installer signing identity: `3rd Party Mac Developer Installer: Hainan Wanxing Technology Co., Ltd. (CR3J54M8BQ)`
- Embedded provisioning profile: `TagLauncher Mac App Store 20260707`
- Provisioning profile UUID: `3ee49163-ef5c-4467-8971-f93cc1fc6b6d`
- Team ID: `CR3J54M8BQ`
- Bundle ID: `com.taglauncher.app`
- Application identifier: `CR3J54M8BQ.com.taglauncher.app`
## Codegraph / Codebase Memory
- `CODEGRAPH.md`: checked; Quick Search tag filter entries were updated in the 8.4.0 source work.
- `codebase-memory`: refreshed before release packaging.
- `codebase-memory` project: `Users-ar-Projects-Taglauncher-03-O`
- Indexed commit: `7a94f69bb0dc5206a2e68d82486fd5959d16e664`
- Nodes / edges: `121768 / 130956`
- Shared artifact: `.codebase-memory/graph.db.zst` and `.codebase-memory/artifact.json`
## Verification Summary
Passed:
- `bash Scripts/quick_search_tag_filter_qa.sh`
- `bash Scripts/quick_search_launch_contract_qa.sh`
- `bash Scripts/windowserver_event_safety_qa.sh`
- `bash Scripts/quick_search_system_app_qa.sh`
- `bash Scripts/macos14_availability_typecheck_qa.sh`
- `bash Scripts/macos14_build_metadata_qa.sh build/TagLauncher.app`
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`
- signed entitlements match embedded provisioning profile for application identifier and Team ID
- `pkgutil --check-signature Upload/TagLauncher-8.4.0-build20260828.2219.pkg`
- expanded package payload `codesign --verify --deep --strict --verbose=2`
- expanded package payload permission check: no root-only readable app bundle directories/files
- `hdiutil verify Archive/TagLauncher-8.4.0-build20260828.2219.dmg`
- DMG read-only mount content/signature/version check
- `shasum -a 256 -c SHA256SUMS.txt`
Non-gate note:
- Direct local launch/open smoke was not used as an App Store upload gate for this Mac App Store signed package. Owner already accepted the local installable DMG, and this upload candidate differs only by build metadata, App Store signing, embedded profile and packaging channel. Final availability still depends on Transporter/App Store Connect processing.
## Hashes
```text
1dc42dc6577721ef27985243e9f7b40537f3bcac6f340b8e330be7bf8ce4e4d6  Archive/TagLauncher-8.4.0-build20260828.2219.dmg
0d374a03b25c11a4bf9bad19238defd7c3043b72386ee70f7857e01f4f9d6318  Upload/TagLauncher-8.4.0-build20260828.2219.pkg
```
## Current Status
Ready for Transporter / App Store Connect upload. This does not mean App Store processing has passed; Apple platform feedback must still be recorded after upload.
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/SHA256SUMS.txt
New file
@@ -0,0 +1,2 @@
1dc42dc6577721ef27985243e9f7b40537f3bcac6f340b8e330be7bf8ce4e4d6  Archive/TagLauncher-8.4.0-build20260828.2219.dmg
0d374a03b25c11a4bf9bad19238defd7c3043b72386ee70f7857e01f4f9d6318  Upload/TagLauncher-8.4.0-build20260828.2219.pkg
K3.运营与发布资料/Release/AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg
Binary files differ
K3.运营与发布资料/Release/README.md
@@ -1,6 +1,6 @@
# Release
本目录是未来目标版本的 release gate 入口。当前没有活跃发布包、发布清单或提交任务。
本目录是未来目标版本的 release gate 入口。当前推荐上传包为 `AppStore-8.4.0-20260828.2219/Upload/TagLauncher-8.4.0-build20260828.2219.pkg`,状态为 ready for Transporter / App Store Connect upload;尚未记录 Apple processing 结果。
8.3.5 release evidence 已归档到 `../_archive/8.3.5-20260702.1046/ReleaseEvidence/`。7.6.0 历史手工清单已归档到 `../_archive/7.6.0/`。