{
  "format" : 1,
  "manifest" : {
    "accent" : "blue",
    "capabilities" : [
      {
        "clipboard" : {

        }
      }
    ],
    "homepage" : "https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/Clipboard",
    "hotkey" : "cmd+shift+v",
    "id" : "io.navbytes.stackbar.clipboard",
    "minimumStackbar" : "0.17.0",
    "name" : "Clipboard History",
    "summary" : "Everything you copy, searchable and one keystroke from pasting back.",
    "surfaces" : [
      "menuBar",
      "panel",
      "window",
      "settings"
    ],
    "symbol" : "doc.on.clipboard.fill",
    "triggers" : [
      {
        "launch" : {

        }
      },
      {
        "clipboard" : {

        }
      },
      {
        "every" : {
          "seconds" : 60
        }
      }
    ],
    "version" : "1.5.0"
  },
  "publisher" : {
    "name" : "navbytes",
    "publicKey" : "4dpLtXEpFXdi+6cviY/KdYuUulJPbEmCxxIK0EEN2/w="
  },
  "signature" : "ov9zEhmrWYOmldXB7Q5HCwNontUvP6YYPmBuNX8hdzQIWphR0XaulfT1jtVhvU5pT8EQCp1zXI3vxi3pQa1iDw==",
  "source" : "import StackbarKit\nimport Foundation\n\n/// Clipboard history: everything you copy, searchable, pinnable and one\n/// keystroke from being pasted back.\n///\n/// ⌘⇧V opens it from anywhere. Type to filter, ↑↓ and Return (or ⌘1...⌘9)\n/// to paste into the app you were in, ⌥Return to paste as plain text, ⌘P to\n/// pin, ⌥⌫ to delete. Formatted text keeps its formatting. Pinned items stay\n/// at the top and are never trimmed. Passwords are skipped: the host never hands out items an app\n/// marks as concealed or transient, and items from password managers (or any\n/// app you add) are ignored. Images are kept by the host and pasted back.\n@main\nfinal class Clipboard: StackbarPlugin {\n    static let manifest = Manifest(\n        id: \"io.navbytes.stackbar.clipboard\",\n        name: \"Clipboard History\",\n        version: \"1.5.0\",\n        summary: \"Everything you copy, searchable and one keystroke from pasting back.\",\n        surfaces: [.menuBar, .panel, .window, .settings],\n        // The minute tick only lets secrets expire on time.\n        triggers: [.launch, .clipboard, .every(.seconds(60))],\n        capabilities: [.clipboard],\n        hotkey: \"cmd+shift+v\",\n        minimumStackbar: \"0.17.0\",\n        homepage: \"https://github.com/navbytes/stackbar-plugins/tree/main/Productivity/Clipboard\",\n        accent: .blue,\n        symbol: \"doc.on.clipboard.fill\"\n    )\n\n    // MARK: Previews\n\n    static let previews: [PreviewScene<Clipboard>] = [\n        PreviewScene(\"hero\", \"Everything you copied, one keystroke away\") { plugin in\n            plugin.entries = samples()\n        },\n        PreviewScene(\"pinned\", \"Pin what you paste every day\") { plugin in\n            plugin.entries = samples().enumerated().map { index, entry in\n                var entry = entry\n                entry.pinned = [1, 4, 6].contains(index)\n                return entry\n            }\n        },\n        PreviewScene(\"insights\", \"What you copy, and from where\", surface: .window) { plugin in\n            plugin.entries = samples()\n            let counts = [14, 22, 9, 31, 27, 6, 3, 18, 25, 40, 33, 21, 12, 29]\n            plugin.copiesByDay = Dictionary(uniqueKeysWithValues: counts.enumerated().map { offset, count in\n                (day(Date().addingTimeInterval(Double(offset - 13) * 86400)), count)\n            })\n        },\n    ]\n\n    /// A believable afternoon of copies, newest first.\n    static func samples(now: Date = Date()) -> [Entry] {\n        let copies: [(ClipboardItem.Kind, String, String, String, Double, Int, String?)] = [\n            (.text, \"func greet(_ name: String) -> String {\\n    \\\"Hello, \\\\(name)!\\\"\\n}\", \"com.apple.dt.Xcode\", \"Xcode\", 0.5, 1, nil),\n            (.url, \"https://developer.apple.com/documentation/swiftui/view/task(priority:_:)\", \"com.apple.Safari\", \"Safari\", 3, 2, nil),\n            (.text, \"example-token-not-real-0000\", \"com.apple.Terminal\", \"Terminal\", 6, 1, \"GitHub token\"),\n            (.text, \"Thanks for the review! I moved the retry into the client and added a test for the timeout.\", \"com.apple.mail\", \"Mail\", 12, 1, nil),\n            (.text, \"brew install --cask stackbar\", \"com.apple.Terminal\", \"Terminal\", 18, 4, nil),\n            (.text, \"Invoice #2041 · 3 Nov 2026 · €1,240.00\", \"com.apple.Preview\", \"Preview\", 26, 1, nil),\n            (.text, \"git rebase -i origin/main\", \"com.apple.Terminal\", \"Terminal\", 41, 7, nil),\n            (.url, \"https://stackbar.navbytes.io/plugins/\", \"com.apple.Safari\", \"Safari\", 55, 1, nil),\n            (.text, \"Meeting notes: ship the store page on Friday, previews first, then the hero.\", \"com.apple.Notes\", \"Notes\", 80, 1, nil),\n            (.text, \"SELECT id, email FROM users WHERE created_at > now() - interval '7 days';\", \"com.apple.dt.Xcode\", \"Xcode\", 120, 2, nil),\n            (.text, \"#FF5FA2\", \"com.apple.Safari\", \"Safari\", 150, 3, nil),\n            (.text, \"42 Wallaby Way, Sydney\", \"com.apple.Notes\", \"Notes\", 210, 1, nil),\n        ]\n        return copies.enumerated().map { index, copy in\n            let (kind, text, appID, appName, minutesAgo, count, secret) = copy\n            let when = now.addingTimeInterval(-minutesAgo * 60)\n            return Entry(id: \"sample-\\(index)\", kind: kind, text: text, appID: appID, appName: appName,\n                         firstCopied: when.addingTimeInterval(-Double(count - 1) * 3600), lastCopied: when, count: count, secret: secret)\n        }\n    }\n\n    struct Entry: Codable, Equatable {\n        var id: String\n        var kind: ClipboardItem.Kind\n        var text: String?\n        var files: [String] = []\n        var imageID: String?\n        var imageSize: [Int]?\n        /// The host's copy of the original, formatting included.\n        var contentID: String?\n        var appID: String?\n        var appName: String?\n        var firstCopied: Date\n        var lastCopied: Date\n        var count: Int = 1\n        var pinned = false\n        /// What kind of secret this looks like (\"GitHub token\"), when it\n        /// looks like one: shown masked, and dropped after a while.\n        var secret: String?\n\n        /// What makes two copies the same item.\n        var fingerprint: String {\n            switch kind {\n            case .image: return \"image:\" + (imageID ?? id)\n            case .files: return \"files:\" + files.joined(separator: \"\\n\")\n            case .text, .url: return \"text:\" + (text ?? \"\")\n            }\n        }\n\n        var content: ClipboardContent {\n            switch kind {\n            case .image: return .image(id: imageID ?? \"\")\n            case .files: return .files(files)\n            case .text, .url: return contentID.map { .stored(id: $0) } ?? .text(text ?? \"\")\n            }\n        }\n\n        var plainText: String? {\n            switch kind {\n            case .text, .url, .files: return text\n            case .image: return nil\n            }\n        }\n\n        var title: String {\n            switch kind {\n            case .image:\n                let size = imageSize.map { \"\\($0.first ?? 0)×\\($0.last ?? 0)\" } ?? \"\"\n                return \"Image \\(size)\".trimmingCharacters(in: .whitespaces)\n            case .files:\n                let names = files.map { ($0 as NSString).lastPathComponent }\n                return names.count == 1 ? names[0] : \"\\(names.count) files: \" + names.prefix(3).joined(separator: \", \")\n            case .text, .url:\n                if let secret { return \"•••••••• \\(secret)\" }\n                let line = (text ?? \"\").split(whereSeparator: \\.isNewline).first.map(String.init) ?? \"\"\n                let trimmed = line.trimmingCharacters(in: .whitespaces)\n                let shown = trimmed.isEmpty ? \"(whitespace)\" : trimmed\n                return shown.count > 90 ? String(shown.prefix(89)) + \"…\" : shown\n            }\n        }\n\n        var symbol: String {\n            if secret != nil { return \"key.fill\" }\n            switch kind {\n            case .text: return contentID == nil ? \"doc.plaintext\" : \"doc.richtext\"\n            case .url: return \"link\"\n            case .files: return \"doc.on.doc\"\n            case .image: return \"photo\"\n            }\n        }\n\n        var bytes: Int { (text?.utf8.count ?? 0) + files.reduce(0) { $0 + $1.utf8.count } + 200 }\n    }\n\n    enum Order: String, CaseIterable, Codable {\n        case recent = \"Last copied\"\n        case first = \"First copied\"\n        case frequent = \"Most used\"\n    }\n\n    /// Password managers whose copies are never recorded, on top of the\n    /// concealed-type marker most of them already set.\n    static let defaultIgnored = [\n        \"com.1password.1password\", \"com.agilebits.onepassword7\", \"com.bitwarden.desktop\",\n        \"com.apple.keychainaccess\", \"com.apple.Passwords\", \"com.dashlane.dashlanephonefinal\", \"org.keepassxc.keepassxc\",\n    ]\n\n    /// Text that looks like a secret, by what kind it is. Only whole copies\n    /// count: a token inside a paragraph is left alone.\n    static let secretPatterns: [(kind: String, pattern: String)] = [\n        (\"private key\", #\"^-----BEGIN [A-Z ]*PRIVATE KEY-----\"#),\n        (\"JSON web token\", #\"^eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}$\"#),\n        (\"GitHub token\", #\"^(gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{40,})$\"#),\n        (\"API key\", #\"^sk-(ant-|proj-)?[A-Za-z0-9_-]{20,}$\"#),\n        (\"Stripe key\", #\"^(sk|rk)_(live|test)_[A-Za-z0-9]{16,}$\"#),\n        (\"AWS access key\", #\"^(AKIA|ASIA)[0-9A-Z]{16}$\"#),\n        (\"Slack token\", #\"^xox[abposr]-[A-Za-z0-9-]{10,}$\"#),\n        (\"Google API key\", #\"^AIza[0-9A-Za-z_-]{35}$\"#),\n        (\"npm token\", #\"^npm_[A-Za-z0-9]{36}$\"#),\n        (\"one-time code\", #\"^\\d{3}[ -]?\\d{3}$\"#),\n    ]\n\n    /// What kind of secret `text` looks like, or nil.\n    static func secretKind(_ text: String) -> String? {\n        let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)\n        guard !trimmed.isEmpty, trimmed.count <= 10_000 else { return nil }\n        for (kind, pattern) in secretPatterns {\n            guard let regex = try? Regex(pattern) else { continue }\n            // A pattern ending in $ must match the whole copy; the rest, its start.\n            let matched = pattern.hasSuffix(\"$\") ? trimmed.wholeMatch(of: regex) != nil : trimmed.prefixMatch(of: regex) != nil\n            if matched { return kind }\n        }\n        return isCardNumber(trimmed) ? \"card number\" : nil\n    }\n\n    /// 13 to 19 digits, spaces or dashes allowed, that pass the Luhn check.\n    static func isCardNumber(_ text: String) -> Bool {\n        guard text.allSatisfy({ $0.isASCII && ($0.isNumber || $0 == \" \" || $0 == \"-\") }) else { return false }\n        let digits = text.compactMap { $0.wholeNumberValue }\n        guard (13...19).contains(digits.count), Set(digits).count > 1 else { return false }\n        var sum = 0\n        for (index, digit) in digits.reversed().enumerated() {\n            let doubled = index % 2 == 1 ? digit * 2 : digit\n            sum += doubled > 9 ? doubled - 9 : doubled\n        }\n        return sum % 10 == 0\n    }\n\n    /// How long a secret stays in the history, in minutes: 0 doesn't keep\n    /// it at all, and -1 keeps it like anything else (still masked).\n    static let secretChoices: [(title: String, minutes: Double)] = [(\"Don't keep\", 0), (\"5 minutes\", 5), (\"15 minutes\", 15), (\"1 hour\", 60), (\"Like any copy\", -1)]\n\n    /// The plugin's 1 MB of storage, less room for settings.\n    static let storageBudget = 850_000\n    static let maxEntryCharacters = 100_000\n\n    @Stored(\"limit\") var limit = 200.0\n    @Stored(\"order\") var order = Order.recent\n    @Stored(\"pasteDirectly\") var pasteDirectly = true\n    @Stored(\"keepImages\") var keepImages = true\n    @Stored(\"ignoredApps\") var ignoredApps = Clipboard.defaultIgnored\n    @Stored(\"paused\") var paused = false\n    @Stored(\"plainByDefault\") var plainByDefault = false\n    /// Regular expressions; a copy whose text matches one is not recorded\n    /// (tokens, one-time codes).\n    @Stored(\"ignoredPatterns\") var ignoredPatterns: [String] = []\n    /// How long copies that look like secrets are kept; see `secretChoices`.\n    @Stored(\"secretMinutes\") var secretMinutes = 15.0\n    /// Copies recorded per day (yyyy-MM-dd), for the last 30 days.\n    @Stored(\"copiesByDay\") var copiesByDay: [String: Int] = [:]\n\n    /// The history, read from storage once and written back when it changes\n    /// (a `@Stored` array would be decoded on every render).\n    private var cached: [Entry]?\n    private var lastChange: Int?\n    private var ignoreNext = false\n    /// When this plugin last put something on the clipboard, so its own\n    /// writes don't count as new copies from whichever app is frontmost.\n    private var lastWrite: Date?\n    var lastError: String?\n    /// A pattern typed in Settings that isn't a regex, shown where it was typed.\n    var settingsError: String?\n\n    var entries: [Entry] {\n        get {\n            if let cached { return cached }\n            let loaded = Stackbar.storage.value(\"history\", as: [Entry].self) ?? []\n            cached = loaded\n            return loaded\n        }\n        set {\n            cached = newValue\n            Stackbar.storage.set(\"history\", newValue)\n        }\n    }\n\n    // MARK: Recording\n\n    func refresh() async throws {\n        expireSecrets()\n        guard let item = try await Stackbar.clipboard() else { return }\n        // The first read after a launch records what is on the clipboard\n        // now; if it is already in the history it just moves to the top.\n        defer { lastChange = item.changeCount }\n        guard item.changeCount != lastChange else { return }\n        if ignoreNext {\n            ignoreNext = false\n            return\n        }\n        guard !paused, !item.concealed else { return }\n        if let app = item.sourceAppID, isIgnored(app: app, name: item.sourceAppName) { return }\n        if item.kind == .image && !keepImages { return }\n        if let text = item.text, matchesIgnoredPattern(text) { return }\n        if secretMinutes == 0, item.kind == .text || item.kind == .url, let text = item.text, Self.secretKind(text) != nil { return }\n        record(item)\n    }\n\n    /// Drops unpinned secrets older than the setting allows.\n    func expireSecrets(now: Date = Date()) {\n        guard secretMinutes >= 0 else { return }\n        let limit = secretMinutes * 60\n        let history = entries\n        let kept = history.filter { $0.pinned || $0.secret == nil || now.timeIntervalSince($0.lastCopied) < limit }\n        if kept.count != history.count { entries = kept }\n    }\n\n    func record(_ item: ClipboardItem) {\n        var entry = Entry(\n            id: UUID().uuidString, kind: item.kind, text: item.text.map { String($0.prefix(Self.maxEntryCharacters)) }, files: item.files,\n            imageID: item.imageID, imageSize: item.imageSize, contentID: item.contentID, appID: item.sourceAppID, appName: item.sourceAppName,\n            firstCopied: item.copiedAt, lastCopied: item.copiedAt\n        )\n        if entry.kind != .image && entry.kind != .files && (entry.text ?? \"\").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return }\n        if entry.kind == .text || entry.kind == .url { entry.secret = entry.text.flatMap(Self.secretKind) }\n        // Our own paste coming back says nothing new about where it came from.\n        let ownWrite = item.sourceAppID == \"io.navbytes.stackbar\" || (lastWrite.map { item.copiedAt.timeIntervalSince($0) < 3 } ?? false)\n        var history = entries\n        if let index = history.firstIndex(where: { $0.fingerprint == entry.fingerprint }) {\n            var existing = history.remove(at: index)\n            existing.lastCopied = item.copiedAt\n            existing.contentID = item.contentID ?? existing.contentID\n            if !ownWrite {\n                existing.count += 1\n                existing.appID = item.sourceAppID ?? existing.appID\n                existing.appName = item.sourceAppName ?? existing.appName\n            }\n            entry = existing\n        }\n        history.insert(entry, at: 0)\n        entries = trimmed(history)\n        if !ownWrite { countCopy(on: item.copiedAt) }\n    }\n\n    func countCopy(on date: Date) {\n        var days = copiesByDay\n        days[Self.day(date), default: 0] += 1\n        let oldest = Self.day(date.addingTimeInterval(-29 * 86400))\n        copiesByDay = days.filter { $0.key >= oldest }\n    }\n\n    static func day(_ date: Date) -> String {\n        let parts = Calendar.current.dateComponents([.year, .month, .day], from: date)\n        return String(format: \"%04d-%02d-%02d\", parts.year ?? 0, parts.month ?? 0, parts.day ?? 0)\n    }\n\n    /// Drops the oldest unpinned items past the limit or the storage budget.\n    func trimmed(_ history: [Entry]) -> [Entry] {\n        var kept: [Entry] = []\n        var unpinned = 0\n        var bytes = 0\n        for entry in history {\n            bytes += entry.bytes\n            if entry.pinned {\n                kept.append(entry)\n                continue\n            }\n            guard unpinned < Int(limit), bytes <= Self.storageBudget else { continue }\n            unpinned += 1\n            kept.append(entry)\n        }\n        return kept\n    }\n\n    func matchesIgnoredPattern(_ text: String) -> Bool {\n        ignoredPatterns.contains { pattern in\n            (try? Regex(pattern)).map { text.contains($0) } ?? false\n        }\n    }\n\n    func isIgnored(app id: String, name: String?) -> Bool {\n        ignoredApps.contains { ignored in\n            ignored.caseInsensitiveCompare(id) == .orderedSame || (name.map { ignored.caseInsensitiveCompare($0) == .orderedSame } ?? false)\n        }\n    }\n\n    // MARK: Acting on an entry\n\n    func use(_ entry: Entry, paste: Bool, plain: Bool = false) async {\n        let content: ClipboardContent = plain || plainByDefault ? entry.plainText.map { .text($0) } ?? entry.content : entry.content\n        lastWrite = Date()\n        do {\n            if paste {\n                try await Stackbar.paste(content)\n            } else if case .text(let text) = content {\n                Stackbar.copy(text)\n            } else {\n                try await Stackbar.setClipboard(content)\n            }\n            lastError = nil\n        } catch {\n            lastError = \"\\(error)\"\n        }\n    }\n\n    func update(_ id: String, _ change: (inout Entry) -> Void) {\n        var history = entries\n        guard let index = history.firstIndex(where: { $0.id == id }) else { return }\n        change(&history[index])\n        entries = history\n    }\n\n    func delete(_ id: String) {\n        entries = entries.filter { $0.id != id }\n    }\n\n    func clear(keepPinned: Bool = true) {\n        entries = keepPinned ? entries.filter(\\.pinned) : []\n    }\n\n    var sorted: [Entry] {\n        let history = entries\n        switch order {\n        case .recent: return history.sorted { $0.lastCopied > $1.lastCopied }\n        case .first: return history.sorted { $0.firstCopied > $1.firstCopied }\n        case .frequent: return history.sorted { ($0.count, $0.lastCopied) > ($1.count, $1.lastCopied) }\n        }\n    }\n\n    // MARK: Surfaces\n\n    var menuBar: ViewContent {\n        Label(\"\", symbol: paused ? \"pause.circle\" : \"list.clipboard\").role(paused ? .warning : .primary)\n    }\n\n    let theme = Theme.blue\n\n    var panel: ViewContent {\n        let all = sorted\n        let pinned = all.filter(\\.pinned)\n        let history = all.filter { !$0.pinned }\n        let detail = [\"⌘⇧V anywhere\", pinned.isEmpty ? nil : \"\\(pinned.count) pinned\"].compactMap { $0 }.joined(separator: \" · \")\n        PanelHeader(paused ? \"Paused\" : all.count == 1 ? \"1 item\" : \"\\(all.count) items\", subtitle: detail,\n                    symbol: paused ? \"pause.circle.fill\" : \"list.clipboard.fill\", theme: paused ? .warning : theme) {\n            HStack(spacing: Space.xs) {\n                IconButton(\"eye.slash\", label: ignoreNext ? \"Next copy will be ignored\" : \"Ignore next copy\", theme: theme,\n                           prominence: ignoreNext ? .filled : .plain) { self.ignoreNext.toggle() }\n                IconButton(paused ? \"play.fill\" : \"pause.fill\", label: paused ? \"Resume recording\" : \"Pause recording\",\n                           theme: paused ? .warning : theme, prominence: paused ? .filled : .plain) { self.paused.toggle() }\n            }\n        }\n        if let lastError {\n            Banner(lastError)\n        }\n        if all.isEmpty {\n            if paused {\n                EmptyState(\"Recording is paused\", symbol: \"pause.circle\", message: \"Copies aren't kept until you resume.\", theme: .warning) {\n                    ActionButton(\"Resume\", symbol: \"play.fill\", theme: theme, prominent: true) { self.paused = false }\n                }\n            } else {\n                EmptyState(\"Nothing copied yet\", symbol: \"list.clipboard\", message: \"Copy something in any app and it shows up here.\", theme: theme)\n            }\n        } else {\n            if let latest = history.first { lastCopied(latest) }\n            List(searchable: true) {\n                if !pinned.isEmpty {\n                    Section(\"Pinned\") {\n                        ForEach(pinned) { entry in row(entry) }\n                    }\n                }\n                if !history.isEmpty {\n                    Section(pinned.isEmpty ? nil : \"History\") {\n                        ForEach(history) { entry in row(entry) }\n                    }\n                }\n            }\n        }\n    }\n\n    /// The newest copy as a card: its first lines in a code face, where it\n    /// came from, and one button to use it again.\n    func lastCopied(_ entry: Entry) -> some StackbarView {\n        HeroCard(theme: theme, look: .glow, spacing: Space.s) {\n            HStack(spacing: Space.s) {\n                IconTile(entry.symbol, theme: theme, size: 30).symbolEffect(.bounce)\n                VStack(spacing: 0) {\n                    Text(\"LAST COPIED\").typography(.overline).foreground(theme.textPaint)\n                    Text([entry.appName, Self.relative(entry.lastCopied)].compactMap { $0 }.joined(separator: \" · \")).role(.secondary).typography(.caption)\n                }\n                Spacer(minLength: Space.xs)\n                IconButton(pasteDirectly ? \"arrow.down.doc.fill\" : \"doc.on.doc.fill\", label: pasteDirectly ? \"Paste\" : \"Copy\", theme: theme,\n                           prominence: .filled, size: 32, key: \"use-latest\") { await self.use(entry, paste: self.pasteDirectly) }\n            }\n            Text(Self.snippet(entry))\n                .typography(Typography(size: 12, design: entry.kind == .text ? .monospaced : .default))\n                .padding(Space.s)\n                .frame(fillWidth: true, alignment: .leading)\n                .background(.raised, in: .roundedRectangle(cornerRadius: Radius.medium))\n        }\n    }\n\n    /// Up to three lines of a copy, each cut to fit the card.\n    static func snippet(_ entry: Entry) -> String {\n        guard entry.kind == .text || entry.kind == .url, entry.secret == nil, let text = entry.text else { return entry.title }\n        let lines = text.split(whereSeparator: \\.isNewline).map { $0.trimmingCharacters(in: .whitespaces) }.filter { !$0.isEmpty }\n        let shown = lines.prefix(3).map { $0.count > 52 ? String($0.prefix(51)) + \"…\" : $0 }\n        let more = lines.count > 3 ? \"\\n+ \\(lines.count - 3) more lines\" : \"\"\n        return shown.isEmpty ? entry.title : shown.joined(separator: \"\\n\") + more\n    }\n\n    /// The window has room for the history and what it says about you.\n    var window: ViewContent {\n        panel\n        insights\n    }\n\n    @ViewBuilder var insights: ViewContent {\n        let all = entries\n        if !all.isEmpty {\n            Card(\"Insights\") {\n                let calendar = Calendar.current\n                let dates = (0..<14).reversed().map { Date().addingTimeInterval(Double(-$0) * 86400) }\n                Chart(\"Copies, last 14 days\", values: dates.map { Double(copiesByDay[Self.day($0)] ?? 0) }, kind: .bar, labels: dates.map { String(calendar.component(.day, from: $0)) }, highlight: 13)\n                let byApp = Dictionary(grouping: all, by: { $0.appName ?? \"Unknown\" }).mapValues { $0.reduce(0) { $0 + $1.count } }.sorted { $0.value > $1.value }\n                let top = byApp.prefix(5)\n                let rest = byApp.dropFirst(5).reduce(0) { $0 + $1.value }\n                Chart(\"Where your copies come from\", values: top.map { Double($0.value) } + (rest > 0 ? [Double(rest)] : []), kind: .donut, labels: top.map(\\.key) + (rest > 0 ? [\"Other\"] : []))\n                let kinds: [(String, Int)] = [(\"Text\", all.filter { $0.kind == .text }.count), (\"Links\", all.filter { $0.kind == .url }.count), (\"Images\", all.filter { $0.kind == .image }.count), (\"Files\", all.filter { $0.kind == .files }.count)].filter { $0.1 > 0 }\n                Chart(\"What you copy\", values: kinds.map { Double($0.1) }, kind: .donut, role: .success, labels: kinds.map(\\.0))\n            }\n        }\n    }\n\n    /// Stackbar's Settings window, so the panel stays about the history.\n    var settings: ViewContent {\n        Section(\"Pasting\") {\n            Toggle(\"Paste into the app on click\", isOn: pasteDirectly) { self.pasteDirectly = $0 }\n            Toggle(\"Always paste as plain text\", isOn: plainByDefault) { self.plainByDefault = $0 }\n        }\n        Section(\"History\") {\n            Picker(\"Order\", options: Order.allCases.map(\\.rawValue), selection: order.rawValue, style: .segmented) { self.order = Order(rawValue: $0) ?? .recent }\n            Slider(\"Keep \\(Int(limit)) items\", value: limit, in: 25...1000, step: 25) { value in\n                self.limit = value\n                self.entries = self.trimmed(self.entries)\n            }\n            Toggle(\"Keep images\", isOn: keepImages) { self.keepImages = $0 }\n            Button(\"Clear history (keeps pinned)\", symbol: \"trash\", role: .critical) { self.clear() }\n        }\n        Section(\"Ignored apps\") {\n            ignoredAppsContent\n        }\n        Section(\"Secrets\") {\n            Picker(\"Keep copies that look like secrets\", options: Self.secretChoices.map(\\.title), selection: Self.secretChoices.first { $0.minutes == secretMinutes }?.title ?? \"15 minutes\", key: \"secrets\") { title in\n                self.secretMinutes = Self.secretChoices.first { $0.title == title }?.minutes ?? 15\n                if self.secretMinutes == 0 { self.entries = self.entries.filter { $0.pinned || $0.secret == nil } }\n                self.expireSecrets()\n            }\n            Text(\"API keys and tokens (GitHub, OpenAI, Anthropic, AWS, Stripe, Slack, Google, npm), JSON web tokens, private keys, card numbers and six-digit codes are shown masked, kept out of agent tools, and dropped after this long unless you pin them.\").style(.caption).role(.secondary)\n        }\n        Section(\"Ignored text\") {\n            ignoredText\n        }\n        Section {\n            Button(\"Clear everything, including pinned\", symbol: \"trash.slash\", role: .critical) { self.clear(keepPinned: false) }\n        }\n    }\n\n    @ViewBuilder var ignoredAppsContent: ViewContent {\n        TextField(\"Ignore an app (bundle id or name)\") { app in\n            let app = app.trimmingCharacters(in: .whitespaces)\n            guard !app.isEmpty, !self.ignoredApps.contains(app) else { return }\n            self.ignoredApps.append(app)\n        }\n        if let recent = entries.first(where: { $0.appID != nil && $0.appID != \"io.navbytes.stackbar\" }), let app = recent.appID, !isIgnored(app: app, name: recent.appName) {\n            Button(\"Ignore \\(recent.appName ?? app)\", symbol: \"hand.raised\", key: \"ignore-recent\") { self.ignoredApps.append(app) }\n        }\n        ForEach(ignoredApps) { app in\n            Row(app, subtitle: \"Ignored\", symbol: \"hand.raised\", key: \"ignored-\\(app)\")\n                .action(\"Stop ignoring\") { self.ignoredApps.removeAll { $0 == app } }\n        }\n    }\n\n    @ViewBuilder var ignoredText: ViewContent {\n        TextField(\"Ignore copies matching a pattern (regex)\") { pattern in\n            let pattern = pattern.trimmingCharacters(in: .whitespaces)\n            guard !pattern.isEmpty, !self.ignoredPatterns.contains(pattern) else { return }\n            guard (try? Regex(pattern)) != nil else {\n                self.settingsError = \"\\\"\\(pattern)\\\" isn't a valid regular expression\"\n                return\n            }\n            self.settingsError = nil\n            self.ignoredPatterns.append(pattern)\n            self.entries = self.entries.filter { $0.pinned || !self.matchesIgnoredPattern($0.text ?? \"\") }\n        }\n        if let settingsError { Text(settingsError).style(.caption).role(.warning) }\n        ForEach(ignoredPatterns) { pattern in\n            Row(pattern, subtitle: \"Copies matching this aren't recorded\", symbol: \"line.3.horizontal.decrease\", key: \"pattern-\\(pattern)\")\n                .action(\"Remove\") { self.ignoredPatterns.removeAll { $0 == pattern } }\n        }\n    }\n\n    /// `paste` picks the primary action: the panel follows the setting, the\n    /// palette always pastes.\n    func row(_ entry: Entry) -> Row {\n        row(entry, paste: pasteDirectly)\n    }\n\n    func row(_ entry: Entry, paste pasteDirectly: Bool) -> Row {\n        let source = [entry.appName, Self.relative(entry.lastCopied), entry.count > 1 ? \"×\\(entry.count)\" : nil, entry.kind == .text && entry.secret == nil ? \"\\(entry.text?.count ?? 0) chars\" : nil]\n            .compactMap { $0 }.joined(separator: \" · \")\n        var row = Row(entry.title, subtitle: source, symbol: entry.pinned ? \"pin.fill\" : entry.symbol, key: entry.id).appIcon(entry.appID)\n        if let preview = preview(entry) { row = preview(row) }\n        row = row.action(pasteDirectly ? \"Paste\" : \"Copy\") { await self.use(entry, paste: self.pasteDirectly) }\n        if entry.plainText != nil, entry.contentID != nil || entry.kind == .files {\n            row = row.action(pasteDirectly ? \"Paste as plain text\" : \"Copy as plain text\", shortcut: \"option+return\") { await self.use(entry, paste: self.pasteDirectly, plain: true) }\n        }\n        row = row.action(pasteDirectly ? \"Copy\" : \"Paste\") { await self.use(entry, paste: !self.pasteDirectly) }\n            .action(entry.pinned ? \"Unpin\" : \"Pin\", shortcut: \"cmd+p\") { self.update(entry.id) { $0.pinned.toggle() } }\n        if entry.kind == .url, let url = entry.text {\n            row = row.action(\"Open link\") { Stackbar.open(url.trimmingCharacters(in: .whitespacesAndNewlines)) }\n        }\n        return row.action(\"Delete\", shortcut: \"option+delete\", role: .critical) { self.delete(entry.id) }\n    }\n\n    /// The selected row's preview: the text past its first line, the image,\n    /// or the files, with when and how often it was copied.\n    func preview(_ entry: Entry) -> ((Row) -> Row)? {\n        var caption = [\"First copied \\(Self.relative(entry.firstCopied))\"]\n        if entry.count > 1 { caption.append(\"copied \\(entry.count) times\") }\n        switch entry.kind {\n        case .image:\n            if let size = entry.imageSize, size.count == 2 { caption.insert(\"\\(size[0]) × \\(size[1])\", at: 0) }\n            return { $0.preview(imageID: entry.imageID, caption: caption.joined(separator: \" · \")) }\n        case .files:\n            return { $0.preview(text: entry.files.joined(separator: \"\\n\"), caption: caption.joined(separator: \" · \")) }\n        case .text, .url:\n            if let secret = entry.secret {\n                let expiry = secretMinutes > 0 && !entry.pinned ? \" · gone \\(Self.relative(entry.lastCopied.addingTimeInterval(secretMinutes * 60), now: Date(), future: true))\" : \"\"\n                return { $0.preview(text: \"Hidden: this looks like a \\(secret).\", caption: caption.joined(separator: \" · \") + expiry) }\n            }\n            guard let text = entry.text, text.count > 60 || text.contains(\"\\n\") else { return nil }\n            let lines = text.split(separator: \"\\n\", omittingEmptySubsequences: false).count\n            caption.insert(\"\\(text.count) characters\" + (lines > 1 ? \", \\(lines) lines\" : \"\"), at: 0)\n            return { $0.preview(text: String(text.prefix(2000)), caption: caption.joined(separator: \" · \")) }\n        }\n    }\n\n    static func relative(_ date: Date, now: Date = Date(), future: Bool = false) -> String {\n        if future {\n            let minutes = max(1, Int((date.timeIntervalSince(now) / 60).rounded(.up)))\n            return minutes < 60 ? \"in \\(minutes) min\" : \"in \\(minutes / 60)h\"\n        }\n        let seconds = now.timeIntervalSince(date)\n        if seconds < 60 { return \"just now\" }\n        if seconds < 3600 { return \"\\(Int(seconds / 60))m ago\" }\n        if seconds < 86400 { return \"\\(Int(seconds / 3600))h ago\" }\n        return date.formatted(date: .abbreviated, time: .omitted)\n    }\n\n    // MARK: Commands\n\n    var commands: [Command] {\n        Command(\"search\", title: \"Search Clipboard History\", subtitle: \"Paste anything you copied\", symbol: \"list.clipboard\", keywords: [\"paste\", \"copy\", \"history\"])\n            .view { _ in\n                let all = self.sorted\n                if all.isEmpty {\n                    ContentUnavailableView(\"Nothing copied yet\", symbol: \"list.clipboard\", message: self.paused ? \"Recording is paused.\" : \"Copy something in any app and it shows up here.\")\n                } else {\n                    List(searchable: true) {\n                        let pinned = all.filter(\\.pinned)\n                        if !pinned.isEmpty {\n                            Section(\"Pinned\") {\n                                ForEach(pinned) { entry in self.row(entry, paste: true) }\n                            }\n                        }\n                        Section(pinned.isEmpty ? nil : \"History\") {\n                            ForEach(all.filter { !$0.pinned }) { entry in self.row(entry, paste: true) }\n                        }\n                    }\n                }\n            }\n        Command(\"paste-plain\", title: \"Paste Last Copy as Plain Text\", symbol: \"doc.plaintext\", keywords: [\"clipboard\", \"format\"]) { _ in\n            guard let latest = self.sorted.first(where: { !$0.pinned }) ?? self.sorted.first else { return .message(\"Nothing copied yet\") }\n            await self.use(latest, paste: true, plain: true)\n            if let error = self.lastError { return .failure(error) }\n            return .done\n        }\n        Command(\"pause\", title: self.paused ? \"Resume Clipboard History\" : \"Pause Clipboard History\", symbol: self.paused ? \"play.fill\" : \"pause.fill\", keywords: [\"clipboard\", \"record\"]) { _ in\n            self.paused.toggle()\n            return .message(self.paused ? \"Clipboard history paused\" : \"Clipboard history recording\")\n        }\n        Command(\"ignore-next\", title: \"Ignore Next Copy\", symbol: \"eye.slash\", keywords: [\"clipboard\", \"password\", \"secret\"]) { _ in\n            self.ignoreNext = true\n            return .message(\"The next copy won't be kept\")\n        }\n    }\n\n    // MARK: Agents\n\n    var tools: [AgentTool] {\n        AgentTool(\"recent_clips\", description: \"The most recent clipboard items, newest first\", parameters: [ToolParameter(\"count\", .number, description: \"How many (default 10)\", required: false)]) { arguments in\n            let count = Int(arguments.number(\"count\") ?? 10)\n            return self.sorted.prefix(max(1, min(count, 50))).enumerated().map { \"\\($0.offset + 1). \\($0.element.title)\" }.joined(separator: \"\\n\")\n        }\n        AgentTool(\"search_clips\", description: \"Clipboard items containing the text, newest first, in full\", parameters: [ToolParameter(\"query\", description: \"Text to look for\")]) { arguments in\n            let query = arguments.string(\"query\") ?? \"\"\n            let found = self.sorted.filter { $0.secret == nil && ($0.text ?? \"\").localizedCaseInsensitiveContains(query) }.prefix(10)\n            return found.isEmpty ? \"Nothing in the history contains \\\"\\(query)\\\".\" : found.map { $0.text ?? $0.title }.joined(separator: \"\\n---\\n\")\n        }\n        AgentTool(\"copy_clip\", description: \"Put a history item back on the clipboard, by its position in recent_clips\", parameters: [ToolParameter(\"position\", .number, description: \"1 is the newest\")]) { arguments in\n            let position = Int(arguments.number(\"position\") ?? 0)\n            let items = self.sorted\n            guard position >= 1, position <= items.count else { return \"There is no item \\(position).\" }\n            await self.use(items[position - 1], paste: false)\n            return \"Copied \\(items[position - 1].title)\"\n        }\n    }\n}\n",
  "sourceDigest" : "4501ef12321b44e07ca2cf72c83a8ee7a2647105d187a45ee0ae43d267d70922"
}