gui: clarify search provider help (#878)

Explain manual-only relay selection and current searcher eligibility without a hard-coded provider list. Make quota and cost guidance apply to all search providers.

Update English, Chinese, Japanese, and German help while preserving existing ellipsis, hover titles, and search routing. Extend picker and help regression coverage.

Validation: 28 Chromium regression tests pass; picker/help tests pass three consecutive runs. WebKit is unverified due to missing host libraries. The separate search-api copy assertion has a pre-existing timing race reproduced on original and changed assets.
This commit is contained in:
cjyrainbow authored and GitHub committed 2026-10-05 14:59:02 +08:00
1 parent 37ec725916
commit 9e77092173
4 files changed
+143 -31

No files matched your search

+4 -5
View File
@@ -16499,7 +16499,7 @@ function renderSearcher(s, keep, box) {
const icOf = (id) => choices.find((x) => x.id === id.split("/")[0])?.icon;
const r = el("div", "row pref searcher-row");
const who = el("div", "who");
const sub = el("div", "sub", t("When a model can't search the web, this provider searches for it, and gives it what it found"));
const sub = el("div", "sub", t("When a model can't search the web directly, the selected provider searches for it and returns the results. Searches may use the service's quota or incur charges; if a search fails, magpie tries other available sources."));
if (v && s.searchUnused) {
const why = { gone: t("it is no longer in magpie"), off: t("it is turned off"), cant: t("it can't search the web by itself"), nomodel: t("it lists no model") }[s.searchUnused] || s.searchUnused;
sub.append(" · ", el("span", "warn searcher-unused", t("{who} isn't used: {why}, so magpie picks one", { who: named(v), why })));
@@ -16511,11 +16511,10 @@ function renderSearcher(s, keep, box) {
if (googles.length) sub.append(" · ", el("span", "searcher-own",
t("{names} search for their own models first, with Gemini's Google Search", { names: googles.join(", ") })));
if (s.searchRelays?.length) sub.append(" · ", el("span", "searcher-relays",
t("Relays said to search ({names}) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back", { names: s.searchRelays.join(", ") })));
// only a provider that searches by itself can search for another
// model; the rest are left out of the picker, and the row says so (#825)
t("These relays must be selected manually and are not used for automatic selection or fallback: {names}.", { names: s.searchRelays.join(", ") })));
// Being left out can also mean no usable model, not just no search support.
if (s.searchLeftOut?.length) sub.append(" · ", el("span", "searcher-left-out",
t("Only providers that search the web by themselves are offered (Claude, Codex and Grok accounts; the APIs of Anthropic, OpenAI, DeepSeek, xAI, Zhipu and OpenRouter; Gemini on a Google sign-in; a Kimi Code plan). {names} can't, so for their models the search APIs below search", { names: s.searchLeftOut.length > 6
t("These providers can't be selected to search for other models with the current configuration: {names}. Their models can still get search results through other available search providers or configured search APIs.", { names: s.searchLeftOut.length > 6
? t("{names} and {n} more", { names: s.searchLeftOut.slice(0, 5).join(", "), n: s.searchLeftOut.length - 5 })
: s.searchLeftOut.join(", ") })));
who.append(el("div", "name", t("Searches for other models")), sub);
+9 -9
View File
@@ -550,7 +550,7 @@ const I18N = {
"Now done by {who}; these come after it": "目前由 {who} 代搜,这些排在它之后",
"No provider can search, so these are asked": "没有能搜索的供应商,由这些 API 来搜",
"Searches for other models": "代搜供应商",
"When a model can't search the web, this provider searches for it, and gives it what it found": "模型不能联网搜索时,由此供应商代为搜索并返回结果",
"When a model can't search the web directly, the selected provider searches for it and returns the results. Searches may use the service's quota or incur charges; if a search fails, magpie tries other available sources.": "当模型无法直接联网搜索时,由所选供应商代为搜索并返回结果。代搜可能消耗所用服务的额度或产生费用;失败时会尝试其他可用来源。",
"no provider that searches": "没有能搜索的供应商",
"{model}, its small model": "{model}(它的小模型)",
"its web search": "它自带的联网搜索",
@@ -561,8 +561,8 @@ const I18N = {
"it can't search the web by itself": "它自己不能联网搜索",
"it lists no model": "它没有列出模型",
"{who} isn't used: {why}, so magpie picks one": "没有用 {who}:{why},改由 magpie 自动选择",
"Relays said to search ({names}) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back": "标为可搜索的中转站({names})不会被自动选择,以免为其他模型的搜索消耗中转站额度;若它拒绝 magpie 的请求,会退回其他选项",
"Only providers that search the web by themselves are offered (Claude, Codex and Grok accounts; the APIs of Anthropic, OpenAI, DeepSeek, xAI, Zhipu and OpenRouter; Gemini on a Google sign-in; a Kimi Code plan). {names} can't, so for their models the search APIs below search": "仅列出自己能联网搜索的供应商(Claude、Codex、Grok 账号;Anthropic、OpenAI、DeepSeek、xAI、智谱、OpenRouter 的 API;Google 登录的 Gemini;Kimi Code 套餐)。{names} 自己不能搜索,它们的模型由下面的搜索 API 代搜",
"These relays must be selected manually and are not used for automatic selection or fallback: {names}.": "以下中转服务需手动指定,不参与自动选择或自动回退:{names}。",
"These providers can't be selected to search for other models with the current configuration: {names}. Their models can still get search results through other available search providers or configured search APIs.": "以下供应商在当前配置下不可选为代搜供应商:{names}。它们的模型仍可通过其他可用的代搜供应商或已配置的搜索 API 获取搜索结果。",
"{names} and {n} more": "{names} 以及另外 {n} 个",
"API key, if it needs one": "API 密钥(如需要)",
"your own": "自建",
@@ -3715,7 +3715,7 @@ const I18N = {
"Now done by {who}; these come after it": "現在は {who} が検索しています。これらはその後に使われます",
"No provider can search, so these are asked": "検索できるプロバイダがないため、これらを使います",
"Searches for other models": "他のモデルの代わりに検索",
"When a model can't search the web, this provider searches for it, and gives it what it found": "モデルがウェブ検索できないとき、このプロバイダが代わりに検索し、結果をモデルに渡します",
"When a model can't search the web directly, the selected provider searches for it and returns the results. Searches may use the service's quota or incur charges; if a search fails, magpie tries other available sources.": "モデルが直接ウェブ検索できない場合、選択したプロバイダが代わりに検索し、結果を返します。検索にはサービスの利用枠を消費したり、料金が発生したりする場合があります。検索に失敗すると、magpie は他の利用可能な検索元を試します。",
"no provider that searches": "検索できるプロバイダなし",
"{model}, its small model": "{model}(その小型モデル)",
"its web search": "そのウェブ検索",
@@ -3726,8 +3726,8 @@ const I18N = {
"it can't search the web by itself": "自身ではウェブ検索できません",
"it lists no model": "モデルを一覧にしていません",
"{who} isn't used: {why}, so magpie picks one": "{who} は使われていません:{why}。そのため magpie が選びます",
"Relays said to search ({names}) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back": "検索可能とされている中継サービス({names})は自動では選ばれません。他のモデルの検索に中継サービスのクォータを使ってしまうためです。magpie 自身のリクエストを拒否された場合は、magpie はフォールバックします",
"Only providers that search the web by themselves are offered (Claude, Codex and Grok accounts; the APIs of Anthropic, OpenAI, DeepSeek, xAI, Zhipu and OpenRouter; Gemini on a Google sign-in; a Kimi Code plan). {names} can't, so for their models the search APIs below search": "自分でウェブ検索できるプロバイダだけが表示されます(Claude・Codex・Grok のアカウント、Anthropic・OpenAI・DeepSeek・xAI・Zhipu・OpenRouter の API、Google ログインの Gemini、Kimi Code プラン)。{names} は自分で検索できないため、それらのモデルには下の検索 API が検索します",
"These relays must be selected manually and are not used for automatic selection or fallback: {names}.": "次の中継サービスは手動で選択する必要があり、自動選択や自動フォールバックの対象にはなりません:{names}。",
"These providers can't be selected to search for other models with the current configuration: {names}. Their models can still get search results through other available search providers or configured search APIs.": "次のプロバイダは現在の設定では他のモデルの検索用に選択できません:{names}。これらのモデルも、他の利用可能な検索用プロバイダや設定済みの検索 API を通じて検索結果を取得できます。",
"{names} and {n} more": "{names} ほか {n} 件",
"API key, if it needs one": "APIキー(必要な場合)",
"your own": "独自",
@@ -6866,7 +6866,7 @@ const I18N = {
"Now done by {who}; these come after it": "Derzeit übernimmt das {who}; diese folgen danach",
"No provider can search, so these are asked": "Kein Anbieter kann suchen, daher werden diese gefragt",
"Searches for other models": "Sucht für andere Modelle",
"When a model can't search the web, this provider searches for it, and gives it what it found": "Kann ein Modell nicht im Web suchen, sucht dieser Anbieter stellvertretend und übergibt ihm die Ergebnisse",
"When a model can't search the web directly, the selected provider searches for it and returns the results. Searches may use the service's quota or incur charges; if a search fails, magpie tries other available sources.": "Wenn ein Modell nicht direkt im Web suchen kann, übernimmt der ausgewählte Anbieter die Suche und liefert die Ergebnisse. Suchen können das Kontingent des Dienstes verbrauchen oder Kosten verursachen. Schlägt eine Suche fehl, versucht magpie andere verfügbare Quellen.",
"no provider that searches": "kein Anbieter mit Suche",
"{model}, its small model": "{model}, sein kleines Modell",
"its web search": "seine Websuche",
@@ -6877,8 +6877,8 @@ const I18N = {
"it can't search the web by itself": "er kann nicht selbst im Web suchen",
"it lists no model": "er listet kein Modell",
"{who} isn't used: {why}, so magpie picks one": "{who} wird nicht verwendet: {why}, daher wählt magpie einen",
"Relays said to search ({names}) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back": "Relays mit Suchfunktion ({names}) werden nie automatisch gewählt: sie würden das Kontingent des Relays für Suchen anderer Modelle verbrauchen; lehnt eines die Anfrage von magpie ab, weicht magpie aus",
"Only providers that search the web by themselves are offered (Claude, Codex and Grok accounts; the APIs of Anthropic, OpenAI, DeepSeek, xAI, Zhipu and OpenRouter; Gemini on a Google sign-in; a Kimi Code plan). {names} can't, so for their models the search APIs below search": "Nur Anbieter, die selbst im Web suchen, werden angeboten (Claude-, Codex- und Grok-Konten; die APIs von Anthropic, OpenAI, DeepSeek, xAI, Zhipu und OpenRouter; Gemini mit Google-Anmeldung; ein Kimi-Code-Abo). {names} können das nicht, daher suchen für ihre Modelle die Such-APIs unten",
"These relays must be selected manually and are not used for automatic selection or fallback: {names}.": "Diese Relays müssen manuell ausgewählt werden und werden weder automatisch noch als Fallback gewählt: {names}.",
"These providers can't be selected to search for other models with the current configuration: {names}. Their models can still get search results through other available search providers or configured search APIs.": "Diese Anbieter können mit der aktuellen Konfiguration nicht für die Suche anderer Modelle ausgewählt werden: {names}. Ihre Modelle können weiterhin über andere verfügbare Suchanbieter oder konfigurierte Such-APIs Suchergebnisse erhalten.",
"{names} and {n} more": "{names} und {n} weitere",
"API key, if it needs one": "API-Schlüssel, falls nötig",
"your own": "eigener",
+11 -2
View File
@@ -1447,12 +1447,21 @@ models" row (01huadalang on Discord: pick which provider searches for a model
that can't). With none named it shows Automatic and magpie's own pick; its
picker offers Automatic first, each provider that can search by its small
model, and each of its models, including a relay said to search; the row says
those relays are never picked automatically and why. A model picked is saved as `searcher:
those relays require manual selection and are not automatic fallback choices.
Costs and fallback are explained for every search provider; providers left out
are described as unavailable in the current configuration, with both other
search providers and configured search APIs named as alternatives.
A model picked is saved as `searcher:
"<provider>/<model>"`, a provider by its small model as `searcher:
"<provider>"`, and shown; another setting saved still sends the pick; one
named that magpie can't use (turned off) is said in the row with magpie's
pick shown instead; Automatic sends `searcher: ""`. No click moves the page.
English and Chinese, Chromium and WebKit, with the API faked.
The picker is checked in English and Chinese; the help also in Japanese and
German, at 560 and 1000 pixels in light and dark themes, including a selected
relay and empty relay/unavailable lists. Desktop text keeps its single-line
ellipsis and full hover title; the existing narrow-web layout is unchanged.
Chromium and WebKit, with the API faked.
Set `ARTIFACT_DIR` to retain screenshots of the help.
`provider-file-error.test.cjs` checks a providers.json that is there but
can't be read (#415's review): the Providers page says over the list that the
+119 -15
View File
@@ -5,12 +5,14 @@
// its small model, and each of its models, including a relay said to search;
// a pick is saved as searcher ("<provider>" or "<provider>/<model>") and shown;
// one named that magpie can't use (turned off) is said in the row, magpie's pick shown instead;
// relays said to search are said not to be picked automatically; a Kimi Code plan, which
// relays said to search are manual-only, including during fallback; a Kimi Code plan, which
// searches by its web search with no model, is offered by itself and said to search for its
// own models first. Another setting saved
// keeps the pick (prefsKeep). No click moves the page. English and Chinese, Chromium and
// WebKit, with the API faked. The providers left out, which can't search by
// themselves, are named, and why (#825).
// WebKit, with the API faked. Search help covers costs for every provider and
// explains current eligibility without claiming a provider can never search.
// The help is also checked in Japanese and German, preserving desktop ellipsis
// and hover titles as well as the existing narrow-web layout.
const assert = require("node:assert/strict");
const fs = require("node:fs/promises");
const path = require("node:path");
@@ -32,26 +34,46 @@ const choices = [
{ id: "antigravity/gemini-3-flash", name: "Gemini 3 Flash", provider: "antigravity", providerName: "Antigravity" }] },
];
const words = {
en: { name: "Searches for other models", auto: "Automatic", small: "GPT-5 Mini Named, its small model", unused: "isn't used: it is turned off", relays: "Relays said to search (MyRelay) are never picked automatically: they would spend the relay's quota on other models' searches; if one refuses magpie's own request, magpie falls back",
en: { name: "Searches for other models", auto: "Automatic", small: "GPT-5 Mini Named, its small model", unused: "isn't used: it is turned off",
own: "A Kimi Code plan (Kimi Code) searches for its own models first, with its web search; for other models only when named here", web: "its web search",
google: "Antigravity search for their own models first, with Gemini's Google Search",
more: "A1, A2, A3, A4, A5 and 3 more can't",
left: "MiniMax, Kimi For Coding can't, so for their models the search APIs below search" },
zh: { name: "代搜供应商", auto: "自动", small: "GPT-5 Mini Named(它的小模型)", unused: "没有用 OpenAI · GPT-5 Mini Named:它已关闭", relays: "标为可搜索的中转站(MyRelay)不会被自动选择,以免为其他模型的搜索消耗中转站额度;若它拒绝 magpie 的请求,会退回其他选项",
more: "A1, A2, A3, A4, A5 and 3 more." },
zh: { name: "代搜供应商", auto: "自动", small: "GPT-5 Mini Named(它的小模型)", unused: "没有用 OpenAI · GPT-5 Mini Named:它已关闭",
own: "Kimi Code 套餐(Kimi Code)的模型优先用套餐自带的联网搜索;其他模型仅在此处选中时使用", web: "它自带的联网搜索",
google: "Antigravity 的模型优先用 Gemini 自带的 Google 搜索",
more: "A1, A2, A3, A4, A5 以及另外 3 个 自己不能搜索",
left: "MiniMax, Kimi For Coding 自己不能搜索,它们的模型由下面的搜索 API 代搜" },
more: "A1, A2, A3, A4, A5 以及另外 3 个。" },
};
const helpWords = {
en: {
help: "When a model can't search the web directly, the selected provider searches for it and returns the results. Searches may use the service's quota or incur charges; if a search fails, magpie tries other available sources.",
relays: "These relays must be selected manually and are not used for automatic selection or fallback: MyRelay.",
left: "These providers can't be selected to search for other models with the current configuration: MiniMax, Kimi For Coding. Their models can still get search results through other available search providers or configured search APIs.",
},
zh: {
help: "当模型无法直接联网搜索时,由所选供应商代为搜索并返回结果。代搜可能消耗所用服务的额度或产生费用;失败时会尝试其他可用来源。",
relays: "以下中转服务需手动指定,不参与自动选择或自动回退:MyRelay。",
left: "以下供应商在当前配置下不可选为代搜供应商:MiniMax, Kimi For Coding。它们的模型仍可通过其他可用的代搜供应商或已配置的搜索 API 获取搜索结果。",
},
ja: {
help: "モデルが直接ウェブ検索できない場合、選択したプロバイダが代わりに検索し、結果を返します。検索にはサービスの利用枠を消費したり、料金が発生したりする場合があります。検索に失敗すると、magpie は他の利用可能な検索元を試します。",
relays: "次の中継サービスは手動で選択する必要があり、自動選択や自動フォールバックの対象にはなりません:MyRelay。",
left: "次のプロバイダは現在の設定では他のモデルの検索用に選択できません:MiniMax, Kimi For Coding。これらのモデルも、他の利用可能な検索用プロバイダや設定済みの検索 API を通じて検索結果を取得できます。",
},
de: {
help: "Wenn ein Modell nicht direkt im Web suchen kann, übernimmt der ausgewählte Anbieter die Suche und liefert die Ergebnisse. Suchen können das Kontingent des Dienstes verbrauchen oder Kosten verursachen. Schlägt eine Suche fehl, versucht magpie andere verfügbare Quellen.",
relays: "Diese Relays müssen manuell ausgewählt werden und werden weder automatisch noch als Fallback gewählt: MyRelay.",
left: "Diese Anbieter können mit der aktuellen Konfiguration nicht für die Suche anderer Modelle ausgewählt werden: MiniMax, Kimi For Coding. Ihre Modelle können weiterhin über andere verfügbare Suchanbieter oder konfigurierte Such-APIs Suchergebnisse erhalten.",
},
};
function serve(lang, posted, st) {
const settings = () => ({ lang, theme: "light", searchVendors: [], searchAPIs: [], searchChoices: choices,
const settings = () => ({ lang, theme: st.theme || "light", searchVendors: [], searchAPIs: [], searchChoices: choices,
searchAuto: "Claude · claude-haiku-4-5", searchProvider: st.unused || !st.searcher ? "Claude · claude-haiku-4-5" : st.searcher,
searchRelays: ["MyRelay"], searchLeftOut: st.leftOut || ["MiniMax", "Kimi For Coding"], searcher: st.searcher, searchUnused: st.unused });
searchRelays: st.relays || ["MyRelay"], searchLeftOut: st.leftOut || ["MiniMax", "Kimi For Coding"], searcher: st.searcher, searchUnused: st.unused });
return async (r) => {
const url = new URL(r.request().url());
const json = (data, status = 200) => r.fulfill({ status, json: data });
if (url.pathname === "/boot.js") return r.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:true};` });
if (url.pathname === "/boot.js") return r.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = ${JSON.stringify({ lang, theme: st.theme || "light", web: true })};` });
if (url.pathname === "/wails/runtime.js") return r.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: settings() });
if (url.pathname === "/api/settings") {
@@ -76,6 +98,7 @@ function serve(lang, posted, st) {
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
const w = words[lang];
const h = helpWords[lang];
test(`${engine} ${lang}: the provider that searches for other models is picked in Settings`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
@@ -92,11 +115,11 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await row.scrollIntoViewIfNeeded();
assert.equal(await row.locator(".name").innerText(), w.name);
assert.equal(await row.locator("button.searcher-pick").innerText(), `${w.auto} · Claude · claude-haiku-4-5`);
assert((await row.locator(".sub").innerText()).includes(w.relays), "the relays said to search are named");
assert((await row.locator(".sub").innerText()).startsWith(h.help), "costs and fallback apply to every search provider");
assert.equal(await row.locator(".searcher-relays").innerText(), h.relays);
assert((await row.locator(".sub").innerText()).includes(w.own), "the Kimi Code plan is said to search for its own models");
assert((await row.locator(".sub").innerText()).includes(w.google), "a Google sign-in is said to search for its own models (#757)");
// the providers left out of the picker are named, and why (#825)
assert((await row.locator(".searcher-left-out").innerText()).includes(w.left), "the providers that can't search are said to be left out");
assert.equal(await row.locator(".searcher-left-out").innerText(), h.left);
// a long list is cut short
st.leftOut = ["A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8"];
await page.evaluate(() => fetch("/api/settings").then((r) => r.json()).then((s) => { prefs = s; state.settings = s; renderSettings(); }));
@@ -135,6 +158,16 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "OpenAI · GPT-5 Mini Named");
assert.equal(posted.at(-1).searcher, "openai");
// Manual-only is not unavailable: selecting a relay still saves and uses it.
await click(row.locator("button.searcher-pick"));
await page.locator("#pop").waitFor({ state: "visible" });
const relay = page.locator("#list li:not(.group)", { hasText: "MyRelay" }).last();
await relay.scrollIntoViewIfNeeded();
await click(relay);
await page.waitForFunction(() => document.querySelector("#searchList button.searcher-pick")?.innerText === "Claude Haiku 4.5 · MyRelay");
assert.equal(posted.at(-1).searcher, "relay/claude-haiku-4-5");
assert.equal(await row.locator(".searcher-relays").innerText(), h.relays);
// a Kimi Code plan, by its web search: no model of it is offered
await click(row.locator("button.searcher-pick"));
await page.locator("#pop").waitFor({ state: "visible" });
@@ -179,4 +212,75 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
assert.deepEqual(errors, []);
});
}
for (const lang of Object.keys(helpWords)) {
test(`${engine} ${lang}: search help preserves ellipsis and hover titles`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
const page = await (await browser.newContext({ reducedMotion: "reduce" })).newPage();
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
const posted = [];
const st = { searcher: "relay/claude-haiku-4-5", unused: "" };
const h = helpWords[lang];
await page.route("**/*", serve(lang, posted, st));
for (const theme of ["light", "dark"]) {
st.theme = theme;
for (const width of [560, 1000]) {
await page.mouse.move(0, 0);
await page.setViewportSize({ width, height: 900 });
await page.goto("http://magpie.test/?view=settings&tab=models");
const row = page.locator("#searchList .searcher-row");
await row.waitFor();
// Hover retries if Settings' startup refresh replaces the row.
await row.locator("button.searcher-pick").hover();
assert((await row.locator(".sub").innerText()).startsWith(h.help));
assert.equal(await row.locator(".searcher-relays").innerText(), h.relays);
assert.equal(await row.locator(".searcher-left-out").innerText(), h.left);
assert.equal(await row.locator("button.searcher-pick").innerText(), "Claude Haiku 4.5 · MyRelay");
const layout = await row.evaluate((r) => {
const sub = r.querySelector(".sub");
const b = r.querySelector("button.searcher-pick").getBoundingClientRect();
const s = sub.getBoundingClientRect(), bounds = r.getBoundingClientRect();
return { whiteSpace: getComputedStyle(sub).whiteSpace, textOverflow: getComputedStyle(sub).textOverflow,
overflow: sub.scrollWidth - sub.clientWidth,
clipped: sub.scrollHeight - sub.clientHeight,
fits: b.left >= bounds.left && b.right <= bounds.right + 1 &&
b.top >= bounds.top && b.bottom <= bounds.bottom + 1 &&
(s.right <= b.left + 1 || s.bottom <= b.top + 1) };
});
assert(layout.clipped <= 1 && layout.fits, JSON.stringify(layout));
const sub = row.locator(".sub");
assert.equal(await sub.getAttribute("class"), "sub", "copy changes must not opt into wrapping");
if (width === 1000) {
assert.equal(layout.whiteSpace, "nowrap", "desktop help stays on one line");
assert.equal(layout.textOverflow, "ellipsis");
assert(layout.overflow > 0, "long help is truncated");
assert.equal(await sub.getAttribute("title"), null);
await sub.hover();
const full = await sub.evaluate((e) => e.textContent.replace(/\s+/g, " ").trim());
assert.equal(await sub.getAttribute("title"), full, "hover exposes the complete updated help");
assert.equal(await sub.getAttribute("data-full-tip"), full);
} else {
assert.equal(layout.whiteSpace, "normal", "retain the existing narrow-web layout");
assert(layout.overflow <= 1, JSON.stringify(layout));
}
if (process.env.ARTIFACT_DIR) {
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
await row.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-${theme}-${width}-search-help.png`) });
}
}
}
// Costs and fallback still matter when no manual-only or unavailable providers are listed.
st.relays = [];
st.leftOut = [];
st.searcher = "";
await page.goto("http://magpie.test/?view=settings&tab=models");
const row = page.locator("#searchList .searcher-row");
await row.waitFor();
assert((await row.locator(".sub").innerText()).startsWith(h.help));
assert.equal(await row.locator(".searcher-relays, .searcher-left-out").count(), 0);
assert.deepEqual(posted, [], "reading help must not change settings");
assert.deepEqual(errors, []);
});
}
}