From 01e28bc3456ce6b06839b963970ed49ac7521f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 17 Mar 2026 17:59:38 -0600 Subject: [PATCH] feat(prefs): add search_provider to preferences.md (#1001) Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/preferences.ts | 23 +++++++++++++++++++ .../search-the-web/native-search.ts | 7 ++++++ .../extensions/search-the-web/provider.ts | 9 +++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/preferences.ts b/src/resources/extensions/gsd/preferences.ts index 83faf0d53..912f5f0f6 100644 --- a/src/resources/extensions/gsd/preferences.ts +++ b/src/resources/extensions/gsd/preferences.ts @@ -80,6 +80,7 @@ const KNOWN_PREFERENCE_KEYS = new Set([ "verification_commands", "verification_auto_fix", "verification_max_retries", + "search_provider", ]); export interface GSDSkillRule { @@ -182,6 +183,8 @@ export interface GSDPreferences { verification_commands?: string[]; verification_auto_fix?: boolean; verification_max_retries?: number; + /** Search provider preference. "brave"/"tavily"/"ollama" force that backend and disable native Anthropic search. "native" forces native only. "auto" = current default behavior. */ + search_provider?: "brave" | "tavily" | "ollama" | "native" | "auto"; } export interface LoadedGSDPreferences { @@ -759,6 +762,15 @@ export function resolveInlineLevel(): InlineLevel { } } +/** + * Resolve the search provider preference from preferences.md. + * Returns undefined if not configured (caller falls back to existing behavior). + */ +export function resolveSearchProviderFromPreferences(): GSDPreferences["search_provider"] | undefined { + const prefs = loadEffectiveGSDPreferences(); + return prefs?.preferences.search_provider; +} + function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPreferences { return { version: override.version ?? base.version, @@ -801,6 +813,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr verification_commands: mergeStringLists(base.verification_commands, override.verification_commands), verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix, verification_max_retries: override.verification_max_retries ?? base.verification_max_retries, + search_provider: override.search_provider ?? base.search_provider, }; } @@ -935,6 +948,16 @@ export function validatePreferences(preferences: GSDPreferences): { } } + // ─── Search Provider ───────────────────────────────────────────── + if (preferences.search_provider !== undefined) { + const validSearchProviders = new Set(["brave", "tavily", "ollama", "native", "auto"]); + if (typeof preferences.search_provider === "string" && validSearchProviders.has(preferences.search_provider)) { + validated.search_provider = preferences.search_provider as GSDPreferences["search_provider"]; + } else { + errors.push(`search_provider must be one of: brave, tavily, ollama, native, auto`); + } + } + // ─── Phase Skip Preferences ───────────────────────────────────────── if (preferences.phases !== undefined) { if (typeof preferences.phases === "object" && preferences.phases !== null) { diff --git a/src/resources/extensions/search-the-web/native-search.ts b/src/resources/extensions/search-the-web/native-search.ts index 3c50eda67..d57c528b7 100644 --- a/src/resources/extensions/search-the-web/native-search.ts +++ b/src/resources/extensions/search-the-web/native-search.ts @@ -5,6 +5,8 @@ * the heavy tool-registration modules. */ +import { resolveSearchProviderFromPreferences } from "../gsd/preferences.js"; + /** Tool names for the Brave-backed custom search tools */ export const BRAVE_TOOL_NAMES = ["search-the-web", "search_and_read"]; @@ -16,6 +18,11 @@ const THINKING_TYPES = new Set(["thinking", "redacted_thinking"]); /** When true, skip native web search injection and keep Brave/custom tools active on Anthropic. */ export function preferBraveSearch(): boolean { + // preferences.md takes priority over env var + const prefsPref = resolveSearchProviderFromPreferences(); + if (prefsPref === "brave" || prefsPref === "tavily" || prefsPref === "ollama") return true; + if (prefsPref === "native") return false; + // Fall back to env var return process.env.PREFER_BRAVE_SEARCH === "1" || process.env.PREFER_BRAVE_SEARCH === "true"; } diff --git a/src/resources/extensions/search-the-web/provider.ts b/src/resources/extensions/search-the-web/provider.ts index b730be837..65ee265aa 100644 --- a/src/resources/extensions/search-the-web/provider.ts +++ b/src/resources/extensions/search-the-web/provider.ts @@ -12,6 +12,7 @@ import { AuthStorage } from '@gsd/pi-coding-agent' import { homedir } from 'os' import { join } from 'path' +import { resolveSearchProviderFromPreferences } from '../gsd/preferences.js' // Compute authFilePath locally instead of importing from app-paths.ts, // because extensions are copied to ~/.gsd/agent/extensions/ at runtime @@ -94,9 +95,11 @@ export function resolveSearchProvider(overridePreference?: string): SearchProvid if (overridePreference && VALID_PREFERENCES.has(overridePreference)) { pref = overridePreference as SearchProviderPreference } else { - // Invalid override or no override — read stored preference - // If overridePreference is provided but invalid, treat as 'auto' - if (overridePreference !== undefined && !VALID_PREFERENCES.has(overridePreference)) { + // preferences.md takes priority over auth.json + const mdPref = resolveSearchProviderFromPreferences() + if (mdPref && mdPref !== 'auto' && mdPref !== 'native') { + pref = mdPref as SearchProviderPreference + } else if (overridePreference !== undefined && !VALID_PREFERENCES.has(overridePreference)) { pref = 'auto' } else { pref = getSearchProviderPreference()