diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index fdd5ae758..9b9664b96 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixed - Fixed Claude Code marketplace plugins ignoring the `enabledPlugins` switch in `~/.claude/settings.json` and `.claude/settings(.local).json`: a plugin turned off for a project no longer loads there, and a local-scope install enabled for a project loads even when its recorded `projectPath` is a different directory +- Fixed the TinyFish web search provider ignoring the `lang:`/`language:` query directive, so every request fell back to the API's US/English geolocation. `parsed.lang` now maps onto TinyFish's `location`/`language` parameters (e.g. `lang:it-it` → `location=IT&language=it`), matching the DuckDuckGo, Perplexity, and SearXNG providers ([#8913](https://github.com/can1357/oh-my-pi/issues/8913)). - Fixed task and eval subagents discovering newly added agent definitions while resolving their role aliases from stale startup settings. Subagent preflight now atomically reloads persisted settings before agent discovery while preserving live runtime overrides. - Fixed images returned by tools mounted under `xd://` rendering only as file links instead of inline terminal graphics. - Resume Cursor idle-stall turns after completed MCP/todo tool results. The watchdog already closes the Connect stream, so unmarked blocks no longer need the `exec-resolved` marker to continue. diff --git a/packages/coding-agent/src/web/search/providers/tinyfish.ts b/packages/coding-agent/src/web/search/providers/tinyfish.ts index 47971e3cc..aa671742d 100644 --- a/packages/coding-agent/src/web/search/providers/tinyfish.ts +++ b/packages/coding-agent/src/web/search/providers/tinyfish.ts @@ -35,6 +35,10 @@ export interface TinyFishSearchParams { page?: number; include_domains?: string[]; exclude_domains?: string[]; + /** ISO 3166-1 alpha-2 region, e.g. `IT`. Geolocates results. */ + location?: string; + /** ISO 639-1 language, e.g. `it`. */ + language?: string; signal?: AbortSignal; timeoutMs?: number; fetch?: FetchImpl; @@ -74,6 +78,12 @@ async function callTinyFishSearch(apiKey: string, params: TinyFishSearchParams): if (params.exclude_domains?.length) { url.searchParams.set("exclude_domains", params.exclude_domains.join(",")); } + if (params.location) { + url.searchParams.set("location", params.location); + } + if (params.language) { + url.searchParams.set("language", params.language); + } if (params.num_results !== undefined) { url.searchParams.set("num_results", String(params.num_results)); } @@ -133,6 +143,18 @@ function siteHosts(sites: readonly string[]): string[] { return [...hosts]; } +/** + * Derive TinyFish `location` (ISO 3166-1 alpha-2, uppercase) and `language` + * (ISO 639-1, lowercase) from a parsed `lang:` directive. The region subtag is + * optional: `lang:it` yields language only, `lang:it-it` yields both. + */ +function tinyFishLocale(lang: string | undefined): { location?: string; language?: string } { + if (!lang) return {}; + const match = /^([a-z]{2})(?:[-_]([a-z]{2}))?/.exec(lang.toLowerCase()); + if (!match) return {}; + return { language: match[1], location: match[2]?.toUpperCase() }; +} + /** Execute TinyFish web search. */ export async function searchTinyFish(params: SearchParams): Promise { const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS); @@ -152,6 +174,9 @@ export async function searchTinyFish(params: SearchParams): Promise 0) tinyFishParams.include_domains = includeDomains; if (excludeDomains.length > 0) tinyFishParams.exclude_domains = excludeDomains; } + const { location, language } = tinyFishLocale(parsed.lang); + if (location) tinyFishParams.location = location; + if (language) tinyFishParams.language = language; const keyOrResolver: ApiKey = params.authStorage.resolver("tinyfish", { sessionId: params.sessionId, }); diff --git a/packages/coding-agent/test/tools/web-search-tinyfish.test.ts b/packages/coding-agent/test/tools/web-search-tinyfish.test.ts index f8c43fd18..a5fd596b8 100644 --- a/packages/coding-agent/test/tools/web-search-tinyfish.test.ts +++ b/packages/coding-agent/test/tools/web-search-tinyfish.test.ts @@ -107,6 +107,45 @@ describe("TinyFish web search provider", () => { expect(captured[0].searchParams.get("query")).toBe("plain query with ordinary words"); }); + it("maps a lang: locale directive onto location and language", async () => { + const captured: URL[] = []; + const fetchMock: FetchImpl = async input => { + const url = input instanceof URL ? input : new URL(typeof input === "string" ? input : input.url); + captured.push(url); + return new Response(JSON.stringify(tinyFishPage(tinyFishResults("tinyfish", 3))), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + await searchTinyFish({ ...makeParams("Best Cheap Android Tablet lang:it-it"), fetch: fetchMock }); + + expect(captured).toHaveLength(1); + expect(captured[0].searchParams.get("query")).toBe("Best Cheap Android Tablet"); + expect(captured[0].searchParams.get("location")).toBe("IT"); + expect(captured[0].searchParams.get("language")).toBe("it"); + expectTinyFishParams(captured[0], ["query", "num_results", "page", "location", "language"]); + }); + + it("sends language only when the lang: directive omits a region", async () => { + const captured: URL[] = []; + const fetchMock: FetchImpl = async input => { + const url = input instanceof URL ? input : new URL(typeof input === "string" ? input : input.url); + captured.push(url); + return new Response(JSON.stringify(tinyFishPage(tinyFishResults("tinyfish", 3))), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }; + + await searchTinyFish({ ...makeParams("cheap tablets lang:it"), fetch: fetchMock }); + + expect(captured).toHaveLength(1); + expect(captured[0].searchParams.get("language")).toBe("it"); + expect(captured[0].searchParams.has("location")).toBe(false); + expectTinyFishParams(captured[0], ["query", "num_results", "page", "language"]); + }); + it("passes TinyFish num_results and applies numSearchResults across pages", async () => { const captured: { url: URL; init?: RequestInit }[] = []; const pages = new Map([