From 7e2fc88260f63e1b34c8180d68629a526facdfdc Mon Sep 17 00:00:00 2001 From: yetone Date: Thu, 8 Oct 2026 15:53:26 +0800 Subject: [PATCH] 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. --- internal/gui/assets/app.css | 4 + internal/gui/assets/app.js | 15 +++- internal/gui/assets/i18n.js | 4 + .../gui/tests/codex-app-menu-cap.test.cjs | 83 +++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 internal/gui/tests/codex-app-menu-cap.test.cjs diff --git a/internal/gui/assets/app.css b/internal/gui/assets/app.css index 2fb25119..3ac70400 100644 --- a/internal/gui/assets/app.css +++ b/internal/gui/assets/app.css @@ -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); } diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index f1d33589..11db533c 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -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. diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index e7b1547c..a58d15b9 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -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", diff --git a/internal/gui/tests/codex-app-menu-cap.test.cjs b/internal/gui/tests/codex-app-menu-cap.test.cjs new file mode 100644 index 00000000..1538e786 --- /dev/null +++ b/internal/gui/tests/codex-app-menu-cap.test.cjs @@ -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(); + } + }); + } + } +}