Ariver
2026-05-22 8478c55e513bbbb4b1676d1d9207f7feb2509465
Freeze 7.6.0 release candidate and move docs out of repo
48 files modified
25 files deleted
33 files added
558448 ■■■■ changed files
AppStore_Submission.md 285 ●●●●● patch | view | raw | blame | history
AppStore_TODO_7.3.5_744.md 101 ●●●●● patch | view | raw | blame | history
Apptag/AppDefaults.swift 2 ●●● patch | view | raw | blame | history
Apptag/AppDragCoordinator.swift 80 ●●●● patch | view | raw | blame | history
Apptag/AppGridCollectionView.swift 1089 ●●●●● patch | view | raw | blame | history
Apptag/AppGridItem.swift 46 ●●●●● patch | view | raw | blame | history
Apptag/AppIdentity.swift 16 ●●●●● patch | view | raw | blame | history
Apptag/ApptagApp.swift 144 ●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 174 ●●●● patch | view | raw | blame | history
Apptag/DataLayer.swift 162 ●●●●● patch | view | raw | blame | history
Apptag/Info.plist 6 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ar-Najdi.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ar.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/cs.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/da.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/de.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/en.json 23 ●●●●● patch | view | raw | blame | history
Apptag/Localization/es.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/fr.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/id.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/it.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ja.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ko.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ms.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/nb.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/nl.json 47 ●●●● patch | view | raw | blame | history
Apptag/Localization/nn.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/no.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/pl.json 55 ●●●●● patch | view | raw | blame | history
Apptag/Localization/pt-BR.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ro.json 53 ●●●●● patch | view | raw | blame | history
Apptag/Localization/ru.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/sr-Cyrl.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/sv.json 55 ●●●●● patch | view | raw | blame | history
Apptag/Localization/th.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/tr.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/uk.json 59 ●●●●● patch | view | raw | blame | history
Apptag/Localization/vi.json 57 ●●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hans.json 23 ●●●●● patch | view | raw | blame | history
Apptag/Localization/zh-Hant.json 23 ●●●●● patch | view | raw | blame | history
Apptag/PreferencesView.swift 2 ●●● patch | view | raw | blame | history
Apptag/QuickSearch.swift 109 ●●●● patch | view | raw | blame | history
Apptag/SmartCategorization/SmartCategorizationDraft.swift 12 ●●●●● patch | view | raw | blame | history
Apptag/SmartCategorization/SmartCategory.swift 150 ●●●●● patch | view | raw | blame | history
Apptag/SmartCategorization/SmartStartService.swift 454 ●●●● patch | view | raw | blame | history
Apptag/TagGroupView.swift 6 ●●●●● patch | view | raw | blame | history
Apptag/TagLauncher.entitlements 4 ●●●● patch | view | raw | blame | history
CHANGELOG.md 27 ●●●● patch | view | raw | blame | history
Decisions.md 20 ●●●●● patch | view | raw | blame | history
Docs/PRD/InitialDefaults_PRD.md 39 ●●●●● patch | view | raw | blame | history
Docs/PRD/SmartStart_PRD_UltimateDefaultCatalog.md 255 ●●●●● patch | view | raw | blame | history
Docs/PRD/TagLauncher_QuickSearch_CurrentRules_Snapshot.md 815 ●●●●● patch | view | raw | blame | history
Docs/PRD/TagLauncher_QuickSearch_PRD_7.5_PMD_Rewrite_ZH.md 493 ●●●●● patch | view | raw | blame | history
Docs/PRD/TagLauncher_QuickSuggestions_PRD_7.5.md 720 ●●●●● patch | view | raw | blame | history
Docs/PRD/X-存档/TagLauncher_QuickSearch_PRD_7.5.md 594 ●●●●● patch | view | raw | blame | history
Docs/PRD/X-存档/TagLauncher_QuickSearch_PRD_7.5_PMD_Rewrite.md 492 ●●●●● patch | view | raw | blame | history
Docs/ProjectManagement/Apptag_Development_Workflow_Standard_v1.0.md 238 ●●●●● patch | view | raw | blame | history
Docs/ProjectManagement/SmartStart_UltimateDefaultCatalog_ImplementationPlan.md 42 ●●●●● patch | view | raw | blame | history
Docs/QA/InitialDefaults_TestCases.md 72 ●●●●● patch | view | raw | blame | history
Docs/QA/SmartStart_TestCases_UltimateDefaultCatalog.md 376 ●●●●● patch | view | raw | blame | history
Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs 777 ●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json 46699 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv 8755 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json 79717 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json 225 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ar-Najdi.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ar.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.cs.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.da.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.de.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.en.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.es.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.fr.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.id.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.it.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ja.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ko.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ms.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nb.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nl.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nn.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.no.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.pl.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.pt-BR.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ro.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ru.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.sr-Cyrl.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.sv.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.th.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.tr.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.uk.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.vi.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.zh-Hant.json 14129 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_DuplicateReview.csv 1417 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_Report.md 64 ●●●●● patch | view | raw | blame | history
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_TranslationQA.md 17 ●●●●● patch | view | raw | blame | history
Research/quick search/quicksearch-匹配规则C版-简化版-范围大大缩小.md 33 ●●●●● patch | view | raw | blame | history
Research/quick search/搜索方案-保守版.md 62 ●●●●● patch | view | raw | blame | history
SmartStart_GTD.md 888 ●●●●● patch | view | raw | blame | history
SmartStart_ProjectPlan.md 645 ●●●●● patch | view | raw | blame | history
X-KM/tager-20260519_142747.markdown 91 ●●●●● patch | view | raw | blame | history
aaaindex.markdown 247 ●●●●● patch | view | raw | blame | history
build.sh 72 ●●●●● patch | view | raw | blame | history
generate_icon.py 2 ●●● patch | view | raw | blame | history
plan-deepthink.md 444 ●●●●● patch | view | raw | blame | history
AppStore_Submission.md
File was deleted
AppStore_TODO_7.3.5_744.md
File was deleted
Apptag/AppDefaults.swift
@@ -30,7 +30,7 @@
    }
    static func hasStoredValue(for key: String) -> Bool {
        let domain = Bundle.main.bundleIdentifier ?? "com.apptag.launcher"
        let domain = Bundle.main.bundleIdentifier ?? AppIdentity.bundleIdentifier
        return UserDefaults.standard.persistentDomain(forName: domain)?[key] != nil
    }
Apptag/AppDragCoordinator.swift
@@ -6,9 +6,8 @@
    static let shared = AppDragCoordinator()
    struct DropTarget {
        weak var view: NSView?
        weak var view: AppDropTargetReceivingView?
        var tag: String
        var onDrop: (String, String, Bool) -> Void
    }
    private var targets: [UUID: DropTarget] = [:]
@@ -24,8 +23,18 @@
    private init() {}
    func register(id: UUID, view: NSView, tag: String, onDrop: @escaping (String, String, Bool) -> Void) {
        targets[id] = DropTarget(view: view, tag: tag, onDrop: onDrop)
    var hasActiveDrag: Bool {
        dragLayer != nil || dragWindow != nil || !activePayload.isEmpty
    }
    func register(id: UUID, view: AppDropTargetReceivingView, tag: String) {
        if let existing = targets[id], existing.view === view, existing.tag == tag {
            return
        }
        if targets.count > 256 {
            pruneDeadTargets()
        }
        targets[id] = DropTarget(view: view, tag: tag)
    }
    func unregister(id: UUID) {
@@ -58,7 +67,7 @@
            panel.isFloatingPanel = true
            panel.hidesOnDeactivate = false
            panel.ignoresMouseEvents = true
            panel.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
            panel.level = .normal
            panel.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary, .stationary, .transient, .ignoresCycle]
            panel.isReleasedWhenClosed = false
            let contentView = NSView(frame: NSRect(origin: .zero, size: image.size))
@@ -120,16 +129,20 @@
        let parts = activePayload.components(separatedBy: "\n")
        guard let path = parts.first, !path.isEmpty else { return }
        let source = parts.dropFirst().first ?? ""
        pruneDeadTargets()
        let hitTarget = targets.values
            .compactMap { target -> (DropTarget, CGFloat)? in
                guard let frame = target.view?.screenFrame(), frame.contains(screenPoint) else { return nil }
                return (target, frame.width * frame.height)
            .compactMap { target -> (AppDropTargetReceivingView, CGFloat)? in
                guard let view = target.view,
                      let frame = view.screenFrame(),
                      frame.contains(screenPoint)
                else { return nil }
                return (view, frame.width * frame.height)
            }
            .sorted { $0.1 < $1.1 }
            .first?.0
        hitTarget?.onDrop(path, source, copy)
        hitTarget?.performDrop(path: path, source: source, copy: copy)
    }
    func cancelDrag() {
@@ -157,6 +170,10 @@
        dragWindow = nil
        activePayload = ""
        dragImageSize = .zero
    }
    private func pruneDeadTargets() {
        targets = targets.filter { $0.value.view != nil }
    }
    private static func cgImage(from image: NSImage) -> CGImage? {
@@ -209,7 +226,12 @@
    }
}
private extension NSView {
protocol AppDropTargetReceivingView: AnyObject {
    func screenFrame() -> NSRect?
    func performDrop(path: String, source: String, copy: Bool)
}
extension AppDropTargetReceivingView where Self: NSView {
    func screenFrame() -> NSRect? {
        guard let window else { return nil }
        let rectInWindow = convert(bounds, to: nil)
@@ -223,39 +245,53 @@
    func makeNSView(context: Context) -> AppDropTargetNSView {
        let view = AppDropTargetNSView()
        view.targetTag = targetTag
        view.onDropApp = onDropApp
        view.configure(targetTag: targetTag, onDropApp: onDropApp)
        return view
    }
    func updateNSView(_ view: AppDropTargetNSView, context: Context) {
        view.targetTag = targetTag
        view.onDropApp = onDropApp
        view.registerTarget()
        view.configure(targetTag: targetTag, onDropApp: onDropApp)
    }
    static func dismantleNSView(_ view: AppDropTargetNSView, coordinator: ()) {
        view.onDropApp = nil
        AppDragCoordinator.shared.unregister(id: view.id)
    }
}
final class AppDropTargetNSView: NSView {
final class AppDropTargetNSView: NSView, AppDropTargetReceivingView {
    let id = UUID()
    var targetTag = ""
    var onDropApp: ((String, String, Bool) -> Void)?
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
    func configure(targetTag: String, onDropApp: @escaping (String, String, Bool) -> Void) {
        let tagChanged = self.targetTag != targetTag
        self.targetTag = targetTag
        self.onDropApp = onDropApp
        if tagChanged {
        registerTarget()
    }
    }
    override func layout() {
        super.layout()
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if window == nil {
            AppDragCoordinator.shared.unregister(id: id)
        } else {
        registerTarget()
        }
    }
    func registerTarget() {
        guard window != nil, let onDropApp else { return }
        AppDragCoordinator.shared.register(id: id, view: self, tag: targetTag, onDrop: onDropApp)
        guard window != nil else { return }
        AppDragCoordinator.shared.register(id: id, view: self, tag: targetTag)
    }
    func performDrop(path: String, source: String, copy: Bool) {
        onDropApp?(path, source, copy)
    }
    deinit {
        AppDragCoordinator.shared.unregister(id: id)
    }
}
Apptag/AppGridCollectionView.swift
New file
@@ -0,0 +1,1089 @@
import SwiftUI
import AppKit
struct AppGridCollectionView: NSViewRepresentable {
    let groups: [TagGroup]
    let tagColors: [String: Int]
    let displayMode: String
    let iconSize: CGFloat
    let showNames: Bool
    let bubbleDisabled: Bool
    let showUncommonAppBubbles: Bool
    let highlightedGroupName: String?
    let contentRevision: Int
    let scrollTargetID: String?
    let scrollRequestToken: Int
    let onSelectApp: (AppInfo) -> Void
    let onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void
    let onEditNote: (AppInfo, CGRect) -> Void
    let onDropApp: (String, String, String, Bool) -> Void
    let onGroupActivate: (String) -> Void
    let onScrollActivity: () -> Void
    let onDragModeChange: (Bool) -> Void
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    func makeNSView(context: Context) -> AppGridCollectionHostView {
        let view = AppGridCollectionHostView()
        view.configure(coordinator: context.coordinator)
        return view
    }
    func updateNSView(_ view: AppGridCollectionHostView, context: Context) {
        context.coordinator.update(
            groups: groups,
            tagColors: tagColors,
            displayMode: displayMode,
            iconSize: iconSize,
            showNames: showNames,
            bubbleDisabled: bubbleDisabled,
            showUncommonAppBubbles: showUncommonAppBubbles,
            highlightedGroupName: highlightedGroupName,
            contentRevision: contentRevision,
            scrollTargetID: scrollTargetID,
            scrollRequestToken: scrollRequestToken,
            onSelectApp: onSelectApp,
            onBubbleHover: onBubbleHover,
            onEditNote: onEditNote,
            onDropApp: onDropApp,
            onGroupActivate: onGroupActivate,
            onScrollActivity: onScrollActivity,
            onDragModeChange: onDragModeChange
        )
        view.applyCoordinatorUpdate()
    }
    final class Coordinator: NSObject, NSCollectionViewDataSource, NSCollectionViewDelegate {
        var groups: [TagGroup] = []
        var tagColors: [String: Int] = [:]
        var displayMode = AppDefaults.displayMode
        var iconSize: CGFloat = AppDefaults.iconSize
        var showNames = true
        var bubbleDisabled = false
        var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
        var highlightedGroupName: String?
        var contentRevision = 0
        var scrollTargetID: String?
        var scrollRequestToken = 0
        var lastHandledScrollRequestToken = 0
        var contentSignature = ""
        var needsReload = true
        var onSelectApp: (AppInfo) -> Void = { _ in }
        var onBubbleHover: (AppInfo, CGRect, AppBubbleHoverEvent) -> Void = { _, _, _ in }
        var onEditNote: (AppInfo, CGRect) -> Void = { _, _ in }
        var onDropApp: (String, String, String, Bool) -> Void = { _, _, _, _ in }
        var onGroupActivate: (String) -> Void = { _ in }
        var onScrollActivity: () -> Void = {}
        var onDragModeChange: (Bool) -> Void = { _ in }
        func update(
            groups: [TagGroup],
            tagColors: [String: Int],
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
            bubbleDisabled: Bool,
            showUncommonAppBubbles: Bool,
            highlightedGroupName: String?,
            contentRevision: Int,
            scrollTargetID: String?,
            scrollRequestToken: Int,
            onSelectApp: @escaping (AppInfo) -> Void,
            onBubbleHover: @escaping (AppInfo, CGRect, AppBubbleHoverEvent) -> Void,
            onEditNote: @escaping (AppInfo, CGRect) -> Void,
            onDropApp: @escaping (String, String, String, Bool) -> Void,
            onGroupActivate: @escaping (String) -> Void,
            onScrollActivity: @escaping () -> Void,
            onDragModeChange: @escaping (Bool) -> Void
        ) {
            self.groups = groups
            self.tagColors = tagColors
            self.displayMode = displayMode
            self.iconSize = iconSize
            self.showNames = showNames
            self.bubbleDisabled = bubbleDisabled
            self.showUncommonAppBubbles = showUncommonAppBubbles
            self.highlightedGroupName = highlightedGroupName
            self.contentRevision = contentRevision
            self.scrollTargetID = scrollTargetID
            self.scrollRequestToken = scrollRequestToken
            self.onSelectApp = onSelectApp
            self.onBubbleHover = onBubbleHover
            self.onEditNote = onEditNote
            self.onDropApp = onDropApp
            self.onGroupActivate = onGroupActivate
            self.onScrollActivity = onScrollActivity
            self.onDragModeChange = onDragModeChange
            let nextSignature = Self.signature(
                tagColors: tagColors,
                displayMode: displayMode,
                iconSize: iconSize,
                showNames: showNames,
                showUncommonAppBubbles: showUncommonAppBubbles,
                highlightedGroupName: highlightedGroupName,
                contentRevision: contentRevision
            )
            if nextSignature != contentSignature {
                contentSignature = nextSignature
                needsReload = true
            }
        }
        func numberOfSections(in collectionView: NSCollectionView) -> Int {
            1
        }
        func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
            groups.count
        }
        func collectionView(
            _ collectionView: NSCollectionView,
            itemForRepresentedObjectAt indexPath: IndexPath
        ) -> NSCollectionViewItem {
            let item = collectionView.makeItem(
                withIdentifier: AppGridGroupCollectionItem.reuseIdentifier,
                for: indexPath
            )
            guard let groupItem = item as? AppGridGroupCollectionItem,
                  indexPath.item < groups.count
            else { return item }
            groupItem.configure(group: groups[indexPath.item], coordinator: self)
            return groupItem
        }
        func scrollIndex(for tagID: String) -> Int? {
            groups.firstIndex { $0.id == tagID || $0.name == tagID }
        }
        private static func signature(
            tagColors: [String: Int],
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
            showUncommonAppBubbles: Bool,
            highlightedGroupName: String?,
            contentRevision: Int
        ) -> String {
            let colorPart = tagColors
                .sorted { $0.key < $1.key }
                .map { "\($0.key)=\($0.value)" }
                .joined(separator: ",")
            return [
                displayMode,
                "\(Int(iconSize.rounded()))",
                showNames ? "names" : "nonames",
                showUncommonAppBubbles ? "uncommon" : "allbubbles",
                highlightedGroupName ?? "",
                colorPart,
                "rev=\(contentRevision)"
            ].joined(separator: "|")
        }
    }
}
final class AppGridCollectionHostView: NSView {
    private let scrollView = NSScrollView()
    private let collectionView = NSCollectionView()
    private let gridLayout = AppGridContainerCollectionLayout()
    private weak var coordinator: AppGridCollectionView.Coordinator?
    private var scrollObserver: NSObjectProtocol?
    private var lastLayoutSize: NSSize = .zero
    private var lastReportedBoundsOrigin: NSPoint?
    override var isFlipped: Bool { true }
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        setup()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    deinit {
        if let scrollObserver {
            NotificationCenter.default.removeObserver(scrollObserver)
        }
    }
    func configure(coordinator: AppGridCollectionView.Coordinator) {
        self.coordinator = coordinator
        gridLayout.coordinator = coordinator
        collectionView.dataSource = coordinator
        collectionView.delegate = coordinator
    }
    func applyCoordinatorUpdate() {
        guard let coordinator else { return }
        if coordinator.needsReload {
            coordinator.needsReload = false
            collectionView.reloadData()
            gridLayout.invalidateLayout()
        } else {
            collectionView.visibleItems().forEach { item in
                (item as? AppGridGroupCollectionItem)?.refreshRuntimeState()
            }
        }
        if coordinator.scrollRequestToken != coordinator.lastHandledScrollRequestToken {
            coordinator.lastHandledScrollRequestToken = coordinator.scrollRequestToken
            if let target = coordinator.scrollTargetID,
               let index = coordinator.scrollIndex(for: target) {
                DispatchQueue.main.async { [weak self] in
                    self?.collectionView.scrollToItems(
                        at: [IndexPath(item: index, section: 0)],
                        scrollPosition: .top
                    )
                }
            }
        }
    }
    override func layout() {
        super.layout()
        scrollView.frame = bounds
        if lastLayoutSize != bounds.size {
            lastLayoutSize = bounds.size
            gridLayout.invalidateLayout()
        }
    }
    private func setup() {
        wantsLayer = true
        layer?.backgroundColor = NSColor.clear.cgColor
        collectionView.collectionViewLayout = gridLayout
        collectionView.backgroundColors = [.clear]
        collectionView.isSelectable = false
        collectionView.register(
            AppGridGroupCollectionItem.self,
            forItemWithIdentifier: AppGridGroupCollectionItem.reuseIdentifier
        )
        scrollView.drawsBackground = false
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalScroller = false
        scrollView.autohidesScrollers = true
        scrollView.borderType = .noBorder
        scrollView.documentView = collectionView
        addSubview(scrollView)
        scrollView.contentView.postsBoundsChangedNotifications = true
        scrollObserver = NotificationCenter.default.addObserver(
            forName: NSView.boundsDidChangeNotification,
            object: scrollView.contentView,
            queue: .main
        ) { [weak self] _ in
            guard let self,
                  self.recordScrollIfNeeded()
            else { return }
            self.coordinator?.onScrollActivity()
        }
    }
    private func recordScrollIfNeeded() -> Bool {
        let origin = scrollView.contentView.bounds.origin
        guard let last = lastReportedBoundsOrigin else {
            lastReportedBoundsOrigin = origin
            return false
        }
        let didScroll = abs(origin.x - last.x) > 0.5 || abs(origin.y - last.y) > 0.5
        if didScroll {
            lastReportedBoundsOrigin = origin
        }
        return didScroll
    }
}
private final class AppGridContainerCollectionLayout: NSCollectionViewLayout {
    weak var coordinator: AppGridCollectionView.Coordinator?
    private var itemAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:]
    private var contentSize: NSSize = .zero
    override var collectionViewContentSize: NSSize {
        contentSize
    }
    override func prepare() {
        super.prepare()
        guard let collectionView else {
            itemAttributes = [:]
            contentSize = .zero
            return
        }
        let groups = coordinator?.groups ?? []
        let itemCount = collectionView.numberOfItems(inSection: 0)
        let visibleGroups = Array(groups.prefix(itemCount))
        let iconSize: CGFloat = coordinator?.iconSize ?? CGFloat(AppDefaults.iconSize)
        let plan = Self.makePlan(
            groups: visibleGroups,
            contentWidth: collectionView.bounds.width,
            iconSize: iconSize
        )
        var nextAttributes: [IndexPath: NSCollectionViewLayoutAttributes] = [:]
        for item in plan.items {
            let indexPath = IndexPath(item: item.index, section: 0)
            let attributes = NSCollectionViewLayoutAttributes(forItemWith: indexPath)
            attributes.frame = item.frame
            nextAttributes[indexPath] = attributes
        }
        itemAttributes = nextAttributes
        contentSize = plan.contentSize
    }
    override func layoutAttributesForElements(in rect: NSRect) -> [NSCollectionViewLayoutAttributes] {
        itemAttributes.values.filter { $0.frame.intersects(rect) }
    }
    override func layoutAttributesForItem(at indexPath: IndexPath) -> NSCollectionViewLayoutAttributes? {
        itemAttributes[indexPath]
    }
    override func shouldInvalidateLayout(forBoundsChange newBounds: NSRect) -> Bool {
        false
    }
    private struct LayoutItem {
        let index: Int
        let frame: NSRect
    }
    private struct LayoutPlan {
        let items: [LayoutItem]
        let contentSize: NSSize
    }
    private struct Candidate {
        let spans: [Int]
        let cost: CGFloat
    }
    private static func makePlan(
        groups: [TagGroup],
        contentWidth: CGFloat,
        iconSize: CGFloat
    ) -> LayoutPlan {
        let boundedContentWidth = max(1, contentWidth)
        let outerPadding = AppGridCollectionMetrics.outerPadding
        let gap = AppGridCollectionMetrics.cardGap
        let availableWidth = max(1, boundedContentWidth - outerPadding * 2)
        let trackCount = preferredTrackCount(availableWidth: availableWidth, iconSize: iconSize)
        let trackWidth = floor((availableWidth - gap * CGFloat(trackCount - 1)) / CGFloat(trackCount))
        let rows = layoutRows(
            groups: groups,
            trackCount: trackCount,
            trackWidth: trackWidth,
            availableWidth: availableWidth,
            gap: gap,
            iconSize: iconSize
        )
        var y = outerPadding
        var items: [LayoutItem] = []
        for row in rows {
            var x = outerPadding
            let height = AppGridCollectionMetrics.cardHeight(rowCount: row.fixedRows, iconSize: iconSize)
            for item in row.items {
                let frame = NSRect(x: x, y: y, width: item.width, height: height)
                items.append(LayoutItem(index: item.index, frame: frame))
                x += item.width + gap
            }
            y += height + gap
        }
        let contentHeight = rows.isEmpty ? outerPadding * 2 : y - gap + outerPadding
        return LayoutPlan(
            items: items,
            contentSize: NSSize(width: boundedContentWidth, height: max(1, contentHeight))
        )
    }
    private struct RowItem {
        let index: Int
        let width: CGFloat
    }
    private struct Row {
        let items: [RowItem]
        let fixedRows: Int
    }
    private static func layoutRows(
        groups: [TagGroup],
        trackCount: Int,
        trackWidth: CGFloat,
        availableWidth: CGFloat,
        gap: CGFloat,
        iconSize: CGFloat
    ) -> [Row] {
        let n = groups.count
        guard n > 0 else { return [] }
        let patterns = spanPatterns(trackCount: trackCount)
        var bestCost = Array(repeating: CGFloat.greatestFiniteMagnitude, count: n + 1)
        var bestPattern = Array(repeating: [Int](), count: n)
        bestCost[n] = 0
        for index in stride(from: n - 1, through: 0, by: -1) {
            for pattern in patterns where index + pattern.count <= n {
                let candidate = rowCandidate(
                    groups: groups,
                    startIndex: index,
                    spans: pattern,
                    trackWidth: trackWidth,
                    availableWidth: availableWidth,
                    gap: gap,
                    iconSize: iconSize
                )
                let totalCost = candidate.cost + bestCost[index + pattern.count]
                if totalCost < bestCost[index] {
                    bestCost[index] = totalCost
                    bestPattern[index] = candidate.spans
                }
            }
        }
        var rows: [Row] = []
        var index = 0
        while index < n {
            let spans = bestPattern[index].isEmpty ? [trackCount] : bestPattern[index]
            let widths = spans.map { width(trackWidth: trackWidth, span: $0, gap: gap) }
            let fixedRows = widths.indices.map {
                AppGridCollectionMetrics.iconRows(
                    appCount: groups[index + $0].apps.count,
                    width: widths[$0],
                    iconSize: iconSize
                )
            }.max() ?? 1
            let items = widths.indices.map {
                RowItem(index: index + $0, width: widths[$0])
            }
            rows.append(Row(items: items, fixedRows: fixedRows))
            index += spans.count
        }
        return rows
    }
    private static func rowCandidate(
        groups: [TagGroup],
        startIndex: Int,
        spans: [Int],
        trackWidth: CGFloat,
        availableWidth: CGFloat,
        gap: CGFloat,
        iconSize: CGFloat
    ) -> Candidate {
        let widths = spans.map { width(trackWidth: trackWidth, span: $0, gap: gap) }
        let rowCounts = widths.indices.map {
            AppGridCollectionMetrics.iconRows(
                appCount: groups[startIndex + $0].apps.count,
                width: widths[$0],
                iconSize: iconSize
            )
        }
        let fixedRows = rowCounts.max() ?? 1
        let rowArea = CGFloat(fixedRows) * AppGridCollectionMetrics.iconCellHeight(iconSize: iconSize) * availableWidth
        let paddingCost = CGFloat(spans.count) * 0.001
        return Candidate(spans: spans, cost: rowArea + paddingCost)
    }
    private static func preferredTrackCount(availableWidth: CGFloat, iconSize: CGFloat) -> Int {
        let minCardWidth = max(260, AppGridCollectionMetrics.iconCellWidth(iconSize: iconSize) * 3 + 64)
        if availableWidth >= minCardWidth * 3 + AppGridCollectionMetrics.cardGap * 2 { return 3 }
        if availableWidth >= minCardWidth * 2 + AppGridCollectionMetrics.cardGap { return 2 }
        return 1
    }
    private static func spanPatterns(trackCount: Int) -> [[Int]] {
        switch trackCount {
        case 3:
            return [[1, 1, 1], [1, 2], [2, 1], [3]]
        case 2:
            return [[1, 1], [2]]
        default:
            return [[1]]
        }
    }
    private static func width(trackWidth: CGFloat, span: Int, gap: CGFloat) -> CGFloat {
        let safeSpan = max(1, span)
        return trackWidth * CGFloat(safeSpan) + gap * CGFloat(safeSpan - 1)
    }
}
private enum AppGridCollectionMetrics {
    static let outerPadding: CGFloat = 20
    static let cardGap: CGFloat = 16
    static let cardPadding: CGFloat = 16
    static let headerHeight: CGFloat = 28
    static let headerBottomGap: CGFloat = 6
    static let iconColumnGap: CGFloat = 6
    static let iconRowGap: CGFloat = 2
    static let hoverScale: CGFloat = 1.22
    static let labelHeight: CGFloat = 14
    static func iconCellWidth(iconSize: CGFloat) -> CGFloat {
        iconSize * hoverScale + 8
    }
    static func iconCellHeight(iconSize: CGFloat) -> CGFloat {
        iconSize * hoverScale + labelHeight + 22
    }
    static func columns(width: CGFloat, iconSize: CGFloat) -> Int {
        let inner = max(1, width - cardPadding * 2)
        let itemW = iconCellWidth(iconSize: iconSize)
        return max(1, Int((inner + iconColumnGap) / (itemW + iconColumnGap)))
    }
    static func cardHeight(appCount: Int, width: CGFloat, iconSize: CGFloat) -> CGFloat {
        cardHeight(rowCount: iconRows(appCount: appCount, width: width, iconSize: iconSize), iconSize: iconSize)
    }
    static func cardHeight(rowCount: Int, iconSize: CGFloat) -> CGFloat {
        let rows = max(1, rowCount)
        return cardPadding * 2
            + headerHeight
            + headerBottomGap
            + CGFloat(rows) * iconCellHeight(iconSize: iconSize)
            + CGFloat(max(0, rows - 1)) * iconRowGap
    }
    static func iconRows(appCount: Int, width: CGFloat, iconSize: CGFloat) -> Int {
        let cols = columns(width: width, iconSize: iconSize)
        return max(1, (appCount + cols - 1) / cols)
    }
}
private final class AppGridGroupCollectionItem: NSCollectionViewItem {
    static let reuseIdentifier = NSUserInterfaceItemIdentifier("AppGridGroupCollectionItem")
    private var cardView: AppGridGroupCardView {
        view as! AppGridGroupCardView
    }
    override func loadView() {
        view = AppGridGroupCardView()
    }
    func configure(group: TagGroup, coordinator: AppGridCollectionView.Coordinator) {
        cardView.configure(group: group, coordinator: coordinator)
    }
    func refreshRuntimeState() {
        cardView.refreshRuntimeState()
    }
    override func prepareForReuse() {
        super.prepareForReuse()
        cardView.prepareForReuse()
    }
}
private final class AppGridGroupCardView: NSView, AppDropTargetReceivingView {
    private let dropTargetID = UUID()
    private weak var coordinator: AppGridCollectionView.Coordinator?
    private var group: TagGroup?
    private var iconViews: [AppGridIconNSView] = []
    private var isHovered = false
    private var trackingAreaRef: NSTrackingArea?
    override var isFlipped: Bool { true }
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        wantsLayer = true
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        wantsLayer = true
    }
    deinit {
        AppDragCoordinator.shared.unregister(id: dropTargetID)
    }
    func configure(group: TagGroup, coordinator: AppGridCollectionView.Coordinator) {
        self.group = group
        self.coordinator = coordinator
        rebuildIconViews()
        refreshRuntimeState()
        registerDropTargetIfNeeded()
        needsDisplay = true
        needsLayout = true
    }
    override func prepareForReuse() {
        AppDragCoordinator.shared.unregister(id: dropTargetID)
        group = nil
        coordinator = nil
        iconViews.forEach {
            $0.prepareForReuse()
            $0.removeFromSuperview()
        }
        iconViews = []
    }
    func refreshRuntimeState() {
        let runtime = AppGridIconRuntimeState(
            bubbleDisabled: coordinator?.bubbleDisabled ?? false,
            showUncommonAppBubbles: coordinator?.showUncommonAppBubbles ?? AppDefaults.showUncommonAppBubbles
        )
        iconViews.forEach { $0.runtimeState = runtime }
        needsDisplay = true
    }
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if window == nil {
            AppDragCoordinator.shared.unregister(id: dropTargetID)
        } else {
            registerDropTargetIfNeeded()
        }
    }
    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if let trackingAreaRef {
            removeTrackingArea(trackingAreaRef)
        }
        let area = NSTrackingArea(
            rect: .zero,
            options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
            owner: self,
            userInfo: nil
        )
        addTrackingArea(area)
        trackingAreaRef = area
    }
    override func mouseEntered(with event: NSEvent) {
        isHovered = true
        needsDisplay = true
    }
    override func mouseExited(with event: NSEvent) {
        isHovered = false
        needsDisplay = true
    }
    override func mouseUp(with event: NSEvent) {
        guard let group,
              bounds.contains(convert(event.locationInWindow, from: nil))
        else { return }
        coordinator?.onGroupActivate(group.name)
    }
    override func layout() {
        super.layout()
        guard let coordinator, let group else { return }
        let cols = AppGridCollectionMetrics.columns(width: bounds.width, iconSize: coordinator.iconSize)
        let innerWidth = max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2)
        let cellWidth = max(
            1,
            (innerWidth - AppGridCollectionMetrics.iconColumnGap * CGFloat(cols - 1)) / CGFloat(cols)
        )
        let cellHeight = AppGridCollectionMetrics.iconCellHeight(iconSize: coordinator.iconSize)
        let startY = AppGridCollectionMetrics.cardPadding
            + AppGridCollectionMetrics.headerHeight
            + AppGridCollectionMetrics.headerBottomGap
        for index in group.apps.indices {
            guard index < iconViews.count else { continue }
            let row = index / cols
            let col = index % cols
            let x = AppGridCollectionMetrics.cardPadding
                + CGFloat(col) * (cellWidth + AppGridCollectionMetrics.iconColumnGap)
            let y = startY + CGFloat(row) * (cellHeight + AppGridCollectionMetrics.iconRowGap)
            iconViews[index].frame = NSRect(x: x, y: y, width: cellWidth, height: cellHeight)
        }
    }
    override func draw(_ dirtyRect: NSRect) {
        guard let coordinator, let group else { return }
        let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
        let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
        let tagColor = TagColor.nsColor(for: coordinator.tagColors[group.name] ?? 0)
        let isColored = coordinator.displayMode == "coloredGridContainer"
        let isColorlessActive = coordinator.displayMode == "gridContainer"
            && (isHovered || coordinator.highlightedGroupName == group.name)
        cardSurfaceColor().setFill()
        path.fill()
        if isColored || isColorlessActive {
            tagColor.withAlphaComponent(0.30).setFill()
            path.fill()
        }
        NSColor.labelColor.withAlphaComponent(0.08).setStroke()
        path.lineWidth = 1
        path.stroke()
        drawHeader(title: group.name)
    }
    func performDrop(path: String, source: String, copy: Bool) {
        guard let group else { return }
        coordinator?.onDropApp(path, source, group.name, copy)
        if source != group.name || copy {
            coordinator?.onDragModeChange(false)
        }
    }
    private func rebuildIconViews() {
        iconViews.forEach {
            $0.prepareForReuse()
            $0.removeFromSuperview()
        }
        guard let coordinator, let group else {
            iconViews = []
            return
        }
        iconViews = group.apps.map { app in
            let iconView = AppGridIconNSView()
            iconView.configure(
                app: app,
                sourceTag: group.name,
                iconSize: coordinator.iconSize,
                showName: coordinator.showNames,
                coordinator: coordinator
            )
            addSubview(iconView)
            return iconView
        }
    }
    private func registerDropTargetIfNeeded() {
        guard window != nil, let group else { return }
        AppDragCoordinator.shared.register(id: dropTargetID, view: self, tag: group.name)
    }
    private func cardSurfaceColor() -> NSColor {
        if effectiveAppearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua {
            return NSColor.white.withAlphaComponent(0.055)
        }
        return NSColor.white.withAlphaComponent(0.62)
    }
    private func drawHeader(title: String) {
        let headerRect = NSRect(
            x: AppGridCollectionMetrics.cardPadding,
            y: AppGridCollectionMetrics.cardPadding,
            width: max(1, bounds.width - AppGridCollectionMetrics.cardPadding * 2),
            height: AppGridCollectionMetrics.headerHeight
        )
        let attributes: [NSAttributedString.Key: Any] = [
            .font: NSFont.systemFont(ofSize: 18, weight: .semibold),
            .foregroundColor: NSColor.secondaryLabelColor,
            .paragraphStyle: centeredParagraph(lineBreak: .byTruncatingMiddle)
        ]
        let titleSize = title.size(withAttributes: attributes)
        let titleWidth = min(headerRect.width * 0.64, titleSize.width + 20)
        let titleRect = NSRect(
            x: headerRect.midX - titleWidth / 2,
            y: headerRect.minY + 3,
            width: titleWidth,
            height: headerRect.height - 6
        )
        let lineY = headerRect.midY
        NSColor.secondaryLabelColor.withAlphaComponent(0.25).setStroke()
        let leftLine = NSBezierPath()
        leftLine.move(to: NSPoint(x: headerRect.minX, y: lineY))
        leftLine.line(to: NSPoint(x: max(headerRect.minX, titleRect.minX - 2), y: lineY))
        leftLine.stroke()
        let rightLine = NSBezierPath()
        rightLine.move(to: NSPoint(x: min(headerRect.maxX, titleRect.maxX + 2), y: lineY))
        rightLine.line(to: NSPoint(x: headerRect.maxX, y: lineY))
        rightLine.stroke()
        title.draw(with: titleRect, options: [.usesLineFragmentOrigin], attributes: attributes)
    }
    private func centeredParagraph(lineBreak: NSLineBreakMode) -> NSParagraphStyle {
        let paragraph = NSMutableParagraphStyle()
        paragraph.alignment = .center
        paragraph.lineBreakMode = lineBreak
        return paragraph
    }
}
private struct AppGridIconRuntimeState {
    var bubbleDisabled: Bool
    var showUncommonAppBubbles: Bool
}
private final class AppGridIconNSView: NSView {
    private weak var coordinator: AppGridCollectionView.Coordinator?
    private var app: AppInfo?
    private var sourceTag = ""
    private var iconSize: CGFloat = AppDefaults.iconSize
    private var showName = true
    private var isHovered = false
    private var trackingAreaRef: NSTrackingArea?
    private var mouseDownEvent: NSEvent?
    private var didStartDrag = false
    private var isLongPressActive = false
    private var longPressWorkItem: DispatchWorkItem?
    var runtimeState = AppGridIconRuntimeState(
        bubbleDisabled: false,
        showUncommonAppBubbles: AppDefaults.showUncommonAppBubbles
    ) {
        didSet {
            if runtimeState.bubbleDisabled {
                setHover(false, notify: true)
            }
        }
    }
    override var isFlipped: Bool { true }
    func configure(
        app: AppInfo,
        sourceTag: String,
        iconSize: CGFloat,
        showName: Bool,
        coordinator: AppGridCollectionView.Coordinator
    ) {
        self.app = app
        self.sourceTag = sourceTag
        self.iconSize = iconSize
        self.showName = showName
        self.coordinator = coordinator
        runtimeState = AppGridIconRuntimeState(
            bubbleDisabled: coordinator.bubbleDisabled,
            showUncommonAppBubbles: coordinator.showUncommonAppBubbles
        )
        needsDisplay = true
    }
    override func prepareForReuse() {
        longPressWorkItem?.cancel()
        if isHovered {
            setHover(false, notify: true)
        }
        mouseDownEvent = nil
        didStartDrag = false
        isLongPressActive = false
        longPressWorkItem = nil
    }
    override func updateTrackingAreas() {
        super.updateTrackingAreas()
        if let trackingAreaRef {
            removeTrackingArea(trackingAreaRef)
        }
        let area = NSTrackingArea(
            rect: .zero,
            options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect],
            owner: self,
            userInfo: nil
        )
        addTrackingArea(area)
        trackingAreaRef = area
    }
    override func draw(_ dirtyRect: NSRect) {
        guard let app else { return }
        NSGraphicsContext.current?.imageInterpolation = .high
        let scale = isHovered ? AppGridCollectionMetrics.hoverScale : 1
        let drawSize = iconSize * scale
        let iconSlot = iconSize * AppGridCollectionMetrics.hoverScale
        let iconRect = NSRect(
            x: (bounds.width - drawSize) / 2,
            y: 8 + (iconSlot - drawSize) / 2,
            width: drawSize,
            height: drawSize
        )
        if isHovered {
            let shadow = NSShadow()
            shadow.shadowColor = NSColor.black.withAlphaComponent(0.35)
            shadow.shadowBlurRadius = 14
            shadow.shadowOffset = NSSize(width: 0, height: -8)
            NSGraphicsContext.saveGraphicsState()
            shadow.set()
            app.icon.draw(in: iconRect)
            NSGraphicsContext.restoreGraphicsState()
        } else {
            app.icon.draw(in: iconRect)
        }
        if showName || shouldShowHoverName {
            let labelRect = NSRect(
                x: 0,
                y: 8 + iconSlot + 6,
                width: bounds.width,
                height: AppGridCollectionMetrics.labelHeight
            )
            let paragraph = NSMutableParagraphStyle()
            paragraph.alignment = .center
            paragraph.lineBreakMode = .byTruncatingTail
            let alpha: CGFloat = showName ? 1 : 0.85
            let attributes: [NSAttributedString.Key: Any] = [
                .font: NSFont.systemFont(ofSize: 11, weight: .medium),
                .foregroundColor: NSColor.labelColor.withAlphaComponent(alpha),
                .paragraphStyle: paragraph
            ]
            app.name.draw(with: labelRect, options: [.usesLineFragmentOrigin], attributes: attributes)
        }
    }
    override func mouseEntered(with event: NSEvent) {
        guard !runtimeState.bubbleDisabled else { return }
        setHover(true, notify: true)
    }
    override func mouseExited(with event: NSEvent) {
        setHover(false, notify: true)
    }
    override func mouseDown(with event: NSEvent) {
        mouseDownEvent = event
        didStartDrag = false
        isLongPressActive = false
        let workItem = DispatchWorkItem { [weak self] in
            guard let self, self.mouseDownEvent != nil else { return }
            self.isLongPressActive = true
            self.coordinator?.onDragModeChange(true)
            self.setHover(false, notify: true)
        }
        longPressWorkItem = workItem
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: workItem)
    }
    override func mouseDragged(with event: NSEvent) {
        if didStartDrag {
            AppDragCoordinator.shared.updateDragLocation(
                screenPoint(for: event),
                copy: event.modifierFlags.contains(.option)
            )
            return
        }
        guard let mouseDownEvent, isLongPressActive else { return }
        let dx = event.locationInWindow.x - mouseDownEvent.locationInWindow.x
        let dy = event.locationInWindow.y - mouseDownEvent.locationInWindow.y
        guard hypot(dx, dy) > 3 else { return }
        didStartDrag = true
        longPressWorkItem?.cancel()
        AppDragCoordinator.shared.beginDrag(
            image: makeDragImage(),
            payload: dragPayload,
            at: screenPoint(for: event),
            copy: event.modifierFlags.contains(.option),
            in: window
        )
    }
    override func mouseUp(with event: NSEvent) {
        longPressWorkItem?.cancel()
        if didStartDrag {
            AppDragCoordinator.shared.finishDrag(
                at: screenPoint(for: event),
                copy: event.modifierFlags.contains(.option)
            )
            coordinator?.onDragModeChange(false)
        } else if !isLongPressActive, let app {
            coordinator?.onSelectApp(app)
        } else {
            coordinator?.onDragModeChange(false)
        }
        didStartDrag = false
        isLongPressActive = false
        mouseDownEvent = nil
        longPressWorkItem = nil
    }
    override func rightMouseDown(with event: NSEvent) {
        guard let app else { return }
        coordinator?.onEditNote(app, rootLocalFrame())
    }
    override func menu(for event: NSEvent) -> NSMenu? {
        let menu = NSMenu()
        let item = NSMenuItem(title: tr("appNote.edit"), action: #selector(editNoteFromMenu), keyEquivalent: "")
        item.target = self
        menu.addItem(item)
        return menu
    }
    @objc private func editNoteFromMenu() {
        guard let app else { return }
        coordinator?.onEditNote(app, rootLocalFrame())
    }
    private var shouldShowAppBubble: Bool {
        guard let app else { return false }
        return !runtimeState.showUncommonAppBubbles || app.isUncommon
    }
    private var shouldShowHoverName: Bool {
        !showName && !shouldShowAppBubble && isHovered
    }
    private var dragPayload: String {
        guard let app else { return "" }
        return "\(app.path.path)\n\(sourceTag)"
    }
    private func setHover(_ hover: Bool, notify: Bool) {
        guard isHovered != hover else { return }
        isHovered = hover
        needsDisplay = true
        guard notify, let app else { return }
        if hover {
            coordinator?.onBubbleHover(app, rootLocalFrame(), .entered(canShowBubble: shouldShowAppBubble))
        } else {
            coordinator?.onBubbleHover(app, rootLocalFrame(), .exited)
        }
    }
    private func makeDragImage() -> NSImage {
        guard let app else { return NSImage(size: .zero) }
        let scale: CGFloat = AppGridCollectionMetrics.hoverScale * 1.5
        let imageSize = iconSize * scale
        let padding = iconSize * 0.45
        let canvasSize = NSSize(width: imageSize + padding * 2, height: imageSize + padding * 2)
        let dragImage = NSImage(size: canvasSize)
        dragImage.lockFocus()
        NSGraphicsContext.current?.imageInterpolation = .high
        let shadow = NSShadow()
        shadow.shadowColor = NSColor.black.withAlphaComponent(0.48)
        shadow.shadowBlurRadius = 24
        shadow.shadowOffset = NSSize(width: 0, height: -14)
        shadow.set()
        app.icon.draw(in: NSRect(x: padding, y: padding, width: imageSize, height: imageSize))
        dragImage.unlockFocus()
        return dragImage
    }
    private func rootLocalFrame() -> CGRect {
        guard let contentView = window?.contentView else { return .zero }
        let rectInContent = contentView.convert(bounds, from: self)
        let y = contentView.isFlipped
            ? rectInContent.minY
            : contentView.bounds.height - rectInContent.maxY
        return CGRect(
            x: rectInContent.minX,
            y: y,
            width: rectInContent.width,
            height: rectInContent.height
        )
    }
    private func screenPoint(for event: NSEvent) -> NSPoint {
        window?.convertPoint(toScreen: event.locationInWindow) ?? NSEvent.mouseLocation
    }
}
Apptag/AppGridItem.swift
@@ -13,13 +13,13 @@
    var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    var bubbleDisabled: Bool = false
    var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles
    var itemID: String
    var dragResetToken: Int = 0
    @Binding var hoveredAppItemID: String?
    let onSelect: () -> Void
    @State private var interactionFrame: CGRect = .zero
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
    @State private var isHovered = false
    static let hoverScale: CGFloat = 1.22
    static let labelHeight: CGFloat = 14
@@ -37,7 +37,6 @@
    private var iconSlotSize: CGFloat { iconSize * Self.hoverScale }
    private var labelWidth: CGFloat { iconSize + 20 }
    private var labelHeight: CGFloat { Self.labelHeight }
    private var isHovered: Bool { hoveredAppItemID == itemID }
    var body: some View {
        VStack(spacing: 6) {
@@ -46,6 +45,7 @@
                    icon: app.icon,
                    iconSize: iconSize,
                    payload: "\(app.path.path)\n\(sourceTag ?? "")",
                    resetToken: dragResetToken,
                    onLongPress: { onDragModeChange?(true) },
                    onDragEnd: { onDragModeChange?(false) },
                    onClick: onSelect
@@ -71,7 +71,7 @@
        .padding(.horizontal, 4)
        .frame(width: Self.stableWidth(iconSize: iconSize), height: Self.stableHeight(iconSize: iconSize))
        .background(
            AppGridItemHoverTracker { hovering, frame in
            AppHoverTrackingView { hovering, frame in
                interactionFrame = frame
                if bubbleDisabled || dragModeActive {
                    setHoverState(false)
@@ -97,7 +97,6 @@
        }
        .opacity(dragModeActive ? 0.92 : 1)
        .animation(.easeOut(duration: 0.08), value: dragModeActive)
        .id("\(itemID)|reset-\(dragResetToken)")
        .onChange(of: dragModeActive) { _, active in
            if active {
                setHoverState(false)
@@ -111,8 +110,8 @@
            }
        }
        .onDisappear {
            if hoveredAppItemID == itemID {
                hoveredAppItemID = nil
            if isHovered {
                isHovered = false
                onBubbleHover?(app, interactionFrame, .exited)
            }
        }
@@ -127,12 +126,9 @@
    }
    private func setHoverState(_ hovering: Bool) {
        guard isHovered != hovering else { return }
        withAnimation(hovering ? Self.hoverInAnimation : Self.hoverOutAnimation) {
            if hovering {
                hoveredAppItemID = itemID
            } else if hoveredAppItemID == itemID {
                hoveredAppItemID = nil
            }
            isHovered = hovering
        }
    }
}
@@ -142,21 +138,21 @@
    case exited
}
private struct AppGridItemHoverTracker: NSViewRepresentable {
struct AppHoverTrackingView: NSViewRepresentable {
    let onHover: (Bool, CGRect) -> Void
    func makeNSView(context: Context) -> HoverTrackingNSView {
        let view = HoverTrackingNSView()
    func makeNSView(context: Context) -> AppHoverTrackingNSView {
        let view = AppHoverTrackingNSView()
        view.onHover = onHover
        return view
    }
    func updateNSView(_ view: HoverTrackingNSView, context: Context) {
    func updateNSView(_ view: AppHoverTrackingNSView, context: Context) {
        view.onHover = onHover
    }
}
private final class HoverTrackingNSView: NSView {
final class AppHoverTrackingNSView: NSView {
    var onHover: ((Bool, CGRect) -> Void)?
    override func updateTrackingAreas() {
@@ -310,6 +306,7 @@
    let icon: NSImage
    let iconSize: CGFloat
    let payload: String
    let resetToken: Int
    let onLongPress: () -> Void
    let onDragEnd: () -> Void
    let onClick: () -> Void
@@ -319,6 +316,7 @@
        view.image = icon
        view.iconSize = iconSize
        view.payload = payload
        view.resetToken = resetToken
        view.onLongPress = onLongPress
        view.onDragEnd = onDragEnd
        view.onClick = onClick
@@ -331,6 +329,10 @@
        view.image = icon
        view.iconSize = iconSize
        view.payload = payload
        if view.resetToken != resetToken {
            view.resetToken = resetToken
            view.resetInteractionState()
        }
        view.onLongPress = onLongPress
        view.onDragEnd = onDragEnd
        view.onClick = onClick
@@ -344,6 +346,7 @@
    var image: NSImage = NSImage()
    var iconSize: CGFloat = 56
    var payload: String = ""
    var resetToken = 0
    var onLongPress: (() -> Void)?
    var onDragEnd: (() -> Void)?
    var onClick: (() -> Void)?
@@ -353,6 +356,14 @@
    private var isLongPressActive = false
    private var longPressWorkItem: DispatchWorkItem?
    override var isFlipped: Bool { true }
    func resetInteractionState() {
        longPressWorkItem?.cancel()
        longPressWorkItem = nil
        mouseDownEvent = nil
        didStartDrag = false
        isLongPressActive = false
    }
    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
@@ -419,6 +430,7 @@
        didStartDrag = false
        isLongPressActive = false
        mouseDownEvent = nil
        longPressWorkItem = nil
    }
    private func makeDragImage() -> NSImage {
Apptag/AppIdentity.swift
New file
@@ -0,0 +1,16 @@
import Foundation
enum AppIdentity {
    static let displayName = "TagLauncher"
    static let bundleIdentifier = "com.taglauncher.app"
    static let applicationSupportDirectoryName = "TagLauncher"
    static var applicationSupportDirectory: URL {
        FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Library/Application Support/\(applicationSupportDirectoryName)")
    }
    static let statusItemAutosaveName = "\(bundleIdentifier).statusItem"
    static let launchAgentLabel = bundleIdentifier
    static let categorySchemeBatchQueueLabel = "\(bundleIdentifier).category-scheme-batch"
}
Apptag/ApptagApp.swift
@@ -29,12 +29,12 @@
final class AppDelegate: NSObject, NSApplicationDelegate {
    private static let showDockIconKey = "showDockIcon"
    private static let statusItemAutosaveName = "com.apptag.launcher.statusItem"
    private static let statusItemAutosaveName = AppIdentity.statusItemAutosaveName
    private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
    private static let statusItemAccessibilityLabel = "TagLauncher"
    private static let statusItemAccessibilityLabel = AppIdentity.displayName
    private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
    private static let overlayDefaultLevel = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
    private static let overlayTextInputLevel = NSWindow.Level.modalPanel
    private static let overlayDefaultLevel = NSWindow.Level.normal
    private static let overlayTextInputLevel = NSWindow.Level.normal
    private var statusItem: NSStatusItem?
    private var overlayWindow: NSWindow?
@@ -50,6 +50,32 @@
    private var isEditingAppNote = false
    private var isConfiguringApplicationMenu = false
    private var lastShowDockIcon: Bool?
    private var isOverlayVisible: Bool {
        overlayWindow?.isVisible == true
    }
    private var isSettingsVisible: Bool {
        settingsWindow?.isVisible == true
    }
    private var requiresForegroundOwnership: Bool {
        isOverlayVisible || isSettingsVisible
    }
    private var currentOverlayLevel: NSWindow.Level {
        if isEditingAppNote || isQuickSearchOpen {
            return Self.overlayTextInputLevel
        }
        return Self.overlayDefaultLevel
    }
    private func overlayLevel(initialQuickSearchSource: String? = nil) -> NSWindow.Level {
        if initialQuickSearchSource != nil {
            return Self.overlayTextInputLevel
        }
        return currentOverlayLevel
    }
    static func refreshChromeSettings() {
        (NSApp.delegate as? AppDelegate)?.syncChromeSettings(force: true)
@@ -87,7 +113,11 @@
    }
    func applicationWillTerminate(_ notification: Notification) {
        hideOverlay(force: true, discardWindow: true)
        unregisterHotkey(for: .main)
        unregisterHotkey(for: .quickSearch)
        TagDatabase.flushPendingCategorySchemeBackupBatch()
        removeOverlayKeyMonitor()
        removeQuickSearchExternalMouseMonitor()
    }
@@ -124,13 +154,59 @@
        let dockChanged = lastShowDockIcon != showDock
        if force || dockChanged {
            NSApp.setActivationPolicy(showDock ? .regular : .accessory)
            lastShowDockIcon = showDock
            refreshLauncherChromeState()
        }
        if force {
            setupMenuBar()
        }
    }
    private func beginLauncherForegroundOwnership(activate: Bool = true) {
        if NSApp.activationPolicy() != .regular {
            NSApp.setActivationPolicy(.regular)
        }
        if activate {
            NSApp.activate(ignoringOtherApps: true)
            configureApplicationMenuWhenAvailable(retries: 4)
        }
    }
    private func refreshLauncherChromeState(activate: Bool = false) {
        let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
        lastShowDockIcon = showDock
        let desiredPolicy: NSApplication.ActivationPolicy = requiresForegroundOwnership
            ? .regular
            : (showDock ? .regular : .accessory)
        if NSApp.activationPolicy() != desiredPolicy {
            NSApp.setActivationPolicy(desiredPolicy)
        }
        let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
        if NSApp.presentationOptions != desiredPresentation {
            NSApp.presentationOptions = desiredPresentation
        }
        if activate && requiresForegroundOwnership {
            NSApp.activate(ignoringOtherApps: true)
            configureApplicationMenuWhenAvailable(retries: 4)
        }
    }
    private func handleApplicationDidResignActive() {
        if isQuickSearchOpen {
            NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
        }
        guard isOverlayVisible else {
            refreshLauncherChromeState()
            return
        }
        // Once another app takes the menu bar, don't leave the overlay stranded onscreen.
        hideOverlay(force: true)
    }
    /// Keep app chrome in sync when language changes from any entry point.
@@ -146,7 +222,7 @@
    // MARK: - Launch at Login (LaunchAgent, zero permissions)
    private static let launchAgentLabel = "com.apptag.launcher"
    private static let launchAgentLabel = AppIdentity.launchAgentLabel
    static var supportsLaunchAtLogin: Bool {
        ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] == nil
    }
@@ -506,7 +582,7 @@
    @objc private func toggleOverlay() {
        if overlayWindow?.isVisible == true {
            hideOverlay()
            hideOverlay(force: true)
        } else {
            showOverlay()
        }
@@ -520,19 +596,45 @@
        }) ?? NSScreen.main ?? NSScreen.screens.first else { return }
        let window: NSWindow
        let createdWindow: Bool
        if let existingWindow = overlayWindow {
            window = existingWindow
            createdWindow = false
        } else {
            window = makeOverlayWindow(on: screen, initialQuickSearchSource: initialQuickSearchSource)
            overlayWindow = window
            createdWindow = true
        }
        window.setFrame(screen.frame, display: true)
        window.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel
        installOverlayKeyMonitor()
        beginLauncherForegroundOwnership()
        let targetFrame = screen.frame
        let targetLevel = overlayLevel(initialQuickSearchSource: initialQuickSearchSource)
        let canReuseVisibleWindow = !createdWindow
            && window.isVisible
            && NSEqualRects(window.frame, targetFrame)
            && window.level == targetLevel
        if canReuseVisibleWindow {
            if !window.isKeyWindow {
                window.makeKeyAndOrderFront(nil)
            }
            refreshLauncherChromeState(activate: true)
        if let initialQuickSearchSource {
                NotificationCenter.default.post(
                    name: .tagLauncherQuickSearchRequested,
                    object: nil,
                    userInfo: ["source": initialQuickSearchSource]
                )
            }
            return
        }
        window.setFrame(targetFrame, display: true)
        window.level = targetLevel
        if let initialQuickSearchSource, !createdWindow {
            NotificationCenter.default.post(
                name: .tagLauncherQuickSearchRequested,
                object: nil,
@@ -542,6 +644,7 @@
        window.makeKeyAndOrderFront(nil)
        window.orderFrontRegardless()
        refreshLauncherChromeState(activate: true)
        NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
    }
@@ -554,7 +657,7 @@
                    NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
                    return nil
                }
                self.hideOverlay()
                self.hideOverlay(force: true)
                return nil
            }
            if self.shouldOpenQuickSearch(for: event) {
@@ -612,7 +715,7 @@
    private func makeOverlayWindow(on screen: NSScreen, initialQuickSearchSource: String? = nil) -> NSWindow {
        let panel = OverlayPanel(
            contentRect: screen.frame,
            styleMask: [.borderless, .fullSizeContentView, .nonactivatingPanel],
            styleMask: [.borderless, .fullSizeContentView],
            backing: .buffered,
            defer: false
        )
@@ -645,7 +748,7 @@
        return panel
    }
    private func hideOverlay(force: Bool = false) {
    private func hideOverlay(force: Bool = false, discardWindow: Bool = false) {
        guard force || !isInEditMode else { return }
        TagDatabase.flushPendingCategorySchemeBackupBatch()
        if let settingsWindow, settingsWindow.parent == overlayWindow {
@@ -654,8 +757,9 @@
        overlayWindow?.orderOut(nil)
        removeOverlayKeyMonitor()
        removeQuickSearchExternalMouseMonitor()
        refreshLauncherChromeState()
        NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
        if force {
        if discardWindow {
            overlayWindow = nil
        }
    }
@@ -838,6 +942,7 @@
        window.makeKeyAndOrderFront(nil)
        window.orderFrontRegardless()
        settingsWindow = window
        refreshLauncherChromeState(activate: true)
    }
    private func attachSettingsWindow(_ window: NSWindow, to overlayWindow: NSWindow) {
@@ -891,6 +996,7 @@
            else { return }
            self.detachSettingsWindow(closingWindow)
            self.settingsWindow = nil
            self.refreshLauncherChromeState()
        }
    }
@@ -905,7 +1011,7 @@
        }
    }
    /// Lower the overlay while editing app notes so IME candidate windows are not hidden behind it.
    /// Lower the overlay while text input is active so IME and cursor services are not hidden behind it.
    private func observeAppNoteEditing() {
        NotificationCenter.default.addObserver(
            forName: .tagLauncherAppNoteEditingChanged,
@@ -926,6 +1032,7 @@
        ) { [weak self] notification in
            guard let self else { return }
            self.isQuickSearchOpen = (notification.userInfo?["active"] as? Bool) ?? false
            self.updateOverlayLevelForTextInput()
            if self.isQuickSearchOpen {
                self.installQuickSearchExternalMouseMonitor()
            } else {
@@ -946,8 +1053,7 @@
            object: NSApp,
            queue: .main
        ) { [weak self] _ in
            guard self?.isQuickSearchOpen == true else { return }
            NotificationCenter.default.post(name: .tagLauncherQuickSearchDismissRequested, object: nil)
            self?.handleApplicationDidResignActive()
        }
    }
@@ -972,7 +1078,7 @@
    private func updateOverlayLevelForTextInput() {
        guard let overlayWindow else { return }
        overlayWindow.level = isEditingAppNote ? Self.overlayTextInputLevel : Self.overlayDefaultLevel
        overlayWindow.level = currentOverlayLevel
        if let settingsWindow, settingsWindow.parent == overlayWindow {
            settingsWindow.level = overlayWindow.level
        }
@@ -991,12 +1097,12 @@
    @objc private func openPreferences() {
        TagDatabase.flushPendingCategorySchemeBackupBatch()
        beginLauncherForegroundOwnership()
        // Don't hide overlay — keep it visible for real-time setting preview.
        if let overlayWindow, overlayWindow.isVisible {
            overlayWindow.makeKeyAndOrderFront(nil)
            overlayWindow.orderFrontRegardless()
        }
        NSApp.activate(ignoringOtherApps: true)
        if let settingsWindow {
            prepareSettingsWindow(settingsWindow)
Apptag/ContentView.swift
@@ -201,7 +201,6 @@
    let colorIndex: Int
    var dragModeActive: Bool = false
    var isDragging: Bool = false
    var dragResetToken: Int = 0
    let action: () -> Void
    private var bgColor: Color {
@@ -227,7 +226,6 @@
            .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0)
            .animation(.easeOut(duration: 0.08), value: isDragging)
            .animation(.easeOut(duration: 0.08), value: dragModeActive)
            .id("\(name)|reset-\(dragResetToken)")
            .contentShape(RoundedRectangle(cornerRadius: 7))
            .onTapGesture {
                action()
@@ -242,7 +240,6 @@
    let colorIndex: Int
    var dragModeActive: Bool = false
    var isDragging: Bool = false
    var dragResetToken: Int = 0
    let action: () -> Void
    private var bgColor: Color {
@@ -264,7 +261,6 @@
            .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0)
            .animation(.easeOut(duration: 0.08), value: isDragging)
            .animation(.easeOut(duration: 0.08), value: dragModeActive)
            .id("\(name)|reset-\(dragResetToken)")
            .contentShape(RoundedRectangle(cornerRadius: 6))
            .onTapGesture {
                action()
@@ -339,6 +335,7 @@
    let hideOverlay: () -> Void
    private let initialQuickSearchSource: String?
    @Environment(\.colorScheme) private var colorScheme
    @State private var allApps: [AppInfo] = []
    @State private var displayGroups: [TagGroup] = []
    @State private var tagColors: [String: Int] = [:]
@@ -347,6 +344,8 @@
    @State private var groupLayoutVersion = 0
    @State private var cachedGridContainerRowsKey: GridContainerRowsKey? = nil
    @State private var cachedGridContainerRows: [GridContainerLayoutRow] = []
    @State private var appGridScrollTargetID: String? = nil
    @State private var appGridScrollRequestToken = 0
    // Edit mode
    @State private var editPhase: EditPhase = .none
@@ -360,7 +359,6 @@
    @State private var tagReorderFrames: [String: CGRect] = [:]
    @State private var tagNavDragModeActive = false
    @State private var tagNavDragItem: String? = nil
    @State private var tagNavDragResetToken = 0
    @State private var tagNavReorderFrames: [String: CGRect] = [:]
    @State private var tagNavReorderDidMove = false
    @State private var hoveredContainer: String? = nil  // colored container lift
@@ -375,7 +373,6 @@
    @State private var refreshInProgress = false
    @State private var refreshAgainAfterCurrent = false
    @State private var refreshAgainForceLayout = false
    @State private var hoveredAppItemID: String? = nil
    @State private var hoveredBubble: AppBubbleContext? = nil
    @State private var editingBubble: AppBubbleContext? = nil
    @State private var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil
@@ -391,6 +388,7 @@
    @State private var quickSearchSelectedID: URL? = nil
    @State private var quickSearchManualSelection = false
    @State private var quickSearchFocusToken = 0
    @State private var quickSearchSelectionScrollToken = 0
    @State private var quickSearchCloseHidesOverlay = false
    @State private var quickSearchErrorMessage: String? = nil
    @State private var initialQuickSearchConsumed = false
@@ -412,6 +410,7 @@
    @State private var notchHeight: CGFloat = 0
    @AppStorage("displayMode") private var displayMode = AppDefaults.displayMode
    @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
    private let editSidebarWidth: CGFloat = 188
    private let editSidebarHorizontalInset: CGFloat = 12
@@ -421,6 +420,18 @@
        appDragModeActive || pendingUncategorizedDrop != nil || scrollInteractionState.isFrozen
    }
    private let rightSidebarFloatingClearance: CGFloat = 44
    private var cardSurfaceColor: Color {
        colorScheme == .dark
            ? Color.white.opacity(0.055)
            : Color.white.opacity(0.62)
    }
    private var floatingButtonSurfaceColor: Color {
        colorScheme == .dark
            ? Color.white.opacity(0.10)
            : Color.white.opacity(0.78)
    }
    private var isSideLayout: Bool {
        tagPosition == "left" || tagPosition == "right"
@@ -438,19 +449,19 @@
        displayMode == "coloredContainer" || displayMode == "coloredGridContainer"
    }
    private var quickSearchOnlyMode: Bool {
        quickSearchVisible && quickSearchCloseHidesOverlay
    private var usesAppKitContainerGrid: Bool {
        displayMode == "gridContainer" || displayMode == "coloredGridContainer"
    }
    var body: some View {
        ZStack {
            if !quickSearchOnlyMode {
            if !quickSearchVisible {
                VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
                    .ignoresSafeArea()
                    .allowsHitTesting(false)
            }
            if !quickSearchOnlyMode {
            if !quickSearchVisible {
                if notchHeight > 0 {
                    VStack {
                        Rectangle().fill(.black)
@@ -478,7 +489,7 @@
            quickSearchOverlay
            if !quickSearchOnlyMode, let message = dropWarningToast {
            if !quickSearchVisible, let message = dropWarningToast {
                Text(message)
                    .font(.system(size: 16, weight: .semibold))
                    .foregroundStyle(.primary)
@@ -493,7 +504,7 @@
                    .allowsHitTesting(false)
            }
            if !quickSearchOnlyMode && dropRefreshVisible {
            if !quickSearchVisible && dropRefreshVisible {
                Color.black.opacity(0.08)
                    .ignoresSafeArea()
                    .transition(.opacity)
@@ -519,8 +530,11 @@
            if let initialQuickSearchSource, !initialQuickSearchConsumed {
                initialQuickSearchConsumed = true
                quickSearchCloseHidesOverlay = initialQuickSearchSource == QuickSearchOpenSource.globalHidden
                if !quickSearchVisible {
                quickSearchVisible = true
                quickSearchFocusToken &+= 1
                }
                refreshQuickSearchResults()
                NotificationCenter.default.post(
                    name: .tagLauncherQuickSearchVisibilityChanged,
                    object: nil,
@@ -531,11 +545,7 @@
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
            resetTransientDragState()
            refreshNotchHeight()
            if quickSearchVisible {
                refreshAppsIfNeeded()
            } else {
                refreshApps()
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
            resetTransientDragState()
@@ -642,6 +652,7 @@
                    results: quickSearchResults,
                    selectedID: quickSearchSelectedID,
                    focusToken: quickSearchFocusToken,
                    selectionScrollToken: quickSearchSelectionScrollToken,
                    isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
                    maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
                    errorMessage: quickSearchErrorMessage,
@@ -773,9 +784,11 @@
        let nextIndex = min(max(currentIndex + delta, 0), quickSearchResults.count - 1)
        quickSearchSelectedID = quickSearchResults[nextIndex].id
        quickSearchManualSelection = true
        quickSearchSelectionScrollToken &+= 1
    }
    private func selectQuickSearchResult(_ result: QuickSearchResult) {
        guard quickSearchSelectedID != result.id || !quickSearchManualSelection else { return }
        quickSearchSelectedID = result.id
        quickSearchManualSelection = true
    }
@@ -836,7 +849,7 @@
    ) -> some View {
        NativeFloatingIconButton(systemImage: systemImage, action: action)
            .frame(width: 36, height: 36)
            .background(Circle().fill(.ultraThinMaterial))
            .background(Circle().fill(floatingButtonSurfaceColor))
    }
    // MARK: - Top / Side Layouts
@@ -869,7 +882,6 @@
                    TagPill(name: tag.name, colorIndex: tag.colorIndex,
                            dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
                            isDragging: tagNavDragItem == tag.name,
                            dragResetToken: tagNavDragResetToken,
                            action: {
                        activateTagNavigation(tag.id)
                    })
@@ -911,7 +923,6 @@
                    SideTagPill(name: tag.name, colorIndex: tag.colorIndex,
                                dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
                                isDragging: tagNavDragItem == tag.name,
                                dragResetToken: tagNavDragResetToken,
                                action: {
                        activateTagNavigation(tag.id)
                    })
@@ -944,8 +955,33 @@
                Spacer()
                ProgressView().scaleEffect(0.8)
                Spacer()
            } else if displayMode == "gridContainer" || displayMode == "coloredGridContainer" {
                gridContainerGrid
            } else if usesAppKitContainerGrid {
                AppGridCollectionView(
                    groups: displayGroups,
                    tagColors: tagColors,
                    displayMode: displayMode,
                    iconSize: iconSize,
                    showNames: !hideAppNames,
                    bubbleDisabled: appBubbleDisabled,
                    showUncommonAppBubbles: showUncommonAppBubbles,
                    highlightedGroupName: filledColorlessContainer,
                    contentRevision: groupLayoutVersion,
                    scrollTargetID: appGridScrollTargetID,
                    scrollRequestToken: appGridScrollRequestToken,
                    onSelectApp: { app in openApp(app) },
                    onBubbleHover: handleBubbleHover,
                    onEditNote: beginEditingBubbleNote,
                    onDropApp: { path, source, target, copy in
                        dropApp(path: path, sourceTag: source, targetTag: target, copy: copy)
                    },
                    onGroupActivate: { groupName in
                        if displayMode == "gridContainer" {
                            toggleColorlessFill(groupName)
                        }
                    },
                    onScrollActivity: handleAppGridScrollActivity,
                    onDragModeChange: { setAppDragMode($0) }
                )
            } else if displayMode == "container" || displayMode == "coloredContainer" {
                containerGrid
            } else {
@@ -1092,8 +1128,8 @@
                            onBubbleHover: handleBubbleHover,
                            onEditNote: beginEditingBubbleNote,
                            bubbleDisabled: appBubbleDisabled,
                            showUncommonAppBubbles: showUncommonAppBubbles,
                            dragResetToken: appDragResetToken,
                            hoveredAppItemID: $hoveredAppItemID,
                            onDropApp: { path, source, copy in
                                dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
                            }
@@ -1200,9 +1236,9 @@
                        onBubbleHover: handleBubbleHover,
                        onEditNote: beginEditingBubbleNote,
                        bubbleDisabled: appBubbleDisabled,
                        showUncommonAppBubbles: showUncommonAppBubbles,
                        itemID: "\(group.name)|\(app.path.path)",
                        dragResetToken: appDragResetToken,
                        hoveredAppItemID: $hoveredAppItemID,
                        onSelect: { openApp(app) }
                    )
                }
@@ -1215,7 +1251,7 @@
                .fill((isColored || isColorlessActive) ? tagColor.opacity(0.30) : Color.clear)
                .background(
                    RoundedRectangle(cornerRadius: 14)
                        .fill(.ultraThinMaterial)
                        .fill(cardSurfaceColor)
                )
        )
        .overlay(
@@ -1223,10 +1259,12 @@
                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
        )
        .overlay {
            if appDragModeActive {
            AppDropTargetView(targetTag: group.name) { path, source, copy in
                dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
            }
            .allowsHitTesting(false)
            }
        }
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessActive ? 8 : 0,
@@ -1234,7 +1272,7 @@
        .zIndex(isHovered ? 50 : 0)
        .animation(.easeOut(duration: 0.045), value: isHovered)
        .animation(.easeOut(duration: 0.08), value: isColorlessFilled)
        .onHover { hovering in
        .background(AppHoverTrackingView { hovering, _ in
            guard !scrollInteractionState.isFrozen else {
                if !hovering && hoveredContainer == group.name {
                    hoveredContainer = nil
@@ -1244,7 +1282,7 @@
            if isColored || isColorless {
                hoveredContainer = hovering ? group.name : nil
            }
        }
        })
        .contentShape(RoundedRectangle(cornerRadius: 14))
        .onTapGesture {
            if isColorless {
@@ -1519,9 +1557,9 @@
                                onBubbleHover: handleBubbleHover,
                                onEditNote: beginEditingBubbleNote,
                                bubbleDisabled: appBubbleDisabled,
                                showUncommonAppBubbles: showUncommonAppBubbles,
                                itemID: "\(group.name)|\(app.path.path)",
                                dragResetToken: appDragResetToken,
                                hoveredAppItemID: $hoveredAppItemID,
                                onSelect: { openApp(app) }
                            )
                                .frame(width: cellWidth)
@@ -1546,7 +1584,7 @@
                .fill((isColored || isColorlessGridActive) ? tagColor.opacity(0.30) : Color.clear)
                .background(
                    RoundedRectangle(cornerRadius: 14)
                        .fill(.ultraThinMaterial)
                        .fill(cardSurfaceColor)
                )
        )
        .overlay(
@@ -1554,10 +1592,12 @@
                .stroke(Color.primary.opacity(0.08), lineWidth: 1)
        )
        .overlay {
            if appDragModeActive {
            AppDropTargetView(targetTag: group.name) { path, source, copy in
                dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
            }
            .allowsHitTesting(false)
            }
        }
        .shadow(color: .black.opacity((isColored && isHovered) || isColorlessGridActive ? 0.22 : 0),
                radius: (isColored && isHovered) || isColorlessGridActive ? 8 : 0,
@@ -1565,7 +1605,7 @@
        .zIndex(isHovered ? 50 : 0)
        .animation(.easeOut(duration: 0.045), value: isHovered)
        .animation(.easeOut(duration: 0.08), value: isColorlessFilled)
        .onHover { hovering in
        .background(AppHoverTrackingView { hovering, _ in
            guard !scrollInteractionState.isFrozen else {
                if !hovering && hoveredContainer == group.name {
                    hoveredContainer = nil
@@ -1577,7 +1617,7 @@
            } else if hovering {
                fillColorlessContainer(group.name)
            }
        }
        })
        .contentShape(RoundedRectangle(cornerRadius: 14))
        .onTapGesture {
            if isColorlessGrid {
@@ -2222,7 +2262,6 @@
    private func handleAppGridScrollActivity() {
        if !scrollInteractionState.isFrozen {
            hoveredAppItemID = nil
            hoveredBubble = nil
            hoveredContainer = nil
        }
@@ -2284,13 +2323,16 @@
    }
    private func clearAppBubbleState() {
        hoveredAppItemID = nil
        if hoveredBubble != nil {
        hoveredBubble = nil
        }
        if editingBubble != nil {
            notifyAppNoteEditing(active: false)
        }
        editingBubble = nil
        }
        if bubbleNoteFocused {
        bubbleNoteFocused = false
        }
    }
    private func notifyAppNoteEditing(active: Bool) {
@@ -2354,7 +2396,12 @@
    }
    func scrollTo(_ id: String) {
        if usesAppKitContainerGrid {
            appGridScrollTargetID = id
            appGridScrollRequestToken &+= 1
        } else {
        withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) }
        }
    }
    private func activateTagNavigation(_ id: String) {
@@ -2425,13 +2472,14 @@
    }
    private func endTagNavReorder() {
        let hadDragState = tagNavDragModeActive || tagNavDragItem != nil
        guard hadDragState else { return }
        if tagNavDragModeActive && tagNavReorderDidMove {
            TagEditor.reorderTags(draggedTagNames)
        }
        tagNavDragModeActive = false
        tagNavDragItem = nil
        tagNavReorderDidMove = false
        tagNavDragResetToken &+= 1
    }
    private func cancelTagNavReorderVisualState() {
@@ -2439,7 +2487,6 @@
        tagNavDragModeActive = false
        tagNavDragItem = nil
        tagNavReorderDidMove = false
        tagNavDragResetToken &+= 1
    }
    private func reorderTagNavItem(at location: CGPoint) {
@@ -2610,6 +2657,7 @@
    }
    private func setAppDragMode(_ active: Bool) {
        guard appDragModeActive != active else { return }
        if active {
            endTagNavReorder()
            clearAppBubbleState()
@@ -2617,7 +2665,7 @@
        appDragModeActive = active
        if active {
            DispatchQueue.main.asyncAfter(deadline: .now() + 8) {
                if appDragModeActive {
                if appDragModeActive && !AppDragCoordinator.shared.hasActiveDrag {
                    appDragModeActive = false
                }
            }
@@ -2625,19 +2673,32 @@
    }
    private func resetTransientDragState(keepingPendingUncategorizedDrop: Bool = false) {
        let hadAppDragState = appDragModeActive
        AppDragCoordinator.shared.cancelDrag()
        scrollInteractionState.reset()
        if appDragModeActive {
        appDragModeActive = false
        }
        if hadAppDragState {
        appDragResetToken &+= 1
        }
        if tagNavDragModeActive {
        tagNavDragModeActive = false
        }
        if tagNavDragItem != nil {
        tagNavDragItem = nil
        }
        if tagNavReorderDidMove {
        tagNavReorderDidMove = false
        tagNavDragResetToken &+= 1
        }
        if dragItem != nil {
        dragItem = nil
        hoveredAppItemID = nil
        }
        if hoveredContainer != nil {
        hoveredContainer = nil
        }
        clearAppBubbleState()
        if !keepingPendingUncategorizedDrop {
        if !keepingPendingUncategorizedDrop, pendingUncategorizedDrop != nil {
            pendingUncategorizedDrop = nil
        }
    }
@@ -2732,7 +2793,8 @@
        }
        unfreezeWorkItem?.cancel()
        let workItem = DispatchWorkItem { [weak self] in
            self?.isFrozen = false
            guard let self, self.isFrozen else { return }
            self.isFrozen = false
        }
        unfreezeWorkItem = workItem
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.18, execute: workItem)
@@ -2741,7 +2803,9 @@
    func reset() {
        unfreezeWorkItem?.cancel()
        unfreezeWorkItem = nil
        if isFrozen {
        isFrozen = false
        }
    }
}
@@ -2761,13 +2825,13 @@
    func updateNSView(_ view: ScrollActivityNSView, context: Context) {
        context.coordinator.onScroll = onScroll
        view.coordinator = context.coordinator
        view.installObserverIfPossible()
    }
    final class Coordinator {
        var onScroll: () -> Void
        private weak var observedClipView: NSClipView?
        private var observer: NSObjectProtocol?
        private var lastReportedBoundsOrigin: NSPoint?
        init(onScroll: @escaping () -> Void) {
            self.onScroll = onScroll
@@ -2778,13 +2842,18 @@
            guard observedClipView !== clipView else { return }
            removeObserver()
            observedClipView = clipView
            lastReportedBoundsOrigin = clipView.bounds.origin
            clipView.postsBoundsChangedNotifications = true
            observer = NotificationCenter.default.addObserver(
                forName: NSView.boundsDidChangeNotification,
                object: clipView,
                queue: .main
            ) { [weak self] _ in
                self?.onScroll()
            ) { [weak self, weak clipView] _ in
                guard let self,
                      let clipView,
                      self.recordScrollIfNeeded(from: clipView)
                else { return }
                self.onScroll()
            }
        }
@@ -2794,6 +2863,21 @@
            }
            observer = nil
            observedClipView = nil
            lastReportedBoundsOrigin = nil
        }
        private func recordScrollIfNeeded(from clipView: NSClipView) -> Bool {
            let origin = clipView.bounds.origin
            guard let last = lastReportedBoundsOrigin else {
                lastReportedBoundsOrigin = origin
                return false
            }
            let didScroll = abs(origin.x - last.x) > 0.5
                || abs(origin.y - last.y) > 0.5
            if didScroll {
                lastReportedBoundsOrigin = origin
            }
            return didScroll
        }
        deinit {
@@ -2803,6 +2887,7 @@
    final class ScrollActivityNSView: NSView {
        weak var coordinator: Coordinator?
        private var installScheduled = false
        override func viewDidMoveToWindow() {
            super.viewDidMoveToWindow()
@@ -2815,8 +2900,11 @@
        }
        func installObserverIfPossible() {
            guard !installScheduled else { return }
            installScheduled = true
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
                installScheduled = false
                coordinator?.install(from: self)
            }
        }
Apptag/DataLayer.swift
@@ -1,5 +1,6 @@
import Foundation
import AppKit
import CoreServices
// MARK: - Data Models
@@ -9,6 +10,7 @@
    let path: URL
    let tags: [String]
    let bundleIdentifier: String?
    let localizedNames: [String]
    let icon: NSImage  // Pre-loaded during background scan
    var isUncommon: Bool = false
    var note: String? = nil
@@ -176,16 +178,72 @@
        let icon = NSWorkspace.shared.icon(forFile: displayURL.path)
        icon.size = NSSize(width: 96, height: 96)
        let bundleId = Bundle(url: displayURL)?.bundleIdentifier
            ?? Bundle(url: resolvedURL)?.bundleIdentifier
        let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
        let bundleId = bundle?.bundleIdentifier
        let localizedNames = localizedAppNames(
            for: displayURL,
            bundle: bundle,
            fallbackName: name
        )
        apps.append(AppInfo(
            name: name,
            path: displayURL,
            tags: [],
            bundleIdentifier: bundleId,
            localizedNames: localizedNames,
            icon: icon
        ))
    }
    private static func localizedAppNames(
        for appURL: URL,
        bundle: Bundle?,
        fallbackName: String
    ) -> [String] {
        var values: [String?] = []
        values.append(bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String)
        values.append(bundle?.localizedInfoDictionary?["CFBundleName"] as? String)
        values.append(bundle?.infoDictionary?["CFBundleDisplayName"] as? String)
        values.append(bundle?.infoDictionary?["CFBundleName"] as? String)
        values.append(spotlightDisplayName(for: appURL))
        values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
        return uniqueLocalizedNames(values, excluding: fallbackName)
    }
    private static func spotlightDisplayName(for appURL: URL) -> String? {
        guard let item = MDItemCreate(nil, appURL.path as CFString),
              let value = MDItemCopyAttribute(item, kMDItemDisplayName) as? String
        else { return nil }
        let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
        return trimmed.isEmpty ? nil : trimmed
    }
    private static func uniqueLocalizedNames(_ values: [String?], excluding excludedValue: String) -> [String] {
        let normalizedExcluded = normalizedLocalizedName(excludedValue)
        var seen = Set<String>()
        var result: [String] = []
        for value in values {
            guard let value else { continue }
            let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
            let normalized = normalizedLocalizedName(trimmed)
            guard !trimmed.isEmpty,
                  normalized != normalizedExcluded,
                  seen.insert(normalized).inserted
            else { continue }
            result.append(trimmed)
        }
        return result
    }
    private static func normalizedLocalizedName(_ value: String) -> String {
        value
            .folding(options: [.diacriticInsensitive, .caseInsensitive], locale: .current)
            .lowercased()
            .trimmingCharacters(in: .whitespacesAndNewlines)
    }
    private static func isNestedInsideAppBundle(_ url: URL) -> Bool {
@@ -324,6 +382,18 @@
        case manual
    }
    enum AppNoteOrigin: String, Codable {
        case catalogDefault
        case appleDefault
        case manual
    }
    struct AppNoteMetadata: Codable, Equatable {
        var origin: AppNoteOrigin
        var catalog: SmartDefaultNoteProvenance? = nil
        var noteFingerprint: String
    }
    // MARK: Storage types
    struct TagDef: Codable, Equatable {
@@ -342,6 +412,7 @@
        var appLastOpenedAt: [String: Date] = [:]  // successful launches opened from TagLauncher
        var knownAppPaths: [String] = []  // baseline set to detect newly installed apps
        var appNotes: [String: String] = [:]  // path → user note; retained even if marker is removed
        var appNoteMetadata: [String: AppNoteMetadata] = [:]  // path → note source and edit protection
        var disabledSystemCategoryIDs: [SmartCategoryID] = []  // system categories the user deleted
        var smartStart: SmartStartState = SmartStartState()
        var categoryScheme: CategorySchemeState = CategorySchemeState()
@@ -357,6 +428,7 @@
            case appLastOpenedAt
            case knownAppPaths
            case appNotes
            case appNoteMetadata
            case disabledSystemCategoryIDs
            case smartStart
            case categoryScheme
@@ -376,6 +448,10 @@
            appLastOpenedAt = try container.decodeIfPresent([String: Date].self, forKey: .appLastOpenedAt) ?? [:]
            knownAppPaths = try container.decodeIfPresent([String].self, forKey: .knownAppPaths) ?? []
            appNotes = try container.decodeIfPresent([String: String].self, forKey: .appNotes) ?? [:]
            appNoteMetadata = try container.decodeIfPresent(
                [String: AppNoteMetadata].self,
                forKey: .appNoteMetadata
            ) ?? [:]
            disabledSystemCategoryIDs = try container.decodeIfPresent(
                [SmartCategoryID].self,
                forKey: .disabledSystemCategoryIDs
@@ -416,8 +492,7 @@
    // MARK: Paths
    private static var storeDir: URL {
        let dir = FileManager.default.homeDirectoryForCurrentUser
            .appendingPathComponent("Library/Application Support/Apptag")
        let dir = AppIdentity.applicationSupportDirectory
        try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        return dir
    }
@@ -436,25 +511,9 @@
        return dir
    }
    private static var legacyStoreURL: URL? {
        guard ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil else {
            return nil
        }
        let home = FileManager.default.homeDirectoryForCurrentUser
        let bundleID = Bundle.main.bundleIdentifier ?? "com.apptag.launcher"
        let marker = "/Library/Containers/\(bundleID)/Data"
        guard let range = home.path.range(of: marker) else { return nil }
        let realHomePath = String(home.path[..<range.lowerBound])
        return URL(fileURLWithPath: realHomePath)
            .appendingPathComponent("Library/Application Support/Apptag/tags.json")
    }
    // MARK: Load / Save
    static func load() -> Store {
        migrateLegacyStoreIfNeeded()
        guard let data = try? Data(contentsOf: storeURL),
              let store = try? JSONDecoder().decode(Store.self, from: data)
        else { return Store() }
@@ -474,6 +533,16 @@
        encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
        guard let data = try? encoder.encode(store) else { return }
        try? data.write(to: storeURL, options: .atomic)
    }
    static func noteFingerprint(_ value: String) -> String {
        let normalized = String(value.trimmingCharacters(in: .whitespacesAndNewlines).prefix(maxAppNoteLength))
        var hash: UInt64 = 0xcbf29ce484222325
        for byte in normalized.utf8 {
            hash ^= UInt64(byte)
            hash &*= 0x100000001b3
        }
        return String(format: "%016llx", hash)
    }
    static func backup(_ store: Store, reason: String, in directory: URL? = nil) -> URL? {
@@ -511,7 +580,7 @@
    private static let categorySchemeBatchDebounceSeconds: TimeInterval = 90
    private static let categorySchemeRetentionSeconds: TimeInterval = 30 * 24 * 60 * 60
    private static let categorySchemeBatchQueue = DispatchQueue(label: "com.apptag.category-scheme-batch")
    private static let categorySchemeBatchQueue = DispatchQueue(label: AppIdentity.categorySchemeBatchQueueLabel)
    private static var categorySchemeBatchActive = false
    private static var categorySchemeBatchResetWorkItem: DispatchWorkItem?
@@ -653,33 +722,6 @@
        return true
    }
    /// App Store sandbox builds read Application Support inside the app container.
    /// Older non-sandbox builds stored the same database directly under ~/Library.
    /// On first sandbox launch, migrate the richer legacy database before seeding defaults.
    static func migrateLegacyStoreIfNeeded() {
        let fm = FileManager.default
        guard let legacyURL = legacyStoreURL,
              fm.fileExists(atPath: legacyURL.path)
        else { return }
        guard let legacyData = try? Data(contentsOf: legacyURL),
              let legacyStore = try? JSONDecoder().decode(Store.self, from: legacyData)
        else { return }
        if let currentData = try? Data(contentsOf: storeURL),
           let currentStore = try? JSONDecoder().decode(Store.self, from: currentData),
           storeScore(currentStore) >= storeScore(legacyStore) {
            return
        }
        try? fm.createDirectory(at: storeDir, withIntermediateDirectories: true)
        try? legacyData.write(to: storeURL, options: .atomic)
    }
    private static func storeScore(_ store: Store) -> Int {
        store.appTags.count * 100 + store.tagOrder.count * 10 + store.tags.count
    }
    // MARK: Localization
    @discardableResult
@@ -775,7 +817,6 @@
    // MARK: Export / Import
    static func exportTo(_ url: URL) throws {
        migrateLegacyStoreIfNeeded()
        let store = loadWithEnsuredCategoryScheme()
        let data = try JSONEncoder().encode(store)
        try data.write(to: url, options: .atomic)
@@ -961,17 +1002,16 @@
    /// Seed starter system tags on first launch. Smart Start adds any additional
    /// system tags it needs after scanning the user's installed apps.
    static func seedDefaultTags() {
        migrateLegacyStoreIfNeeded()
        guard !FileManager.default.fileExists(atPath: storeURL.path) else { return }
        let starterCategoryIDs: [SmartCategoryID] = [
            .design,
            .development,
            .uiPrototyping,
            .ide,
            .writing,
            .game,
            .entertainment,
            .system,
            .productivity
            .gtd
        ]
        var store = Store()
@@ -1039,7 +1079,9 @@
            let appTags = store.appTags[app.path.path] ?? []
            return AppInfo(
                name: app.name, path: app.path, tags: appTags,
                bundleIdentifier: app.bundleIdentifier, icon: app.icon,
                bundleIdentifier: app.bundleIdentifier,
                localizedNames: app.localizedNames,
                icon: app.icon,
                isUncommon: uncommonPaths.contains(app.path.path),
                note: store.appNotes[app.path.path]
            )
@@ -1236,7 +1278,12 @@
            }
            guard let note = AppleDefaultAppNotes.note(for: app) else { continue }
            store.appNotes[path] = String(note.prefix(TagDatabase.maxAppNoteLength))
            let limited = String(note.prefix(TagDatabase.maxAppNoteLength))
            store.appNotes[path] = limited
            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                origin: .appleDefault,
                noteFingerprint: TagDatabase.noteFingerprint(limited)
            )
            changed = true
        }
@@ -1290,8 +1337,13 @@
        let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength))
        if limited.isEmpty {
            store.appNotes.removeValue(forKey: path)
            store.appNoteMetadata.removeValue(forKey: path)
        } else {
            store.appNotes[path] = limited
            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                origin: .manual,
                noteFingerprint: TagDatabase.noteFingerprint(limited)
            )
            var uncommonPaths = Set(store.uncommonAppPaths)
            if uncommonPaths.insert(path).inserted {
                store.uncommonAppPaths = uncommonPaths.sorted()
Apptag/Info.plist
@@ -11,7 +11,7 @@
    <key>CFBundleIconFile</key>
    <string>AppIcon</string>
    <key>CFBundleIdentifier</key>
    <string>com.apptag.launcher</string>
    <string>com.taglauncher.app</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>7.0</string>
    <key>CFBundleName</key>
@@ -21,7 +21,9 @@
    <key>CFBundleShortVersionString</key>
    <string>7.6.0</string>
    <key>CFBundleVersion</key>
    <string>20260520.1250</string>
    <string>20260520.1452</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
    <string>15.0</string>
    <key>NSHighResolutionCapable</key>
Apptag/Localization/ar-Najdi.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "هالعملية اضافت %tagCount% من الوسوم على %appCount% تطبيقات (%appNames%).",
  "edit.feedbackRemoveFormat": "هالعملية شالت %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم اللي انشالت: %tagNames%.",
  "edit.removeHint": "هنا ما تقدر تشيل الصح الا من الوسوم الموجودة من قبل. اي وسم يطلع معه رمز حذف احمر بينشال بعد التاكيد.",
  "smart.category.browser": "المتصفحات",
  "smart.category.communication": "التواصل",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "الإنتاجية",
  "smart.category.file-management": "إدارة الملفات",
  "smart.category.transfer": "الرفع والتنزيل",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "البرمجة",
  "smart.category.design": "التصميم",
  "smart.category.writing": "الكتابة",
  "smart.category.media": "الوسائط",
  "smart.category.video": "الفيديو",
  "smart.category.audio": "الصوت",
  "smart.category.picture-photo": "الصور",
  "smart.category.utilities": "الأدوات",
  "smart.category.system": "تطبيقات النظام",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "تعزيز النظام",
  "smart.category.entertainment": "الترفيه",
  "smart.category.game": "الألعاب",
  "smart.category.finance": "المال",
  "smart.category.education": "التعلّم",
  "smart.category.ai-tools": "أدوات الذكاء الاصطناعي",
  "smart.category.security": "الأمان",
  "smart.category.other": "أخرى",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start رتّب تطبيقاتك",
  "smartstart.auto.messageFormat": "ترتّبت %appCount% تطبيقات مع %tagCount% تعيينات وسوم.",
  "smartstart.suggestion.title": "اقتراحات Smart Start جاهزة",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ar.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "اضافت هذه العملية %tagCount% من الوسوم عبر %appCount% تطبيقات (%appNames%).",
  "edit.feedbackRemoveFormat": "ازالت هذه العملية %tagCount% من الوسوم من %appCount% تطبيقات (%appNames%). الوسوم التي تمت ازالتها: %tagNames%.",
  "edit.removeHint": "هنا يمكنك الغاء تحديد الوسوم الموجودة بالفعل فقط. الوسوم التي يظهر بجانبها رمز حذف احمر ستزال بعد التاكيد.",
  "smart.category.browser": "المتصفحات",
  "smart.category.communication": "التواصل",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "الإنتاجية",
  "smart.category.file-management": "إدارة الملفات",
  "smart.category.transfer": "الرفع والتنزيل",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "البرمجة",
  "smart.category.design": "التصميم",
  "smart.category.writing": "الكتابة",
  "smart.category.media": "الوسائط",
  "smart.category.video": "الفيديو",
  "smart.category.audio": "الصوت",
  "smart.category.picture-photo": "الصور والفوتوغرافيا",
  "smart.category.utilities": "الأدوات",
  "smart.category.system": "تطبيقات النظام",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "تعزيز النظام",
  "smart.category.entertainment": "الترفيه",
  "smart.category.game": "الألعاب",
  "smart.category.finance": "المال",
  "smart.category.education": "التعلّم",
  "smart.category.ai-tools": "أدوات الذكاء الاصطناعي",
  "smart.category.security": "الأمان",
  "smart.category.other": "أخرى",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "نظّم Smart Start تطبيقاتك",
  "smartstart.auto.messageFormat": "تم تنظيم %appCount% تطبيقًا مع %tagCount% تعيينات وسوم.",
  "smartstart.suggestion.title": "اقتراحات Smart Start جاهزة",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/cs.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Tato akce pridala %tagCount% stitku do %appCount% aplikaci (%appNames%).",
  "edit.feedbackRemoveFormat": "Tato akce odebrala %tagCount% stitku z %appCount% aplikaci (%appNames%). Odebrane stitky: %tagNames%.",
  "edit.removeHint": "Zde lze odskrtnout jen jiz existujici stitky. Stitky s cervenou ikonou odstraneni budou po potvrzeni odebrany.",
  "smart.category.browser": "Prohlížeče",
  "smart.category.communication": "Komunikace",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivita",
  "smart.category.file-management": "Správa souborů",
  "smart.category.transfer": "Nahrávání a stahování",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programování",
  "smart.category.design": "Design",
  "smart.category.writing": "Psaní",
  "smart.category.media": "Média",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Obrázky a fotky",
  "smart.category.utilities": "Nástroje",
  "smart.category.system": "Systémové aplikace",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Vylepšení systému",
  "smart.category.entertainment": "Zábava",
  "smart.category.game": "Hry",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Vzdělávání",
  "smart.category.ai-tools": "Nástroje AI",
  "smart.category.security": "Zabezpečení",
  "smart.category.other": "Ostatní",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start uspořádal aplikace",
  "smartstart.auto.messageFormat": "Uspořádáno %appCount% aplikací pomocí %tagCount% přiřazení tagů.",
  "smartstart.suggestion.title": "Návrhy Smart Start jsou připravené",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/da.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Denne handling tilfojede %tagCount% tags til %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handling fjernede %tagCount% tags fra %appCount% apps (%appNames%). Fjernede tags: %tagNames%.",
  "edit.removeHint": "Her kan kun eksisterende tags fjernes fra markeringen. Tags med et rodt sletteikon fjernes efter bekraeftelse.",
  "smart.category.browser": "Browsere",
  "smart.category.communication": "Kommunikation",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitet",
  "smart.category.file-management": "Filhåndtering",
  "smart.category.transfer": "Uploads og downloads",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmering",
  "smart.category.design": "Design",
  "smart.category.writing": "Skrivning",
  "smart.category.media": "Medier",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Lyd",
  "smart.category.picture-photo": "Billeder og fotos",
  "smart.category.utilities": "Værktøjer",
  "smart.category.system": "Systemapps",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemforbedringer",
  "smart.category.entertainment": "Underholdning",
  "smart.category.game": "Spil",
  "smart.category.finance": "Finans",
  "smart.category.education": "Uddannelse",
  "smart.category.ai-tools": "AI-værktøjer",
  "smart.category.security": "Sikkerhed",
  "smart.category.other": "Andet",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organiserede dine apps",
  "smartstart.auto.messageFormat": "Organiserede %appCount% apps med %tagCount% tagtildelinger.",
  "smartstart.suggestion.title": "Smart Start-forslag er klar",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/de.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Dieser Vorgang hat %tagCount% Tags zu %appCount% Apps hinzugefugt (%appNames%).",
  "edit.feedbackRemoveFormat": "Dieser Vorgang hat %tagCount% Tags von %appCount% Apps entfernt (%appNames%). Entfernte Tags: %tagNames%.",
  "edit.removeHint": "Hier konnen nur bereits vorhandene Tags abgewahlt werden. Tags mit rotem Loschsymbol werden nach der Bestatigung entfernt.",
  "smart.category.browser": "Browser",
  "smart.category.communication": "Kommunikation",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivität",
  "smart.category.file-management": "Dateiverwaltung",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmierung",
  "smart.category.design": "Design",
  "smart.category.writing": "Schreiben",
  "smart.category.media": "Medien",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Bilder & Fotos",
  "smart.category.utilities": "Dienstprogramme",
  "smart.category.system": "System-Apps",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemerweiterungen",
  "smart.category.entertainment": "Unterhaltung",
  "smart.category.game": "Spiele",
  "smart.category.finance": "Finanzen",
  "smart.category.education": "Lernen",
  "smart.category.ai-tools": "KI-Tools",
  "smart.category.security": "Sicherheit",
  "smart.category.other": "Sonstiges",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start hat deine Apps sortiert",
  "smartstart.auto.messageFormat": "%appCount% Apps mit %tagCount% Tag-Zuweisungen sortiert.",
  "smartstart.suggestion.title": "Smart-Start-Vorschläge sind bereit",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/en.json
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/es.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Esta operacion agrego %tagCount% etiquetas a %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Esta operacion elimino %tagCount% etiquetas de %appCount% apps (%appNames%). Etiquetas eliminadas: %tagNames%.",
  "edit.removeHint": "Aqui solo se pueden desmarcar etiquetas ya existentes. Las etiquetas con icono rojo de eliminacion se quitaran tras confirmar.",
  "smart.category.browser": "Navegadores",
  "smart.category.communication": "Comunicación",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Productividad",
  "smart.category.file-management": "Gestión de archivos",
  "smart.category.transfer": "Subidas y descargas",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programación",
  "smart.category.design": "Diseño",
  "smart.category.writing": "Escritura",
  "smart.category.media": "Medios",
  "smart.category.video": "Vídeo",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Imágenes y fotos",
  "smart.category.utilities": "Utilidades",
  "smart.category.system": "Apps del sistema",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Mejoras del sistema",
  "smart.category.entertainment": "Entretenimiento",
  "smart.category.game": "Juegos",
  "smart.category.finance": "Finanzas",
  "smart.category.education": "Educación",
  "smart.category.ai-tools": "Herramientas de IA",
  "smart.category.security": "Seguridad",
  "smart.category.other": "Otros",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organizó tus apps",
  "smartstart.auto.messageFormat": "Se organizaron %appCount% apps con %tagCount% asignaciones de etiquetas.",
  "smartstart.suggestion.title": "Sugerencias de Smart Start listas",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/fr.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Cette operation a ajoute %tagCount% etiquettes sur %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Cette operation a supprime %tagCount% etiquettes sur %appCount% apps (%appNames%). Etiquettes retirees : %tagNames%.",
  "edit.removeHint": "Ici, seuls les tags existants peuvent etre decoches. Les tags affichant une icone rouge de suppression seront retires apres confirmation.",
  "smart.category.browser": "Navigateurs",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Productivité",
  "smart.category.file-management": "Gestion des fichiers",
  "smart.category.transfer": "Téléversements et téléchargements",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmation",
  "smart.category.design": "Design",
  "smart.category.writing": "Écriture",
  "smart.category.media": "Média",
  "smart.category.video": "Vidéo",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Images et photos",
  "smart.category.utilities": "Utilitaires",
  "smart.category.system": "Apps système",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Améliorations système",
  "smart.category.entertainment": "Divertissement",
  "smart.category.game": "Jeux",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Éducation",
  "smart.category.ai-tools": "Outils IA",
  "smart.category.security": "Sécurité",
  "smart.category.other": "Autre",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start a organisé vos apps",
  "smartstart.auto.messageFormat": "%appCount% apps organisées avec %tagCount% attributions de tags.",
  "smartstart.suggestion.title": "Suggestions Smart Start prêtes",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/id.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Operasi ini menambahkan %tagCount% tag ke %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Operasi ini menghapus %tagCount% tag dari %appCount% app (%appNames%). Tag yang dihapus: %tagNames%.",
  "edit.removeHint": "Di sini hanya tag yang sudah ada yang bisa dihapus centangnya. Tag dengan ikon hapus merah akan dihapus setelah konfirmasi.",
  "smart.category.browser": "Browser",
  "smart.category.communication": "Komunikasi",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitas",
  "smart.category.file-management": "Manajemen file",
  "smart.category.transfer": "Unggah dan unduh",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Pemrograman",
  "smart.category.design": "Desain",
  "smart.category.writing": "Menulis",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Gambar dan foto",
  "smart.category.utilities": "Utilitas",
  "smart.category.system": "App sistem",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Peningkatan sistem",
  "smart.category.entertainment": "Hiburan",
  "smart.category.game": "Game",
  "smart.category.finance": "Keuangan",
  "smart.category.education": "Pendidikan",
  "smart.category.ai-tools": "Alat AI",
  "smart.category.security": "Keamanan",
  "smart.category.other": "Lainnya",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start mengatur app Anda",
  "smartstart.auto.messageFormat": "%appCount% app diatur dengan %tagCount% penetapan tag.",
  "smartstart.suggestion.title": "Saran Smart Start siap",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/it.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Questa operazione ha aggiunto %tagCount% tag a %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Questa operazione ha rimosso %tagCount% tag da %appCount% app (%appNames%). Tag rimossi: %tagNames%.",
  "edit.removeHint": "Qui puoi deselezionare solo i tag gia esistenti. I tag che mostrano un'icona rossa di eliminazione verranno rimossi dopo la conferma.",
  "smart.category.browser": "Browser",
  "smart.category.communication": "Comunicazione",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produttività",
  "smart.category.file-management": "Gestione file",
  "smart.category.transfer": "Caricamenti e download",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmazione",
  "smart.category.design": "Design",
  "smart.category.writing": "Scrittura",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Immagini e foto",
  "smart.category.utilities": "Utility",
  "smart.category.system": "App di sistema",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Potenziamenti del sistema",
  "smart.category.entertainment": "Intrattenimento",
  "smart.category.game": "Giochi",
  "smart.category.finance": "Finanza",
  "smart.category.education": "Formazione",
  "smart.category.ai-tools": "Strumenti IA",
  "smart.category.security": "Sicurezza",
  "smart.category.other": "Altro",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start ha organizzato le tue app",
  "smartstart.auto.messageFormat": "Organizzate %appCount% app con %tagCount% assegnazioni di tag.",
  "smartstart.suggestion.title": "Suggerimenti Smart Start pronti",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ja.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)に%tagCount%個のタグを追加しました。",
  "edit.feedbackRemoveFormat": "今回の操作で、%appCount%個のアプリ(%appNames%)から%tagCount%個のタグを削除しました。削除したタグ: %tagNames%。",
  "edit.removeHint": "ここでは既存のタグだけチェックを外せます。赤い削除アイコンが表示されたタグは、確認後に削除されます。",
  "smart.category.browser": "ブラウザ",
  "smart.category.communication": "コミュニケーション",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "生産性",
  "smart.category.file-management": "ファイル管理",
  "smart.category.transfer": "アップロードとダウンロード",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "プログラミング",
  "smart.category.design": "デザイン",
  "smart.category.writing": "執筆",
  "smart.category.media": "メディア",
  "smart.category.video": "動画",
  "smart.category.audio": "音声",
  "smart.category.picture-photo": "画像と写真",
  "smart.category.utilities": "ユーティリティ",
  "smart.category.system": "システムアプリ",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "システム強化",
  "smart.category.entertainment": "エンタメ",
  "smart.category.game": "ゲーム",
  "smart.category.finance": "ファイナンス",
  "smart.category.education": "学習",
  "smart.category.ai-tools": "AIツール",
  "smart.category.security": "セキュリティ",
  "smart.category.other": "その他",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start がアプリを整理しました",
  "smartstart.auto.messageFormat": "%appCount% 個のアプリに %tagCount% 件のタグを割り当てました。",
  "smartstart.suggestion.title": "Smart Start の提案があります",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ko.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에 태그 %tagCount%개가 새로 추가되었습니다.",
  "edit.feedbackRemoveFormat": "이번 작업으로 %appCount%개 앱(%appNames%)에서 태그 %tagCount%개가 제거되었습니다. 제거된 태그: %tagNames%.",
  "edit.removeHint": "여기서는 이미 있는 태그만 체크 해제할 수 있습니다. 빨간 삭제 아이콘이 보이는 태그는 확인 후 제거됩니다.",
  "smart.category.browser": "브라우저",
  "smart.category.communication": "커뮤니케이션",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "생산성",
  "smart.category.file-management": "파일 관리",
  "smart.category.transfer": "업로드 및 다운로드",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "프로그래밍",
  "smart.category.design": "디자인",
  "smart.category.writing": "글쓰기",
  "smart.category.media": "미디어",
  "smart.category.video": "비디오",
  "smart.category.audio": "오디오",
  "smart.category.picture-photo": "이미지 및 사진",
  "smart.category.utilities": "유틸리티",
  "smart.category.system": "시스템 앱",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "시스템 강화",
  "smart.category.entertainment": "엔터테인먼트",
  "smart.category.game": "게임",
  "smart.category.finance": "금융",
  "smart.category.education": "교육",
  "smart.category.ai-tools": "AI 도구",
  "smart.category.security": "보안",
  "smart.category.other": "기타",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start가 앱을 정리했습니다",
  "smartstart.auto.messageFormat": "%appCount%개 앱에 %tagCount%개 태그를 지정했습니다.",
  "smartstart.suggestion.title": "Smart Start 제안 준비 완료",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ms.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Operasi ini menambah %tagCount% tag pada %appCount% app (%appNames%).",
  "edit.feedbackRemoveFormat": "Operasi ini membuang %tagCount% tag daripada %appCount% app (%appNames%). Tag yang dibuang: %tagNames%.",
  "edit.removeHint": "Di sini hanya tag yang sedia ada boleh dinyah tanda. Tag yang memaparkan ikon padam merah akan dibuang selepas pengesahan.",
  "smart.category.browser": "Pelayar",
  "smart.category.communication": "Komunikasi",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktiviti",
  "smart.category.file-management": "Pengurusan fail",
  "smart.category.transfer": "Muat naik dan muat turun",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Pengaturcaraan",
  "smart.category.design": "Reka bentuk",
  "smart.category.writing": "Penulisan",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Imej dan foto",
  "smart.category.utilities": "Utiliti",
  "smart.category.system": "App sistem",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Peningkatan sistem",
  "smart.category.entertainment": "Hiburan",
  "smart.category.game": "Permainan",
  "smart.category.finance": "Kewangan",
  "smart.category.education": "Pendidikan",
  "smart.category.ai-tools": "Alat AI",
  "smart.category.security": "Keselamatan",
  "smart.category.other": "Lain-lain",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start menyusun app anda",
  "smartstart.auto.messageFormat": "Menyusun %appCount% app dengan %tagCount% penugasan tag.",
  "smartstart.suggestion.title": "Cadangan Smart Start sedia",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/nb.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.",
  "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse.",
  "smart.category.browser": "Nettlesere",
  "smart.category.communication": "Kommunikasjon",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitet",
  "smart.category.file-management": "Filbehandling",
  "smart.category.transfer": "Opplastinger og nedlastinger",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmering",
  "smart.category.design": "Design",
  "smart.category.writing": "Skriving",
  "smart.category.media": "Medier",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Lyd",
  "smart.category.picture-photo": "Bilder og foto",
  "smart.category.utilities": "Verktøy",
  "smart.category.system": "Systemapper",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemforbedringer",
  "smart.category.entertainment": "Underholdning",
  "smart.category.game": "Spill",
  "smart.category.finance": "Finans",
  "smart.category.education": "Utdanning",
  "smart.category.ai-tools": "AI-verktøy",
  "smart.category.security": "Sikkerhet",
  "smart.category.other": "Annet",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organiserade dina appar",
  "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.",
  "smartstart.suggestion.title": "Smart Start-förslag är klara",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/nl.json
@@ -90,27 +90,27 @@
  "edit.feedbackRemoveFormat": "Deze actie heeft %tagCount% tags verwijderd van %appCount% apps (%appNames%). Verwijderde tags: %tagNames%.",
  "edit.removeHint": "Hier kun je alleen bestaande tags uitvinken. Tags met een rood verwijderpictogram worden na bevestiging verwijderd.",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communicatie",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Productiviteit",
  "smart.category.file-management": "Bestandsbeheer",
  "smart.category.transfer": "Uploads en downloads",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmeren",
  "smart.category.design": "Ontwerp",
  "smart.category.writing": "Schrijven",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Afbeeldingen en foto’s",
  "smart.category.utilities": "Hulpprogramma’s",
  "smart.category.system": "Systeemapps",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systeemuitbreidingen",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Financiën",
  "smart.category.education": "Onderwijs",
  "smart.category.ai-tools": "AI-tools",
  "smart.category.security": "Beveiliging",
  "smart.category.other": "Overig",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start heeft je apps georganiseerd",
  "smartstart.auto.messageFormat": "%appCount% apps georganiseerd met %tagCount% tagtoewijzingen.",
  "smartstart.suggestion.title": "Smart Start-suggesties zijn klaar",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/nn.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Denne handlinga la til %tagCount% taggar i %appCount% appar (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlinga fjerna %tagCount% taggar fra %appCount% appar (%appNames%). Fjerna taggar: %tagNames%.",
  "edit.removeHint": "Her kan berre eksisterande taggar fjernast frå avkryssinga. Taggar med raudt sletteikon blir fjerna etter stadfesting.",
  "smart.category.browser": "Nettlesere",
  "smart.category.communication": "Kommunikasjon",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitet",
  "smart.category.file-management": "Filbehandling",
  "smart.category.transfer": "Opplastinger og nedlastinger",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmering",
  "smart.category.design": "Design",
  "smart.category.writing": "Skriving",
  "smart.category.media": "Medier",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Lyd",
  "smart.category.picture-photo": "Bilder og foto",
  "smart.category.utilities": "Verktøy",
  "smart.category.system": "Systemapper",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemforbedringer",
  "smart.category.entertainment": "Underholdning",
  "smart.category.game": "Spill",
  "smart.category.finance": "Finans",
  "smart.category.education": "Utdanning",
  "smart.category.ai-tools": "AI-verktøy",
  "smart.category.security": "Sikkerhet",
  "smart.category.other": "Annet",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organiserade dina appar",
  "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.",
  "smartstart.suggestion.title": "Smart Start-förslag är klara",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/no.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Denne handlingen la til %tagCount% tagger i %appCount% apper (%appNames%).",
  "edit.feedbackRemoveFormat": "Denne handlingen fjernet %tagCount% tagger fra %appCount% apper (%appNames%). Fjernede tagger: %tagNames%.",
  "edit.removeHint": "Her kan bare eksisterende tagger fjernes fra avkrysningen. Tagger med rodt sletteikon blir fjernet etter bekreftelse.",
  "smart.category.browser": "Nettlesere",
  "smart.category.communication": "Kommunikasjon",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitet",
  "smart.category.file-management": "Filbehandling",
  "smart.category.transfer": "Opplastinger og nedlastinger",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmering",
  "smart.category.design": "Design",
  "smart.category.writing": "Skriving",
  "smart.category.media": "Medier",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Lyd",
  "smart.category.picture-photo": "Bilder og foto",
  "smart.category.utilities": "Verktøy",
  "smart.category.system": "Systemapper",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemforbedringer",
  "smart.category.entertainment": "Underholdning",
  "smart.category.game": "Spill",
  "smart.category.finance": "Finans",
  "smart.category.education": "Utdanning",
  "smart.category.ai-tools": "AI-verktøy",
  "smart.category.security": "Sikkerhet",
  "smart.category.other": "Annet",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organiserade dina appar",
  "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.",
  "smartstart.suggestion.title": "Smart Start-förslag är klara",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/pl.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Ta operacja dodala %tagCount% tagow do %appCount% aplikacji (%appNames%).",
  "edit.feedbackRemoveFormat": "Ta operacja usunela %tagCount% tagow z %appCount% aplikacji (%appNames%). Usuniete tagi: %tagNames%.",
  "edit.removeHint": "Tutaj mozna odznaczyc tylko istniejace tagi. Tagi z czerwona ikona usuniecia zostana usuniete po potwierdzeniu.",
  "smart.category.browser": "Przeglądarki",
  "smart.category.communication": "Komunikacja",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktywność",
  "smart.category.file-management": "Zarządzanie plikami",
  "smart.category.transfer": "Wysyłanie i pobieranie",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programowanie",
  "smart.category.design": "Projektowanie",
  "smart.category.writing": "Pisanie",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Wideo",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Obrazy i zdjęcia",
  "smart.category.utilities": "Narzędzia",
  "smart.category.system": "Aplikacje systemowe",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Ulepszenia systemu",
  "smart.category.entertainment": "Rozrywka",
  "smart.category.game": "Gry",
  "smart.category.finance": "Finanse",
  "smart.category.education": "Edukacja",
  "smart.category.ai-tools": "Narzędzia AI",
  "smart.category.security": "Bezpieczeństwo",
  "smart.category.other": "Inne",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start uporządkował aplikacje",
  "smartstart.auto.messageFormat": "Uporządkowano %appCount% aplikacji z %tagCount% przypisaniami tagów.",
  "smartstart.suggestion.title": "Sugestie Smart Start są gotowe",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/pt-BR.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Esta operacao adicionou %tagCount% tags em %appCount% apps (%appNames%).",
  "edit.feedbackRemoveFormat": "Esta operacao removeu %tagCount% tags de %appCount% apps (%appNames%). Tags removidas: %tagNames%.",
  "edit.removeHint": "Aqui so e possivel desmarcar tags ja existentes. As tags com icone vermelha de remocao serao removidas apos a confirmacao.",
  "smart.category.browser": "Navegadores",
  "smart.category.communication": "Comunicação",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produtividade",
  "smart.category.file-management": "Gerenciamento de arquivos",
  "smart.category.transfer": "Uploads e downloads",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programação",
  "smart.category.design": "Design",
  "smart.category.writing": "Escrita",
  "smart.category.media": "Mídia",
  "smart.category.video": "Vídeo",
  "smart.category.audio": "Áudio",
  "smart.category.picture-photo": "Imagens e fotos",
  "smart.category.utilities": "Utilitários",
  "smart.category.system": "Apps do sistema",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Melhorias do sistema",
  "smart.category.entertainment": "Entretenimento",
  "smart.category.game": "Jogos",
  "smart.category.finance": "Finanças",
  "smart.category.education": "Educação",
  "smart.category.ai-tools": "Ferramentas de IA",
  "smart.category.security": "Segurança",
  "smart.category.other": "Outros",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "O Smart Start organizou seus apps",
  "smartstart.auto.messageFormat": "%appCount% apps organizados com %tagCount% atribuições de tags.",
  "smartstart.suggestion.title": "Sugestões do Smart Start prontas",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ro.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Aceasta actiune a adaugat %tagCount% etichete in %appCount% aplicatii (%appNames%).",
  "edit.feedbackRemoveFormat": "Aceasta actiune a eliminat %tagCount% etichete din %appCount% aplicatii (%appNames%). Etichete eliminate: %tagNames%.",
  "edit.removeHint": "Aici pot fi debifate doar etichetele deja existente. Etichetele care afiseaza o pictograma rosie de stergere vor fi eliminate dupa confirmare.",
  "smart.category.browser": "Browsere",
  "smart.category.communication": "Comunicare",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Productivitate",
  "smart.category.file-management": "Gestionare fișiere",
  "smart.category.transfer": "Încărcări și descărcări",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programare",
  "smart.category.design": "Design",
  "smart.category.writing": "Scriere",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Imagini și fotografii",
  "smart.category.utilities": "Utilitare",
  "smart.category.system": "Aplicații de sistem",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Îmbunătățiri de sistem",
  "smart.category.entertainment": "Divertisment",
  "smart.category.game": "Jocuri",
  "smart.category.finance": "Finanțe",
  "smart.category.education": "Educație",
  "smart.category.ai-tools": "Instrumente AI",
  "smart.category.security": "Securitate",
  "smart.category.other": "Altele",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start ți-a organizat aplicațiile",
  "smartstart.auto.messageFormat": "Au fost organizate %appCount% aplicații cu %tagCount% atribuiri de etichete.",
  "smartstart.suggestion.title": "Sugestiile Smart Start sunt gata",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/ru.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Эта операция добавила %tagCount% тегов для %appCount% приложений (%appNames%).",
  "edit.feedbackRemoveFormat": "Эта операция удалила %tagCount% тегов у %appCount% приложений (%appNames%). Удаленные теги: %tagNames%.",
  "edit.removeHint": "Здесь можно снять отметку только с уже существующих тегов. Теги с красным значком удаления будут удалены после подтверждения.",
  "smart.category.browser": "Браузеры",
  "smart.category.communication": "Общение",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Продуктивность",
  "smart.category.file-management": "Управление файлами",
  "smart.category.transfer": "Загрузки и выгрузки",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Программирование",
  "smart.category.design": "Дизайн",
  "smart.category.writing": "Письмо",
  "smart.category.media": "Медиа",
  "smart.category.video": "Видео",
  "smart.category.audio": "Аудио",
  "smart.category.picture-photo": "Изображения и фото",
  "smart.category.utilities": "Утилиты",
  "smart.category.system": "Системные приложения",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Улучшения системы",
  "smart.category.entertainment": "Развлечения",
  "smart.category.game": "Игры",
  "smart.category.finance": "Финансы",
  "smart.category.education": "Обучение",
  "smart.category.ai-tools": "Инструменты ИИ",
  "smart.category.security": "Безопасность",
  "smart.category.other": "Другое",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start упорядочил приложения",
  "smartstart.auto.messageFormat": "Упорядочено приложений: %appCount%, назначений тегов: %tagCount%.",
  "smartstart.suggestion.title": "Предложения Smart Start готовы",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/sr-Cyrl.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Ова радња је додала %tagCount% ознака за %appCount% апликација (%appNames%).",
  "edit.feedbackRemoveFormat": "Ова радња је уклонила %tagCount% ознака са %appCount% апликација (%appNames%). Уклоњене ознаке: %tagNames%.",
  "edit.removeHint": "Овде можете одчекирати само постојеће ознаке. Ознаке са црвеном иконицом брисања биће уклоњене након потврде.",
  "smart.category.browser": "Прегледачи",
  "smart.category.communication": "Комуникација",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Продуктивност",
  "smart.category.file-management": "Управљање датотекама",
  "smart.category.transfer": "Отпремања и преузимања",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Програмирање",
  "smart.category.design": "Дизајн",
  "smart.category.writing": "Писање",
  "smart.category.media": "Медији",
  "smart.category.video": "Видео",
  "smart.category.audio": "Аудио",
  "smart.category.picture-photo": "Слике и фотографије",
  "smart.category.utilities": "Алатке",
  "smart.category.system": "Системске апликације",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Побољшања система",
  "smart.category.entertainment": "Забава",
  "smart.category.game": "Игре",
  "smart.category.finance": "Финансије",
  "smart.category.education": "Образовање",
  "smart.category.ai-tools": "AI алатке",
  "smart.category.security": "Безбедност",
  "smart.category.other": "Остало",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start је средио апликације",
  "smartstart.auto.messageFormat": "Сређено је %appCount% апликација са %tagCount% додела ознака.",
  "smartstart.suggestion.title": "Smart Start предлози су спремни",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/sv.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Den har atgarden lade till %tagCount% taggar i %appCount% appar (%appNames%).",
  "edit.feedbackRemoveFormat": "Den har atgarden tog bort %tagCount% taggar fran %appCount% appar (%appNames%). Borttagna taggar: %tagNames%.",
  "edit.removeHint": "Har kan bara befintliga taggar avmarkeras. Taggar med en rod raderingsikon tas bort efter bekraftelse.",
  "smart.category.browser": "Webbläsare",
  "smart.category.communication": "Kommunikation",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Produktivitet",
  "smart.category.file-management": "Filhantering",
  "smart.category.transfer": "Uppladdningar och nedladdningar",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programmering",
  "smart.category.design": "Design",
  "smart.category.writing": "Skrivande",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Ljud",
  "smart.category.picture-photo": "Bilder och foton",
  "smart.category.utilities": "Verktyg",
  "smart.category.system": "Systemappar",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Systemförbättringar",
  "smart.category.entertainment": "Underhållning",
  "smart.category.game": "Spel",
  "smart.category.finance": "Ekonomi",
  "smart.category.education": "Utbildning",
  "smart.category.ai-tools": "AI-verktyg",
  "smart.category.security": "Säkerhet",
  "smart.category.other": "Annat",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start organiserade dina appar",
  "smartstart.auto.messageFormat": "Organiserade %appCount% appar med %tagCount% taggtilldelningar.",
  "smartstart.suggestion.title": "Smart Start-förslag är klara",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/th.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "การทำงานครั้งนี้ได้เพิ่มแท็ก %tagCount% รายการให้กับแอป %appCount% แอป (%appNames%).",
  "edit.feedbackRemoveFormat": "การทำงานครั้งนี้ได้ลบแท็ก %tagCount% รายการออกจากแอป %appCount% แอป (%appNames%). แท็กที่ลบ: %tagNames%.",
  "edit.removeHint": "ที่นี่สามารถยกเลิกเครื่องหมายได้เฉพาะแท็กที่มีอยู่แล้วเท่านั้น แท็กที่แสดงไอคอนลบสีแดงจะถูกลบหลังจากยืนยัน",
  "smart.category.browser": "เบราว์เซอร์",
  "smart.category.communication": "การสื่อสาร",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "ประสิทธิภาพการทำงาน",
  "smart.category.file-management": "การจัดการไฟล์",
  "smart.category.transfer": "อัปโหลดและดาวน์โหลด",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "การเขียนโปรแกรม",
  "smart.category.design": "ออกแบบ",
  "smart.category.writing": "การเขียน",
  "smart.category.media": "สื่อ",
  "smart.category.video": "วิดีโอ",
  "smart.category.audio": "เสียง",
  "smart.category.picture-photo": "รูปภาพและภาพถ่าย",
  "smart.category.utilities": "ยูทิลิตี้",
  "smart.category.system": "แอประบบ",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "การเสริมความสามารถระบบ",
  "smart.category.entertainment": "ความบันเทิง",
  "smart.category.game": "เกม",
  "smart.category.finance": "การเงิน",
  "smart.category.education": "การศึกษา",
  "smart.category.ai-tools": "เครื่องมือ AI",
  "smart.category.security": "ความปลอดภัย",
  "smart.category.other": "อื่น ๆ",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start จัดระเบียบแอปของคุณแล้ว",
  "smartstart.auto.messageFormat": "จัดระเบียบ %appCount% แอปด้วยการกำหนดแท็ก %tagCount% รายการ",
  "smartstart.suggestion.title": "คำแนะนำ Smart Start พร้อมแล้ว",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/tr.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Bu islem, %appCount% uygulamada (%appNames%) %tagCount% etiket ekledi.",
  "edit.feedbackRemoveFormat": "Bu islem, %appCount% uygulamadan (%appNames%) %tagCount% etiketi kaldirdi. Kaldirilan etiketler: %tagNames%.",
  "edit.removeHint": "Burada sadece mevcut etiketlerin isareti kaldirilabilir. Kirmizi silme simgesi gorunen etiketler onaydan sonra kaldirilir.",
  "smart.category.browser": "Tarayıcılar",
  "smart.category.communication": "İletişim",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Üretkenlik",
  "smart.category.file-management": "Dosya yönetimi",
  "smart.category.transfer": "Yüklemeler ve indirmeler",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Programlama",
  "smart.category.design": "Tasarım",
  "smart.category.writing": "Yazma",
  "smart.category.media": "Medya",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Ses",
  "smart.category.picture-photo": "Görseller ve fotoğraflar",
  "smart.category.utilities": "Araçlar",
  "smart.category.system": "Sistem uygulamaları",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Sistem geliştirmeleri",
  "smart.category.entertainment": "Eğlence",
  "smart.category.game": "Oyunlar",
  "smart.category.finance": "Finans",
  "smart.category.education": "Eğitim",
  "smart.category.ai-tools": "AI araçları",
  "smart.category.security": "Güvenlik",
  "smart.category.other": "Diğer",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start uygulamalarını düzenledi",
  "smartstart.auto.messageFormat": "%appCount% uygulama %tagCount% etiket atamasıyla düzenlendi.",
  "smartstart.suggestion.title": "Smart Start önerileri hazır",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/uk.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Ця дія додала %tagCount% тегів для %appCount% застосунків (%appNames%).",
  "edit.feedbackRemoveFormat": "Ця дія вилучила %tagCount% тегів у %appCount% застосунків (%appNames%). Вилучені теги: %tagNames%.",
  "edit.removeHint": "Тут можна зняти позначку лише з наявних тегів. Теги з червоною іконкою видалення буде вилучено після підтвердження.",
  "smart.category.browser": "Браузери",
  "smart.category.communication": "Спілкування",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Продуктивність",
  "smart.category.file-management": "Керування файлами",
  "smart.category.transfer": "Передавання й завантаження",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Програмування",
  "smart.category.design": "Дизайн",
  "smart.category.writing": "Письмо",
  "smart.category.media": "Медіа",
  "smart.category.video": "Відео",
  "smart.category.audio": "Аудіо",
  "smart.category.picture-photo": "Зображення й фото",
  "smart.category.utilities": "Утиліти",
  "smart.category.system": "Системні програми",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Покращення системи",
  "smart.category.entertainment": "Розваги",
  "smart.category.game": "Ігри",
  "smart.category.finance": "Фінанси",
  "smart.category.education": "Навчання",
  "smart.category.ai-tools": "Інструменти ШІ",
  "smart.category.security": "Безпека",
  "smart.category.other": "Інше",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start упорядкував програми",
  "smartstart.auto.messageFormat": "Упорядковано %appCount% програм із %tagCount% призначеннями тегів.",
  "smartstart.suggestion.title": "Пропозиції Smart Start готові",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/vi.json
@@ -89,28 +89,28 @@
  "edit.feedbackAddFormat": "Lan nay da them %tagCount% the cho %appCount% ung dung (%appNames%).",
  "edit.feedbackRemoveFormat": "Lan nay da go %tagCount% the khoi %appCount% ung dung (%appNames%). The da go: %tagNames%.",
  "edit.removeHint": "Tai day chi co the bo chon cac the da ton tai. Cac the hien bieu tuong xoa mau do se bi go sau khi xac nhan.",
  "smart.category.browser": "Trình duyệt",
  "smart.category.communication": "Liên lạc",
  "smart.category.browser": "Browsers",
  "smart.category.communication": "Communication",
  "smart.category.productivity": "Năng suất",
  "smart.category.file-management": "Quản lý tệp",
  "smart.category.transfer": "Tải lên và tải xuống",
  "smart.category.file-management": "File Management",
  "smart.category.transfer": "Uploads & Downloads",
  "smart.category.development": "Lập trình",
  "smart.category.design": "Thiết kế",
  "smart.category.writing": "Viết",
  "smart.category.media": "Phương tiện",
  "smart.category.writing": "Writing",
  "smart.category.media": "Media",
  "smart.category.video": "Video",
  "smart.category.audio": "Âm thanh",
  "smart.category.picture-photo": "Ảnh và hình ảnh",
  "smart.category.utilities": "Tiện ích",
  "smart.category.system": "Ứng dụng hệ thống",
  "smart.category.audio": "Audio",
  "smart.category.picture-photo": "Pictures & Photos",
  "smart.category.utilities": "Utilities",
  "smart.category.system": "System Apps",
  "smart.category.system-enhancement": "Tăng cường hệ thống",
  "smart.category.entertainment": "Giải trí",
  "smart.category.game": "Trò chơi",
  "smart.category.finance": "Tài chính",
  "smart.category.education": "Giáo dục",
  "smart.category.ai-tools": "Công cụ AI",
  "smart.category.security": "Bảo mật",
  "smart.category.other": "Khác",
  "smart.category.entertainment": "Entertainment",
  "smart.category.game": "Games",
  "smart.category.finance": "Finance",
  "smart.category.education": "Education",
  "smart.category.ai-tools": "AI Tools",
  "smart.category.security": "Security",
  "smart.category.other": "Other",
  "smartstart.auto.title": "Smart Start đã sắp xếp ứng dụng",
  "smartstart.auto.messageFormat": "Đã sắp xếp %appCount% ứng dụng với %tagCount% lượt gán thẻ.",
  "smartstart.suggestion.title": "Gợi ý Smart Start đã sẵn sàng",
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "App list only",
  "quickSearch.globalHotkey": "Quick Search (global)",
  "quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
  "quickSearch.spaceDisplay": "Space"
  "quickSearch.spaceDisplay": "Space",
  "smart.category.GTD": "Tasks & GTD",
  "smart.category.Notes": "Notes",
  "smart.category.Meeting": "Meetings",
  "smart.category.office": "Office",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API Tools",
  "smart.category.database-tools": "Databases",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDEs",
  "smart.category.runtime-sdk": "Runtimes & SDKs",
  "smart.category.terminal-tools": "Terminal Tools",
  "smart.category.Font": "Fonts",
  "smart.category.ui-prototyping": "UI Prototyping",
  "smart.category.3d-cad": "3D & CAD",
  "smart.category.diagramming": "Diagramming",
  "smart.category.system-maintenance": "System Maintenance",
  "smart.category.window-management": "Window Management",
  "smart.category.device-management": "Device Management",
  "smart.category.input-tools": "Input Tools",
  "smart.category.Automation": "Automation",
  "smart.category.network-tools": "Network Tools"
}
Apptag/Localization/zh-Hans.json
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "仅主界面有效",
  "quickSearch.globalHotkey": "快捷搜索(全局)",
  "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主界面时,直接打开快捷搜索。全局有效。",
  "quickSearch.spaceDisplay": "Space(空格键)"
  "quickSearch.spaceDisplay": "Space(空格键)",
  "smart.category.GTD": "任务与 GTD",
  "smart.category.Notes": "笔记",
  "smart.category.Meeting": "会议",
  "smart.category.office": "办公",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API 工具",
  "smart.category.database-tools": "数据库",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDE",
  "smart.category.runtime-sdk": "运行时与 SDK",
  "smart.category.terminal-tools": "终端工具",
  "smart.category.Font": "字体",
  "smart.category.ui-prototyping": "UI 原型",
  "smart.category.3d-cad": "3D 与 CAD",
  "smart.category.diagramming": "图表绘制",
  "smart.category.system-maintenance": "系统维护",
  "smart.category.window-management": "窗口管理",
  "smart.category.device-management": "设备管理",
  "smart.category.input-tools": "输入工具",
  "smart.category.Automation": "自动化",
  "smart.category.network-tools": "网络工具"
}
Apptag/Localization/zh-Hant.json
@@ -166,5 +166,26 @@
  "quickSearch.internalHotkeyStatus": "僅主介面有效",
  "quickSearch.globalHotkey": "快捷搜尋(全域)",
  "quickSearch.globalHotkeyDesc": "不在 TagLauncher 主介面時,直接打開快捷搜尋。全域有效。",
  "quickSearch.spaceDisplay": "Space(空格鍵)"
  "quickSearch.spaceDisplay": "Space(空格鍵)",
  "smart.category.GTD": "任務與 GTD",
  "smart.category.Notes": "筆記",
  "smart.category.Meeting": "會議",
  "smart.category.office": "辦公",
  "smart.category.PDF": "PDF",
  "smart.category.api-tools": "API 工具",
  "smart.category.database-tools": "資料庫",
  "smart.category.devops": "DevOps",
  "smart.category.ide": "IDE",
  "smart.category.runtime-sdk": "執行環境與 SDK",
  "smart.category.terminal-tools": "終端工具",
  "smart.category.Font": "字型",
  "smart.category.ui-prototyping": "UI 原型",
  "smart.category.3d-cad": "3D 與 CAD",
  "smart.category.diagramming": "圖表繪製",
  "smart.category.system-maintenance": "系統維護",
  "smart.category.window-management": "視窗管理",
  "smart.category.device-management": "裝置管理",
  "smart.category.input-tools": "輸入工具",
  "smart.category.Automation": "自動化",
  "smart.category.network-tools": "網路工具"
}
Apptag/PreferencesView.swift
@@ -767,7 +767,7 @@
                        Text("万物之中,希望最美")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                        Text("永桔@2026-18602102518")
                        Text("永桔@shanghai3168@gmail.com")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
Apptag/QuickSearch.swift
@@ -244,6 +244,8 @@
    let normalized: String
    let acronym: String
    let pinyinCandidates: [String]
    let allowPinyinSubstring: Bool
    let allowPinyinFuzzySubsequence: Bool
}
private struct QuickSearchMatchOptions {
@@ -286,7 +288,7 @@
enum QuickSearchEngine {
    static func makeDocuments(apps: [AppInfo], store: TagDatabase.Store) -> [QuickSearchDocument] {
        apps.map { app in
            let localizedNames = localizedNames(for: app)
            let localizedNames = uniqueOrdered(app.localizedNames)
            let internalBundleNames = internalBundleNames(for: app)
            let note = store.appNotes[app.path.path] ?? app.note ?? ""
            let bundleIdentifier = app.bundleIdentifier ?? ""
@@ -377,12 +379,15 @@
    ) -> [QuickSearchIndexedField] {
        let names = uniqueOrdered([appName] + localizedNames)
        let nameFields = names.map {
            QuickSearchIndexedField(
            let allowsRomanizedFuzzy = containsNonLatinLetter($0)
            return QuickSearchIndexedField(
                kind: .name,
                text: $0,
                normalized: normalizeField($0),
                acronym: acronym(for: $0),
                pinyinCandidates: pinyinCandidates(for: $0)
                pinyinCandidates: pinyinCandidates(for: $0),
                allowPinyinSubstring: allowsRomanizedFuzzy,
                allowPinyinFuzzySubsequence: allowsRomanizedFuzzy
            )
        }
        let tagFields = tagNames.map {
@@ -391,7 +396,9 @@
                text: $0,
                normalized: normalizeField($0),
                acronym: "",
                pinyinCandidates: pinyinCandidates(for: $0)
                pinyinCandidates: pinyinCandidates(for: $0),
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        }
        let noteFields = note.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? [] : [
@@ -400,7 +407,9 @@
                text: note,
                normalized: normalizeField(note),
                acronym: "",
                pinyinCandidates: pinyinCandidates(for: note)
                pinyinCandidates: pinyinCandidates(for: note),
                allowPinyinSubstring: true,
                allowPinyinFuzzySubsequence: false
            )
        ]
        let bundleFields = bundleIdentifier.isEmpty ? [] : [
@@ -409,7 +418,9 @@
                text: bundleIdentifier,
                normalized: normalizeField(bundleIdentifier),
                acronym: "",
                pinyinCandidates: []
                pinyinCandidates: [],
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        ]
        let internalBundleNameFields = internalBundleNames.map {
@@ -418,7 +429,9 @@
                text: $0,
                normalized: normalizeField($0),
                acronym: "",
                pinyinCandidates: []
                pinyinCandidates: [],
                allowPinyinSubstring: false,
                allowPinyinFuzzySubsequence: false
            )
        }
        return nameFields + tagFields + noteFields + bundleFields + internalBundleNameFields
@@ -453,7 +466,8 @@
            if let pinyinCandidate = matchPinyinCandidate(
                token: token,
                normalized: pinyin,
                allowSubstring: field.kind == .note
                allowSubstring: field.allowPinyinSubstring,
                allowFuzzySubsequence: field.allowPinyinFuzzySubsequence
            ) {
                candidates.append(pinyinCandidate)
            }
@@ -502,7 +516,8 @@
    private static func matchPinyinCandidate(
        token: String,
        normalized: String,
        allowSubstring: Bool = false
        allowSubstring: Bool = false,
        allowFuzzySubsequence: Bool = false
    ) -> (QuickSearchMatchKind, Int)? {
        guard !normalized.isEmpty else { return nil }
        if normalized == token {
@@ -513,6 +528,9 @@
        }
        if allowSubstring, let range = normalized.range(of: token) {
            return (.substring, normalized.distance(from: normalized.startIndex, to: range.lowerBound))
        }
        if allowFuzzySubsequence, token.count >= 3, isSubsequence(token, of: normalized) {
            return (.fuzzy, 10)
        }
        return nil
    }
@@ -588,7 +606,7 @@
    }
    private static func pinyinCandidates(for value: String) -> [String] {
        guard value.range(of: #"\p{Han}"#, options: .regularExpression) != nil else { return [] }
        guard containsNonLatinLetter(value) else { return [] }
        let mutable = NSMutableString(string: value)
        CFStringTransform(mutable, nil, kCFStringTransformToLatin, false)
        CFStringTransform(mutable, nil, kCFStringTransformStripCombiningMarks, false)
@@ -606,6 +624,31 @@
            .map(String.init)
            .joined()
        return uniqueOrdered([spaced, compact, initials].filter { !$0.isEmpty })
    }
    private static func containsNonLatinLetter(_ value: String) -> Bool {
        value.unicodeScalars.contains { scalar in
            CharacterSet.letters.contains(scalar) && !isLatinScriptLetter(scalar)
        }
    }
    private static func isLatinScriptLetter(_ scalar: UnicodeScalar) -> Bool {
        switch scalar.value {
        case 0x0041...0x005A, // Basic Latin uppercase
             0x0061...0x007A, // Basic Latin lowercase
             0x00AA,
             0x00BA,
             0x00C0...0x024F, // Latin-1 Supplement, Extended-A/B
             0x1E00...0x1EFF, // Latin Extended Additional
             0x2C60...0x2C7F, // Latin Extended-C
             0xA720...0xA7FF, // Latin Extended-D
             0xAB30...0xAB6F, // Latin Extended-E
             0xFF21...0xFF3A, // Fullwidth Latin uppercase
             0xFF41...0xFF5A: // Fullwidth Latin lowercase
            return true
        default:
            return false
        }
    }
    private static func isSubsequence(_ token: String, of value: String) -> Bool {
@@ -692,6 +735,7 @@
    let results: [QuickSearchResult]
    let selectedID: URL?
    let focusToken: Int
    let selectionScrollToken: Int
    let isLoading: Bool
    let maxVisibleRows: Int
    let errorMessage: String?
@@ -699,8 +743,16 @@
    let onHover: (QuickSearchResult) -> Void
    let onLaunch: (QuickSearchResult) -> Void
    @Environment(\.colorScheme) private var colorScheme
    private let panelWidth: CGFloat = 760
    private let rowHeight: CGFloat = 74
    private var panelBackgroundColor: Color {
        colorScheme == .dark
            ? Color(red: 0.105, green: 0.110, blue: 0.125).opacity(0.97)
            : Color.white.opacity(0.97)
    }
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
@@ -725,17 +777,11 @@
            Divider().opacity(0.35)
            if isLoading {
                HStack(spacing: 10) {
                    ProgressView()
                        .controlSize(.small)
                    Text(tr("quickSearch.loading"))
                        .font(.system(size: 15, weight: .medium))
                        .foregroundStyle(.secondary)
                    Spacer(minLength: 0)
                }
                .padding(.horizontal, 28)
                .padding(.vertical, 24)
                .frame(minHeight: 86)
                QuickSearchMessageRow(
                    systemImage: "hourglass",
                    message: tr("quickSearch.loading"),
                    tint: .secondary
                )
            } else if let errorMessage {
                QuickSearchMessageRow(
                    systemImage: "exclamationmark.triangle.fill",
@@ -774,8 +820,8 @@
                        .padding(.vertical, 10)
                    }
                    .frame(height: CGFloat(min(results.count, maxVisibleRows)) * (rowHeight + 2) + 20)
                    .onChange(of: selectedID) { _, id in
                        guard let id else { return }
                    .onChange(of: selectionScrollToken) { _, _ in
                        guard let id = selectedID else { return }
                        withAnimation(.easeOut(duration: 0.08)) {
                            scrollProxy.scrollTo(id, anchor: .center)
                        }
@@ -786,7 +832,7 @@
        .frame(width: panelWidth)
        .background(
            RoundedRectangle(cornerRadius: 34, style: .continuous)
                .fill(.regularMaterial)
                .fill(panelBackgroundColor)
                .shadow(color: .black.opacity(0.18), radius: 36, y: 18)
        )
        .overlay(
@@ -922,9 +968,7 @@
        field.onCommand = onCommand
        field.setAccessibilityLabel(tr("quickSearch.inputAccessibility"))
        context.coordinator.field = field
        DispatchQueue.main.async {
            field.window?.makeFirstResponder(field)
        }
        requestFocus(field)
        return field
    }
@@ -936,10 +980,17 @@
        field.onCommand = onCommand
        if context.coordinator.lastFocusToken != focusToken {
            context.coordinator.lastFocusToken = focusToken
            DispatchQueue.main.async {
                field.window?.makeFirstResponder(field)
            requestFocus(field)
            }
        }
    private func requestFocus(_ field: QuickSearchNativeTextField) {
        DispatchQueue.main.async { [weak field] in
            guard let field,
                  field.window?.firstResponder !== field
            else { return }
            field.window?.makeFirstResponder(field)
        }
    }
    func makeCoordinator() -> Coordinator {
Apptag/SmartCategorization/SmartCategorizationDraft.swift
@@ -44,6 +44,13 @@
    }
}
struct SmartDefaultNoteProvenance: Codable, Hashable {
    let entryID: String
    let languageCode: String
    let notesVersion: Int
    let noteFingerprint: String
}
struct SmartAppCategorizationAssignment: Codable, Hashable, Identifiable {
    let appName: String
    let bundleIdentifier: String?
@@ -55,6 +62,7 @@
    let provenance: [String]
    let defaultNote: String?
    let defaultNoteCandidates: [String]
    let defaultNoteProvenance: SmartDefaultNoteProvenance?
    var id: String {
        if let bundleIdentifier, !bundleIdentifier.isEmpty {
@@ -80,7 +88,8 @@
        reason: String,
        provenance: [String] = [],
        defaultNote: String? = nil,
        defaultNoteCandidates: [String] = []
        defaultNoteCandidates: [String] = [],
        defaultNoteProvenance: SmartDefaultNoteProvenance? = nil
    ) {
        self.appName = appName
        self.bundleIdentifier = bundleIdentifier
@@ -92,6 +101,7 @@
        self.provenance = provenance
        self.defaultNote = defaultNote
        self.defaultNoteCandidates = defaultNoteCandidates
        self.defaultNoteProvenance = defaultNoteProvenance
    }
    private static func uniqueOrdered(_ ids: [SmartCategoryID]) -> [SmartCategoryID] {
Apptag/SmartCategorization/SmartCategory.swift
@@ -5,11 +5,27 @@
enum SmartCategoryID: String, CaseIterable, Codable, Hashable, Identifiable {
    case browser
    case communication
    case productivity
    case gtd = "GTD"
    case notes = "Notes"
    case meeting = "Meeting"
    case office
    case pdf = "PDF"
    case fileManagement = "file-management"
    case transfer
    case productivity
    case development
    case design
    case aiTools = "ai-tools"
    case apiTools = "api-tools"
    case databaseTools = "database-tools"
    case devops
    case ide
    case runtimeSDK = "runtime-sdk"
    case terminalTools = "terminal-tools"
    case font = "Font"
    case uiPrototyping = "ui-prototyping"
    case threeDCAD = "3d-cad"
    case diagramming
    case writing
    case media
    case video
@@ -18,11 +34,16 @@
    case utilities
    case system
    case systemEnhancement = "system-enhancement"
    case systemMaintenance = "system-maintenance"
    case windowManagement = "window-management"
    case deviceManagement = "device-management"
    case inputTools = "input-tools"
    case automation = "Automation"
    case networkTools = "network-tools"
    case entertainment
    case game
    case finance
    case education
    case aiTools = "ai-tools"
    case security
    case other
@@ -38,16 +59,48 @@
            return "Browsers"
        case .communication:
            return "Communication"
        case .productivity:
            return "Productivity"
        case .gtd:
            return "Tasks & GTD"
        case .notes:
            return "Notes"
        case .meeting:
            return "Meetings"
        case .office:
            return "Office"
        case .pdf:
            return "PDF"
        case .fileManagement:
            return "File Management"
        case .transfer:
            return "Uploads & Downloads"
        case .productivity:
            return "Productivity"
        case .development:
            return "Development"
        case .design:
            return "Design"
        case .aiTools:
            return "AI Tools"
        case .apiTools:
            return "API Tools"
        case .databaseTools:
            return "Databases"
        case .devops:
            return "DevOps"
        case .ide:
            return "IDEs"
        case .runtimeSDK:
            return "Runtimes & SDKs"
        case .terminalTools:
            return "Terminal Tools"
        case .font:
            return "Fonts"
        case .uiPrototyping:
            return "UI Prototyping"
        case .threeDCAD:
            return "3D & CAD"
        case .diagramming:
            return "Diagramming"
        case .writing:
            return "Writing"
        case .media:
@@ -64,6 +117,18 @@
            return "System Apps"
        case .systemEnhancement:
            return "System Enhancements"
        case .systemMaintenance:
            return "System Maintenance"
        case .windowManagement:
            return "Window Management"
        case .deviceManagement:
            return "Device Management"
        case .inputTools:
            return "Input Tools"
        case .automation:
            return "Automation"
        case .networkTools:
            return "Network Tools"
        case .entertainment:
            return "Entertainment"
        case .game:
@@ -72,8 +137,6 @@
            return "Finance"
        case .education:
            return "Education"
        case .aiTools:
            return "AI Tools"
        case .security:
            return "Security"
        case .other:
@@ -101,15 +164,47 @@
            return 4
        case .communication:
            return 4
        case .productivity:
        case .gtd:
            return 2
        case .notes:
            return 2
        case .meeting:
            return 4
        case .office:
            return 2
        case .pdf:
            return 2
        case .fileManagement:
            return 4
        case .transfer:
            return 4
        case .productivity:
            return 2
        case .development:
            return 3
        case .design:
            return 7
        case .aiTools:
            return 3
        case .apiTools:
            return 3
        case .databaseTools:
            return 5
        case .devops:
            return 5
        case .ide:
            return 3
        case .runtimeSDK:
            return 3
        case .terminalTools:
            return 0
        case .font:
            return 7
        case .uiPrototyping:
            return 7
        case .threeDCAD:
            return 7
        case .diagramming:
            return 7
        case .writing:
            return 2
@@ -127,6 +222,18 @@
            return 0
        case .systemEnhancement:
            return 0
        case .systemMaintenance:
            return 0
        case .windowManagement:
            return 0
        case .deviceManagement:
            return 0
        case .inputTools:
            return 0
        case .automation:
            return 5
        case .networkTools:
            return 5
        case .entertainment:
            return 7
        case .game:
@@ -135,8 +242,6 @@
            return 2
        case .education:
            return 2
        case .aiTools:
            return 3
        case .security:
            return 1
        case .other:
@@ -168,11 +273,24 @@
    static let orderedIDs: [SmartCategoryID] = [
        .browser,
        .communication,
        .productivity,
        .gtd,
        .notes,
        .meeting,
        .office,
        .pdf,
        .fileManagement,
        .transfer,
        .development,
        .design,
        .aiTools,
        .apiTools,
        .databaseTools,
        .devops,
        .ide,
        .runtimeSDK,
        .terminalTools,
        .font,
        .uiPrototyping,
        .threeDCAD,
        .diagramming,
        .writing,
        .media,
        .video,
@@ -180,12 +298,16 @@
        .picturePhoto,
        .utilities,
        .system,
        .systemEnhancement,
        .systemMaintenance,
        .windowManagement,
        .deviceManagement,
        .inputTools,
        .automation,
        .networkTools,
        .entertainment,
        .game,
        .finance,
        .education,
        .aiTools,
        .security,
        .other
    ]
Apptag/SmartCategorization/SmartStartService.swift
@@ -1,3 +1,4 @@
import Compression
import Foundation
// MARK: - Smart Start Catalog Runtime
@@ -23,33 +24,84 @@
}
private struct SmartStartCatalogEntry {
    let entryID: String
    let rank: Int
    let name: String
    let normalizedName: String
    let bundleIdentifier: String?
    let categoryIDs: [SmartCategoryID]
    let localizedNote: String?
    let notes: [String: String]?
    let localizedNote: SmartStartLocalizedNote?
    let sourceEvidence: [String]
}
private struct SmartStartRuntimeCatalog: Decodable {
    let version: Int
    let entries: [SmartStartRuntimeCatalogEntry]
private struct SmartStartLocalizedNote {
    let note: String
    let languageCode: String
    let provenance: SmartDefaultNoteProvenance
}
private struct SmartStartRuntimeCatalogEntry: Decodable {
private struct SmartStartBaseCatalog: Decodable {
    let resourceFormatVersion: Int
    let catalogContentVersion: Int
    let noteLimit: Int?
    let supportedLanguages: [String]
    let fallbackLanguages: [String]
    let entries: [SmartStartBaseCatalogEntry]
}
private struct SmartStartBaseCatalogEntry: Decodable {
    let entryID: String
    let rank: Int?
    let name: String
    let normalizedName: String?
    let bundleIdentifier: String?
    let defaultTag: [String]
    let notes: [String: String]?
    let sourceEvidence: [String]?
}
private struct SmartStartNotesCatalog: Decodable {
    let resourceFormatVersion: Int
    let catalogContentVersion: Int
    let notesVersion: Int
    let language: String
    let entries: [SmartStartNoteEntry]
}
private struct SmartStartNoteEntry: Decodable {
    let entryID: String
    let note: String
}
private struct SmartStartBaseSnapshot {
    let catalogContentVersion: Int
    let supportedLanguages: [String]
    let fallbackLanguages: [String]
    let entries: [SmartStartBaseCatalogEntry]
    let bundleIndex: [String: SmartStartBaseCatalogEntry]
    let nameIndex: [String: SmartStartBaseCatalogEntry]
    let entryIndex: [String: SmartStartBaseCatalogEntry]
}
private struct SmartStartNotesSnapshot {
    let languageCode: String
    let notesVersion: Int
    let notes: [String: String]
}
private struct SmartStartCatalogSnapshot {
    let entries: [SmartStartCatalogEntry]
    let bundleIndex: [String: SmartStartCatalogEntry]
    let nameIndex: [String: SmartStartCatalogEntry]
    let entryIndex: [String: SmartStartCatalogEntry]
}
enum SmartStartService {
    static let catalogVersion = 2
    private static let catalogCacheLock = NSLock()
    private static var cachedBaseSnapshot: SmartStartBaseSnapshot?
    private static var cachedCatalogSnapshots: [String: SmartStartCatalogSnapshot] = [:]
    private static var cachedNotesSnapshots: [String: SmartStartNotesSnapshot] = [:]
    static let systemInitialSchemeCreatedAt: Date = {
        var components = DateComponents()
        components.calendar = Calendar(identifier: .gregorian)
@@ -146,45 +198,30 @@
    @discardableResult
    static func relocalizeDefaultNotesForCurrentLanguage(apps: [AppInfo]) -> Bool {
        let catalog = loadCatalog()
        guard !catalog.isEmpty else { return false }
        var bundleIndex: [String: SmartStartCatalogEntry] = [:]
        for entry in catalog {
            guard let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty else {
                continue
            }
            if let existing = bundleIndex[bundleIdentifier], existing.rank <= entry.rank {
                continue
            }
            bundleIndex[bundleIdentifier] = entry
        }
        let nameIndex = Dictionary(
            grouping: catalog,
            by: { $0.normalizedName }
        ).compactMapValues { entries in
            entries.sorted { $0.rank < $1.rank }.first
        }
        let catalog = loadCatalogSnapshot()
        guard !catalog.entries.isEmpty else { return false }
        var store = TagDatabase.load()
        var changed = false
        let appPaths = Set(apps.map { $0.path.path })
        for app in apps {
            let path = app.path.path
        for path in appPaths {
            guard let currentNote = normalizedNote(store.appNotes[path]) else { continue }
            guard let matched = match(app: app, bundleIndex: bundleIndex, nameIndex: nameIndex),
                  let notes = matched.notes,
                  !notes.isEmpty
            guard let metadata = store.appNoteMetadata[path],
                  metadata.origin == .catalogDefault,
                  let currentCatalog = metadata.catalog,
                  currentCatalog.noteFingerprint == TagDatabase.noteFingerprint(currentNote),
                  let matched = catalog.entryIndex[currentCatalog.entryID],
                  let localizedNote = matched.localizedNote,
                  localizedNote.note != currentNote
            else { continue }
            let knownDefaultNotes = Set(notes.values.compactMap(normalizedNote))
            guard knownDefaultNotes.contains(currentNote),
                  let localizedNote = localizedNote(from: notes),
                  localizedNote != currentNote
            else { continue }
            store.appNotes[path] = localizedNote
            store.appNotes[path] = localizedNote.note
            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                origin: .catalogDefault,
                catalog: localizedNote.provenance,
                noteFingerprint: localizedNote.provenance.noteFingerprint
            )
            changed = true
        }
@@ -215,30 +252,18 @@
    }
    static func makeDraft(apps: [AppInfo]) -> SmartCategorizationDraft {
        let catalog = loadCatalog()
        var bundleIndex: [String: SmartStartCatalogEntry] = [:]
        for entry in catalog {
            guard let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty else {
                continue
            }
            if let existing = bundleIndex[bundleIdentifier], existing.rank <= entry.rank {
                continue
            }
            bundleIndex[bundleIdentifier] = entry
        }
        let nameIndex = Dictionary(
            grouping: catalog,
            by: { $0.normalizedName }
        ).compactMapValues { entries in
            entries.sorted { $0.rank < $1.rank }.first
        }
        let catalog = loadCatalogSnapshot()
        var seenAssignments = Set<String>()
        var assignments: [SmartAppCategorizationAssignment] = []
        var unassigned: [SmartUnassignedApp] = []
        for app in apps {
            let matched = match(app: app, bundleIndex: bundleIndex, nameIndex: nameIndex)
            let matched = match(
                app: app,
                bundleIndex: catalog.bundleIndex,
                nameIndex: catalog.nameIndex
            )
            guard let matched else {
                unassigned.append(SmartUnassignedApp(
                    appName: app.name,
@@ -261,8 +286,9 @@
                source: .localCatalog,
                reason: "Matched Smart Start catalog entry: \(matched.name)",
                provenance: matched.sourceEvidence,
                defaultNote: matched.localizedNote,
                defaultNoteCandidates: matched.notes?.values.map { $0 } ?? []
                defaultNote: matched.localizedNote?.note,
                defaultNoteCandidates: matched.localizedNote.map { [$0.note] } ?? [],
                defaultNoteProvenance: matched.localizedNote?.provenance
            ))
        }
@@ -355,13 +381,24 @@
            if let defaultNote = assignment.defaultNote?.trimmingCharacters(in: .whitespacesAndNewlines),
               !defaultNote.isEmpty {
                let currentNote = store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines)
                let knownDefaultNotes = Set(assignment.defaultNoteCandidates.compactMap(normalizedNote))
                let normalizedCurrentNote = normalizedNote(currentNote)
                let existingMetadata = store.appNoteMetadata[path]
                let currentMatchesCatalogDefault = existingMetadata?.origin == .catalogDefault
                    && normalizedCurrentNote.map { TagDatabase.noteFingerprint($0) } == existingMetadata?.catalog?.noteFingerprint
                let knownDefaultNotes = Set(assignment.defaultNoteCandidates.compactMap(normalizedNote))
                let shouldSeedNote = currentNote?.isEmpty != false
                    || currentMatchesCatalogDefault
                    || normalizedCurrentNote.map { knownDefaultNotes.contains($0) } == true
                if shouldSeedNote {
                    store.appNotes[path] = String(defaultNote.prefix(TagDatabase.maxAppNoteLength))
                    if let provenance = assignment.defaultNoteProvenance {
                        store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                            origin: .catalogDefault,
                            catalog: provenance,
                            noteFingerprint: provenance.noteFingerprint
                        )
                    }
                    if currentNote?.isEmpty != false {
                        uncommonPaths.insert(path)
                        if store.uncommonSources[path] == nil {
@@ -427,26 +464,45 @@
        return 0.68
    }
    private static func loadCatalog() -> [SmartStartCatalogEntry] {
        if let catalog = loadRuntimeCatalog() {
            return catalog
    private static func loadCatalogSnapshot() -> SmartStartCatalogSnapshot {
        let languageCode = L10n.currentCode
        catalogCacheLock.lock()
        if let snapshot = cachedCatalogSnapshots[languageCode] {
            catalogCacheLock.unlock()
            return snapshot
        }
        return loadLegacyCSVCatalog()
        catalogCacheLock.unlock()
        guard let base = loadBaseSnapshot() else {
            return SmartStartCatalogSnapshot(entries: [], bundleIndex: [:], nameIndex: [:], entryIndex: [:])
        }
        let snapshot = makeCatalogSnapshot(base: base, languageCode: languageCode)
        catalogCacheLock.lock()
        if let cached = cachedCatalogSnapshots[languageCode] {
            catalogCacheLock.unlock()
            return cached
        }
        cachedCatalogSnapshots[languageCode] = snapshot
        catalogCacheLock.unlock()
        return snapshot
    }
    private static func loadRuntimeCatalog() -> [SmartStartCatalogEntry]? {
        guard let url = Bundle.main.url(forResource: "SmartStartUltimateDefaultCatalog", withExtension: "json"),
              let data = try? Data(contentsOf: url),
              let catalog = try? JSONDecoder().decode(SmartStartRuntimeCatalog.self, from: data)
        else { return nil }
        return catalog.entries.compactMap { entry in
    private static func makeCatalogSnapshot(
        base: SmartStartBaseSnapshot,
        languageCode: String
    ) -> SmartStartCatalogSnapshot {
        let localizedNotes = loadLocalizedNotes(
            for: languageFallbacks(preferredCode: languageCode, base: base)
        )
        let entries = base.entries.compactMap { entry -> SmartStartCatalogEntry? in
            let tagIDs = entry.defaultTag
                .compactMap { SmartCategoryID(rawValue: $0) }
                .filter { $0 != .other }
            guard !tagIDs.isEmpty else { return nil }
            return SmartStartCatalogEntry(
                entryID: entry.entryID,
                rank: entry.rank ?? Int.max,
                name: entry.name,
                normalizedName: entry.normalizedName.flatMap {
@@ -454,64 +510,230 @@
                } ?? normalizedName(entry.name),
                bundleIdentifier: normalizedBundleIdentifier(entry.bundleIdentifier),
                categoryIDs: uniqueOrdered(tagIDs),
                localizedNote: localizedNote(from: entry.notes),
                notes: entry.notes,
                localizedNote: localizedNotes[entry.entryID],
                sourceEvidence: entry.sourceEvidence ?? []
            )
        }
        var bundleIndex: [String: SmartStartCatalogEntry] = [:]
        var nameIndex: [String: SmartStartCatalogEntry] = [:]
        var entryIndex: [String: SmartStartCatalogEntry] = [:]
        for entry in entries {
            entryIndex[entry.entryID] = entry
            if let bundleIdentifier = entry.bundleIdentifier?.lowercased(), !bundleIdentifier.isEmpty {
                if bundleIndex[bundleIdentifier].map({ $0.rank <= entry.rank }) != true {
                    bundleIndex[bundleIdentifier] = entry
                }
    }
    private static func loadLegacyCSVCatalog() -> [SmartStartCatalogEntry] {
        guard let url = Bundle.main.url(forResource: "SmartStartAppDefaultTags", withExtension: "csv"),
              let csv = try? String(contentsOf: url, encoding: .utf8)
        else { return [] }
            if nameIndex[entry.normalizedName].map({ $0.rank <= entry.rank }) != true {
                nameIndex[entry.normalizedName] = entry
            }
        }
        let rows = parseCSV(csv)
        guard let header = rows.first else { return [] }
        let dataRows = rows.dropFirst()
        return SmartStartCatalogSnapshot(
            entries: entries,
            bundleIndex: bundleIndex,
            nameIndex: nameIndex,
            entryIndex: entryIndex
        )
    }
        return dataRows.compactMap { row in
            let values = Dictionary(uniqueKeysWithValues: header.enumerated().map { index, key in
                (key, index < row.count ? row[index] : "")
            })
            let tagIDs = splitPipe(values["defaultTagIDs"] ?? "")
                .compactMap { SmartCategoryID(rawValue: $0) }
                .filter { $0 != .other }
            guard !tagIDs.isEmpty else { return nil }
    private static func loadBaseSnapshot() -> SmartStartBaseSnapshot? {
        catalogCacheLock.lock()
        if let snapshot = cachedBaseSnapshot {
            catalogCacheLock.unlock()
            return snapshot
        }
        catalogCacheLock.unlock()
            return SmartStartCatalogEntry(
                rank: Int(values["rank"] ?? "") ?? Int.max,
                name: values["name"] ?? "",
                normalizedName: values["normalizedName"].flatMap {
                    $0.isEmpty ? nil : $0
                } ?? normalizedName(values["name"] ?? ""),
                bundleIdentifier: normalizedBundleIdentifier(values["bundleIdentifier"]),
                categoryIDs: uniqueOrdered(tagIDs),
                localizedNote: nil,
                notes: nil,
                sourceEvidence: splitPipe(values["sourceEvidence"] ?? "")
        guard let url = Bundle.main.url(
            forResource: "SmartStartUltimateDefaultCatalog.base",
            withExtension: "json"
        ),
              let data = try? Data(contentsOf: url),
              let catalog = try? JSONDecoder().decode(SmartStartBaseCatalog.self, from: data),
              catalog.resourceFormatVersion == 1
        else { return nil }
        var bundleIndex: [String: SmartStartBaseCatalogEntry] = [:]
        var nameIndex: [String: SmartStartBaseCatalogEntry] = [:]
        var entryIndex: [String: SmartStartBaseCatalogEntry] = [:]
        for entry in catalog.entries {
            entryIndex[entry.entryID] = entry
            if let bundleIdentifier = normalizedBundleIdentifier(entry.bundleIdentifier)?.lowercased(),
               !bundleIdentifier.isEmpty,
               bundleIndex[bundleIdentifier].map({ ($0.rank ?? Int.max) <= (entry.rank ?? Int.max) }) != true {
                bundleIndex[bundleIdentifier] = entry
            }
            let name = entry.normalizedName.flatMap { $0.isEmpty ? nil : $0 } ?? normalizedName(entry.name)
            if nameIndex[name].map({ ($0.rank ?? Int.max) <= (entry.rank ?? Int.max) }) != true {
                nameIndex[name] = entry
            }
        }
        let snapshot = SmartStartBaseSnapshot(
            catalogContentVersion: catalog.catalogContentVersion,
            supportedLanguages: catalog.supportedLanguages,
            fallbackLanguages: catalog.fallbackLanguages,
            entries: catalog.entries,
            bundleIndex: bundleIndex,
            nameIndex: nameIndex,
            entryIndex: entryIndex
        )
        catalogCacheLock.lock()
        if let cached = cachedBaseSnapshot {
            catalogCacheLock.unlock()
            return cached
        }
        cachedBaseSnapshot = snapshot
        catalogCacheLock.unlock()
        return snapshot
    }
    private static func loadLocalizedNotes(for languageCodes: [String]) -> [String: SmartStartLocalizedNote] {
        var result: [String: SmartStartLocalizedNote] = [:]
        for languageCode in languageCodes {
            guard let notes = loadNotesSnapshot(languageCode: languageCode) else { continue }
            for (entryID, note) in notes.notes where result[entryID] == nil {
                let fingerprint = TagDatabase.noteFingerprint(note)
                result[entryID] = SmartStartLocalizedNote(
                    note: note,
                    languageCode: notes.languageCode,
                    provenance: SmartDefaultNoteProvenance(
                        entryID: entryID,
                        languageCode: notes.languageCode,
                        notesVersion: notes.notesVersion,
                        noteFingerprint: fingerprint
                    )
            )
        }
    }
    private static func localizedNote(from notes: [String: String]?, preferredCode: String = L10n.currentCode) -> String? {
        guard let notes, !notes.isEmpty else { return nil }
        let preferredCodes = [
            preferredCode,
            "en",
            "zh-Hans",
            "zh-Hant"
        ]
        for code in preferredCodes {
            if let note = normalizedNote(notes[code]) {
                return note
            }
        return result
        }
        return notes.values
            .compactMap(normalizedNote)
            .first
    private static func loadNotesSnapshot(languageCode: String) -> SmartStartNotesSnapshot? {
        catalogCacheLock.lock()
        if let snapshot = cachedNotesSnapshots[languageCode] {
            catalogCacheLock.unlock()
            return snapshot
        }
        catalogCacheLock.unlock()
        guard let data = loadNotesCatalogData(languageCode: languageCode),
              let catalog = try? JSONDecoder().decode(SmartStartNotesCatalog.self, from: data),
              catalog.resourceFormatVersion == 1
        else { return nil }
        let notes = Dictionary(uniqueKeysWithValues: catalog.entries.compactMap { entry in
            normalizedNote(entry.note).map { (entry.entryID, $0) }
        })
        let snapshot = SmartStartNotesSnapshot(
            languageCode: catalog.language,
            notesVersion: catalog.notesVersion,
            notes: notes
        )
        catalogCacheLock.lock()
        if let cached = cachedNotesSnapshots[languageCode] {
            catalogCacheLock.unlock()
            return cached
        }
        cachedNotesSnapshots[languageCode] = snapshot
        catalogCacheLock.unlock()
        return snapshot
    }
    private static func loadNotesCatalogData(languageCode: String) -> Data? {
        let resourceName = "SmartStartUltimateDefaultCatalog.notes.\(languageCode)"
        if let url = Bundle.main.url(forResource: resourceName, withExtension: "json.deflate"),
           let compressedData = try? Data(contentsOf: url),
           let data = inflatedDeflateData(from: compressedData) {
            return data
        }
        guard let url = Bundle.main.url(forResource: resourceName, withExtension: "json") else {
            return nil
        }
        return try? Data(contentsOf: url)
    }
    private static func inflatedDeflateData(from compressedData: Data) -> Data? {
        guard !compressedData.isEmpty else { return Data() }
        let maxOutputSize = 16 * 1024 * 1024
        var outputSize = max(compressedData.count * 8, 64 * 1024)
        while outputSize <= maxOutputSize {
            let decoded = compressedData.withUnsafeBytes { sourceBuffer -> Data? in
                guard let sourcePointer = sourceBuffer.bindMemory(to: UInt8.self).baseAddress else {
                    return nil
                }
                let destinationPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: outputSize)
                defer { destinationPointer.deallocate() }
                let decodedCount = compression_decode_buffer(
                    destinationPointer,
                    outputSize,
                    sourcePointer,
                    compressedData.count,
                    nil,
                    COMPRESSION_ZLIB
                )
                guard decodedCount > 0 else { return nil }
                return Data(bytes: destinationPointer, count: decodedCount)
            }
            if let decoded {
                return decoded
            }
            outputSize *= 2
        }
        return nil
    }
    private static func languageFallbacks(
        preferredCode: String,
        base: SmartStartBaseSnapshot
    ) -> [String] {
        var codes: [String] = []
        func append(_ code: String) {
            guard !code.isEmpty,
                  base.supportedLanguages.contains(code),
                  !codes.contains(code)
            else { return }
            codes.append(code)
        }
        append(preferredCode)
        switch preferredCode {
        case "ar-Najdi":
            append("ar")
        case "nn":
            append("nb")
            append("no")
        case "no":
            append("nb")
            append("nn")
        case "nb":
            append("no")
            append("nn")
        default:
            break
        }
        for code in base.fallbackLanguages {
            append(code)
        }
        append("en")
        append("zh-Hans")
        append("zh-Hant")
        return codes
    }
    private static func normalizedNote(_ value: String?) -> String? {
Apptag/TagGroupView.swift
@@ -14,8 +14,8 @@
    var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    var bubbleDisabled: Bool = false
    var showUncommonAppBubbles: Bool = AppDefaults.showUncommonAppBubbles
    var dragResetToken: Int = 0
    @Binding var hoveredAppItemID: String?
    var onDropApp: ((String, String, Bool) -> Void)? = nil
    /// Adaptive columns — auto-fit based on icon size and available width.
@@ -61,9 +61,9 @@
                        onBubbleHover: onBubbleHover,
                        onEditNote: onEditNote,
                        bubbleDisabled: bubbleDisabled,
                        showUncommonAppBubbles: showUncommonAppBubbles,
                        itemID: "\(group.name)|\(app.path.path)",
                        dragResetToken: dragResetToken,
                        hoveredAppItemID: $hoveredAppItemID,
                        onSelect: { onSelectApp(app) }
                    )
                }
@@ -71,11 +71,13 @@
        }
        .contentShape(Rectangle())
        .overlay {
            if dragModeActive {
            AppDropTargetView(targetTag: group.name) { path, source, copy in
                onDropApp?(path, source, copy)
            }
            .allowsHitTesting(false)
        }
        }
        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
            handleDrop(providers)
        }
Apptag/TagLauncher.entitlements
@@ -6,9 +6,5 @@
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
    <key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
    <array>
        <string>/Library/Application Support/Apptag/</string>
    </array>
</dict>
</plist>
CHANGELOG.md
@@ -1,14 +1,27 @@
# Apptag Changelog
# TagLauncher Changelog
## [7.6.0] — 2026-05-20
- 重写默认 App 网格主路径:`coloredGridContainer` / `gridContainer` 改为 AppKit-backed 主网格,使用 `NSCollectionView` 管理分组卡片、滚动、图标 hover 和拖拽 drop,默认路径不再依赖 SwiftUI `LazyVStack` / `LazyVGrid` 布局事务
- 修复 AppKit-backed 网格容器对齐:用自定义 `NSCollectionViewLayout` 复刻原来的行/span 规则,同一行卡片顶边一致,并按该行最高图标行数统一容器高度
- 针对假死报告中的 SwiftUI `AttributeGraph` / Lazy layout 栈,新增主网格热键开关与打开后等待采样验证,20 次 overlay 开关后 CPU 回到空闲水平且采样未再出现 `LazyLayoutViewCache`、`LazyStack`、`ForEachState`
- 统一运行时品牌标识:本地数据目录改为 `~/Library/Application Support/TagLauncher`,Bundle ID、LaunchAgent、状态栏 autosave 名和 Smart Start 内置目录中的 TagLauncher 自身条目同步改为 TagLauncher 命名空间,并移除不再需要的旧路径沙盒临时例外
- 优化 Smart Start 包体:App bundle 只打包 base/manifest 与按语言独立压缩的 notes 资源,构建时拒绝旧单体 JSON、CSV、translation cache、invalid cache 和未压缩 notes 混入包内
- 扩展 Smart Start 运行时分类体系,支持新版终极目录中的 GTD、笔记、会议、Office、PDF、API、数据库、DevOps、IDE、终端、窗口管理等更细分类
- Smart Start 运行时资源改为 base catalog + 29 个按语言拆分的 notes 文件,正常路径只加载当前语言和必要 fallback,不再长期持有 29 种语言 notes
- Smart Start 资源生成器新增 Swift runtime 分类兼容门禁、稳定 `entryID`、manifest 和 split resource 输出,避免未知分类被静默丢弃
- Smart Start 默认用途备注新增来源 metadata,系统默认备注可随语言切换更新,用户手写备注不会被语言切换覆盖
- 打包脚本改为只复制 split Smart Start 资源到 App bundle,不再把旧单体 runtime JSON/CSV 作为首发运行时资源
- 移除 Smart Start 终极默认目录中错误的 `thunder` 条目,避免将 Thunder 错分为 VPN/安全网络工具并写入错误用途备注
- 优化主界面滚动性能:网格容器改为懒加载,并在滚动期间临时冻结 App hover 放大、用途气泡和容器 hover 高亮
- 将主界面的分组结果与网格容器行布局改为缓存状态,避免鼠标 hover、搜索输入等轻量状态变化反复触发分组和布局计算
- 优化 Quick Search:打开浮层时复用已加载 App 数据,不再每次打开都触发全量刷新;搜索字段在索引阶段预计算,输入时避免重复规格化、首字母和拼音候选生成
- Quick Search 增加 App 本地化显示名索引:读取所有语言 `InfoPlist.strings` 显示名,并用 Spotlight display name 兜底,让 AweSun 这类英文文件名 App 可通过“贝锐向日葵/向日葵”搜索命中
- 对含非拉丁文字的 App 显示名启用拉丁化/转写候选,支持 `xiangrikui`、`beirui`、`xrk` 等连续片段和非连续缩写命中;该规则仅限 App 显示名,不扩散到 Bundle ID 或纯拉丁 App 名
- 减少 App 图标重绘:只有图标对象或尺寸变化时才触发 `NSView` 重绘
- 将当前满意版标记为 `7.6.0`,作为后续性能优化前的稳定归档基线
- 保留 7.5.1 的 Quick Search、快捷键状态、Smart Start 目录和文档沉淀成果,不引入新的功能变更
- 版本号更新为 `7.6.0`,基线 Build 为 `20260520.1250`;本次性能优化验证 Build 为 `20260520.1400`
- 版本号更新为 `7.6.0`,基线 Build 为 `20260520.1250`;本次本地化显示名搜索验证 Build 为 `20260520.1452`
## [7.5.1] — 2026-05-19
@@ -392,7 +405,7 @@
## [3.1.35] — 2026-05-10
- 修复 overlay 显示时打开设置窗口不可见的问题:设置窗口会强制浮动在 Apptag 列表之上,确保用户能实时预览设置效果
- 修复 overlay 显示时打开设置窗口不可见的问题:设置窗口会强制浮动在 TagLauncher 列表之上,确保用户能实时预览设置效果
## [3.1.34] — 2026-05-10
@@ -420,7 +433,7 @@
## [3.1.28] — 2026-05-10
- 修复全屏 Space 下 Shift+Option+Space 唤出 Apptag 后 overlay 被全屏应用遮挡的问题:显示时刷新当前屏幕 frame,提升窗口层级并强制前置
- 修复全屏 Space 下 Shift+Option+Space 唤出 TagLauncher 后 overlay 被全屏应用遮挡的问题:显示时刷新当前屏幕 frame,提升窗口层级并强制前置
## [3.1.27] — 2026-05-07
@@ -477,7 +490,7 @@
## [3.1.13] — 2026-05-06
- 新增全局快捷键设置(Data 页签):点击按钮后按下新快捷键即可更改,支持任意组合键
- 菜单栏「Show Apptag」右侧显示当前快捷键
- 菜单栏「Show TagLauncher」右侧显示当前快捷键
- HotkeyHelper:Carbon keycode → 人类可读字符串(⇧⌥Space, ⌘A, F1 等)
## [3.1.12] — 2026-05-06
@@ -488,7 +501,7 @@
## [3.1.11] — 2026-05-06
- General 标签页:改用 Grid 布局替代固定 frame,标签列按内容宽度自动右对齐
- 点击 Dock 图标现在等同于菜单栏 "Show Apptag",直接全屏显示 APP 列表
- 点击 Dock 图标现在等同于菜单栏 "Show TagLauncher",直接全屏显示 APP 列表
## [3.1.10] — 2026-05-06
@@ -602,7 +615,7 @@
## [2.0] — 2026-05-06
- **架构级变更**:标签完全脱离 Finder,独立存储在本地 JSON 数据库 `~/Library/Application Support/Apptag/tags.json`
- **架构级变更**:标签完全脱离 Finder,独立存储在本地 JSON 数据库 `~/Library/Application Support/TagLauncher/tags.json`
- 首次启动一次性导入 Finder 存量标签(含"Mac自带"),之后不再触碰 Finder xattr
- 所有 CRUD(assign/rename/delete/setColor)写入本地 DB
- Preferences → Data 标签页:Export / Import JSON 备份恢复
Decisions.md
File was deleted
Docs/PRD/InitialDefaults_PRD.md
File was deleted
Docs/PRD/SmartStart_PRD_UltimateDefaultCatalog.md
File was deleted
Docs/PRD/TagLauncher_QuickSearch_CurrentRules_Snapshot.md
File was deleted
Docs/PRD/TagLauncher_QuickSearch_PRD_7.5_PMD_Rewrite_ZH.md
File was deleted
Docs/PRD/TagLauncher_QuickSuggestions_PRD_7.5.md
File was deleted
Docs/PRD/X-存档/TagLauncher_QuickSearch_PRD_7.5.md
File was deleted
Docs/PRD/X-存档/TagLauncher_QuickSearch_PRD_7.5_PMD_Rewrite.md
File was deleted
Docs/ProjectManagement/Apptag_Development_Workflow_Standard_v1.0.md
File was deleted
Docs/ProjectManagement/SmartStart_UltimateDefaultCatalog_ImplementationPlan.md
File was deleted
Docs/QA/InitialDefaults_TestCases.md
File was deleted
Docs/QA/SmartStart_TestCases_UltimateDefaultCatalog.md
File was deleted
Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs
@@ -2,6 +2,8 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import crypto from "node:crypto";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
@@ -18,19 +20,40 @@
const outputs = {
  reviewCSV: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.csv"),
  runtimeJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.json"),
  runtimeBaseJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.base.json"),
  runtimeManifestJSON: path.join(outputDir, "SmartStart_UltimateDefaultCatalog.manifest.json"),
  report: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_Report.md"),
  duplicateReview: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_DuplicateReview.csv"),
  translationQA: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_TranslationQA.md"),
  sourceNoteTranslationCache: path.join(outputDir, "SmartStart_UltimateDefaultCatalog_SourceNoteTranslationCache.json"),
};
const resourceFormatVersion = 1;
const catalogContentVersion = 2;
const notesVersion = 1;
const requiredFallbackLanguages = ["en", "zh-Hans", "zh-Hant"];
const stableTagOrder = [
  "browser",
  "communication",
  "productivity",
  "GTD",
  "Notes",
  "Meeting",
  "office",
  "PDF",
  "file-management",
  "transfer",
  "development",
  "design",
  "ai-tools",
  "api-tools",
  "database-tools",
  "devops",
  "ide",
  "runtime-sdk",
  "terminal-tools",
  "Font",
  "ui-prototyping",
  "3d-cad",
  "diagramming",
  "writing",
  "media",
  "video",
@@ -38,58 +61,31 @@
  "picture-photo",
  "utilities",
  "system",
  "system-enhancement",
  "system-maintenance",
  "window-management",
  "device-management",
  "input-tools",
  "Automation",
  "network-tools",
  "entertainment",
  "game",
  "finance",
  "education",
  "ai-tools",
  "security",
  "other",
];
const stableTags = new Set(stableTagOrder);
const noteLimit = 80;
const tagerToStableTags = new Map(Object.entries({
  "3d-cad": ["design"],
  "Automation": ["productivity", "system-enhancement"],
  "Font": ["design", "utilities"],
  "GTD": ["productivity"],
  "Meeting": ["communication", "productivity"],
  "Notes": ["productivity", "writing"],
  "PDF": ["writing", "utilities"],
  "ai-tools": ["ai-tools"],
  "api-tools": ["development"],
  "audio": ["media", "audio"],
  "browser": ["browser"],
  "communication": ["communication"],
  "database-tools": ["development"],
  "design": ["design"],
  "device-management": ["utilities", "system-enhancement"],
  "devops": ["development"],
  "diagramming": ["design"],
  "education": ["education"],
  "entertainment": ["entertainment"],
  "file-management": ["file-management", "utilities"],
  "finance": ["finance"],
  "game": ["entertainment", "game"],
  "ide": ["development"],
  "input-tools": ["utilities", "system-enhancement"],
  "media": ["media"],
  "network-tools": ["utilities"],
  "office": ["productivity", "writing"],
  "picture-photo": ["media", "picture-photo"],
  "runtime-sdk": ["development"],
  "security": ["security"],
  "system": ["system", "utilities"],
  "system-maintenance": ["utilities", "system-enhancement"],
  "terminal-tools": ["development"],
  "transfer": ["transfer", "utilities"],
  "ui-prototyping": ["design"],
  "utilities": ["utilities"],
  "video": ["media", "video"],
  "window-management": ["utilities", "system-enhancement"],
  "writing": ["writing"],
const translationProvider = "google-translate-gtx-source-notes-20260520";
const translationBackend = process.env.SMARTSTART_TRANSLATION_BACKEND || "google-gtx";
const lingvaBaseURL = process.env.SMARTSTART_LINGVA_BASE_URL || "https://lingva.ml";
const translationBatchSize = Number(process.env.SMARTSTART_TRANSLATION_BATCH_SIZE || 50);
const translationPauseMS = Number(process.env.SMARTSTART_TRANSLATION_PAUSE_MS || 150);
const translationRetryLimit = Number(process.env.SMARTSTART_TRANSLATION_RETRY_LIMIT || 4);
const equivalentTargetLanguages = new Map(Object.entries({
  "ar-Najdi": ["ar"],
  "nn": ["nb", "no"],
  "no": ["nb", "nn"],
}));
function parseCSV(text) {
@@ -155,7 +151,9 @@
function readCSVObjects(filePath) {
  const rows = parseCSV(fs.readFileSync(filePath, "utf8"));
  const header = rows[0] ?? [];
  const header = (rows[0] ?? []).map((column, index) =>
    index === 0 ? String(column ?? "").replace(/^\uFEFF/, "") : column,
  );
  return rows.slice(1).map((row) =>
    Object.fromEntries(header.map((column, index) => [column, row[index] ?? ""])),
  );
@@ -177,8 +175,7 @@
    cleaned.push(tag);
  }
  const meaningful = cleaned.filter((tag) => tag !== "other");
  const finalTags = meaningful.length > 0 ? meaningful : cleaned;
  return stableTagOrder.filter((tag) => finalTags.includes(tag));
  return meaningful.length > 0 ? meaningful : cleaned;
}
function normalizeName(value) {
@@ -189,6 +186,52 @@
    .replace(/&/g, " and ")
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "") || String(value ?? "").trim().toLowerCase();
}
function sha256(value) {
  return crypto.createHash("sha256").update(value).digest("hex");
}
function smartStartNotesPath(languageCode) {
  return path.join(outputDir, `SmartStart_UltimateDefaultCatalog.notes.${languageCode}.json`);
}
function entryIDFor(row) {
  const baseSource = row.bundleIdentifier || row.normalizedName || row.name || "entry";
  const base = normalizeName(baseSource) || "entry";
  const identity = [
    row.bundleIdentifier ?? "",
    row.normalizedName ?? "",
    row.name ?? "",
  ].join("\u0000");
  return `${base}-${sha256(identity).slice(0, 12)}`;
}
function loadRuntimeSmartCategoryIDs() {
  const filePath = path.join(projectDir, "Apptag", "SmartCategorization", "SmartCategory.swift");
  const source = fs.readFileSync(filePath, "utf8");
  const enumBody = source.match(/enum SmartCategoryID:[\s\S]*?\n}/)?.[0] ?? "";
  const ids = new Set();
  const casePattern = /^\s*case\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*=\s*"([^"]+)")?/gm;
  let match;
  while ((match = casePattern.exec(enumBody)) !== null) {
    ids.add(match[2] ?? match[1]);
  }
  return ids;
}
function normalizedForNoteComparison(value) {
  return String(value ?? "")
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9\u3400-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g, "");
}
function noteRepeatsAppName(note, name) {
  const normalizedNameValue = normalizedForNoteComparison(name);
  if (normalizedNameValue.length < 3) return false;
  return normalizedForNoteComparison(note).includes(normalizedNameValue);
}
function normalizeBundle(value) {
@@ -232,32 +275,442 @@
  return stripTerminalPunctuation(sliced);
}
function loadCategoryTranslations() {
  const translations = new Map();
function normalizeTranslatedNote(value) {
  return truncateNote(value);
}
function loadSupportedLanguageCodes() {
  const codes = [];
  for (const fileName of fs.readdirSync(sources.localizationDir).sort()) {
    if (!fileName.endsWith(".json")) continue;
    const code = fileName.replace(/\.json$/, "");
    const raw = JSON.parse(fs.readFileSync(path.join(sources.localizationDir, fileName), "utf8"));
    const categories = {};
    for (const tag of stableTagOrder) {
      categories[tag] = raw[`smart.category.${tag}`] ?? tag;
    codes.push(fileName.replace(/\.json$/, ""));
    }
    translations.set(code, categories);
  }
  return translations;
  return codes.sort();
}
function inferSourceNoteLanguage(note) {
  const value = String(note ?? "");
  if (/[\u3040-\u30ff]/u.test(value)) return "ja";
  if (/[\uac00-\ud7af]/u.test(value)) return "ko";
  return "zh-Hans";
function googleLanguageCode(code) {
  switch (code) {
  case "zh-Hans": return "zh-CN";
  case "zh-Hant": return "zh-TW";
  case "pt-BR": return "pt";
  case "sr-Cyrl": return "sr";
  case "ar-Najdi": return "ar";
  case "nb":
  case "nn":
  case "no":
    return "no";
  default:
    return code;
  }
}
function buildLocalizedNotes(noteZH) {
  if (!noteZH) return {};
function sourceNotesFingerprint(sourceNotes) {
  return crypto
    .createHash("sha256")
    .update(JSON.stringify([...sourceNotes].sort()))
    .digest("hex");
}
function translationCacheKey(targetLanguage, sourceNote) {
  return ["zh-Hans", targetLanguage, sourceNote].join("\u0001");
}
function loadSourceNoteTranslationCache(sourceFingerprint) {
  if (!fs.existsSync(outputs.sourceNoteTranslationCache)) {
  return {
    [inferSourceNoteLanguage(noteZH)]: truncateNote(noteZH),
      version: 1,
      provider: translationProvider,
      sourceFingerprint,
      entries: {},
      ignoredExistingCache: false,
    };
  }
  const cache = JSON.parse(fs.readFileSync(outputs.sourceNoteTranslationCache, "utf8"));
  if (
    cache.provider !== translationProvider ||
    cache.sourceFingerprint !== sourceFingerprint ||
    cache.version !== 1
  ) {
    return {
      version: 1,
      provider: translationProvider,
      sourceFingerprint,
      entries: {},
      ignoredExistingCache: true,
    };
  }
  return {
    version: 1,
    provider: translationProvider,
    sourceFingerprint,
    entries: cache.entries ?? {},
    ignoredExistingCache: false,
  };
}
function saveSourceNoteTranslationCache(cache) {
  const sortedEntries = Object.fromEntries(
    Object.entries(cache.entries).sort(([left], [right]) => left.localeCompare(right)),
  );
  fs.writeFileSync(outputs.sourceNoteTranslationCache, `${JSON.stringify({
    version: 1,
    provider: translationProvider,
    sourceFingerprint: cache.sourceFingerprint,
    updatedAt: new Date().toISOString(),
    entries: sortedEntries,
  }, null, 2)}\n`);
}
function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
function parseGoogleTranslation(payload) {
  return (payload?.[0] ?? [])
    .map((segment) => segment?.[0] ?? "")
    .join("");
}
async function requestGoogleTranslationPayload(params, attempt = 0) {
  const result = spawnSync("curl", [
    "-sS",
    "--fail",
    "--compressed",
    "--max-time",
    "45",
    "--retry",
    "2",
    "--retry-delay",
    "1",
    "-A",
    "TagLauncher-CatalogOps/1.0",
    "-H",
    "content-type: application/x-www-form-urlencoded;charset=UTF-8",
    "--data",
    params.toString(),
    "https://translate.googleapis.com/translate_a/single",
  ], {
    encoding: "utf8",
    maxBuffer: 16 * 1024 * 1024,
  });
  if (result.status === 0) {
    try {
      return JSON.parse(result.stdout);
    } catch (error) {
      if (attempt < translationRetryLimit) {
        await sleep(2500 * (attempt + 1));
        return requestGoogleTranslationPayload(params, attempt + 1);
      }
      throw new Error(`translation response was not JSON: ${error.message}`);
    }
  }
  if (attempt < translationRetryLimit) {
    await sleep(2500 * (attempt + 1));
    return requestGoogleTranslationPayload(params, attempt + 1);
  }
  throw new Error(`translation request failed: ${result.stderr || result.stdout || result.error?.message}`);
}
async function requestLingvaTranslationBatch(notes, targetLanguage, attempt = 0) {
  const targetCode = googleLanguageCode(targetLanguage);
  const query = encodeURIComponent(notes.map((note) => note.replace(/\//g, "/")).join("\n"));
  const result = spawnSync("curl", [
    "-sS",
    "--fail",
    "--compressed",
    "--max-time",
    "60",
    "--retry",
    "4",
    "--retry-all-errors",
    "--retry-connrefused",
    "--retry-delay",
    "2",
    "-A",
    "TagLauncher-CatalogOps/1.0",
    `${lingvaBaseURL.replace(/\/+$/, "")}/api/v1/zh/${targetCode}/${query}`,
  ], {
    encoding: "utf8",
    maxBuffer: 16 * 1024 * 1024,
  });
  if (result.status === 0) {
    try {
      const payload = JSON.parse(result.stdout);
      if (payload.error) throw new Error(payload.error);
      const translated = String(payload.translation ?? "")
        .replace(/\r\n/g, "\n")
        .replace(/\r/g, "\n");
      const parts = translated.split("\n").map(normalizeTranslatedNote);
      if (parts.length !== notes.length || parts.some((part) => !part)) {
        throw new Error(`translation batch line mismatch: expected ${notes.length}, got ${parts.length}`);
      }
      return parts;
    } catch (error) {
      if (attempt < translationRetryLimit && isRemoteThrottleError(error)) {
        await sleep(2500 * (attempt + 1));
        return requestLingvaTranslationBatch(notes, targetLanguage, attempt + 1);
      }
      throw error;
    }
  }
  if (attempt < translationRetryLimit) {
    await sleep(2500 * (attempt + 1));
    return requestLingvaTranslationBatch(notes, targetLanguage, attempt + 1);
  }
  throw new Error(`lingva translation request failed: ${result.stderr || result.stdout || result.error?.message}`);
}
async function requestOnlineTranslationBatch(notes, targetLanguage) {
  if (translationBackend === "lingva") {
    return requestLingvaTranslationBatch(notes, targetLanguage);
  }
  const params = new URLSearchParams({
    client: "gtx",
    sl: googleLanguageCode("zh-Hans"),
    tl: googleLanguageCode(targetLanguage),
    dt: "t",
    q: notes.join("\n"),
  });
  const translated = parseGoogleTranslation(await requestGoogleTranslationPayload(params))
    .replace(/\r\n/g, "\n")
    .replace(/\r/g, "\n");
  const parts = translated.split("\n").map(normalizeTranslatedNote);
  if (parts.length !== notes.length || parts.some((part) => !part)) {
    throw new Error(`translation batch line mismatch: expected ${notes.length}, got ${parts.length}`);
  }
  return parts;
}
function isRemoteThrottleError(error) {
  const message = String(error?.message ?? "");
  return message.includes("not JSON") || message.includes("translation request failed");
}
async function translateNotesWithSplit(notes, targetLanguage) {
  try {
    return await requestOnlineTranslationBatch(notes, targetLanguage);
  } catch (error) {
    if (isRemoteThrottleError(error) || notes.length <= 1) {
      throw error;
    }
    const middle = Math.ceil(notes.length / 2);
    const left = await translateNotesWithSplit(notes.slice(0, middle), targetLanguage);
    const right = await translateNotesWithSplit(notes.slice(middle), targetLanguage);
    return [...left, ...right];
  }
}
function reusableTranslation(cache, targetLanguage, sourceNote) {
  const directKey = translationCacheKey(targetLanguage, sourceNote);
  if (cache.entries[directKey]) return cache.entries[directKey];
  for (const equivalentLanguage of equivalentTargetLanguages.get(targetLanguage) ?? []) {
    const equivalentKey = translationCacheKey(equivalentLanguage, sourceNote);
    if (cache.entries[equivalentKey]) return cache.entries[equivalentKey];
  }
  return null;
}
async function translateMissingNotes(uniqueSourceNotes, supportedLanguages, cache) {
  let translatedCount = 0;
  let failedCount = 0;
  let reusedEquivalentCount = 0;
  let cacheHits = 0;
  for (const sourceNote of uniqueSourceNotes) {
    cache.entries[translationCacheKey("zh-Hans", sourceNote)] = normalizeTranslatedNote(sourceNote);
  }
  for (const targetLanguage of supportedLanguages) {
    if (targetLanguage === "zh-Hans" || targetLanguage === "zh-Hant") continue;
    const missing = [];
    for (const sourceNote of uniqueSourceNotes) {
      const key = translationCacheKey(targetLanguage, sourceNote);
      if (cache.entries[key]) {
        cacheHits += 1;
        continue;
      }
      const reusable = reusableTranslation(cache, targetLanguage, sourceNote);
      if (reusable) {
        cache.entries[key] = reusable;
        reusedEquivalentCount += 1;
        continue;
      }
      missing.push({ sourceNote, key });
    }
    if (missing.length > 0) {
      console.warn(`Translating ${missing.length} source notes to ${targetLanguage}...`);
    }
    for (let index = 0; index < missing.length; index += translationBatchSize) {
      const batch = missing.slice(index, index + translationBatchSize);
      try {
        const translations = await translateNotesWithSplit(
          batch.map((item) => item.sourceNote),
          targetLanguage,
        );
        translations.forEach((translation, translationIndex) => {
          cache.entries[batch[translationIndex].key] = translation;
          translatedCount += 1;
        });
      } catch (error) {
        failedCount += batch.length;
        saveSourceNoteTranslationCache(cache);
        throw new Error(`Batch failed for ${targetLanguage}; cached progress saved.\n${error.message}`);
      }
      saveSourceNoteTranslationCache(cache);
      await sleep(translationPauseMS);
    }
  }
  saveSourceNoteTranslationCache(cache);
  return { translatedCount, failedCount, reusedEquivalentCount, cacheHits };
}
function convertSimplifiedNotesToTraditional(sourceNotes) {
  const notes = sourceNotes.map(normalizeTranslatedNote).filter(Boolean);
  if (notes.length === 0) return new Map();
  const swiftCode = `
import Foundation
let inputData = FileHandle.standardInput.readDataToEndOfFile()
let notes = try JSONDecoder().decode([String].self, from: inputData)
let converted = notes.map { note -> String in
    let mutable = NSMutableString(string: note)
    CFStringTransform(mutable, nil, "Simplified-Traditional" as CFString, false)
    return mutable as String
}
let outputData = try JSONEncoder().encode(converted)
FileHandle.standardOutput.write(outputData)
`;
  const result = spawnSync("/usr/bin/swift", ["-e", swiftCode], {
    input: JSON.stringify(notes),
    encoding: "utf8",
    maxBuffer: 64 * 1024 * 1024,
  });
  if (result.error) {
    throw result.error;
  }
  if (result.status !== 0) {
    throw new Error(`zh-Hant conversion failed:\n${result.stderr}`);
  }
  const converted = JSON.parse(result.stdout);
  if (!Array.isArray(converted) || converted.length !== notes.length) {
    throw new Error("zh-Hant conversion returned an unexpected result shape");
  }
  return new Map(notes.map((note, index) => [note, normalizeTranslatedNote(converted[index])]));
}
function buildLocalizedNotes(sourceNote, supportedLanguages, cache, traditionalNote) {
  const cleanSourceNote = normalizeTranslatedNote(sourceNote);
  if (!cleanSourceNote) return {};
  const notes = {};
  for (const languageCode of supportedLanguages) {
    if (languageCode === "zh-Hans") {
      notes[languageCode] = cleanSourceNote;
      continue;
    }
    if (languageCode === "zh-Hant") {
      const cleanTraditionalNote = normalizeTranslatedNote(traditionalNote);
      if (cleanTraditionalNote) notes[languageCode] = cleanTraditionalNote;
      continue;
    }
    const translatedNote = normalizeTranslatedNote(cache.entries[translationCacheKey(languageCode, cleanSourceNote)]);
    if (translatedNote) notes[languageCode] = translatedNote;
  }
  return Object.fromEntries(
    Object.entries(notes).sort(([left], [right]) => left.localeCompare(right)),
  );
}
function writeJSON(filePath, value) {
  const text = `${JSON.stringify(value, null, 2)}\n`;
  fs.writeFileSync(filePath, text);
  return {
    file: path.basename(filePath),
    sha256: sha256(text),
    bytes: Buffer.byteLength(text),
  };
}
function buildSplitResources({ finalRows, supportedLanguages, generatedAt, noteLimit }) {
  const baseResource = {
    resourceFormatVersion,
    catalogContentVersion,
    generatedAt,
    noteLimit,
    supportedLanguages,
    fallbackLanguages: requiredFallbackLanguages,
    entries: finalRows.map((row) => ({
      entryID: row.entryID,
      rank: row.rank,
      name: row.name,
      normalizedName: row.normalizedName,
      bundleIdentifier: row.bundleIdentifier,
      defaultTag: row.tags,
      sourceEvidence: row.sourceEvidence,
    })),
  };
  const baseInfo = writeJSON(outputs.runtimeBaseJSON, baseResource);
  const notesResources = {};
  for (const languageCode of supportedLanguages) {
    const notesResource = {
      resourceFormatVersion,
      catalogContentVersion,
      notesVersion,
      generatedAt,
      language: languageCode,
      entries: finalRows
        .map((row) => ({
          entryID: row.entryID,
          note: row.notes[languageCode],
        }))
        .filter((entry) => entry.note),
    };
    const notesPath = smartStartNotesPath(languageCode);
    notesResources[languageCode] = {
      ...writeJSON(notesPath, notesResource),
      count: notesResource.entries.length,
    };
  }
  const manifest = {
    resourceFormatVersion,
    catalogContentVersion,
    notesVersion,
    generatedAt,
    noteLimit,
    supportedLanguages,
    fallbackLanguages: requiredFallbackLanguages,
    baseResource: {
      ...baseInfo,
      count: baseResource.entries.length,
    },
    notesResources,
  };
  const manifestInfo = writeJSON(outputs.runtimeManifestJSON, manifest);
  return {
    baseInfo,
    manifestInfo,
    notesResources,
  };
}
@@ -342,16 +795,15 @@
  const tags = [];
  const unknownTokens = [];
  for (const token of splitTags(value)) {
    const mapped = tagerToStableTags.get(token);
    if (!mapped) {
    if (!stableTags.has(token)) {
      unknownTokens.push(token);
      continue;
    }
    tags.push(...mapped);
    tags.push(token);
  }
  return {
    tags: orderedTags(tags.length > 0 ? tags : ["other"]),
    tags: orderedTags(tags),
    unknownTokens,
  };
}
@@ -400,9 +852,21 @@
  return filtered;
}
function build() {
async function build() {
  fs.mkdirSync(outputDir, { recursive: true });
  const translations = loadCategoryTranslations();
  const supportedLanguages = loadSupportedLanguageCodes();
  const runtimeSmartCategoryIDs = loadRuntimeSmartCategoryIDs();
  const missingRuntimeCategories = stableTagOrder.filter((tag) => !runtimeSmartCategoryIDs.has(tag));
  if (missingRuntimeCategories.length > 0) {
    throw new Error(
      `SmartCategoryID is missing generated catalog tags: ${missingRuntimeCategories.join(", ")}`,
    );
  }
  for (const language of requiredFallbackLanguages) {
    if (!supportedLanguages.includes(language)) {
      throw new Error(`required fallback language is missing from Localization: ${language}`);
    }
  }
  const appleNotes = parseAppleNotes();
  const previousRuntimeEntries = fs.existsSync(outputs.runtimeJSON)
    ? new Map(
@@ -421,12 +885,11 @@
    bundleChangedVsPrevious: 0,
    missingLocalizedNotes: 0,
  };
  const curatedRows = dedupeCuratedRows(rawCuratedRows, stats);
  const finalRows = curatedRows.map((row, index) => {
  const curatedRows = rawCuratedRows;
  const preparedRows = curatedRows.map((row, index) => {
    const name = String(row.Name ?? "").trim();
    const normalizedName = String(row.normalizedName ?? "").trim() || normalizeName(name);
    const bundleIdentifier = normalizeBundle(row.bundleIdentifier);
    const legacyTags = orderedTags(splitTags(row.defaultTag));
    const mapping = mapCuratedTags(row.tager);
    for (const token of mapping.unknownTokens) unknownTagerTokens.add(token);
@@ -444,7 +907,6 @@
      stats.rowsWithSourceNotes += 1;
    }
    const notes = buildLocalizedNotes(noteZH);
    const previous = previousRuntimeEntries.get(normalizedName);
    if (previous) {
      if (JSON.stringify(previous.defaultTag ?? []) !== JSON.stringify(mapping.tags)) {
@@ -464,52 +926,94 @@
      normalizedName,
      bundleIdentifier,
      tags: mapping.tags,
      legacyTags,
      noteZH,
      notes,
      sourceEvidence: usedAppleFallback
        ? ["curated_tager_catalog", "apple_default_notes"]
        : ["curated_tager_catalog"],
    };
  });
  const uniqueSourceNotes = [
    ...new Set(preparedRows.map((row) => normalizeTranslatedNote(row.noteZH)).filter(Boolean)),
  ].sort((left, right) => left.localeCompare(right));
  const sourceFingerprint = sourceNotesFingerprint(uniqueSourceNotes);
  const translationCache = loadSourceNoteTranslationCache(sourceFingerprint);
  const traditionalNotesBySource = convertSimplifiedNotesToTraditional(uniqueSourceNotes);
  for (const [sourceNote, traditionalNote] of traditionalNotesBySource.entries()) {
    translationCache.entries[translationCacheKey("zh-Hant", sourceNote)] = traditionalNote;
  }
  const translationStats = await translateMissingNotes(uniqueSourceNotes, supportedLanguages, translationCache);
  if (translationStats.failedCount > 0) {
    throw new Error(`translation failed for ${translationStats.failedCount} source-note/language pairs; JSON generation blocked`);
  }
  const finalRows = preparedRows.map((row) => ({
    ...row,
    entryID: entryIDFor(row),
    notes: buildLocalizedNotes(
      row.noteZH,
      supportedLanguages,
      translationCache,
      traditionalNotesBySource.get(normalizeTranslatedNote(row.noteZH)),
    ),
  }));
  const invalidTagRows = finalRows.filter((row) => row.tags.some((tag) => !stableTags.has(tag)));
  const runtimeInvalidTagRows = finalRows.filter((row) => row.tags.some((tag) => !runtimeSmartCategoryIDs.has(tag)));
  const entryIDCounts = new Map();
  for (const row of finalRows) {
    entryIDCounts.set(row.entryID, (entryIDCounts.get(row.entryID) ?? 0) + 1);
  }
  const duplicateEntryIDs = [...entryIDCounts.entries()].filter(([, count]) => count > 1);
  const emptyNormalizedRows = finalRows.filter((row) => !row.normalizedName);
  const exactOtherRows = finalRows.filter((row) => row.tags.length === 1 && row.tags[0] === "other");
  const mixedOtherRows = finalRows.filter((row) => row.tags.length > 1 && row.tags.includes("other"));
  const noteRows = finalRows.filter((row) => row.noteZH);
  const actualLocalizedNoteCount = noteRows.reduce(
    (total, row) => total + Object.keys(row.notes).filter((code) => supportedLanguages.includes(code)).length,
    0,
  );
  const missingLocalizedNotes = noteRows.length * supportedLanguages.length - actualLocalizedNoteCount;
  const missingLocalizedNoteRows = noteRows.filter(
    (row) => supportedLanguages.some((code) => !row.notes[code]),
  );
  if (runtimeInvalidTagRows.length > 0) {
    throw new Error(
      `runtime SmartCategoryID cannot decode ${runtimeInvalidTagRows.length} catalog rows`,
    );
  }
  if (duplicateEntryIDs.length > 0) {
    throw new Error(
      `duplicate Smart Start entryID values: ${duplicateEntryIDs.map(([id]) => id).slice(0, 10).join(", ")}`,
    );
  }
  const noteQualityIssues = [];
  for (const row of noteRows) {
    for (const code of supportedLanguages) {
      if (!row.notes[code]) {
        noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "missing_translation" });
      }
    }
    for (const [code, note] of Object.entries(row.notes)) {
      if (charLength(note) > noteLimit) {
        noteQualityIssues.push({ name: row.name, code, issue: `over_limit:${charLength(note)}` });
        noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: `over_limit:${charLength(note)}` });
      } else if (stripTerminalPunctuation(note) != note) {
        noteQualityIssues.push({ name: row.name, code, issue: "trailing_punctuation" });
        noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "trailing_punctuation" });
      } else if (stripLeadingPunctuation(note) != note) {
        noteQualityIssues.push({ name: row.name, code, issue: "leading_punctuation" });
        noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "leading_punctuation" });
      } else if (noteRepeatsAppName(note, row.name)) {
        noteQualityIssues.push({ name: row.name || row.normalizedName, code, issue: "repeats_app_name" });
      }
    }
  }
  const cleanedCuratedRows = [
    ["Name", "normalizedName", "defaultTag", "tager", "bundleIdentifier", "defaultNote-ZH"],
    ...curatedRows.map((row) => [
      String(row.Name ?? "").trim(),
      String(row.normalizedName ?? "").trim() || normalizeName(row.Name ?? ""),
      String(row.defaultTag ?? "").trim(),
      String(row.tager ?? "").trim(),
      normalizeBundle(row.bundleIdentifier) ?? "null",
      truncateNote(row["defaultNote-ZH"]),
    ]),
  ];
  fs.writeFileSync(outputs.reviewCSV, `${stringifyCSV(cleanedCuratedRows)}\n`);
  const runtime = {
    version: 2,
    generatedAt: new Date().toISOString(),
    noteLimit,
    supportedLanguages: [...translations.keys()].sort(),
    supportedLanguages,
    entries: finalRows.map((row) => ({
      entryID: row.entryID,
      rank: row.rank,
      name: row.name,
      normalizedName: row.normalizedName,
@@ -520,16 +1024,24 @@
    })),
  };
  fs.writeFileSync(outputs.runtimeJSON, `${JSON.stringify(runtime, null, 2)}\n`);
  const splitResources = buildSplitResources({
    finalRows,
    supportedLanguages,
    generatedAt: runtime.generatedAt,
    noteLimit,
  });
  const duplicateRows = [
    ["name", "normalizedName", "tager", "legacyDefaultTag", "runtimeDefaultTag", "bundleIdentifier", "defaultNote-ZH"],
    ["name", "normalizedName", "previousRuntimeDefaultTag", "runtimeDefaultTag", "bundleIdentifier", "defaultNote-ZH"],
    ...finalRows
      .filter((row) => row.legacyTags.join("|") !== row.tags.join("|"))
      .filter((row) => {
        const previous = previousRuntimeEntries.get(row.normalizedName);
        return previous && JSON.stringify(previous.defaultTag ?? []) !== JSON.stringify(row.tags);
      })
      .map((row) => [
        row.name,
        row.normalizedName,
        (curatedRows[row.rank - 1]?.tager ?? ""),
        row.legacyTags.join("|"),
        (previousRuntimeEntries.get(row.normalizedName)?.defaultTag ?? []).join("|"),
        row.tags.join("|"),
        row.bundleIdentifier ?? "null",
        row.noteZH ?? "",
@@ -540,8 +1052,7 @@
  const sourceLines = [
    "- curated CSV: `Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv`",
    `- curated rows: ${stats.curatedRows}`,
    `- curated rows after alias dedupe: ${curatedRows.length}`,
    `- alias duplicates removed: ${stats.aliasDuplicatesRemoved}`,
    `- curated rows used for JSON: ${curatedRows.length}`,
    `- unknown tager tokens: ${unknownTagerTokens.size}`,
  ].join("\n");
  const topTagLines = stableTagOrder
@@ -561,8 +1072,12 @@
- Review CSV: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv\`
- Runtime JSON: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json\`
- Split base JSON: \`Research/SmartStart/UltimateDefaultCatalog/${splitResources.baseInfo.file}\`
- Split manifest JSON: \`Research/SmartStart/UltimateDefaultCatalog/${splitResources.manifestInfo.file}\`
- Split notes JSON files: ${Object.keys(splitResources.notesResources).length}
- Duplicate review: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_DuplicateReview.csv\`
- Translation QA: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_TranslationQA.md\`
- Source-note translation cache: \`Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_SourceNoteTranslationCache.json\`
## Summary
@@ -570,7 +1085,24 @@
- Rows with Chinese default notes: ${noteRows.length}
- Rows with source Chinese notes from curated CSV: ${stats.rowsWithSourceNotes}
- Rows with Apple note fallback attached: ${stats.appleNotesAttached}
- Supported languages: ${supportedLanguages.length}
- Unique source notes: ${uniqueSourceNotes.length}
- Expected localized notes: ${noteRows.length * supportedLanguages.length}
- Actual localized notes: ${actualLocalizedNoteCount}
- Missing localized notes: ${missingLocalizedNotes}
- Rows missing at least one localized note: ${missingLocalizedNoteRows.length}
- Source note fingerprint: ${sourceFingerprint}
- Existing source-note cache ignored: ${translationCache.ignoredExistingCache ? "yes" : "no"}
- Translation provider: ${translationProvider}
- Translation backend: ${translationBackend}
- Translation cache hits before run: ${translationStats.cacheHits}
- Equivalent-language cache reuses: ${translationStats.reusedEquivalentCount}
- New non-Chinese translations generated: ${translationStats.translatedCount}
- Translation failures: ${translationStats.failedCount}
- zh-Hant generation: macOS CFStringTransform Simplified-Traditional from zh-Hans source notes
- Invalid tag rows: ${invalidTagRows.length}
- Runtime-invalid tag rows: ${runtimeInvalidTagRows.length}
- Duplicate entryID values: ${duplicateEntryIDs.length}
- Empty normalizedName rows: ${emptyNormalizedRows.length}
- Exact \`other\` rows: ${exactOtherRows.length}
- Mixed \`other\` rows after cleanup: ${mixedOtherRows.length}
@@ -586,9 +1118,10 @@
## Notes
- Runtime tags are generated from the curated CSV \`tager\` column, not the legacy \`defaultTag\` column.
- Runtime notes are generated only from real source notes. The generator must not synthesize notes from app names or category labels.
- Missing translations are intentionally omitted until a real translation pipeline or reviewed translation table provides them.
- Runtime JSON is generated strictly from the curated CSV. The generator does not rewrite the source CSV.
- Runtime tags are copied directly from the curated CSV \`tager\` column.
- Runtime notes are generated only from curated CSV \`defaultNote-ZH\`: \`zh-Hans\` is the source note, \`zh-Hant\` is converted from it, and all other supported languages are translated from the same source note.
- The source-note translation cache is only a fingerprint-validated acceleration artifact. Localized tag-summary notes are not valid sources for runtime notes.
`;
  fs.writeFileSync(outputs.report, report);
@@ -601,7 +1134,22 @@
- Supported languages: ${runtime.supportedLanguages.length}
- Rows with source Chinese notes: ${noteRows.length}
- Source notes: ${noteRows.length}
- Required machine/reviewed translations: ${noteRows.length * (runtime.supportedLanguages.length - 1)}
- Required zh-Hant conversions: ${noteRows.length}
- Required non-Chinese machine/reviewed translations: ${noteRows.length * (runtime.supportedLanguages.length - 2)}
- Unique source notes: ${uniqueSourceNotes.length}
- Expected localized notes including source language: ${noteRows.length * runtime.supportedLanguages.length}
- Actual localized notes: ${actualLocalizedNoteCount}
- Missing localized notes: ${missingLocalizedNotes}
- Rows missing at least one localized note: ${missingLocalizedNoteRows.length}
- Source note fingerprint: ${sourceFingerprint}
- Existing source-note cache ignored: ${translationCache.ignoredExistingCache ? "yes" : "no"}
- Translation provider: ${translationProvider}
- Translation backend: ${translationBackend}
- Translation cache hits before run: ${translationStats.cacheHits}
- Equivalent-language cache reuses: ${translationStats.reusedEquivalentCount}
- New non-Chinese translations generated: ${translationStats.translatedCount}
- Translation failures: ${translationStats.failedCount}
- zh-Hant generation: macOS CFStringTransform Simplified-Traditional from zh-Hans source notes
- Generated placeholder translations: 0
- Note quality issues: ${noteQualityIssues.length}
- Note limit: ${noteLimit}
@@ -615,15 +1163,30 @@
  return {
    finalRows: finalRows.length,
    noteRows: noteRows.length,
    splitBaseBytes: splitResources.baseInfo.bytes,
    splitNotesFiles: Object.keys(splitResources.notesResources).length,
    changedVsPrevious: stats.tagChangedVsPrevious,
    invalidTagRows: invalidTagRows.length,
    runtimeInvalidTagRows: runtimeInvalidTagRows.length,
    duplicateEntryIDs: duplicateEntryIDs.length,
    emptyNormalizedRows: emptyNormalizedRows.length,
    exactOtherRows: exactOtherRows.length,
    mixedOtherRows: mixedOtherRows.length,
    supportedLanguages: supportedLanguages.length,
    uniqueSourceNotes: uniqueSourceNotes.length,
    expectedLocalizedNotes: noteRows.length * supportedLanguages.length,
    actualLocalizedNotes: actualLocalizedNoteCount,
    missingLocalizedNotes,
    sourceFingerprint,
    sourceNoteCacheIgnoredExisting: translationCache.ignoredExistingCache,
    translationCacheHitsBeforeRun: translationStats.cacheHits,
    equivalentLanguageCacheReuses: translationStats.reusedEquivalentCount,
    newNonChineseTranslationsGenerated: translationStats.translatedCount,
    translationFailures: translationStats.failedCount,
    noteQualityIssues: noteQualityIssues.length,
    unknownTagerTokens: [...unknownTagerTokens].sort(),
    outputDir,
  };
}
console.log(JSON.stringify(build(), null, 2));
console.log(JSON.stringify(await build(), null, 2));
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.base.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json
File was deleted
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.manifest.json
New file
@@ -0,0 +1,225 @@
{
  "resourceFormatVersion": 1,
  "catalogContentVersion": 2,
  "notesVersion": 1,
  "generatedAt": "2026-05-21T12:52:12.272Z",
  "noteLimit": 80,
  "supportedLanguages": [
    "ar",
    "ar-Najdi",
    "cs",
    "da",
    "de",
    "en",
    "es",
    "fr",
    "id",
    "it",
    "ja",
    "ko",
    "ms",
    "nb",
    "nl",
    "nn",
    "no",
    "pl",
    "pt-BR",
    "ro",
    "ru",
    "sr-Cyrl",
    "sv",
    "th",
    "tr",
    "uk",
    "vi",
    "zh-Hans",
    "zh-Hant"
  ],
  "fallbackLanguages": [
    "en",
    "zh-Hans",
    "zh-Hant"
  ],
  "baseResource": {
    "file": "SmartStart_UltimateDefaultCatalog.base.json",
    "sha256": "650dcfba5bbf35816aea987baf6c29cfcab827ce72313a53b1aeb592a02949da",
    "bytes": 1025647,
    "count": 3530
  },
  "notesResources": {
    "ar": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ar.json",
      "sha256": "aa62181dfc8e7fa725a38697c3ff04e62afbd3db348c8dc2d29c792db4299c9e",
      "bytes": 642963,
      "count": 3530
    },
    "ar-Najdi": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ar-Najdi.json",
      "sha256": "2a179b7f3301d68ac11baf5b24ea4f352001854767e77c2a007a81c126215b4f",
      "bytes": 642969,
      "count": 3530
    },
    "cs": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.cs.json",
      "sha256": "ee279e32fdf309a54de26f2a8e6020c73b6b8d622e21bc9ea338b980b68005cc",
      "bytes": 526452,
      "count": 3530
    },
    "da": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.da.json",
      "sha256": "a49338d929a60eae3621ff6c3a5d1113588cbdd7c29abc71fb9dc236708e6ecb",
      "bytes": 508571,
      "count": 3530
    },
    "de": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.de.json",
      "sha256": "651c9f73a184ae64fb4d48c33ab2957bbc9bc599bc4ebb7030d577adbb1e47f6",
      "bytes": 507531,
      "count": 3530
    },
    "en": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.en.json",
      "sha256": "70066831bb47b3db8a128586ab3718002c33a67776b86df35ee40df9be8c1a03",
      "bytes": 490288,
      "count": 3530
    },
    "es": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.es.json",
      "sha256": "2850d6155b85466f7ec7fe12043566f1e33a17c9a0b4a7fea42ff3d9e5eb75db",
      "bytes": 520116,
      "count": 3530
    },
    "fr": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.fr.json",
      "sha256": "5f73f2825962be92ff8f9832a9ccb6e6be80f8e798e9e820edab84c894094bd3",
      "bytes": 517955,
      "count": 3530
    },
    "id": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.id.json",
      "sha256": "e267035ac1bd3ff41f4527f8a095b6477a1d3b30ae2918ac778565590e9ec971",
      "bytes": 498449,
      "count": 3530
    },
    "it": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.it.json",
      "sha256": "bc17e00af5b73e64735e6a3fe99f82378c70ece1d61b82443e3331b5a535b3a9",
      "bytes": 514420,
      "count": 3530
    },
    "ja": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ja.json",
      "sha256": "fe60e5986e5c71d2e0ab9c9e191b143caa5444e04b0c448c641921ec5a97b1b9",
      "bytes": 593411,
      "count": 3530
    },
    "ko": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ko.json",
      "sha256": "a33d9c6a38f2c6005e3d12cc856229d6f76a3cc48cf44ca3aeeb107c91d51baa",
      "bytes": 530870,
      "count": 3530
    },
    "ms": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ms.json",
      "sha256": "b52eb038e8824651f23687fde98776e3e972b6874c1708243e25c3fa1d43e8f4",
      "bytes": 499338,
      "count": 3530
    },
    "nb": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.nb.json",
      "sha256": "e48d0f31ce453d73a3bb471738f1efaf37089e449054331983fc895dc3e87730",
      "bytes": 503605,
      "count": 3530
    },
    "nl": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.nl.json",
      "sha256": "fceecd6c12f9192ba37ac5a2946389750d49c6bec5da225fb421878502c6ccf8",
      "bytes": 501730,
      "count": 3530
    },
    "nn": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.nn.json",
      "sha256": "5f65ede5cbcfc257baf4e9fb216b0b9e777868d6e2b8f07bdff3b5cc4ae59f1f",
      "bytes": 503605,
      "count": 3530
    },
    "no": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.no.json",
      "sha256": "643f2c80f8d81b351aaf43bf7d1fcea1004b38c8bd2b4e981ad6b685c59c1fc6",
      "bytes": 503605,
      "count": 3530
    },
    "pl": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.pl.json",
      "sha256": "5e5dfd0e7bcae767cb7012983e703e8f78845291186849031e5a4005a3adeed8",
      "bytes": 522662,
      "count": 3530
    },
    "pt-BR": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.pt-BR.json",
      "sha256": "92574cce7fd2c9f2b4ca557562c43f496fe7e1adf087aad30dc5d540197434af",
      "bytes": 519826,
      "count": 3530
    },
    "ro": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ro.json",
      "sha256": "33a7fba4c5173537b7728e31b999996354644bbc51b1e62946c7b7f59999b7a5",
      "bytes": 521453,
      "count": 3530
    },
    "ru": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.ru.json",
      "sha256": "627ee961ab1a7a97b03dcaaba9f1d580007a60ea2485921b00f47cf38bd99588",
      "bytes": 723763,
      "count": 3530
    },
    "sr-Cyrl": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.sr-Cyrl.json",
      "sha256": "8a29563ab8cef2544b39dadf43079d82b72bfdb07ccd2aeebe507dfb66345fd0",
      "bytes": 702212,
      "count": 3530
    },
    "sv": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.sv.json",
      "sha256": "a2c45d3fe12c5624b90356cce76b2b8a6b12482c0619c2b7095c1409e6882ba0",
      "bytes": 506811,
      "count": 3530
    },
    "th": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.th.json",
      "sha256": "938d00696a97e52c1d45bdcdd006ac281ce5bcb2d022b43f990e7cbb62c43bf4",
      "bytes": 902458,
      "count": 3530
    },
    "tr": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.tr.json",
      "sha256": "2c9edd6f4720963e1b957fc2f2ab1135751420330b6aed3ee86dc750355b10da",
      "bytes": 522673,
      "count": 3530
    },
    "uk": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.uk.json",
      "sha256": "b6dd043d1d3b0ce30c7b60a895426edcb6da2753f81210a14f3daddebef27c6f",
      "bytes": 713776,
      "count": 3530
    },
    "vi": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.vi.json",
      "sha256": "ad93b020bd5815ee3da00cfa1003fb34f81d656c19125e122b2117b015405771",
      "bytes": 569123,
      "count": 3530
    },
    "zh-Hans": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json",
      "sha256": "d4497072e0d45792da7aef44fa32a98eb98594d9c8833828d250981ab6832490",
      "bytes": 456457,
      "count": 3530
    },
    "zh-Hant": {
      "file": "SmartStart_UltimateDefaultCatalog.notes.zh-Hant.json",
      "sha256": "61c3e6e71382c1107259ff3746a99b35731f66df04a2bdaccb20d03adbadad9c",
      "bytes": 456457,
      "count": 3530
    }
  }
}
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ar-Najdi.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ar.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.cs.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.da.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.de.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.en.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.es.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.fr.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.id.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.it.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ja.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ko.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ms.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nb.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nl.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.nn.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.no.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.pl.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.pt-BR.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ro.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.ru.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.sr-Cyrl.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.sv.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.th.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.tr.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.uk.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.vi.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.notes.zh-Hant.json
New file
Diff too large
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_DuplicateReview.csv
File was deleted
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_Report.md
File was deleted
Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog_TranslationQA.md
File was deleted
Research/quick search/quicksearch-匹配规则C版-简化版-范围大大缩小.md
File was deleted
Research/quick search/搜索方案-保守版.md
File was deleted
SmartStart_GTD.md
File was deleted
SmartStart_ProjectPlan.md
File was deleted
X-KM/tager-20260519_142747.markdown
File was deleted
aaaindex.markdown
File was deleted
build.sh
@@ -59,6 +59,8 @@
    -framework AppKit \
    -framework SwiftUI \
    -framework Carbon \
    -framework CoreServices \
    -lcompression \
    -sdk "$SDK_PATH" \
    -target "$TARGET" \
    -Osize \
@@ -84,14 +86,70 @@
cp -r "$SWIFT_DIR/Localization" "$RESOURCES_DIR/Localization"
echo "==> Copying Smart Start catalog..."
if [ -f "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json" ]; then
    cp "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.json"
SMARTSTART_SOURCE_DIR="$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog"
for required in \
    "SmartStart_UltimateDefaultCatalog.base.json" \
    "SmartStart_UltimateDefaultCatalog.manifest.json" \
    "SmartStart_UltimateDefaultCatalog.notes.en.json" \
    "SmartStart_UltimateDefaultCatalog.notes.zh-Hans.json"; do
    if [ ! -f "$SMARTSTART_SOURCE_DIR/$required" ]; then
        echo "❌ Missing Smart Start split resource: $required"
        echo "   Run: node Research/SmartStart/Scripts/build-ultimate-default-catalog.mjs"
        exit 1
fi
if [ -f "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv" ]; then
    cp "$PROJECT_DIR/Research/SmartStart/UltimateDefaultCatalog/SmartStart_UltimateDefaultCatalog.csv" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.csv"
fi
if [ -f "$PROJECT_DIR/Research/SmartStart/AppDefaultTags_Review.csv" ]; then
    cp "$PROJECT_DIR/Research/SmartStart/AppDefaultTags_Review.csv" "$RESOURCES_DIR/SmartStartAppDefaultTags.csv"
done
python3 - "$SMARTSTART_SOURCE_DIR/SmartStart_UltimateDefaultCatalog.base.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.base.json" <<'PY'
import json
import sys
source, destination = sys.argv[1], sys.argv[2]
with open(source, "r", encoding="utf-8") as f:
    data = json.load(f)
with open(destination, "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
PY
python3 - "$SMARTSTART_SOURCE_DIR/SmartStart_UltimateDefaultCatalog.manifest.json" "$RESOURCES_DIR/SmartStartUltimateDefaultCatalog.manifest.json" <<'PY'
import json
import sys
source, destination = sys.argv[1], sys.argv[2]
with open(source, "r", encoding="utf-8") as f:
    data = json.load(f)
with open(destination, "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
PY
for notes_file in "$SMARTSTART_SOURCE_DIR"/SmartStart_UltimateDefaultCatalog.notes.*.json; do
    [ -e "$notes_file" ] || continue
    output_name="$(basename "$notes_file" | sed 's/SmartStart_UltimateDefaultCatalog/SmartStartUltimateDefaultCatalog/').deflate"
    python3 - "$notes_file" "$RESOURCES_DIR/$output_name" <<'PY'
import json
import sys
import zlib
source, destination = sys.argv[1], sys.argv[2]
with open(source, "r", encoding="utf-8") as f:
    data = json.load(f)
payload = json.dumps(data, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
compressor = zlib.compressobj(level=9, wbits=-15)
compressed = compressor.compress(payload) + compressor.flush()
with open(destination, "wb") as f:
    f.write(compressed)
PY
done
if find "$RESOURCES_DIR" -maxdepth 1 -type f \( \
    -name '*TranslationCache*' -o \
    -name '*invalid*' -o \
    -name 'SmartStartUltimateDefaultCatalog.json' -o \
    -name 'SmartStartUltimateDefaultCatalog.csv' -o \
    -name 'SmartStartUltimateDefaultCatalog.notes.*.json' \
    \) | grep -q .; then
    echo "❌ Forbidden Smart Start artifact copied into app bundle"
    find "$RESOURCES_DIR" -maxdepth 1 -type f \( \
        -name '*TranslationCache*' -o \
        -name '*invalid*' -o \
        -name 'SmartStartUltimateDefaultCatalog.json' -o \
        -name 'SmartStartUltimateDefaultCatalog.csv' -o \
        -name 'SmartStartUltimateDefaultCatalog.notes.*.json' \
        \) -print
    exit 1
fi
echo "==> Embedding PkgInfo..."
generate_icon.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generate Apptag app icon — two overlapping tags (red + blue) on macOS squircle."""
"""Generate TagLauncher app icon — two overlapping tags (red + blue) on macOS squircle."""
from PIL import Image, ImageDraw, ImageFilter
import math, os, subprocess, tempfile
plan-deepthink.md
File was deleted