Merge branch farm/5a737841/tinyfish-honor-lang-directive: fix(coding-agent): honor lang: directive in TinyFish search (@roboomp)

This commit is contained in:
can1357
2026-08-19 11:52:55 +02:00
3 changed files with 65 additions and 0 deletions
+1
View File
@@ -21,6 +21,7 @@
- Fixed `omp commit` split-commit crashing with a misleading `No diff found for <path>` when a staged binary (or any payload) pushed `git diff --cached --binary` past the 8 MiB subprocess output cap. The capture is truncated silently, so files sorting after the binary vanished from the parsed diff; the split flow now requests a complete diff and fails fast naming the real cause instead ([#8897](https://github.com/can1357/oh-my-pi/issues/8897)).
- Fixed a mid-run compaction being misread as a phantom overflow: after a compaction rebased the in-flight context snapshot, `getContextBreakdown` used message position (`anchorIndex >= cutoffCount`) as a freshness proxy, so an in-flight provider response whose request predated the compaction out-ranked the rebased estimate and reported the pre-compaction token count (~2.6x the real one). This tripped the "Compaction freed too little context to make progress" guard and drove the frame-rescue path on a byte-identical `tokensBefore`. Assistant context snapshots now carry a monotonic compaction epoch, and a post-cutoff anchor whose epoch predates the last compaction is no longer trusted over the rebased estimate ([#8887](https://github.com/can1357/oh-my-pi/issues/8887)).
- Fixed `after_provider_response` extension handlers receiving the primary session model in `ctx.model` and `ctx.models.current()` for cross-provider side requests. `ExtensionRunner.emitAfterProviderResponse` accepted the response model but discarded it, so a handler revoking a credential on an HTTP 402 could target the wrong provider. It now threads the response model into the context, matching `emitBeforeProviderRequest` ([#8955](https://github.com/can1357/oh-my-pi/issues/8955)).
- 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.
@@ -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<SearchResponse> {
const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
@@ -152,6 +174,9 @@ export async function searchTinyFish(params: SearchParams): Promise<SearchRespon
if (includeDomains.length > 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,
});
@@ -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([