gui, provider: where a model or routing group is named, it says what agents' lists will call it and offers dropping the suffix there; a group the user made counts as a name they set (#868)
The reporter's screenshot was Codex's model menu, where a long name plus " · Relay" or " · routing group" is cut off. Magpie can't widen an agent's menu. What it can do is shorten the label. Models can already be renamed (provider editor › Names & levels), groups have a Name, and Settings › Provider in model names (#335) can drop the suffix. The reporter found none of these. Changes: - The provider editor's Names & levels and a routing group's editor now each have the three choices: Off, Not on names I set, On. - Each says what the agents' lists will show, e.g. "My Sol · Relay" or "Fast · routing group". - The line follows the name as it is typed and the choice as it is picked. - A pick posts settings/plain-names once and doesn't scroll. - "Not on names I set" now covers routing groups the user made or changed (Entry.Named = !g.Auto) as well as renamed models, still only where the name is unique in the list. A group magpie found keeps its suffix. - zh, ja, de strings. Tests: - TestPlainOwnNamesGroups fails on the old code. - name-suffix-where-named.test.cjs (WebKit, en and zh) fails 4/4 on the old assets. - group-fold.test.cjs: viewport raised to 1500 for the taller editor. - Pass on WebKit: group-context, plain-names, provider-same-as, provider-levels-save, gui-ja, gui-de, click-scroll, agent-models, agent-model-groups, group-* and routing-*. - Fail the same on origin/main: group-from-model-add at 600x420, provider-levels-scroll. - go vet passes. The darwin, GOOS=linux nogui and GOOS=windows builds pass. go test -tags nogui -count=1 ./... passes under a temp HOME.
This commit is contained in:
1 parent
af25cc6977
commit
bbb9729748
11 files changed
+249
-13
No files matched your search
@@ -2258,6 +2258,9 @@ body.window .ag-sk-icon { width: 22px; height: 22px; border-radius: 6px; }
|
||||
.mnames { display: grid; gap: 4px; max-height: 240px; overflow-y: auto; padding: 1px; }
|
||||
.mnames[hidden] { display: none; }
|
||||
.mnames > .hint { font-size: 11px; color: var(--faint); }
|
||||
/* how the agents' lists will call them, and the setting that says so (#868) */
|
||||
.msuffix { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 2px 8px 4px; font-size: 12px; }
|
||||
.msuffix .hint { font-size: 11px; color: var(--faint); overflow-wrap: anywhere; }
|
||||
.mname { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; padding: 5px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); }
|
||||
.mname .mwho { display: flex; align-items: center; gap: 8px; flex: 1 1 220px; min-width: 0; }
|
||||
.mname .mwho input { flex: 1; min-width: 0; max-width: 220px; height: 25px; }
|
||||
|
||||
@@ -8184,6 +8184,18 @@ function renderModels(p) {
|
||||
if (!ids.length) { names.append(el("span", "hint", t("Pick a model first."))); return; }
|
||||
const prefs = draft.modelPrefs = draft.modelPrefs || {};
|
||||
draft.onModelPrefs = drawNames;
|
||||
// how the agents' lists will call them, and the setting that says so
|
||||
// (#868), above the names given here
|
||||
const sfx = el("div", "msuffix");
|
||||
const sfxSaid = el("span", "hint");
|
||||
const first = p.models.find((x) => x.id === ids[0]) || { id: ids[0], name: ids[0] };
|
||||
const sayLabel = (mode) => {
|
||||
const name = prefs[first.id]?.name ?? (first.default ? first.name : "");
|
||||
sfxSaid.textContent = t("Agents’ lists show “{label}”", { label: suffixed(name || first.default || first.name || first.id, p.name || p.id, !!name, mode) });
|
||||
};
|
||||
sayLabel();
|
||||
sfx.append(el("span", "", t("Provider in model names")), suffixSegs(sayLabel), sfxSaid);
|
||||
names.append(sfx);
|
||||
// the APIs a model can be asked on alone: those of a key's provider
|
||||
// it has a URL for, when it has more than one (01huadalang on Discord:
|
||||
// 一个 api 里有很多模型但是不同协议)
|
||||
@@ -15870,6 +15882,30 @@ const IN_USE = "|*";
|
||||
// renderTrayUsage: the subscriptions and plans whose windows show beside
|
||||
// the tray icon, side by side. The cards are the Usage page's, asked for
|
||||
// when the menu opens; any number are ticked in it.
|
||||
// suffixSegs: how the agents' lists name a model — with its provider's
|
||||
// (or "routing group") after it, all but the names the user gave models
|
||||
// and the groups they made (#92), or none (#335). Set on its own, so the
|
||||
// agents are told. Offered on Settings and where names are given, the
|
||||
// provider's Names & levels and a group's editor (#868: an agent's narrow
|
||||
// menu cut "· routing group" short, and the setting wasn't found);
|
||||
// drawn(mode) redraws the place it was picked in.
|
||||
function suffixMode(s = prefs || {}) { return s.plainNames ? "off" : s.plainOwnNames ? "own" : "on"; }
|
||||
function suffixSegs(drawn) {
|
||||
const seg = segs([["off", t("Off")], ["own", t("Not on names I set")], ["on", t("On")]], suffixMode(), (v) =>
|
||||
writingPrefs(api("settings/plain-names", { mode: v })).then((ns) => { prefs = ns; renderSettings(); drawn?.(v); })
|
||||
.catch((e) => { status(t(e.message), "err"); renderSettings(); drawn?.(suffixMode()); }));
|
||||
seg.classList.add("suffix-segs");
|
||||
return seg;
|
||||
}
|
||||
// suffixed: what an agent's list calls a model named name, by its
|
||||
// provider's or group's name by, under mode; own when the user gave it
|
||||
// (Labels in internal/provider)
|
||||
function suffixed(name, by, own, mode = suffixMode()) {
|
||||
if (mode === "off" || mode === "own" && own) return name;
|
||||
if (own && name.toLowerCase().includes(by.toLowerCase())) return name;
|
||||
return name + " · " + by;
|
||||
}
|
||||
|
||||
function renderTrayUsage(s, keep) {
|
||||
$("#quotaLeftSegs").replaceChildren(segs([[false, t("Used")], [true, t("Left")]], !!s.quotaLeft,
|
||||
(on) => { if (on !== quotaLeft) setQuotaLeft(on); }));
|
||||
@@ -15879,12 +15915,7 @@ function renderTrayUsage(s, keep) {
|
||||
$("#unitsSegs").replaceChildren(segs([[false, t("K / M / B")], [true, t("万 / 亿")]], !!s.chineseUnits,
|
||||
(v) => savePrefs({ ...keep, chineseUnits: v })));
|
||||
renderAlerts(s, keep);
|
||||
// the agents' lists name a model with its provider's after it, all but
|
||||
// the names the user gave (#92), or none (#335): set on its own, so the
|
||||
// agents are told
|
||||
const suffix = s.plainNames ? "off" : s.plainOwnNames ? "own" : "on";
|
||||
$("#plainNamesSegs").replaceChildren(segs([["off", t("Off")], ["own", t("Not on names I set")], ["on", t("On")]], suffix, (v) =>
|
||||
writingPrefs(api("settings/plain-names", { mode: v })).then((ns) => { prefs = ns; renderSettings(); }).catch((e) => { status(t(e.message), "err"); renderSettings(); })));
|
||||
$("#plainNamesSegs").replaceChildren(suffixSegs());
|
||||
// Codex's OpenAI models in multi-agent V1, so their subagents can run on
|
||||
// magpie's other models (#141)
|
||||
$("#codexAgentsV1Segs").replaceChildren(segs([["off", t("Off")], ["on", t("On")]], s.codexAgentsV1 ? "on" : "off", (v) =>
|
||||
|
||||
@@ -442,6 +442,8 @@ const I18N = {
|
||||
"Provider in model names": "模型名带供应商",
|
||||
"Agents’ lists put each model’s provider after its name, or not after names you set": "Agent 的模型列表在模型名后带上供应商,可设为自定义名称不带",
|
||||
"Not on names I set": "自定义名称不带供应商",
|
||||
"Agents’ lists show “{label}”": "Agent 的模型列表里显示为「{label}」",
|
||||
"In agents’ lists": "Agent 列表里的名称",
|
||||
"Codex subagents on any model": "Codex 子代理可用任意模型",
|
||||
"Long conversations": "长对话",
|
||||
"Codex and Claude Code compact a conversation at 272K tokens on a model with a larger window; in Claude Code a Claude model runs to its own window, 1M for a [1m] one, as Anthropic runs it. The model’s full window lets any conversation grow further, with every turn slower and costlier": "模型窗口超过 272K 时,Codex 和 Claude Code 在 272K tokens 处压缩对话;Claude Code 中的 Claude 模型按自身窗口([1m] 为 1M),与 Anthropic 官方一致。使用完整窗口可让对话更长,但每轮更慢、更贵",
|
||||
@@ -3577,6 +3579,8 @@ const I18N = {
|
||||
"Provider in model names": "モデル名にプロバイダ",
|
||||
"Agents’ lists put each model’s provider after its name, or not after names you set": "エージェントに渡すモデル一覧で、モデル名の後にプロバイダを付けます。自分で付けた名前には付けないこともできます",
|
||||
"Not on names I set": "自分で付けた名前には付けない",
|
||||
"Agents’ lists show “{label}”": "エージェントの一覧では「{label}」と表示されます",
|
||||
"In agents’ lists": "エージェントの一覧での名前",
|
||||
"Codex subagents on any model": "Codex のサブエージェントで任意のモデル",
|
||||
"Long conversations": "長い会話",
|
||||
"Codex and Claude Code compact a conversation at 272K tokens on a model with a larger window; in Claude Code a Claude model runs to its own window, 1M for a [1m] one, as Anthropic runs it. The model’s full window lets any conversation grow further, with every turn slower and costlier": "ウィンドウが大きいモデルでは、Codex と Claude Code は 272K トークンで会話を圧縮します。Claude Code の Claude モデルは Anthropic と同じく自身のウィンドウ([1m] なら 1M)まで使います。モデルのウィンドウ全体を使うとどの会話もさらに伸ばせますが、ターンごとに遅く、高くなります",
|
||||
@@ -6697,6 +6701,8 @@ const I18N = {
|
||||
"Provider in model names": "Anbieter in Modellnamen",
|
||||
"Agents’ lists put each model’s provider after its name, or not after names you set": "Die Listen der Agenten setzen den Anbieter hinter jeden Modellnamen, oder nicht hinter selbst vergebene Namen",
|
||||
"Not on names I set": "Nicht bei eigenen Namen",
|
||||
"Agents’ lists show “{label}”": "In den Listen der Agenten: „{label}“",
|
||||
"In agents’ lists": "In den Listen der Agenten",
|
||||
"Codex subagents on any model": "Codex-Subagenten auf jedem Modell",
|
||||
"Long conversations": "Lange Unterhaltungen",
|
||||
"Codex and Claude Code compact a conversation at 272K tokens on a model with a larger window; in Claude Code a Claude model runs to its own window, 1M for a [1m] one, as Anthropic runs it. The model’s full window lets any conversation grow further, with every turn slower and costlier": "Codex und Claude Code komprimieren eine Unterhaltung bei einem Modell mit größerem Kontextfenster ab 272K Token; in Claude Code nutzt ein Claude-Modell sein eigenes Fenster, 1M bei einem [1m]-Modell, wie Anthropic es betreibt. Mit dem vollen Fenster des Modells wächst jede Unterhaltung weiter, wobei jede Runde langsamer und teurer wird",
|
||||
|
||||
@@ -338,6 +338,8 @@ button.rt-session { cursor: pointer; }
|
||||
.rt-gedit .segs { max-width: 100%; }
|
||||
.rt-gedit .ctx-wrap { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.rt-gedit .ctx-wrap .hint { flex-basis: 100%; }
|
||||
.rt-gedit .gsuffix { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; }
|
||||
.rt-gedit .gsuffix .hint { overflow-wrap: anywhere; }
|
||||
.rt-gedit .ctx-wrap input.ctx-in { width: 96px; flex: none; }
|
||||
.rt-pool { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; padding: 10px 12px 10px 14px; }
|
||||
.rt-pool .nm { flex: 1 1 12em; display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 13px; }
|
||||
|
||||
@@ -2777,11 +2777,22 @@
|
||||
idHint.textContent = t("Agents pick it as {id}", { id: "group/" + idOf() }) +
|
||||
(g && idOf() !== g.id ? " · " + t("an agent set to {id} needs setting again", { id: "group/" + g.id }) : "");
|
||||
};
|
||||
name.oninput = () => { d.name = name.value; showId(); };
|
||||
// what the agents' lists will call it, and the setting that says
|
||||
// whether "· routing group" follows the name (#868: an agent's narrow
|
||||
// menu cut it to "· rou…"); a group saved here is one the user made
|
||||
const sfxSaid = el("span", "hint");
|
||||
const sayLabel = (mode) => {
|
||||
sfxSaid.textContent = t("Agents’ lists show “{label}”", { label: (d.name.trim() || t("New group")) + ((mode || suffixMode()) === "on" ? " · routing group" : "") });
|
||||
};
|
||||
name.oninput = () => { d.name = name.value; showId(); sayLabel(); };
|
||||
const nw = el("div");
|
||||
nw.append(name);
|
||||
if (!g) nw.append(idHint);
|
||||
ed.append(el("label", "", t("Name")), nw);
|
||||
const sw = el("div", "gsuffix");
|
||||
sw.append(suffixSegs(sayLabel), sfxSaid);
|
||||
sayLabel();
|
||||
ed.append(el("label", "", t("In agents’ lists")), sw);
|
||||
if (idIn) {
|
||||
idIn.oninput = () => { d.id = idIn.value; showId(); };
|
||||
idIn.onblur = () => { d.id = idIn.value = idOf(); showId(); };
|
||||
|
||||
@@ -63,7 +63,8 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
|
||||
const w = words[lang];
|
||||
test(`${engine} ${lang}: an open group folds back from its heading`, async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
const page = await (await browser.newContext({ viewport: { width: 1100, height: 1400 }, reducedMotion: "reduce" })).newPage();
|
||||
// tall enough for the whole editor, its Cancel clicked where it is
|
||||
const page = await (await browser.newContext({ viewport: { width: 1100, height: 1500 }, reducedMotion: "reduce" })).newPage();
|
||||
t.after(() => browser.close());
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [], posts = [];
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// #868 (模型名称太长了…路由组的名称也很长。会看不到后面代表什么): an agent's
|
||||
// narrow model menu cut "Long name · routing group" short, and the setting
|
||||
// that drops the suffix (Settings › Provider in model names, #335) wasn't
|
||||
// found. It is now offered where names are given too: the provider editor's
|
||||
// Names & levels and a routing group's editor each have the three choices,
|
||||
// say what the agents' lists will show ("My Sol · Relay", "Fast · routing
|
||||
// group"), and a pick posts settings/plain-names once, the line following
|
||||
// it and the page not scrolled by the click. English and Chinese.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const words = {
|
||||
en: { names: "Names & levels", edit: "Edit", own: "Not on names I set", on: "On", off: "Off", label: "In agents’ lists",
|
||||
shows: (l) => `Agents’ lists show “${l}”` },
|
||||
zh: { names: "名称与推理档位", edit: "编辑", own: "自定义名称不带供应商", on: "开启", off: "关闭", label: "Agent 列表里的名称",
|
||||
shows: (l) => `Agent 的模型列表里显示为「${l}」` },
|
||||
};
|
||||
|
||||
function serve(lang, posts) {
|
||||
let cur = { lang, theme: "light", plainNames: false, plainOwnNames: false };
|
||||
const state = { agents: [{ id: "codex", name: "Codex", path: "/test/codex", fields: [] }], profiles: [], settings: { lang, theme: "light" } };
|
||||
const models = [
|
||||
{ id: "sol-2026-preview-long", name: "My Sol", default: "sol-2026-preview-long", on: true, efforts: [], images: false },
|
||||
];
|
||||
const provider = { id: "relay", name: "Relay", icon: "generic", chat: "https://relay.example/v1", responses: "", anthropic: "", models, agents: [], key: { set: true, masked: "sk-…1234" }, ready: true };
|
||||
const groups = { models: [{ id: "relay/sol-2026-preview-long", name: "My Sol", providerName: "Relay", icon: "generic" }], pools: [],
|
||||
groups: [{ id: "fast", name: "Fast", members: ["relay/sol-2026-preview-long"], routing: "order", ready: true, memberInfo: [{ id: "relay/sol-2026-preview-long", ready: true }] }] };
|
||||
return async (r) => {
|
||||
const req = r.request(), url = new URL(req.url());
|
||||
const json = (data) => r.fulfill({ 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 === "/wails/runtime.js") return r.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json(state);
|
||||
if (url.pathname === "/api/settings/plain-names") {
|
||||
const body = req.postDataJSON();
|
||||
posts.push(body);
|
||||
cur = { ...cur, plainNames: body.mode === "off", plainOwnNames: body.mode === "own" };
|
||||
return json(cur);
|
||||
}
|
||||
if (url.pathname === "/api/settings") return json(cur);
|
||||
if (url.pathname === "/api/plugins") return json({ plugins: [] });
|
||||
if (url.pathname === "/api/gateway/trace") {
|
||||
if (url.searchParams.get("wait")) return new Promise(() => {});
|
||||
return json({ mine: true, now: new Date().toISOString(), seq: 1, totals: { requests: 0, rerouted: 0, errors: 0 }, routes: [] });
|
||||
}
|
||||
if (url.pathname === "/api/gateway/history") return json({ cut: false, days: [], routes: [] });
|
||||
if (url.pathname === "/api/groups") return json(groups);
|
||||
if (url.pathname === "/api/providers") return json({ providers: [provider], presets: [], excluded: [], gateway: { running: true, window: true, url: "http://127.0.0.1:3999" } });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
await r.fulfill({ body: await fs.readFile(file), contentType });
|
||||
};
|
||||
}
|
||||
|
||||
async function launch(t, engine, lang, posts) {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || "chromium" }));
|
||||
const page = await (await browser.newContext({ viewport: { width: 900, height: 1200 }, reducedMotion: "reduce" })).newPage();
|
||||
t.after(async () => {
|
||||
if (process.env.ARTIFACT_DIR) {
|
||||
await fs.mkdir(process.env.ARTIFACT_DIR, { recursive: true });
|
||||
await page.screenshot({ path: path.join(process.env.ARTIFACT_DIR, `${engine}-${lang}-${t.name.includes("group") ? "group" : "names"}-suffix.png`) });
|
||||
}
|
||||
await browser.close();
|
||||
});
|
||||
page.setDefaultTimeout(5000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", serve(lang, posts));
|
||||
return { page, errors };
|
||||
}
|
||||
|
||||
const scrolls = (page) => page.evaluate(() => [scrollY, ...[...document.querySelectorAll(".view")].map((v) => v.scrollTop)]);
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
for (const lang of ["en", "zh"]) {
|
||||
const w = words[lang];
|
||||
test(`${engine} ${lang}: Names & levels says what agents' lists show and drops the provider there`, async (t) => {
|
||||
const posts = [];
|
||||
const { page, errors } = await launch(t, engine, lang, posts);
|
||||
await page.goto("http://magpie.test/?view=providers");
|
||||
await page.locator(".row.provider").click();
|
||||
if (!(await page.locator(".mnames:not([hidden])").count())) await page.getByRole("button", { name: w.names, exact: true }).click();
|
||||
const sfx = page.locator(".mnames:not([hidden]) .msuffix");
|
||||
await sfx.waitFor();
|
||||
assert.deepEqual((await sfx.locator(".segs .opt").allTextContents()).map((s) => s.trim()), [w.off, w.own, w.on]);
|
||||
assert.equal((await sfx.locator(".opt.on").textContent()).trim(), w.on);
|
||||
assert.equal(await sfx.locator(".hint").textContent(), w.shows("My Sol · Relay"));
|
||||
const before = await scrolls(page);
|
||||
await sfx.locator(".opt", { hasText: w.own }).click();
|
||||
await page.locator(".mnames:not([hidden]) .msuffix .opt.on", { hasText: w.own }).waitFor();
|
||||
assert.deepEqual(posts, [{ mode: "own" }]);
|
||||
assert.equal(await sfx.locator(".hint").textContent(), w.shows("My Sol"));
|
||||
assert.deepEqual(await scrolls(page), before, "the click scrolled the page");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
|
||||
test(`${engine} ${lang}: a group's editor says what agents' lists show and drops "· routing group" there`, async (t) => {
|
||||
const posts = [];
|
||||
const { page, errors } = await launch(t, engine, lang, posts);
|
||||
await page.goto("http://magpie.test/?view=routing");
|
||||
const card = page.locator(".rt-group", { hasText: "Fast" });
|
||||
await card.waitFor();
|
||||
await card.locator("button", { hasText: w.edit }).click();
|
||||
const ed = page.locator(".rt-gedit");
|
||||
const row = ed.locator("label", { hasText: new RegExp(`^${w.label}$`) }).locator("xpath=following-sibling::div[1]");
|
||||
await row.waitFor();
|
||||
assert.equal(await row.locator(".hint").textContent(), w.shows("Fast · routing group"));
|
||||
// the name typed is the name shown
|
||||
await ed.locator("input").first().fill("Fast lane");
|
||||
assert.equal(await row.locator(".hint").textContent(), w.shows("Fast lane · routing group"));
|
||||
const before = await scrolls(page);
|
||||
await row.locator(".opt", { hasText: w.own }).click();
|
||||
await row.locator(".opt.on", { hasText: w.own }).waitFor();
|
||||
assert.deepEqual(posts, [{ mode: "own" }]);
|
||||
assert.equal(await row.locator(".hint").textContent(), w.shows("Fast lane"));
|
||||
assert.deepEqual(await scrolls(page), before, "the click scrolled the page");
|
||||
// nothing native, no stripe down the side
|
||||
assert.equal(await ed.locator("select").count(), 0);
|
||||
assert.equal(await row.evaluate((e) => getComputedStyle(e).borderLeftWidth), "0px");
|
||||
const missing = await page.evaluate(() => ["Agents’ lists show “{label}”", "In agents’ lists", "Not on names I set", "Provider in model names"]
|
||||
.filter((k) => !I18N.zh[k] || !I18N.ja[k] || !I18N.de[k]));
|
||||
assert.deepEqual(missing, [], "every string has its Chinese, Japanese and German");
|
||||
assert.deepEqual(errors, []);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -551,7 +551,7 @@ func groupEntries(entries []Entry) []Entry {
|
||||
if len(ms) == 0 {
|
||||
continue
|
||||
}
|
||||
e := Entry{ID: GroupPrefix + g.ID, Model: ms[0].Model, Name: g.Name, Provider: ms[0].Provider, Group: g.ID, Reasoning: true}
|
||||
e := Entry{ID: GroupPrefix + g.ID, Model: ms[0].Model, Name: g.Name, Provider: ms[0].Provider, Group: g.ID, Named: !g.Auto, Reasoning: true}
|
||||
var fixed []string // the efforts members are fixed at
|
||||
levelled := false // a member that follows the agent's effort was met
|
||||
// Codex's ultra (max, with Codex handing parts of the task to agents
|
||||
|
||||
@@ -611,22 +611,25 @@ func (e Entry) Label() string {
|
||||
// name alone — but for two or more the list would call the same, as a
|
||||
// routing group found for a model is called with that model left in the
|
||||
// list, which keep their provider's after it to tell them apart. With
|
||||
// their own names plain (PlainOwnNames, #92), a name the user gave a model
|
||||
// is that name just as they wrote it, and the vendor's keep Label's.
|
||||
// their own names plain (PlainOwnNames, #92), a name the user gave — a
|
||||
// model's, or a routing group's they made (#868: "· routing group" was
|
||||
// what an agent's narrow menu cut off) — is that name just as they wrote
|
||||
// it, but for two the list would call the same, and the rest keep Label's.
|
||||
func Labels(es []Entry) []string {
|
||||
out := make([]string, len(es))
|
||||
s := heldSettings()
|
||||
plain := s.PlainNames
|
||||
own := !plain && s.PlainOwnNames
|
||||
same := map[string]int{}
|
||||
if plain {
|
||||
if plain || own {
|
||||
for _, e := range es {
|
||||
same[strings.ToLower(e.Name)]++
|
||||
}
|
||||
}
|
||||
for i, e := range es {
|
||||
out[i] = e.Label()
|
||||
if plain && e.Name != "" && same[strings.ToLower(e.Name)] == 1 || own && e.Default != "" && e.Name != "" {
|
||||
mine := own && (e.Default != "" || e.Named)
|
||||
if (plain || mine) && e.Name != "" && same[strings.ToLower(e.Name)] == 1 {
|
||||
out[i] = e.Name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1028,6 +1028,9 @@ type Entry struct {
|
||||
Efforts []string `json:"efforts,omitempty"`
|
||||
Provider Provider `json:"-"` // a group's: its first member's
|
||||
Group string `json:"group,omitempty"` // set on a routing group (group.go)
|
||||
// Named is set on a routing group the user made or changed: its name is
|
||||
// theirs, as a model's Default says one is (Labels).
|
||||
Named bool `json:"-"`
|
||||
Icons []string `json:"-"` // a group's: its providers' icons, one per provider
|
||||
Images bool `json:"images,omitempty"` // takes images as input (a group's: a member does)
|
||||
ImageInput *bool `json:"-"` // explicit answer, nil when unknown
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// With names the user set plain (PlainOwnNames, #92), a routing group they
|
||||
// made is one of them (#868: 模型名称太长…路由组的名称也很长, an agent's
|
||||
// narrow menu cut "· routing group" to "· rou…"): its name stands alone in
|
||||
// the agents' lists. A group magpie found keeps "· routing group", the
|
||||
// vendors' names keep their providers', and a name the list would show
|
||||
// twice keeps its suffix, so a picker never shows one name twice.
|
||||
func TestPlainOwnNamesGroups(t *testing.T) {
|
||||
prefsHome(t)
|
||||
if err := SetModelName("a/sol", "My Sol"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SaveGroup(Group{ID: "fast", Name: "Fast", Members: []string{"a/sol", "b/sol"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
labels := func() []string {
|
||||
shown, _ := CatalogFor("opencode")
|
||||
out := Labels(shown)
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
if got, want := labels(), []string{"Fast · routing group", "My Sol · A", "Sol · B", "Sol · routing group"}; !slices.Equal(got, want) {
|
||||
t.Fatalf("by default: %q, want %q", got, want)
|
||||
}
|
||||
if err := SetSuffixMode(SuffixOwn); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := labels(), []string{"Fast", "My Sol", "Sol · B", "Sol · routing group"}; !slices.Equal(got, want) {
|
||||
t.Fatalf("own names plain: %q, want %q", got, want)
|
||||
}
|
||||
// a group named as a model the list has keeps its suffix
|
||||
if err := SaveGroup(Group{ID: "fast", Name: "My Sol", Members: []string{"a/sol", "b/sol"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := labels(), []string{"My Sol · A", "My Sol · routing group", "Sol · B", "Sol · routing group"}; !slices.Equal(got, want) {
|
||||
t.Fatalf("a group named as a model: %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user