diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConfig.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConfig.swift index a62ccef..7a9048f 100644 --- a/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConfig.swift +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/Models/HermesConfig.swift @@ -666,6 +666,18 @@ public struct HermesConfig: Sendable { /// final reply (provider/model/cost/turn count). Off by default; /// useful for cost auditing and screen-recording demos. public var runtimeMetadataFooter: Bool + /// Pre-v0.13: single combined Web Tools backend at `web_tools.backend`. + /// v0.13 split this into per-capability keys (see below). Kept readable + /// for round-trip compatibility on hosts that never migrated; v0.13+ + /// hosts ignore this scalar and read the split keys instead. + public var webToolsBackend: String + /// v0.13+: `web_tools.search.backend`. SearXNG is search-only and + /// can land here. Pre-v0.13 hosts default to the same value as the + /// combined backend. + public var webToolsSearchBackend: String + /// v0.13+: `web_tools.extract.backend`. Pre-v0.13 hosts default to + /// the same value as the combined backend. + public var webToolsExtractBackend: String // Grouped blocks public var display: DisplaySettings @@ -747,11 +759,17 @@ public struct HermesConfig: Sendable { homeAssistant: HomeAssistantSettings, cacheTTL: String = "5m", redactionEnabled: Bool = false, - runtimeMetadataFooter: Bool = false + runtimeMetadataFooter: Bool = false, + webToolsBackend: String = "duckduckgo", + webToolsSearchBackend: String = "duckduckgo", + webToolsExtractBackend: String = "reader" ) { self.cacheTTL = cacheTTL self.redactionEnabled = redactionEnabled self.runtimeMetadataFooter = runtimeMetadataFooter + self.webToolsBackend = webToolsBackend + self.webToolsSearchBackend = webToolsSearchBackend + self.webToolsExtractBackend = webToolsExtractBackend self.model = model self.provider = provider self.maxTurns = maxTurns diff --git a/scarf/Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesConfig+YAML.swift b/scarf/Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesConfig+YAML.swift index d172bbc..215224f 100644 --- a/scarf/Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesConfig+YAML.swift +++ b/scarf/Packages/ScarfCore/Sources/ScarfCore/Parsing/HermesConfig+YAML.swift @@ -284,7 +284,14 @@ public extension HermesConfig { homeAssistant: homeAssistant, cacheTTL: str("prompt_caching.cache_ttl", default: "5m"), redactionEnabled: bool("redaction.enabled", default: false), - runtimeMetadataFooter: bool("agent.runtime_metadata_footer", default: false) + runtimeMetadataFooter: bool("agent.runtime_metadata_footer", default: false), + // Pre-v0.13 hosts wrote a single `web_tools.backend`. v0.13 split + // it into per-capability keys. Read all three so the round-trip + // never loses a value the user already set; the WebTools tab + // chooses which to render based on `hasWebToolsBackendSplit`. + webToolsBackend: str("web_tools.backend", default: "duckduckgo"), + webToolsSearchBackend: str("web_tools.search.backend", default: "duckduckgo"), + webToolsExtractBackend: str("web_tools.extract.backend", default: "reader") ) } } diff --git a/scarf/scarf/Features/Settings/ViewModels/SettingsViewModel.swift b/scarf/scarf/Features/Settings/ViewModels/SettingsViewModel.swift index 369edb3..a27939e 100644 --- a/scarf/scarf/Features/Settings/ViewModels/SettingsViewModel.swift +++ b/scarf/scarf/Features/Settings/ViewModels/SettingsViewModel.swift @@ -143,6 +143,16 @@ final class SettingsViewModel { func setBrowserAllowPrivateURLs(_ value: Bool) { setSetting("browser.allow_private_urls", value: value ? "true" : "false") } func setCamofoxManagedPersistence(_ value: Bool) { setSetting("browser.camofox.managed_persistence", value: value ? "true" : "false") } + // MARK: - Web Tools + + /// Pre-v0.13 combined backend. Pre-v0.13 hosts read this; v0.13+ + /// hosts read it for back-compat but the WebToolsTab gates writes + /// on `hasWebToolsBackendSplit` so the tab only writes the split + /// keys on v0.13. + func setWebToolsBackend(_ value: String) { setSetting("web_tools.backend", value: value) } + func setWebToolsSearchBackend(_ value: String) { setSetting("web_tools.search.backend", value: value) } + func setWebToolsExtractBackend(_ value: String) { setSetting("web_tools.extract.backend", value: value) } + // MARK: - Voice / TTS / STT func setAutoTTS(_ value: Bool) { setSetting("voice.auto_tts", value: value ? "true" : "false") } diff --git a/scarf/scarf/Features/Settings/Views/SettingsView.swift b/scarf/scarf/Features/Settings/Views/SettingsView.swift index 6e361ab..3b5e664 100644 --- a/scarf/scarf/Features/Settings/Views/SettingsView.swift +++ b/scarf/scarf/Features/Settings/Views/SettingsView.swift @@ -26,6 +26,7 @@ struct SettingsView: View { case agent = "Agent" case terminal = "Terminal" case browser = "Browser" + case webTools = "Web Tools" case voice = "Voice" case memory = "Memory" case auxiliary = "Aux Models" @@ -41,6 +42,7 @@ struct SettingsView: View { case .agent: return "Agent" case .terminal: return "Terminal" case .browser: return "Browser" + case .webTools: return "Web Tools" case .voice: return "Voice" case .memory: return "Memory" case .auxiliary: return "Aux Models" @@ -56,6 +58,7 @@ struct SettingsView: View { case .agent: return "brain.head.profile" case .terminal: return "terminal" case .browser: return "globe" + case .webTools: return "globe.americas" case .voice: return "mic" case .memory: return "memorychip" case .auxiliary: return "sparkles.rectangle.stack" @@ -171,6 +174,7 @@ struct SettingsView: View { case .agent: AgentTab(viewModel: viewModel) case .terminal: TerminalTab(viewModel: viewModel) case .browser: BrowserTab(viewModel: viewModel) + case .webTools: WebToolsTab(viewModel: viewModel) case .voice: VoiceTab(viewModel: viewModel) case .memory: MemoryTab(viewModel: viewModel) case .auxiliary: AuxiliaryTab(viewModel: viewModel) diff --git a/scarf/scarf/Features/Settings/Views/Tabs/WebToolsTab.swift b/scarf/scarf/Features/Settings/Views/Tabs/WebToolsTab.swift new file mode 100644 index 0000000..904be1b --- /dev/null +++ b/scarf/scarf/Features/Settings/Views/Tabs/WebToolsTab.swift @@ -0,0 +1,76 @@ +import SwiftUI +import ScarfCore +import ScarfDesign + +/// Web Tools tab — search + extract backend pickers. Pre-v0.13 hosts +/// see a single "Combined backend" row writing to the legacy +/// `web_tools.backend` key. v0.13+ hosts see two rows writing to the +/// per-capability split keys (`web_tools.search.backend` + +/// `web_tools.extract.backend`); SearXNG appears in the search picker +/// only because Hermes registers it as a search-only backend. +struct WebToolsTab: View { + @Bindable var viewModel: SettingsViewModel + @Environment(\.hermesCapabilities) private var capabilitiesStore + + private var split: Bool { + capabilitiesStore?.capabilities.hasWebToolsBackendSplit ?? false + } + + // TODO(WS-7-Q6): Backend lists are curated inline based on the v0.13 + // release notes ("SearXNG joined search-only"). The exact dispatch + // table lives in `~/.hermes/hermes-agent/hermes_cli/web_tools.py` — + // verify during integration. A wrong entry just produces a + // `hermes config set` failure on save (recoverable, not silent). + private static let searchBackends: [String] = [ + "duckduckgo", "tavily", "brave", "exa", "you", "searxng" + ] + private static let extractBackends: [String] = [ + "reader", "browserless", "trafilatura", "firecrawl" + ] + /// v0.12 combined-backend list — superset of the v0.13 search list + /// minus SearXNG (which only dispatches as search) plus the v0.13 + /// extract-only entries that pre-v0.13 hosts handled under the + /// combined key. + private static let combinedBackends: [String] = [ + "duckduckgo", "tavily", "brave", "exa", "you", + "reader", "browserless", "trafilatura", "firecrawl" + ] + + var body: some View { + if split { + SettingsSection(title: "Web Tools", icon: "globe.americas") { + PickerRow( + label: "Search backend", + selection: viewModel.config.webToolsSearchBackend, + options: Self.searchBackends + ) { viewModel.setWebToolsSearchBackend($0) } + PickerRow( + label: "Extract backend", + selection: viewModel.config.webToolsExtractBackend, + options: Self.extractBackends + ) { viewModel.setWebToolsExtractBackend($0) } + } + Text("SearXNG joined v0.13 as a search-only backend. Backend-specific tuning (host URLs, API keys) lives in the raw YAML editor for now.") + .scarfStyle(.caption) + .foregroundStyle(ScarfColor.foregroundMuted) + .padding(.horizontal, ScarfSpace.s4) + } else { + // TODO(WS-7-Q7): Pre-v0.13 hosts fall back to the legacy single + // backend. v0.13 may or may not honour `web_tools.backend` as a + // fallback when the split keys are absent — verify with Hermes + // and consider a one-time migration prompt in a follow-up if + // upgrading from v0.12 silently resets the user's backend. + SettingsSection(title: "Web Tools", icon: "globe.americas") { + PickerRow( + label: "Backend", + selection: viewModel.config.webToolsBackend, + options: Self.combinedBackends + ) { viewModel.setWebToolsBackend($0) } + } + Text("Hermes v0.13 splits search and extract into separate backends. Update Hermes to access the per-capability picker.") + .scarfStyle(.caption) + .foregroundStyle(ScarfColor.foregroundFaint) + .padding(.horizontal, ScarfSpace.s4) + } + } +}