gui: Codex's panel says when the Codex app's model menu can't reach every model: the app asks for 100 and never the next page, so with more the last ones can't be picked there; it points to Pick and Order (#1262)

magpie writes every model to Codex. ChatGPT.app 26.930 asks Codex's
app-server for model/list with limit 100 and doesn't follow nextCursor;
the Codex CLI's /model asks with no limit. Checked with the real codex
0.160.1 app-server in a sandbox CODEX_HOME holding 150 models: limit 100
gave 100 and nextCursor "100", no limit gave all 150.

Run: codex-app-menu-cap.test.cjs (Chromium and WebKit, en and zh, 1000 and
560px; 8/8 fail on the old app.js), gui-ja, gui-de, gui-zh-tw, i18n-keys;
full GUI suite 3708/3715, the failures (azure-endpoint, mobile-web,
routing-kind) pass alone and touch nothing changed here.
Not run: the real ChatGPT.app.
This commit is contained in:
yetone committed 2026-10-08 15:53:30 +08:00
1 parent 52ad2d9845
commit 7e2fc88260
4 files changed
+105 -1

No files matched your search

+4
View File
@@ -3187,6 +3187,10 @@ body.window .row + .row.agent.conn > .who { min-height: 41px; } /* the line abov
:where(html[data-fonts]) .ag-code, :where(html[data-fonts]) .ag-diff code, :where(html[data-fonts]) .ag-path { font-style: var(--code-style, inherit); font-weight: var(--code-weight, inherit); font-stretch: var(--code-stretch, 100%); }
.ag-code { padding: 1px 5px; border-radius: 4px; background: var(--pill); color: var(--fg); }
.ag-warn { gap: 8px; font-size: 12px; }
/* a note of several lines: its dot stays by the first line, the words wrap beside it */
.ag-vl.ag-menu-cap { flex-wrap: nowrap; align-items: flex-start; line-height: 1.6; }
.ag-menu-cap > .ag-dot { margin-top: calc(0.8em - 3px); }
.ag-menu-cap > span:last-child { flex: 1; min-width: 0; }
/* which copy is left on the old list: a sentence that wraps as one, under the warning's words */
.ag-vl.ag-stale-copy { display: block; padding-left: 14px; font-size: 12px; line-height: 1.6; color: var(--fg-2); }
.ag-hint { font-size: 12px; color: var(--muted); }
+14 -1
View File
@@ -1344,7 +1344,14 @@ function connectPanel(a, { fields, fieldBtn }) {
pick.onclick = (ev) => openAgentModels(a, pick, ev);
holdAgentModels(a, pick);
chips.append(pick);
kv(t("Model list"), chips);
const v = kv(t("Model list"), chips);
// the Codex app's menu reaches only so many of them (#1262)
if (a.id === "codex" && a.models.shown > CODEX_APP_MENU) {
const w = line();
w.classList.add("ag-warn", "ag-menu-cap");
w.append(el("span", "ag-dot"), el("span", "", t("The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.", { max: CODEX_APP_MENU, n: a.models.shown - CODEX_APP_MENU })));
v.append(w);
}
}
// what a new session starts on: optional, the agent's own last pick
// unset; Codex's is the very value its /model picks, so one choice
@@ -2191,6 +2198,12 @@ function dragCards(e, handle, card, list, cards, commit, idle = () => {}) {
// ---------- an agent's model list ----------
// CODEX_APP_MENU is how many models the Codex app's model menu reaches: it
// asks Codex's app-server for model/list with limit 100 and never for the
// next page (ChatGPT.app 26.930), so a model past them can't be picked
// there; the Codex CLI's /model asks for every one (#1262).
const CODEX_APP_MENU = 100;
// modelsEntry: the line under an agent's name counting the models its
// lists show ("All 41 models", or "Showing 5 / 32 models"); it opens the
// list to take models out of them and put them back.
+4
View File
@@ -410,6 +410,7 @@ const I18N = {
"Add a Claude subscription": "添加 Claude 订阅",
"claude-* models need a Claude subscription in magpie": "claude-* 模型要先在 magpie 里添加 Claude 订阅",
"Pick": "挑选",
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex App 的模型菜单只显示前 {max} 个:后面 {n} 个模型在那里选不到(Codex CLI 的 /model 能列出全部)。在「挑选」里关掉不用的,或在「排序」里把要用的拖到前面。",
"Model list": "模型列表",
"New sessions": "新会话默认",
"The same choice as {agent}'s {cmd}: change it here or there": "与 {agent} 的 {cmd} 同步,两边均可修改",
@@ -4148,6 +4149,7 @@ const I18N = {
"Add a Claude subscription": "新增 Claude 訂閱",
"claude-* models need a Claude subscription in magpie": "claude-* 模型要先在 magpie 裡新增 Claude 訂閱",
"Pick": "挑選",
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex App 的模型選單只顯示前 {max} 個:後面 {n} 個模型在那裡選不到(Codex CLI 的 /model 能列出全部)。在「挑選」裡關掉不用的,或在「排序」裡把要用的拖到前面。",
"Model list": "模型列表",
"New sessions": "新會話預設",
"The same choice as {agent}'s {cmd}: change it here or there": "與 {agent} 的 {cmd} 同步,兩邊均可修改",
@@ -7851,6 +7853,7 @@ const I18N = {
"Add a Claude subscription": "Claude サブスクリプションを追加",
"claude-* models need a Claude subscription in magpie": "claude-* モデルには magpie の Claude サブスクリプションが必要です",
"Pick": "選択",
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Codex アプリのモデルメニューには先頭の {max} 個しか表示されず、残りの {n} 個のモデルはそこで選べません(Codex CLI の /model にはすべて表示されます)。「選択」で使わないモデルをオフにするか、「並び順」で使うモデルを先頭へドラッグしてください。",
"Model list": "モデル一覧",
"New sessions": "新しいセッション",
"The same choice as {agent}'s {cmd}: change it here or there": "{agent} の {cmd} と同じ選択です。ここでもそちらでも変更できます",
@@ -11572,6 +11575,7 @@ const I18N = {
"Add a Claude subscription": "Claude-Abo hinzufügen",
"claude-* models need a Claude subscription in magpie": "claude-*-Modelle benötigen ein Claude-Abo in magpie",
"Pick": "Wählen",
"The Codex app's model menu shows only the first {max}: the last {n} models can't be picked there (the Codex CLI's /model lists them all). In Pick, turn off the ones you don't use, or drag the ones you use to the front under Order.": "Das Modellmenü der Codex-App zeigt nur die ersten {max}: Die letzten {n} Modelle lassen sich dort nicht wählen (/model in der Codex-CLI listet alle). Schalte unter „Wählen“ ungenutzte aus oder zieh unter „Reihenfolge“ die genutzten nach vorn.",
"Model list": "Modellliste",
"New sessions": "Neue Sitzungen",
"The same choice as {agent}'s {cmd}: change it here or there": "Dieselbe Auswahl wie {cmd} von {agent}: hier oder dort ändern",
@@ -0,0 +1,83 @@
// Run with Node's test runner and Playwright on the module path; see README.md.
// The Codex app's model menu reaches only the first 100 models Codex lists
// (it asks model/list for 100 and never for the next page), so with more
// than that the Codex row says how many it can't pick and where to choose
// which ones it gets (#1262); with 100 or fewer it says nothing. In English
// and Chinese, wide and narrow, with no line wider than the row.
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");
function server(lang, shown) {
return async (route) => {
const url = new URL(route.request().url());
const json = (data) => route.fulfill({ json: data });
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:${JSON.stringify(lang)},theme:"light",web:true};` });
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
if (url.pathname === "/api/state") return json({
agents: [{
id: "codex", name: "Codex", path: "/test/config.toml", icon: "codex-color", wired: true,
fields: [{ key: "model", label: "model", value: "pqh/gpt-6-sol", options: [{ value: "pqh/gpt-6-sol", label: "6 Sol", ref: "pqh/gpt-6-sol" }] }],
models: { shown, listed: shown + 3, by: [{ name: "Routing groups", n: 26 }, { name: "pqh", n: 19 }, { name: "ZCode", n: shown - 45 }] },
}],
profiles: [], settings: { lang, theme: "light" },
});
if (url.pathname === "/api/groups") return json({ groups: [] });
if (url.pathname === "/api/providers") return json({ providers: [], gateway: { running: true, window: true } });
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 route.fulfill({ body: await fs.readFile(file), contentType });
};
}
const said = {
en: ["The Codex app's model menu shows only the first 100", "the last 107 models"],
zh: ["Codex App 的模型菜单只显示前 100 个", "后面 107 个模型"],
};
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
for (const lang of ["en", "zh"]) {
for (const width of [1000, 560]) {
test(`${engine} ${lang} ${width}px: the Codex row says what its app's menu can't reach`, async (t) => {
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
t.after(() => browser.close());
for (const [shown, warned] of [[207, true], [100, false]]) {
const page = await browser.newPage({ viewport: { width, height: 800 } });
page.setDefaultTimeout(5000);
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.route("**/*", server(lang, shown));
await page.goto("http://magpie.test/");
const row = page.locator('.row.agent[data-id="codex"]');
await row.locator(".ag-link").click();
await row.locator(".ag-exp .ag-chips").waitFor();
const cap = row.locator(".ag-exp .ag-menu-cap");
if (!warned) {
assert.equal(await cap.count(), 0, `${shown} models`);
} else {
await cap.waitFor();
const text = await cap.textContent();
for (const s of said[lang]) assert(text.includes(s), text);
// under the model list, in the panel, and no wider than it
const fit = await cap.evaluate((w) => {
const box = w.closest(".ag-exp").getBoundingClientRect(), b = w.getBoundingClientRect();
return { inside: b.left >= box.left - 1 && b.right <= box.right + 1, list: !!w.closest(".ag-kv")?.querySelector(".ag-chips"), overflow: document.documentElement.scrollWidth - document.documentElement.clientWidth };
});
assert.deepEqual(fit, { inside: true, list: true, overflow: 0 });
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}-${width}-codex-cap.png`) });
}
}
assert.deepEqual(errors, []);
await page.close();
}
});
}
}
}