Skip to content

Expand web search provider settings#321

Merged
hrayleung merged 2 commits into
masterfrom
refactor/web-search-plugin
May 9, 2026
Merged

Expand web search provider settings#321
hrayleung merged 2 commits into
masterfrom
refactor/web-search-plugin

Conversation

@hrayleung
Copy link
Copy Markdown
Owner

@hrayleung hrayleung commented May 9, 2026

Summary

  • Add persisted provider-specific web search settings for Exa, Brave, Jina, Firecrawl, Tavily, and Perplexity.
  • Correct provider request serialization for Tavily auto parameters/country mapping, Exa category and content freshness rules, Jina country body field, Firecrawl request bodies, Perplexity filters, and Brave date freshness.
  • Add deterministic request/payload regression tests for provider-specific web search behavior.

Validation

  • swift test passed: 2,245 tests, 0 failures.
  • bash Packaging/package.sh passed and produced dist/Jin.app plus dist/Jin.zip.

Summary by CodeRabbit

New Features

  • Added new Exa search depth options (deep-lite and deep-reasoning) and category/user location/moderation settings
  • Enhanced Brave, Firecrawl, Jina, Perplexity, and Tavily with country and language configuration options
  • Introduced Firecrawl source-type selection (web, news, images)
  • Added Tavily auto-parameter tuning toggle

Tests

  • Added comprehensive test coverage for web search provider request configurations and settings persistence

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@hrayleung has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 40 minutes and 10 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16782ec4-5720-45d9-aa0c-a08b4f583696

📥 Commits

Reviewing files that changed from the base of the PR and between afdb813 and d79e873.

📒 Files selected for processing (8)
  • Sources/Tools/BuiltinSearchProvider+Firecrawl.swift
  • Sources/Tools/BuiltinSearchProvider+Jina.swift
  • Sources/UI/WebSearchPluginSettingsChromeViews.swift
  • Sources/UI/WebSearchPluginSettingsStore.swift
  • Tests/JinTests/BuiltinSearchFirecrawlPayloadTests.swift
  • Tests/JinTests/BuiltinSearchJinaRequestTests.swift
  • Tests/JinTests/BuiltinSearchToolHubTests.swift
  • Tests/JinTests/WebSearchPluginSettingsStoreTests.swift
📝 Walkthrough

Walkthrough

This pull request extends web-search provider configuration by adding new domain types, settings fields, request-body builders, and UI controls for Exa, Firecrawl, Tavily, Perplexity, and Jina. Refactors inline request construction into testable pure helpers and persists expanded settings via AppStorage and UserDefaults.

Changes

Web Search Provider Configuration Expansion

Layer / File(s) Summary
Data Contracts
Sources/Domain/WebSearchAndToolControls.swift
New ExaCategory and FirecrawlSourceKind enums; extended ExaSearchType with deepLite and deepReasoning cases; added exaCategory and firecrawlCountry fields to SearchPluginControls.
Settings Model
Sources/UI/WebSearchPluginSettingsStore.swift
Extended WebSearchPluginSettings with new fields for Exa (category, user location, moderation), Jina (country, locale), Firecrawl (country, language, sources), Tavily (country, auto-parameters), and Perplexity (country, language).
Preference Keys
Sources/UI/AppPreferenceKeys.swift
Added 12 new preference-key constants for plugin Web Search configuration across all extended providers.
Settings Persistence
Sources/UI/WebSearchPluginSettingsStore.swift
Updated load(...) to deserialize new fields from UserDefaults; added encodeFirecrawlSources and decodeFirecrawlSources helpers for source array serialization.
Request Builders
Sources/Tools/BuiltinSearchProvider*.swift, Sources/Tools/BuiltinSearchHelpers.swift, Sources/Tools/BuiltinSearchProviders.swift
Introduced pure testable builders: makeExaRequestBody, makeFirecrawlRequestBody, makeJinaRequest, makeTavilyRequestBody, makePerplexityRequestBody, braveDateRangeFreshness; added domain-filtering, date-formatting, country-mapping, and value-normalization utilities; refactored request construction from inline code.
UI Storage and Wiring
Sources/UI/WebSearchPluginSettingsView.swift
Added @AppStorage properties for all new provider settings; replaced inline .onChange handlers with provider-specific observer modifiers.
Advanced Settings View
Sources/UI/WebSearchPluginSettingsChromeViews.swift
Extended WebSearchAdvancedProviderSettingsView with bindings for all new settings; added UI controls (pickers, toggles, text fields) for Exa category/location/moderation, Jina country/locale, Firecrawl country/language/sources, Tavily country/auto-parameters, Perplexity country/language; introduced Firecrawl source-toggle plumbing.
Observer Modifiers and Labels
Sources/UI/WebSearchPluginSettingsChromeViews.swift
Introduced provider-specific ViewModifier structs (ExaProviderObservers, BraveProviderObservers, etc.) for onChange wiring; added label helpers for Exa enum display.
Tests: Payload Builders
Tests/JinTests/BuiltinSearchExaPayloadTests.swift, BuiltinSearchFirecrawlPayloadTests.swift, BuiltinSearchTavilyPayloadTests.swift, BuiltinSearchPerplexityPayloadTests.swift
New test suites validating request-body field presence/absence, country precedence rules, domain-filter syntax, source encoding, date formatting, and category-specific suppression logic.
Tests: Integration and Helpers
Tests/JinTests/BraveSearchAPITests.swift, BuiltinSearchJinaRequestTests.swift, BuiltinSearchToolHubTests.swift, WebSearchPluginSettingsStoreTests.swift, ChatAuxiliaryControlSupportTests.swift
Tests for Brave date-range freshness, Jina request construction and response parsing, Exa search-type resolution, Firecrawl/Tavily/Perplexity/Jina settings persistence, and updated test fixtures to accommodate expanded settings model.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • hrayleung/Jin#108: Modifies same domain types (ExaSearchType, SearchPluginControls) and aligns provider-specific search parameters across multiple providers.
  • hrayleung/Jin#148: Both PRs extend Perplexity web-search support, including provider settings and request-body construction logic.
  • hrayleung/Jin#100: Introduces foundational web-search plugin abstractions (SearchPluginControls, ExaSearchType) that this PR builds upon with new cases and configuration fields.

Poem

🐰 Whiskers twitching with delight,
Each provider now configured just right,
From Exa's deep to Firecrawl's gleam,
Web searches realized in a dream!
Settings persist through UserDefaults' care,
UI bindings dancing in the air! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.97% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Expand web search provider settings' directly and accurately describes the main change: adding new web search provider-specific settings across multiple providers (Exa, Brave, Jina, Firecrawl, Tavily, Perplexity).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/web-search-plugin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
Sources/Domain/WebSearchAndToolControls.swift (1)

169-171: 💤 Low value

Consider typing exaCategory as ExaCategory? for consistency with exaSearchType.

exaSearchType is stored as the typed enum ExaSearchType?, but exaCategory is kept as String? even though the new ExaCategory enum (and its resolved(from:) helper) exist alongside it. Storing the raw string allows arbitrary/invalid values to roundtrip through Codable. If the only reason for the string form is forward-compat with new Exa categories, that mirrors ExaSearchType (which has .resolved(from:) and a "keyword" → .fast fallback) and could be handled the same way.

Not blocking — this is a low-effort, low-reward style alignment, deferable.

Also applies to: 193-194, 209-210

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Domain/WebSearchAndToolControls.swift` around lines 169 - 171, Change
exaCategory from String? to ExaCategory? and mirror how
exaSearchType/ExaSearchType handle forward-compatibility: use
ExaCategory.resolved(from:) when decoding incoming raw strings so unknown/new
category values map to a safe fallback, and encode using the enum's rawValue (or
original string if you preserved it) when encoding; update any usages and
Codable init/encode paths that reference exaCategory to accept ExaCategory?
instead of String? so parsing/roundtripping behavior matches
exaSearchType/ExaSearchType.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Tools/BuiltinSearchProvider`+Firecrawl.swift:
- Around line 28-33: The code appends parsed buckets from dataDict["news"] and
dataDict["images"] into raw via parseArray but applies the max-results cap
before removing duplicates, allowing duplicate URLs to crowd out unique results;
update the merge logic in BuiltinSearchProvider+Firecrawl.swift (the code around
parseArray(...), the raw array, and the result capping) to deduplicate merged
buckets first (e.g., use a Set keyed by each bucket's URL or unique identifier)
preserving the original order, then apply the existing max-results limit; ensure
you reference and dedupe the same bucket structure returned by parseArray so the
downstream code that enforces the cap sees only unique URLs.

In `@Sources/Tools/BuiltinSearchProvider`+Jina.swift:
- Around line 90-91: The domain filters built in the snippet (variable
operators) must be grouped before being appended to the main query so multiple
domains produce "(site:a.com OR site:b.com)"; change the return logic to wrap
the joined operators in parentheses (e.g., operatorsWrapped = "(\(operators))")
and use operatorsWrapped both when trimmed.isEmpty and when building "\(trimmed)
\(operatorsWrapped)"; reference the operators variable and the existing return
expression to locate and update the code.
- Around line 14-16: The code forces at least one result via resolvedMaxResults
= min(max(args.maxResults, 1), 5) which ignores an explicit args.maxResults ==
0; change the clamping to allow 0 (e.g., clamp between 0 and 5) and add an early
exit when resolvedMaxResults == 0 so the Jina provider call is not made and an
empty result/citation set is returned; touch the resolvedMaxResults computation
and the call site that uses Self.extractJinaResults(from:) to implement the
early return.

In `@Sources/UI/WebSearchPluginSettingsChromeViews.swift`:
- Around line 357-363: In firecrawlSelectedSources(), when firecrawlSourcesRaw
is empty or decoding fails the function currently returns an empty array; change
this to return [.web] so the UI matches Firecrawl's default behavior. Update the
guard/fallback logic in the firecrawlSelectedSources() function (and any use of
FirecrawlSourceKind) to return [FirecrawlSourceKind.web] (or .web) instead of []
when trimmed/decoded input is nil or empty, ensuring the UI toggles reflect the
API default.

---

Nitpick comments:
In `@Sources/Domain/WebSearchAndToolControls.swift`:
- Around line 169-171: Change exaCategory from String? to ExaCategory? and
mirror how exaSearchType/ExaSearchType handle forward-compatibility: use
ExaCategory.resolved(from:) when decoding incoming raw strings so unknown/new
category values map to a safe fallback, and encode using the enum's rawValue (or
original string if you preserved it) when encoding; update any usages and
Codable init/encode paths that reference exaCategory to accept ExaCategory?
instead of String? so parsing/roundtripping behavior matches
exaSearchType/ExaSearchType.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5f9e45e2-852a-40e5-bc86-1b05ed3548c7

📥 Commits

Reviewing files that changed from the base of the PR and between e802ae3 and afdb813.

📒 Files selected for processing (21)
  • Sources/Domain/WebSearchAndToolControls.swift
  • Sources/Tools/BuiltinSearchHelpers.swift
  • Sources/Tools/BuiltinSearchProvider+Brave.swift
  • Sources/Tools/BuiltinSearchProvider+Firecrawl.swift
  • Sources/Tools/BuiltinSearchProvider+Jina.swift
  • Sources/Tools/BuiltinSearchProvider+Perplexity.swift
  • Sources/Tools/BuiltinSearchProvider+Tavily.swift
  • Sources/Tools/BuiltinSearchProviders.swift
  • Sources/UI/AppPreferenceKeys.swift
  • Sources/UI/WebSearchPluginSettingsChromeViews.swift
  • Sources/UI/WebSearchPluginSettingsStore.swift
  • Sources/UI/WebSearchPluginSettingsView.swift
  • Tests/JinTests/BraveSearchAPITests.swift
  • Tests/JinTests/BuiltinSearchExaPayloadTests.swift
  • Tests/JinTests/BuiltinSearchFirecrawlPayloadTests.swift
  • Tests/JinTests/BuiltinSearchJinaRequestTests.swift
  • Tests/JinTests/BuiltinSearchPerplexityPayloadTests.swift
  • Tests/JinTests/BuiltinSearchTavilyPayloadTests.swift
  • Tests/JinTests/BuiltinSearchToolHubTests.swift
  • Tests/JinTests/ChatAuxiliaryControlSupportTests.swift
  • Tests/JinTests/WebSearchPluginSettingsStoreTests.swift

Comment thread Sources/Tools/BuiltinSearchProvider+Firecrawl.swift
Comment thread Sources/Tools/BuiltinSearchProvider+Jina.swift
Comment thread Sources/Tools/BuiltinSearchProvider+Jina.swift Outdated
Comment on lines +357 to +363
private func firecrawlSelectedSources() -> [FirecrawlSourceKind] {
guard let trimmed = firecrawlSourcesRaw.trimmedNonEmpty,
let data = trimmed.data(using: .utf8),
let raw = try? JSONDecoder().decode([String].self, from: data) else {
return []
}
return raw.compactMap(FirecrawlSourceKind.init(rawValue:))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the file in question
find . -name "WebSearchPluginSettingsChromeViews.swift" -type f

Repository: hrayleung/Jin

Length of output: 111


🏁 Script executed:

# Search for FirecrawlSourceKind definition
rg "enum FirecrawlSourceKind|case web" --type swift -A 10

Repository: hrayleung/Jin

Length of output: 4090


🏁 Script executed:

# Check how firecrawlSelectedSources is used in the codebase
rg "firecrawlSelectedSources" --type swift -B 3 -A 3

Repository: hrayleung/Jin

Length of output: 1637


🌐 Web query:

Firecrawl search API sources parameter default behavior

💡 Result:

Firecrawl Search API: the sources parameter defaults to web. If you omit sources (or don’t provide it in the request), Firecrawl uses sources: ['web'] by default, which means you’ll get standard web results (and the response will be shaped accordingly, e.g., web results available). [1][2] When sources is provided, it controls which result-type arrays are returned (e.g., web, news, images). The docs also state that web is the default source type. [2][1]

Citations:


🏁 Script executed:

# Check for the firecrawlSourcesRaw property and persistence logic
rg "firecrawlSourcesRaw" --type swift -B 5 -A 5

Repository: hrayleung/Jin

Length of output: 7858


🏁 Script executed:

# Look for the complete firecrawlSourceBinding method and surrounding context
rg "firecrawlSourceBinding" --type swift -B 2 -A 15

Repository: hrayleung/Jin

Length of output: 4182


🏁 Script executed:

# Check if there are any initialization or default values set elsewhere
rg "FirecrawlSourceKind|firecrawl.*[Ss]ource" --type swift | head -40

Repository: hrayleung/Jin

Length of output: 3909


🏁 Script executed:

# Verify the API request handling one more time to ensure sources omission defaults to web
cat -n Sources/Tools/BuiltinSearchProvider+Firecrawl.swift | grep -A 20 "firecrawlSources"

Repository: hrayleung/Jin

Length of output: 1039


🏁 Script executed:

# Check if there's any initialization or default handling in WebSearchPluginSettingsStore
rg "decodeFirecrawlSources|encodeFirecrawlSources" --type swift -B 3 -A 10

Repository: hrayleung/Jin

Length of output: 6310


Return [.web] instead of [] to match Firecrawl's default behavior.

When firecrawlSourcesRaw is empty, this falls back to [], but Firecrawl treats omitted sources as web by default. The UI renders all toggles disabled while the API still returns web results, misrepresenting the actual state and control the user has over search results.

Suggested fix
     private func firecrawlSelectedSources() -> [FirecrawlSourceKind] {
         guard let trimmed = firecrawlSourcesRaw.trimmedNonEmpty,
               let data = trimmed.data(using: .utf8),
               let raw = try? JSONDecoder().decode([String].self, from: data) else {
-            return []
+            return [.web]
         }
-        return raw.compactMap(FirecrawlSourceKind.init(rawValue:))
+        let decoded = raw.compactMap(FirecrawlSourceKind.init(rawValue:))
+        return decoded.isEmpty ? [.web] : decoded
     }

This ensures the UI accurately reflects the API's default source when no explicit selection is persisted, maintaining coherent interaction flow per your UI/UX guidelines.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/UI/WebSearchPluginSettingsChromeViews.swift` around lines 357 - 363,
In firecrawlSelectedSources(), when firecrawlSourcesRaw is empty or decoding
fails the function currently returns an empty array; change this to return
[.web] so the UI matches Firecrawl's default behavior. Update the guard/fallback
logic in the firecrawlSelectedSources() function (and any use of
FirecrawlSourceKind) to return [FirecrawlSourceKind.web] (or .web) instead of []
when trimmed/decoded input is nil or empty, ensuring the UI toggles reflect the
API default.

@hrayleung hrayleung merged commit de4db79 into master May 9, 2026
3 checks passed
@hrayleung hrayleung deleted the refactor/web-search-plugin branch May 9, 2026 14:05
hrayleung added a commit that referenced this pull request May 10, 2026
Resolve conflict in WebSearchPluginSettingsChromeViews.swift between
master's expanded provider settings (#321 — Exa Category, User location,
moderation; Jina Country/Locale; Firecrawl Country/Language and source
toggles; Tavily Country and auto-tune; Perplexity Country/Language) and
this branch's Apple-density cleanup.

Resolution keeps every new control while applying the same cleanup rules:
self-explanatory toggles ship without supportingText, and Optional X…
captions on country/language/locale fields are replaced with `e.g. US` /
`e.g. en` placeholders. Tavily's "applies on General topic only" gating
note and the auto-tune warning are kept as trimmed supportingText since
the behavior is non-obvious.

swift build and swift test (2258 tests, 0 failures) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant