From e0ca16e316911b4e07ebc38464de9b16524608d8 Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 00:04:57 +0200 Subject: [PATCH 1/5] Add crawler suggestion sections --- .../CrawlBarCrawlerClassifier.swift | 84 +++++++++++++++++++ Sources/CrawlBar/Menu/CrawlBarMenuViews.swift | 62 ++++++++++++++ .../Menu/CrawlBarStatusMenuBuilder.swift | 70 ++++++++++++++-- Sources/CrawlBar/Menu/MenuModel.swift | 34 ++++++-- .../Settings/GeneralSettingsView.swift | 18 ++-- Sources/CrawlBar/Settings/SettingsModel.swift | 20 +++++ .../Settings/SettingsModelStatusActions.swift | 7 +- .../CrawlBar/Settings/SettingsSidebar.swift | 45 ++++++++++ Sources/CrawlBar/Settings/SettingsView.swift | 35 ++++---- .../BuiltInCrawlApps+Birdclaw.swift | 3 +- .../BuiltInCrawlApps+Discrawl.swift | 3 +- .../BuiltInCrawlApps+Gitcrawl.swift | 3 +- .../BuiltInApps/BuiltInCrawlApps+Gogcli.swift | 3 +- .../BuiltInCrawlApps+Graincrawl.swift | 3 +- .../BuiltInCrawlApps+Notcrawl.swift | 3 +- .../BuiltInCrawlApps+Slacrawl.swift | 3 +- .../BuiltInCrawlApps+Telecrawl.swift | 3 +- .../BuiltInApps/BuiltInCrawlApps+Wacli.swift | 3 +- .../BuiltInApps/BuiltInCrawlApps.swift | 7 ++ .../Manifest/CrawlAppManifest.swift | 43 ++++++++++ .../Manifest/CrawlAppManifestInstall.swift | 17 ---- .../CrawlAppManifestInstallSuggestion.swift | 42 ++++++++++ Sources/CrawlBarSelfTest/SelfTestConfig.swift | 1 + 23 files changed, 439 insertions(+), 73 deletions(-) create mode 100644 Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift delete mode 100644 Sources/CrawlBarCore/Manifest/CrawlAppManifestInstall.swift create mode 100644 Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift diff --git a/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift new file mode 100644 index 0000000..d108c8e --- /dev/null +++ b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift @@ -0,0 +1,84 @@ +import CrawlBarCore +import Foundation + +enum CrawlBarCrawlerCategory: CaseIterable, Hashable, Identifiable { + case my + case suggested + case more + + var id: Self { self } + + var title: String { + switch self { + case .my: + "My Crawlers" + case .suggested: + "Suggested" + case .more: + "More Crawlers" + } + } +} + +enum CrawlBarCrawlerClassifier { + @MainActor + static func category( + app: CrawlBarAppConfig?, + installation: CrawlAppInstallation?) + -> CrawlBarCrawlerCategory + { + if self.isMyCrawler(app: app, installation: installation) { + return .my + } + guard let installation, self.matchesSuggestion(installation: installation) else { + return .more + } + return .suggested + } + + nonisolated static func isMyCrawler( + app: CrawlBarAppConfig?, + installation: CrawlAppInstallation?) + -> Bool + { + installation?.binaryPath != nil || self.hasUserCrawlerConfig(app) + } + + nonisolated static func statusInstallations( + _ installations: [CrawlAppInstallation], + appConfigsByID: [CrawlAppID: CrawlBarAppConfig]) + -> [CrawlAppInstallation] + { + installations.filter { installation in + guard let config = appConfigsByID[installation.id], config.enabled else { return false } + return self.isMyCrawler(app: config, installation: installation) + } + } + + @MainActor + static func matchesSuggestion(installation: CrawlAppInstallation) -> Bool { + guard let suggestion = installation.manifest.suggestion else { return false } + switch suggestion.kind { + case .always: + return true + case .app: + return suggestion.bundleIDs.contains { bundleID in + CrawlBarNativeAppLocator.url(for: bundleID) != nil + } + } + } + + nonisolated private static func hasUserCrawlerConfig(_ app: CrawlBarAppConfig?) -> Bool { + guard let app else { return false } + return app.binaryPath?.nilIfBlank != nil + || app.configPath?.nilIfBlank != nil + || app.refreshFrequency != nil + || app.autoRefreshEnabled + || app.shareEnabled + || app.shareAfterRefresh + || app.preferredRefreshAction != "refresh" + || app.preferredShareAction != "publish" + || app.preferredUpdateAction != "update" + || !app.configValues.isEmpty + } +} diff --git a/Sources/CrawlBar/Menu/CrawlBarMenuViews.swift b/Sources/CrawlBar/Menu/CrawlBarMenuViews.swift index f972241..92bb389 100644 --- a/Sources/CrawlBar/Menu/CrawlBarMenuViews.swift +++ b/Sources/CrawlBar/Menu/CrawlBarMenuViews.swift @@ -18,6 +18,7 @@ struct CrawlBarMenuHeaderView: View { let statuses: [CrawlAppID: CrawlAppStatus] let isRefreshing: Bool let refreshFrequency: RefreshFrequency + let suggestedCount: Int var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -57,6 +58,12 @@ struct CrawlBarMenuHeaderView: View { let current = self.installations.filter { self.effectiveState(for: $0, status: self.statuses[$0.id]) == .current }.count + if self.installations.isEmpty, self.suggestedCount > 0 { + return "\(self.suggestedCount) suggested · \(CrawlBarFrequencyLabel.text(for: self.refreshFrequency))" + } + if self.suggestedCount > 0 { + return "\(current) current · \(self.suggestedCount) suggested" + } return "\(current) current · \(CrawlBarFrequencyLabel.text(for: self.refreshFrequency))" } @@ -72,6 +79,61 @@ struct CrawlBarMenuHeaderView: View { } } +struct CrawlBarMenuSectionHeaderView: View { + let title: String + + var body: some View { + Text(self.title) + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.secondary) + .padding(.horizontal, CrawlBarMenuStyle.cardHorizontalPadding) + .padding(.top, 4) + .padding(.bottom, 1) + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct CrawlBarMenuSuggestionView: View { + let installation: CrawlAppInstallation + let onOpen: () -> Void + @Environment(\.crawlBarMenuItemHighlighted) private var isHighlighted + + var body: some View { + HStack(alignment: .center, spacing: 10) { + CrawlBarBrandIcon(manifest: self.installation.manifest, appID: self.installation.id) + .frame(width: 26, height: 26) + .opacity(0.82) + VStack(alignment: .leading, spacing: 3) { + Text(CrawlBarCrawlerTitle.text(for: self.installation.id, manifest: self.installation.manifest)) + .font(.subheadline) + .lineLimit(1) + Text(self.detailText) + .font(.caption) + .foregroundStyle(CrawlBarMenuHighlightStyle.secondary(self.isHighlighted)) + .lineLimit(1) + } + Spacer(minLength: 0) + } + .padding(.horizontal, CrawlBarMenuStyle.cardHorizontalPadding) + .padding(.vertical, 6) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture(perform: self.onOpen) + } + + private var detailText: String { + guard let suggestion = self.installation.manifest.suggestion else { + return "Connect \(self.installation.manifest.binary.name)" + } + switch suggestion.kind { + case .always: + return "Connect \(self.installation.manifest.binary.name)" + case .app: + return "\(suggestion.name) is installed" + } + } +} + struct CrawlBarMenuCardView: View { let installation: CrawlAppInstallation let status: CrawlAppStatus? diff --git a/Sources/CrawlBar/Menu/CrawlBarStatusMenuBuilder.swift b/Sources/CrawlBar/Menu/CrawlBarStatusMenuBuilder.swift index bb3e69f..19ca7c7 100644 --- a/Sources/CrawlBar/Menu/CrawlBarStatusMenuBuilder.swift +++ b/Sources/CrawlBar/Menu/CrawlBarStatusMenuBuilder.swift @@ -22,26 +22,45 @@ struct CrawlBarStatusMenuBuilder { selectors: CrawlBarStatusMenuActionSelectors, openSettings: @escaping (CrawlAppID?) -> Void) { - let visibleInstallations = model.visibleInstallations + let myInstallations = model.myMenuInstallations + let suggestedInstallations = model.suggestedMenuInstallations menu.autoenablesItems = false menu.removeAllItems() menu.addItem(self.viewItem(for: CrawlBarMenuHeaderView( - installations: visibleInstallations, + installations: myInstallations, statuses: model.statuses, isRefreshing: model.isRefreshing, - refreshFrequency: model.refreshFrequency), enabled: false)) + refreshFrequency: model.refreshFrequency, + suggestedCount: suggestedInstallations.count), enabled: false)) - self.addCrawlers( - visibleInstallations, + self.addMyCrawlers( + myInstallations, statuses: model.statuses, to: menu, target: target, selector: selectors.showSettingsForApp, openSettings: openSettings) + self.addSuggestedCrawlers( + suggestedInstallations, + hasMyCrawlers: !myInstallations.isEmpty, + to: menu, + target: target, + selector: selectors.showSettingsForApp, + openSettings: openSettings) + menu.addItem(.separator()) + if model.moreCrawlerCount > 0 { + menu.addItem(self.actionItem( + title: "More Crawlers in Settings...", + action: selectors.showSettings, + target: target, + systemImage: "square.grid.2x2")) + menu.addItem(.separator()) + } + let refreshTitle = model.isRefreshing ? "Refreshing..." : "Refresh All" let refreshItem = self.actionItem( title: refreshTitle, @@ -78,7 +97,7 @@ struct CrawlBarStatusMenuBuilder { self.itemFactory.clearHighlights(in: menu) } - private func addCrawlers( + private func addMyCrawlers( _ installations: [CrawlAppInstallation], statuses: [CrawlAppID: CrawlAppStatus], to menu: NSMenu, @@ -101,6 +120,28 @@ struct CrawlBarStatusMenuBuilder { } } + private func addSuggestedCrawlers( + _ installations: [CrawlAppInstallation], + hasMyCrawlers: Bool, + to menu: NSMenu, + target: AnyObject, + selector: Selector, + openSettings: @escaping (CrawlAppID?) -> Void) + { + guard !installations.isEmpty else { return } + if hasMyCrawlers { + menu.addItem(self.viewItem(for: CrawlBarMenuSeparatorRowView(), enabled: false)) + } + menu.addItem(self.viewItem(for: CrawlBarMenuSectionHeaderView(title: "Suggested"), enabled: false)) + for installation in installations { + menu.addItem(self.suggestedMenuItem( + for: installation, + target: target, + selector: selector, + openSettings: openSettings)) + } + } + private func appMenuItem( for installation: CrawlAppInstallation, status: CrawlAppStatus?, @@ -120,6 +161,23 @@ struct CrawlBarStatusMenuBuilder { selector: selector) } + private func suggestedMenuItem( + for installation: CrawlAppInstallation, + target: AnyObject, + selector: Selector, + openSettings: @escaping (CrawlAppID?) -> Void) + -> NSMenuItem + { + let row = CrawlBarMenuSuggestionView( + installation: installation, + onOpen: { openSettings(installation.id) }) + return self.crawlerItem( + for: row, + installation: installation, + target: target, + selector: selector) + } + private func crawlerItem( for content: some View, installation: CrawlAppInstallation, diff --git a/Sources/CrawlBar/Menu/MenuModel.swift b/Sources/CrawlBar/Menu/MenuModel.swift index 5d1386f..86f4bd4 100644 --- a/Sources/CrawlBar/Menu/MenuModel.swift +++ b/Sources/CrawlBar/Menu/MenuModel.swift @@ -39,20 +39,37 @@ final class CrawlBarMenuModel: NSObject { NotificationCenter.default.removeObserver(self) } - var visibleInstallations: [CrawlAppInstallation] { + var myMenuInstallations: [CrawlAppInstallation] { self.installations.filter { installation in guard installation.manifest.availability == .available else { return false } - return self.appConfigs[installation.id]?.showInMenuBar ?? true + guard let config = self.appConfigs[installation.id] else { return false } + guard config.enabled, config.showInMenuBar else { return false } + return CrawlBarCrawlerClassifier.isMyCrawler(app: config, installation: installation) } } - var statusTargetInstallations: [CrawlAppInstallation] { + var suggestedMenuInstallations: [CrawlAppInstallation] { self.installations.filter { installation in guard installation.manifest.availability == .available else { return false } - return self.appConfigs[installation.id]?.enabled ?? installation.enabled + guard let config = self.appConfigs[installation.id] else { return false } + guard config.enabled, config.showInMenuBar else { return false } + return CrawlBarCrawlerClassifier.category(app: config, installation: installation) == .suggested } } + var moreCrawlerCount: Int { + self.installations.filter { installation in + guard let config = self.appConfigs[installation.id] else { return false } + return CrawlBarCrawlerClassifier.category(app: config, installation: installation) == .more + }.count + } + + var statusTargetInstallations: [CrawlAppInstallation] { + CrawlBarCrawlerClassifier.statusInstallations( + self.installations, + appConfigsByID: self.appConfigs) + } + func appConfig(for id: CrawlAppID) -> CrawlBarAppConfig? { self.appConfigs[id] } @@ -72,15 +89,14 @@ final class CrawlBarMenuModel: NSObject { let generation = UUID() self.refreshGeneration = generation self.isRefreshing = true - let appConfigs = self.appConfigs let registry = self.registry let statusService = self.statusService + let appConfigs = self.appConfigs self.refreshTask = Task.detached { let installations = (try? registry.installationsForStatus(includeDisabled: true)) ?? [] - let statusInstallations = installations.filter { installation in - guard installation.manifest.availability == .available else { return false } - return appConfigs[installation.id]?.enabled ?? installation.enabled - } + let statusInstallations = CrawlBarCrawlerClassifier.statusInstallations( + installations, + appConfigsByID: appConfigs) await MainActor.run { guard self.refreshGeneration == generation else { return } self.installations = installations diff --git a/Sources/CrawlBar/Settings/GeneralSettingsView.swift b/Sources/CrawlBar/Settings/GeneralSettingsView.swift index c07f45b..ae7cc9a 100644 --- a/Sources/CrawlBar/Settings/GeneralSettingsView.swift +++ b/Sources/CrawlBar/Settings/GeneralSettingsView.swift @@ -101,9 +101,9 @@ struct CrawlBarGeneralSettingsView: View { CrawlBarPanel(title: "Crawler Inventory") { Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) { GridRow { - CrawlBarFact(label: "Ready", value: "\(self.readyCount)") - CrawlBarFact(label: "Missing CLI", value: "\(self.missingCount)") - CrawlBarFact(label: "Coming Soon", value: "\(self.comingSoonCount)") + CrawlBarFact(label: "My Crawlers", value: "\(self.count(.my))") + CrawlBarFact(label: "Suggested", value: "\(self.count(.suggested))") + CrawlBarFact(label: "More Crawlers", value: "\(self.count(.more))") } } } @@ -118,15 +118,7 @@ struct CrawlBarGeneralSettingsView: View { .padding(.vertical, CrawlBarSettingsLayout.detailVerticalPadding) } - private var readyCount: Int { - self.model.installations.values.filter { $0.manifest.availability == .available && $0.binaryPath != nil }.count - } - - private var missingCount: Int { - self.model.installations.values.filter { $0.manifest.availability == .available && $0.binaryPath == nil }.count - } - - private var comingSoonCount: Int { - self.model.installations.values.filter { $0.manifest.availability == .comingSoon }.count + private func count(_ section: CrawlBarCrawlerCategory) -> Int { + self.model.crawlerSections.first { $0.kind == section }?.apps.count ?? 0 } } diff --git a/Sources/CrawlBar/Settings/SettingsModel.swift b/Sources/CrawlBar/Settings/SettingsModel.swift index b854b82..f7356cf 100644 --- a/Sources/CrawlBar/Settings/SettingsModel.swift +++ b/Sources/CrawlBar/Settings/SettingsModel.swift @@ -66,6 +66,19 @@ final class CrawlBarSettingsModel: NSObject, ObservableObject { NotificationCenter.default.removeObserver(self) } + var crawlerSections: [CrawlBarCrawlerSection] { + var grouped: [CrawlBarCrawlerCategory: [CrawlBarAppConfig]] = [:] + for app in self.apps { + let installation = self.installations[app.id] + let section = self.listSection(for: app, installation: installation) + grouped[section, default: []].append(app) + } + return CrawlBarCrawlerCategory.allCases.compactMap { section in + guard let apps = grouped[section], !apps.isEmpty else { return nil } + return CrawlBarCrawlerSection(kind: section, apps: apps) + } + } + var sidebarSelectionIsValid: Bool { switch self.selectedSidebarItem { case .general: @@ -77,4 +90,11 @@ final class CrawlBarSettingsModel: NSObject, ObservableObject { } } + func listSection( + for app: CrawlBarAppConfig, + installation: CrawlAppInstallation?) + -> CrawlBarCrawlerCategory + { + CrawlBarCrawlerClassifier.category(app: app, installation: installation) + } } diff --git a/Sources/CrawlBar/Settings/SettingsModelStatusActions.swift b/Sources/CrawlBar/Settings/SettingsModelStatusActions.swift index e726cae..7160fbf 100644 --- a/Sources/CrawlBar/Settings/SettingsModelStatusActions.swift +++ b/Sources/CrawlBar/Settings/SettingsModelStatusActions.swift @@ -14,10 +14,9 @@ extension CrawlBarSettingsModel { self.refreshTask = Task.detached { let installations = (try? registry.installationsForStatus(includeDisabled: true)) ?? [] let appConfigsByID = Dictionary(uniqueKeysWithValues: appsForStatus.map { ($0.id, $0) }) - let statusInstallations = installations.filter { installation in - guard installation.manifest.availability == .available else { return false } - return appConfigsByID[installation.id]?.enabled ?? installation.enabled - } + let statusInstallations = CrawlBarCrawlerClassifier.statusInstallations( + installations, + appConfigsByID: appConfigsByID) await MainActor.run { guard self.refreshGeneration == generation else { return } let installationsByID = Dictionary(uniqueKeysWithValues: installations.map { ($0.id, $0) }) diff --git a/Sources/CrawlBar/Settings/SettingsSidebar.swift b/Sources/CrawlBar/Settings/SettingsSidebar.swift index fb3e32b..a03b428 100644 --- a/Sources/CrawlBar/Settings/SettingsSidebar.swift +++ b/Sources/CrawlBar/Settings/SettingsSidebar.swift @@ -6,6 +6,13 @@ enum CrawlBarSettingsSidebarItem: Hashable { case crawler(CrawlAppID) } +struct CrawlBarCrawlerSection: Identifiable { + let kind: CrawlBarCrawlerCategory + let apps: [CrawlBarAppConfig] + + var id: CrawlBarCrawlerCategory { self.kind } +} + struct CrawlBarGeneralSidebarRow: View { let isSelected: Bool @@ -34,8 +41,21 @@ struct CrawlBarSidebarSelectionBackground: View { } } +struct CrawlBarSidebarSectionHeader: View { + let title: String + + var body: some View { + Text(self.title) + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(.secondary) + .textCase(nil) + .padding(.top, 8) + } +} + struct CrawlBarSidebarRow: View { let app: CrawlBarAppConfig + let section: CrawlBarCrawlerCategory let manifest: CrawlAppManifest? let status: CrawlAppStatus? let binaryPath: String? @@ -66,6 +86,7 @@ struct CrawlBarSidebarRow: View { } private var rowState: CrawlAppState { + if self.section != .my { return .disabled } if self.manifest?.availability == .comingSoon { return .disabled } if !self.app.enabled { return .disabled } if self.binaryPath == nil { return .needsConfig } @@ -78,6 +99,8 @@ struct CrawlBarSidebarRow: View { private var subtitle: String { let binaryName = self.manifest?.binary.name ?? self.app.id.rawValue + if self.section == .suggested { return self.suggestedSubtitle } + if self.section == .more { return self.moreSubtitle } if self.manifest?.availability == .comingSoon { return "\(binaryName) · coming soon" } if !self.app.enabled { return "Disabled" } if self.binaryPath == nil { return "Missing \(binaryName)" } @@ -107,6 +130,27 @@ struct CrawlBarSidebarRow: View { } } + private var moreSubtitle: String { + guard let suggestion = self.manifest?.suggestion, + suggestion.kind == .app + else { + return "Not installed on this computer" + } + return "\(suggestion.name) is not installed" + } + + private var suggestedSubtitle: String { + guard let suggestion = self.manifest?.suggestion else { + return "Available crawler" + } + switch suggestion.kind { + case .always: + return "Available crawler" + case .app: + return "\(suggestion.name) is installed" + } + } + private var syncedAt: Date? { if let lastSyncAt = self.status?.lastSyncAt { return lastSyncAt @@ -119,6 +163,7 @@ struct CrawlBarSidebarRow: View { private var subtitleColor: Color { if self.isSelected { return Color.white.opacity(0.78) } + if self.section != .my { return Color.secondary } switch self.rowState { case .needsConfig, .needsAuth, .error: return Color.red diff --git a/Sources/CrawlBar/Settings/SettingsView.swift b/Sources/CrawlBar/Settings/SettingsView.swift index 04c0b20..b85239b 100644 --- a/Sources/CrawlBar/Settings/SettingsView.swift +++ b/Sources/CrawlBar/Settings/SettingsView.swift @@ -17,21 +17,26 @@ struct CrawlBarSettingsView: View { self.model.selectedSidebarItem = .general } } - Section("Crawlers") { - ForEach(self.model.apps) { app in - let item = CrawlBarSettingsSidebarItem.crawler(app.id) - CrawlBarSidebarRow( - app: app, - manifest: self.model.installations[app.id]?.manifest, - status: self.model.statuses[app.id], - binaryPath: self.model.installations[app.id]?.binaryPath, - isSelected: self.model.selectedSidebarItem == item) - .tag(item as CrawlBarSettingsSidebarItem?) - .contentShape(Rectangle()) - .listRowBackground(CrawlBarSidebarSelectionBackground(isSelected: self.model.selectedSidebarItem == item)) - .onTapGesture { - self.model.selectedSidebarItem = item - } + ForEach(self.model.crawlerSections) { section in + Section { + ForEach(section.apps) { app in + let item = CrawlBarSettingsSidebarItem.crawler(app.id) + CrawlBarSidebarRow( + app: app, + section: section.kind, + manifest: self.model.installations[app.id]?.manifest, + status: self.model.statuses[app.id], + binaryPath: self.model.installations[app.id]?.binaryPath, + isSelected: self.model.selectedSidebarItem == item) + .tag(item as CrawlBarSettingsSidebarItem?) + .contentShape(Rectangle()) + .listRowBackground(CrawlBarSidebarSelectionBackground(isSelected: self.model.selectedSidebarItem == item)) + .onTapGesture { + self.model.selectedSidebarItem = item + } + } + } header: { + CrawlBarSidebarSectionHeader(title: section.kind.title) } } } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift index cad5938..a21ee97 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift @@ -34,5 +34,6 @@ public extension BuiltInCrawlApps { .init(id: "execution", title: "Execution", optionIDs: ["access_path", "execution_mode"]), .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as"]), ], - install: .init(method: .homebrew, package: "steipete/tap/bird")) + install: .init(method: .homebrew, package: "steipete/tap/bird"), + suggestion: Self.alwaysSuggest("X")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift index 2ca024d..2f98c6c 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift @@ -41,5 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "discord", title: "Discord Access", optionIDs: ["discord_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/discrawl")) + install: .init(method: .homebrew, package: "vincentkoc/tap/discrawl"), + suggestion: Self.appSuggest("Discord", ["com.hnc.Discord"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift index 73d4b31..3a3dfd1 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift @@ -39,5 +39,6 @@ public extension BuiltInCrawlApps { .init(id: "github", title: "GitHub Access", optionIDs: ["github_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/gitcrawl")) + install: .init(method: .homebrew, package: "vincentkoc/tap/gitcrawl"), + suggestion: Self.alwaysSuggest("GitHub")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift index f8f6fd9..fda1461 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift @@ -36,5 +36,6 @@ public extension BuiltInCrawlApps { .init(id: "execution", title: "Execution", optionIDs: ["execution_mode"]), .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as", "remote_env_file"]), ], - install: .init(method: .homebrew, package: "openclaw/tap/gogcli")) + install: .init(method: .homebrew, package: "openclaw/tap/gogcli"), + suggestion: Self.alwaysSuggest("Google")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift index f861084..3361bf6 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift @@ -41,5 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "granola", title: "Granola", optionIDs: ["granola_profile", "preferred_source", "allow_private_api", "allow_desktop_cache"]), .init(id: "sync", title: "Sync", optionIDs: ["sync_limit"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/graincrawl")) + install: .init(method: .homebrew, package: "vincentkoc/tap/graincrawl"), + suggestion: Self.appSuggest("Granola", ["com.granola.app"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift index 7b820d3..7b1e697 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift @@ -41,5 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "notion", title: "Notion Access", optionIDs: ["notion_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/notcrawl")) + install: .init(method: .homebrew, package: "vincentkoc/tap/notcrawl"), + suggestion: Self.appSuggest("Notion", ["notion.id"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift index 80d4730..d65d84f 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift @@ -40,5 +40,6 @@ public extension BuiltInCrawlApps { .init(id: "slack", title: "Slack Access", optionIDs: ["slack_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/slacrawl")) + install: .init(method: .homebrew, package: "vincentkoc/tap/slacrawl"), + suggestion: Self.appSuggest("Slack", ["com.tinyspeck.slackmacgap"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift index 14f5471..78c3d62 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift @@ -28,5 +28,6 @@ public extension BuiltInCrawlApps { containsPrivateMessages: true, exportsSecrets: false, localOnlyScopes: ["telegram-desktop", "sqlite", "encrypted-git-backup"]), - install: .init(method: .homebrew, package: "steipete/tap/telecrawl")) + install: .init(method: .homebrew, package: "steipete/tap/telecrawl"), + suggestion: Self.appSuggest("Telegram", ["ru.keepcoder.Telegram", "org.telegram.desktop"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift index 2806086..2dbe4e5 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift @@ -42,5 +42,6 @@ public extension BuiltInCrawlApps { .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as"]), .init(id: "whatsapp", title: "WhatsApp Account", optionIDs: ["account"]), ], - install: .init(method: .homebrew, package: "openclaw/tap/wacli")) + install: .init(method: .homebrew, package: "openclaw/tap/wacli"), + suggestion: Self.appSuggest("WhatsApp", ["net.whatsapp.WhatsApp"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps.swift index 1b8144c..b98a83b 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps.swift @@ -29,4 +29,11 @@ public enum BuiltInCrawlApps { self.allByID[id] } + static func alwaysSuggest(_ name: String) -> CrawlAppManifest.Suggestion { + .init(kind: .always, name: name) + } + + static func appSuggest(_ name: String, _ bundleIDs: [String]) -> CrawlAppManifest.Suggestion { + .init(kind: .app, name: name, bundleIDs: bundleIDs) + } } diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift index 121ccce..2c31865 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift @@ -17,6 +17,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { public var configOptions: [CrawlAppManifest.ConfigOption] public var configSections: [CrawlAppManifest.ConfigSection] public var install: CrawlAppManifest.Install? + package var suggestion: CrawlAppManifest.Suggestion? public init( schemaVersion: Int = 1, @@ -35,6 +36,45 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { configOptions: [CrawlAppManifest.ConfigOption] = [], configSections: [CrawlAppManifest.ConfigSection] = [], install: CrawlAppManifest.Install? = nil) + { + self.init( + schemaVersion: schemaVersion, + id: id, + displayName: displayName, + description: description, + availability: availability, + binary: binary, + execution: execution, + branding: branding, + paths: paths, + commands: commands, + capabilities: capabilities, + statusRequiresSecrets: statusRequiresSecrets, + privacy: privacy, + configOptions: configOptions, + configSections: configSections, + install: install, + suggestion: nil) + } + + package init( + schemaVersion: Int = 1, + id: CrawlAppID, + displayName: String, + description: String, + availability: CrawlAppManifest.Availability = .available, + binary: CrawlAppManifest.Binary, + execution: CrawlAppManifest.Execution? = nil, + branding: CrawlAppManifest.Branding, + paths: CrawlAppManifest.Paths, + commands: [String: [String]], + capabilities: [CrawlAppCapability], + statusRequiresSecrets: Bool? = nil, + privacy: CrawlAppManifest.Privacy = CrawlAppManifest.Privacy(), + configOptions: [CrawlAppManifest.ConfigOption] = [], + configSections: [CrawlAppManifest.ConfigSection] = [], + install: CrawlAppManifest.Install? = nil, + suggestion: CrawlAppManifest.Suggestion? = nil) { self.schemaVersion = schemaVersion self.id = id @@ -52,6 +92,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.configOptions = configOptions self.configSections = configSections self.install = install + self.suggestion = suggestion } private enum CodingKeys: String, CodingKey { @@ -71,6 +112,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { case configOptions = "config_options" case configSections = "config_sections" case install + case suggestion } public init(from decoder: Decoder) throws { @@ -91,6 +133,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.configOptions = try container.decodeIfPresent([CrawlAppManifest.ConfigOption].self, forKey: .configOptions) ?? [] self.configSections = try container.decodeIfPresent([CrawlAppManifest.ConfigSection].self, forKey: .configSections) ?? [] self.install = try container.decodeIfPresent(CrawlAppManifest.Install.self, forKey: .install) + self.suggestion = try container.decodeIfPresent(CrawlAppManifest.Suggestion.self, forKey: .suggestion) } public var needsSecretsForStatus: Bool { diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstall.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstall.swift deleted file mode 100644 index 1a431e6..0000000 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstall.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation - -public extension CrawlAppManifest { - enum InstallMethod: String, Codable, Equatable, Sendable { - case homebrew - } - - struct Install: Codable, Equatable, Sendable { - public var method: InstallMethod - public var package: String - - public init(method: InstallMethod, package: String) { - self.method = method - self.package = package - } - } -} diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift new file mode 100644 index 0000000..0086f84 --- /dev/null +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift @@ -0,0 +1,42 @@ +import Foundation + +public extension CrawlAppManifest { + enum InstallMethod: String, Codable, Equatable, Sendable { + case homebrew + } + + struct Install: Codable, Equatable, Sendable { + public var method: InstallMethod + public var package: String + + public init(method: InstallMethod, package: String) { + self.method = method + self.package = package + } + } +} + +package extension CrawlAppManifest { + enum SuggestionKind: String, Codable, Equatable, Sendable { + case always + case app + } + + struct Suggestion: Codable, Equatable, Sendable { + package var kind: SuggestionKind + package var name: String + package var bundleIDs: [String] + + package init(kind: SuggestionKind, name: String, bundleIDs: [String] = []) { + self.kind = kind + self.name = name + self.bundleIDs = bundleIDs + } + + private enum CodingKeys: String, CodingKey { + case kind + case name + case bundleIDs = "bundle_ids" + } + } +} diff --git a/Sources/CrawlBarSelfTest/SelfTestConfig.swift b/Sources/CrawlBarSelfTest/SelfTestConfig.swift index b5d5e4c..388a88c 100644 --- a/Sources/CrawlBarSelfTest/SelfTestConfig.swift +++ b/Sources/CrawlBarSelfTest/SelfTestConfig.swift @@ -150,6 +150,7 @@ extension CrawlBarSelfTest { try Self.expect(BuiltInCrawlApps.gogcli.commands["status"] == ["auth", "list", "--check", "--json", "--no-input"], "Google status is wired") try Self.expect(BuiltInCrawlApps.wacli.availability == .available, "WhatsApp manifest is available") try Self.expect(BuiltInCrawlApps.wacli.commands["status"] == ["--account", "{config:account}", "--read-only", "--json", "doctor"], "WhatsApp status is wired") + try Self.expect(BuiltInCrawlApps.wacli.suggestion?.name == "WhatsApp", "WhatsApp manifest declares source app suggestion") try Self.expect(BuiltInCrawlApps.birdclaw.binary.name == "bird", "X app id uses bird executable") try Self.expect(BuiltInCrawlApps.telecrawl.availability == .available, "telecrawl is available") try Self.expect(BuiltInCrawlApps.telecrawl.commands["status"] == ["--json", "status"], "telecrawl uses JSON status command") From e169d5ec0f2ce080b700ab54a90d8ab9d1454a4b Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 00:32:10 +0200 Subject: [PATCH 2/5] Fix coming-soon crawler sidebar subtitle --- Sources/CrawlBar/Settings/SettingsSidebar.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CrawlBar/Settings/SettingsSidebar.swift b/Sources/CrawlBar/Settings/SettingsSidebar.swift index a03b428..9fb9068 100644 --- a/Sources/CrawlBar/Settings/SettingsSidebar.swift +++ b/Sources/CrawlBar/Settings/SettingsSidebar.swift @@ -99,9 +99,9 @@ struct CrawlBarSidebarRow: View { private var subtitle: String { let binaryName = self.manifest?.binary.name ?? self.app.id.rawValue + if self.manifest?.availability == .comingSoon { return "\(binaryName) · coming soon" } if self.section == .suggested { return self.suggestedSubtitle } if self.section == .more { return self.moreSubtitle } - if self.manifest?.availability == .comingSoon { return "\(binaryName) · coming soon" } if !self.app.enabled { return "Disabled" } if self.binaryPath == nil { return "Missing \(binaryName)" } if self.rowState == .needsAuth { From c44196f69606d576743e2b72f41e0106da644c78 Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 00:44:17 +0200 Subject: [PATCH 3/5] Filter unavailable crawlers from status targets --- Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift index d108c8e..7906976 100644 --- a/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift +++ b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift @@ -50,6 +50,7 @@ enum CrawlBarCrawlerClassifier { -> [CrawlAppInstallation] { installations.filter { installation in + guard installation.manifest.availability == .available else { return false } guard let config = appConfigsByID[installation.id], config.enabled else { return false } return self.isMyCrawler(app: config, installation: installation) } From debf8e4ca61ee5228fe62a59a32c8305d0dededd Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 18:25:51 +0200 Subject: [PATCH 4/5] Shrink crawler suggestion manifest API --- .../CrawlBarCrawlerClassifier.swift | 4 +- Sources/CrawlBar/Settings/SettingsModel.swift | 9 +--- .../BuiltInCrawlApps+Birdclaw.swift | 4 +- .../BuiltInCrawlApps+Discrawl.swift | 4 +- .../BuiltInCrawlApps+Gitcrawl.swift | 4 +- .../BuiltInApps/BuiltInCrawlApps+Gogcli.swift | 4 +- .../BuiltInCrawlApps+Graincrawl.swift | 4 +- .../BuiltInCrawlApps+Notcrawl.swift | 4 +- .../BuiltInCrawlApps+Slacrawl.swift | 4 +- .../BuiltInCrawlApps+Telecrawl.swift | 4 +- .../BuiltInApps/BuiltInCrawlApps+Wacli.swift | 4 +- .../Manifest/CrawlAppManifest.swift | 41 +------------------ .../CrawlAppManifestInstallSuggestion.swift | 6 +++ 13 files changed, 27 insertions(+), 69 deletions(-) diff --git a/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift index 7906976..6c2b7ad 100644 --- a/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift +++ b/Sources/CrawlBar/CrawlerCatalog/CrawlBarCrawlerClassifier.swift @@ -1,13 +1,11 @@ import CrawlBarCore import Foundation -enum CrawlBarCrawlerCategory: CaseIterable, Hashable, Identifiable { +enum CrawlBarCrawlerCategory: CaseIterable, Hashable { case my case suggested case more - var id: Self { self } - var title: String { switch self { case .my: diff --git a/Sources/CrawlBar/Settings/SettingsModel.swift b/Sources/CrawlBar/Settings/SettingsModel.swift index f7356cf..5c722da 100644 --- a/Sources/CrawlBar/Settings/SettingsModel.swift +++ b/Sources/CrawlBar/Settings/SettingsModel.swift @@ -70,7 +70,7 @@ final class CrawlBarSettingsModel: NSObject, ObservableObject { var grouped: [CrawlBarCrawlerCategory: [CrawlBarAppConfig]] = [:] for app in self.apps { let installation = self.installations[app.id] - let section = self.listSection(for: app, installation: installation) + let section = CrawlBarCrawlerClassifier.category(app: app, installation: installation) grouped[section, default: []].append(app) } return CrawlBarCrawlerCategory.allCases.compactMap { section in @@ -90,11 +90,4 @@ final class CrawlBarSettingsModel: NSObject, ObservableObject { } } - func listSection( - for app: CrawlBarAppConfig, - installation: CrawlAppInstallation?) - -> CrawlBarCrawlerCategory - { - CrawlBarCrawlerClassifier.category(app: app, installation: installation) - } } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift index a21ee97..92f3caf 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Birdclaw.swift @@ -34,6 +34,6 @@ public extension BuiltInCrawlApps { .init(id: "execution", title: "Execution", optionIDs: ["access_path", "execution_mode"]), .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as"]), ], - install: .init(method: .homebrew, package: "steipete/tap/bird"), - suggestion: Self.alwaysSuggest("X")) + install: .init(method: .homebrew, package: "steipete/tap/bird")) + .withSuggestion(Self.alwaysSuggest("X")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift index 2f98c6c..3fc2a8c 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Discrawl.swift @@ -41,6 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "discord", title: "Discord Access", optionIDs: ["discord_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/discrawl"), - suggestion: Self.appSuggest("Discord", ["com.hnc.Discord"])) + install: .init(method: .homebrew, package: "vincentkoc/tap/discrawl")) + .withSuggestion(Self.appSuggest("Discord", ["com.hnc.Discord"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift index 3a3dfd1..cea4c5c 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gitcrawl.swift @@ -39,6 +39,6 @@ public extension BuiltInCrawlApps { .init(id: "github", title: "GitHub Access", optionIDs: ["github_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/gitcrawl"), - suggestion: Self.alwaysSuggest("GitHub")) + install: .init(method: .homebrew, package: "vincentkoc/tap/gitcrawl")) + .withSuggestion(Self.alwaysSuggest("GitHub")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift index fda1461..ae04adb 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Gogcli.swift @@ -36,6 +36,6 @@ public extension BuiltInCrawlApps { .init(id: "execution", title: "Execution", optionIDs: ["execution_mode"]), .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as", "remote_env_file"]), ], - install: .init(method: .homebrew, package: "openclaw/tap/gogcli"), - suggestion: Self.alwaysSuggest("Google")) + install: .init(method: .homebrew, package: "openclaw/tap/gogcli")) + .withSuggestion(Self.alwaysSuggest("Google")) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift index 3361bf6..42e9653 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Graincrawl.swift @@ -41,6 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "granola", title: "Granola", optionIDs: ["granola_profile", "preferred_source", "allow_private_api", "allow_desktop_cache"]), .init(id: "sync", title: "Sync", optionIDs: ["sync_limit"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/graincrawl"), - suggestion: Self.appSuggest("Granola", ["com.granola.app"])) + install: .init(method: .homebrew, package: "vincentkoc/tap/graincrawl")) + .withSuggestion(Self.appSuggest("Granola", ["com.granola.app"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift index 7b1e697..cb33841 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Notcrawl.swift @@ -41,6 +41,6 @@ public extension BuiltInCrawlApps { .init(id: "notion", title: "Notion Access", optionIDs: ["notion_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/notcrawl"), - suggestion: Self.appSuggest("Notion", ["notion.id"])) + install: .init(method: .homebrew, package: "vincentkoc/tap/notcrawl")) + .withSuggestion(Self.appSuggest("Notion", ["notion.id"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift index d65d84f..415fa03 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Slacrawl.swift @@ -40,6 +40,6 @@ public extension BuiltInCrawlApps { .init(id: "slack", title: "Slack Access", optionIDs: ["slack_token"]), .init(id: "ai", title: "Embeddings", optionIDs: ["openai_api_key", "embedding_model"]), ], - install: .init(method: .homebrew, package: "vincentkoc/tap/slacrawl"), - suggestion: Self.appSuggest("Slack", ["com.tinyspeck.slackmacgap"])) + install: .init(method: .homebrew, package: "vincentkoc/tap/slacrawl")) + .withSuggestion(Self.appSuggest("Slack", ["com.tinyspeck.slackmacgap"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift index 78c3d62..e988cc1 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Telecrawl.swift @@ -28,6 +28,6 @@ public extension BuiltInCrawlApps { containsPrivateMessages: true, exportsSecrets: false, localOnlyScopes: ["telegram-desktop", "sqlite", "encrypted-git-backup"]), - install: .init(method: .homebrew, package: "steipete/tap/telecrawl"), - suggestion: Self.appSuggest("Telegram", ["ru.keepcoder.Telegram", "org.telegram.desktop"])) + install: .init(method: .homebrew, package: "steipete/tap/telecrawl")) + .withSuggestion(Self.appSuggest("Telegram", ["ru.keepcoder.Telegram", "org.telegram.desktop"])) } diff --git a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift index 2dbe4e5..c75a5a4 100644 --- a/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift +++ b/Sources/CrawlBarCore/BuiltInApps/BuiltInCrawlApps+Wacli.swift @@ -42,6 +42,6 @@ public extension BuiltInCrawlApps { .init(id: "remote", title: "Remote Host", optionIDs: ["remote_target", "remote_run_as"]), .init(id: "whatsapp", title: "WhatsApp Account", optionIDs: ["account"]), ], - install: .init(method: .homebrew, package: "openclaw/tap/wacli"), - suggestion: Self.appSuggest("WhatsApp", ["net.whatsapp.WhatsApp"])) + install: .init(method: .homebrew, package: "openclaw/tap/wacli")) + .withSuggestion(Self.appSuggest("WhatsApp", ["net.whatsapp.WhatsApp"])) } diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift index 2c31865..a22f9db 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift @@ -36,45 +36,6 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { configOptions: [CrawlAppManifest.ConfigOption] = [], configSections: [CrawlAppManifest.ConfigSection] = [], install: CrawlAppManifest.Install? = nil) - { - self.init( - schemaVersion: schemaVersion, - id: id, - displayName: displayName, - description: description, - availability: availability, - binary: binary, - execution: execution, - branding: branding, - paths: paths, - commands: commands, - capabilities: capabilities, - statusRequiresSecrets: statusRequiresSecrets, - privacy: privacy, - configOptions: configOptions, - configSections: configSections, - install: install, - suggestion: nil) - } - - package init( - schemaVersion: Int = 1, - id: CrawlAppID, - displayName: String, - description: String, - availability: CrawlAppManifest.Availability = .available, - binary: CrawlAppManifest.Binary, - execution: CrawlAppManifest.Execution? = nil, - branding: CrawlAppManifest.Branding, - paths: CrawlAppManifest.Paths, - commands: [String: [String]], - capabilities: [CrawlAppCapability], - statusRequiresSecrets: Bool? = nil, - privacy: CrawlAppManifest.Privacy = CrawlAppManifest.Privacy(), - configOptions: [CrawlAppManifest.ConfigOption] = [], - configSections: [CrawlAppManifest.ConfigSection] = [], - install: CrawlAppManifest.Install? = nil, - suggestion: CrawlAppManifest.Suggestion? = nil) { self.schemaVersion = schemaVersion self.id = id @@ -92,7 +53,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.configOptions = configOptions self.configSections = configSections self.install = install - self.suggestion = suggestion + self.suggestion = nil } private enum CodingKeys: String, CodingKey { diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift index 0086f84..bfbbab6 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift @@ -17,6 +17,12 @@ public extension CrawlAppManifest { } package extension CrawlAppManifest { + func withSuggestion(_ suggestion: Suggestion) -> Self { + var manifest = self + manifest.suggestion = suggestion + return manifest + } + enum SuggestionKind: String, Codable, Equatable, Sendable { case always case app From b08507f59b5ee27d4840926abfece197f67eb482 Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 18:40:33 +0200 Subject: [PATCH 5/5] Keep crawler suggestions built-in only --- Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift | 5 +---- .../Manifest/CrawlAppManifestInstallSuggestion.swift | 12 ++---------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift index a22f9db..7d34117 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifest.swift @@ -17,7 +17,7 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { public var configOptions: [CrawlAppManifest.ConfigOption] public var configSections: [CrawlAppManifest.ConfigSection] public var install: CrawlAppManifest.Install? - package var suggestion: CrawlAppManifest.Suggestion? + package var suggestion: CrawlAppManifest.Suggestion? = nil public init( schemaVersion: Int = 1, @@ -53,7 +53,6 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.configOptions = configOptions self.configSections = configSections self.install = install - self.suggestion = nil } private enum CodingKeys: String, CodingKey { @@ -73,7 +72,6 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { case configOptions = "config_options" case configSections = "config_sections" case install - case suggestion } public init(from decoder: Decoder) throws { @@ -94,7 +92,6 @@ public struct CrawlAppManifest: Codable, Equatable, Sendable, Identifiable { self.configOptions = try container.decodeIfPresent([CrawlAppManifest.ConfigOption].self, forKey: .configOptions) ?? [] self.configSections = try container.decodeIfPresent([CrawlAppManifest.ConfigSection].self, forKey: .configSections) ?? [] self.install = try container.decodeIfPresent(CrawlAppManifest.Install.self, forKey: .install) - self.suggestion = try container.decodeIfPresent(CrawlAppManifest.Suggestion.self, forKey: .suggestion) } public var needsSecretsForStatus: Bool { diff --git a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift index bfbbab6..e0c8447 100644 --- a/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift +++ b/Sources/CrawlBarCore/Manifest/CrawlAppManifestInstallSuggestion.swift @@ -1,5 +1,3 @@ -import Foundation - public extension CrawlAppManifest { enum InstallMethod: String, Codable, Equatable, Sendable { case homebrew @@ -23,12 +21,12 @@ package extension CrawlAppManifest { return manifest } - enum SuggestionKind: String, Codable, Equatable, Sendable { + enum SuggestionKind: Equatable, Sendable { case always case app } - struct Suggestion: Codable, Equatable, Sendable { + struct Suggestion: Equatable, Sendable { package var kind: SuggestionKind package var name: String package var bundleIDs: [String] @@ -38,11 +36,5 @@ package extension CrawlAppManifest { self.name = name self.bundleIDs = bundleIDs } - - private enum CodingKeys: String, CodingKey { - case kind - case name - case bundleIDs = "bundle_ids" - } } }