From 9f7ac1fa67a4e95262e3717d95cf6bd8fcec7ff8 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 11 Oct 2026 17:53:05 +0800 Subject: [PATCH] refactor(codex): make the first row of the model list the default model With a model list, the separate default model field duplicated the list: the first row is now the default model, as in the Aggregation layout, and a help tip next to "Model list" says so. Renaming or removing the first row updates the model; an older card whose default model sits further down shows that row first without counting as an edit. Cards without a list keep the default model field below the empty list. They rely on Codex's built-in /model menu, and adding a list row just to set the model would shrink that menu to the listed entries. Adding the first row to an empty list seeds it with the current default model. --- .../providers/forms/CodexFormFields.tsx | 301 ++++++++++-------- src/i18n/locales/en.json | 6 +- src/i18n/locales/ja.json | 6 +- src/i18n/locales/zh-TW.json | 6 +- src/i18n/locales/zh.json | 6 +- .../CodexFormFields.subagent.test.tsx | 54 +++- .../ProviderForm.codexCopilot.test.tsx | 12 +- 7 files changed, 233 insertions(+), 158 deletions(-) diff --git a/src/components/providers/forms/CodexFormFields.tsx b/src/components/providers/forms/CodexFormFields.tsx index d603b3a40..21d1497da 100644 --- a/src/components/providers/forms/CodexFormFields.tsx +++ b/src/components/providers/forms/CodexFormFields.tsx @@ -240,8 +240,8 @@ function createCatalogRow(seed?: Partial): CodexCatalogRow { }; } -// Stack 布局里第一行就是默认模型(`model`)。直连表单里默认模型可以是列表的任意一行, -// 这样的行在 Stack 布局里挪到最前面;不在列表里时不动。 +// 有模型列表时第一行就是默认模型(`model`)。旧数据里默认模型可以是列表的任意一行, +// 这样的行挪到最前面显示;不在列表里时不动。 function withDefaultModelFirst( models: T[], defaultModel: string, @@ -609,6 +609,9 @@ export function CodexFormFields({ // 对应词条在 grokBuild.* 下。 const isGrokBuild = appId === "grokbuild"; const canEditCatalog = Boolean(onCatalogModelsChange); + // 有模型列表的卡(两种布局):列表第一行就是默认模型,没有单独的默认模型框;列表为空时 + // 直连布局才显示默认模型框。Grok Build 没有列表,官方卡不显示列表。 + const firstRowIsDefault = canEditCatalog && category !== "official"; const canEditReasoning = Boolean(onCodexChatReasoningChange); const supportsThinking = codexChatReasoning.supportsThinking === true || @@ -652,7 +655,7 @@ export function CodexFormFields({ // 显示用的顺序。挪动默认那一行不算修改:没动过列表时保存仍是原来的顺序。 const orderCatalogModels = (models: CodexCatalogModel[]) => - variant === "stack" ? withDefaultModelFirst(models, codexModel) : models; + firstRowIsDefault ? withDefaultModelFirst(models, codexModel) : models; const [catalogRows, setCatalogRows] = useState(() => orderCatalogModels(catalogModels).map((m) => createCatalogRow(m)), @@ -693,7 +696,7 @@ export function CodexFormFields({ // 挪到第一行,行 id 不变,也不回传父组件。编辑中 `model` 只会跟着第一行变,这里不会动。 // 放在子 → 父 effect 之后:同一次渲染里改了的行先回传,再看要不要挪。 useEffect(() => { - if (variant !== "stack") return; + if (!firstRowIsDefault) return; const ordered = withDefaultModelFirst(catalogRows, codexModel); if (ordered === catalogRows) return; lastSentModelsRef.current = ordered.map( @@ -701,7 +704,7 @@ export function CodexFormFields({ ); setCatalogRows(ordered); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [codexModel, variant]); + }, [codexModel, firstRowIsDefault]); const handleReasoningThinkingChange = useCallback( (checked: boolean) => { @@ -927,21 +930,53 @@ export function CodexFormFields({ [catalogRowsRef, commitCatalogRows, fillModelMetadata], ); - const handleAddCatalogRow = useCallback(() => { - if (!onCatalogModelsChange) return; - setCatalogRows((current) => [...current, createCatalogRow()]); - }, [onCatalogModelsChange]); - - // Stack 布局没有默认模型字段,第一行就是 `model`(加载时已挪到最前面):它不在列表里时 - // 没有这样的行;没填时保存会用第一行。 + // 第一行就是 `model`(加载时已挪到最前面):它不在列表里时没有这样的行;没填时保存会用 + // 第一行。 const trimmedDefaultModel = codexModel.trim(); - const stackDefaultIndex = trimmedDefaultModel + const defaultRowIndex = trimmedDefaultModel ? catalogRows.findIndex((row) => row.model.trim() === trimmedDefaultModel) : 0; - // Stack 布局里默认模型那一行就代表 `model`:改它的名字、删掉它,`model` 当场跟着变, - // 两种布局共用这份状态。没有这样的行时是 -1。 + // 默认模型那一行就代表 `model`:改它的名字、删掉它,`model` 当场跟着变。没有这样的行时 + // 是 -1。 const linkedDefaultIndex = - variant === "stack" && trimmedDefaultModel ? stackDefaultIndex : -1; + firstRowIsDefault && trimmedDefaultModel ? defaultRowIndex : -1; + + // 列表从空变成有行时,当前默认模型排第一行(已在新增的行里就挪到最前,否则补一行), + // 默认模型框随之隐藏,默认值不会悄悄换掉。 + const seedWithDefaultModel = useCallback( + (additions: CodexCatalogRow[]): CodexCatalogRow[] => { + if (!firstRowIsDefault || !trimmedDefaultModel) return additions; + const ordered = withDefaultModelFirst(additions, trimmedDefaultModel); + if (ordered[0]?.model.trim() === trimmedDefaultModel) return ordered; + return [ + createCatalogRow({ + model: trimmedDefaultModel, + displayName: trimmedDefaultModel, + }), + ...additions, + ]; + }, + [firstRowIsDefault, trimmedDefaultModel], + ); + + const handleAddCatalogRow = useCallback(() => { + if (!onCatalogModelsChange) return; + const rows = catalogRowsRef.current; + const added = + rows.length === 0 + ? seedWithDefaultModel([createCatalogRow()]) + : [createCatalogRow()]; + commitCatalogRows([...rows, ...added]); + for (const row of added) { + if (row.model) fillCatalogRowMetadata(row.rowId, row.model); + } + }, [ + catalogRowsRef, + commitCatalogRows, + fillCatalogRowMetadata, + onCatalogModelsChange, + seedWithDefaultModel, + ]); const handleUpdateCatalogRow = useCallback( (index: number, patch: Partial) => { @@ -1000,19 +1035,26 @@ export function CodexFormFields({ [catalogRows, linkedDefaultIndex, onModelChange], ); - // 批量勾选拉取到的模型加入列表(Stack 布局)。 + // 批量勾选拉取到的模型加入列表。 const handleAddFetchedCatalogRows = useCallback( (modelIds: string[]) => { const current = catalogRowsRef.current; const configured = new Set(current.map((row) => row.model.trim())); - const additions = modelIds + const picked = modelIds .filter((id) => !configured.has(id)) .map((id) => createCatalogRow({ model: id, displayName: id })); - if (additions.length === 0) return; + if (picked.length === 0) return; + const additions = + current.length === 0 ? seedWithDefaultModel(picked) : picked; commitCatalogRows([...current, ...additions]); for (const row of additions) fillCatalogRowMetadata(row.rowId, row.model); }, - [catalogRowsRef, commitCatalogRows, fillCatalogRowMetadata], + [ + catalogRowsRef, + commitCatalogRows, + fillCatalogRowMetadata, + seedWithDefaultModel, + ], ); // 默认模型下拉建议 = 模型映射的"实际请求模型"列 ∪ 拉取到的 /models 列表 @@ -1050,17 +1092,17 @@ export function CodexFormFields({ model: trimmedDefaultModel, displayName: trimmedDefaultModel, }); - // Stack 布局里默认模型排第一位。 + // 默认模型排第一位。 const rows = catalogRowsRef.current; - commitCatalogRows(variant === "stack" ? [row, ...rows] : [...rows, row]); + commitCatalogRows(firstRowIsDefault ? [row, ...rows] : [...rows, row]); fillCatalogRowMetadata(row.rowId, trimmedDefaultModel); }, [ catalogRowsRef, commitCatalogRows, fillCatalogRowMetadata, + firstRowIsDefault, onCatalogModelsChange, trimmedDefaultModel, - variant, ]); // 子代理模型不在模型映射里:Codex 拿不到它的窗口和推理档位;经路由转成 Chat / Anthropic @@ -1669,6 +1711,28 @@ export function CodexFormFields({ ); + // 默认模型(`model`)不在列表里:没有哪一行是默认,提示并给出加入列表(排第一行)。 + const defaultModelNotInListNotice = isDefaultModelOutsideCatalog && ( +

+ {t("codexConfig.defaultModelNotInList", { + model: trimmedDefaultModel, + defaultValue: + "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。", + })} + +

+ ); + if (variant === "stack") { const showFormatFields = shouldShowSpeedTest && !isXaiOauthPreset; const showReasoning = isChatFormat && canEditReasoning; @@ -1722,26 +1786,7 @@ export function CodexFormFields({ onAdd={handleAddFetchedCatalogRows} /> )} - {isDefaultModelOutsideCatalog && ( -

- {t("codexConfig.stackDefaultNotInList", { - model: trimmedDefaultModel, - defaultValue: - "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。", - })} - -

- )} + {defaultModelNotInListNotice} {catalogRows.length > 0 ? ( renderCatalogRows() ) : ( @@ -1826,16 +1871,28 @@ export function CodexFormFields({ {endpointSection} {/* 模型列表 —— 生成 Codex 的模型目录(model_catalog_json):/model 菜单列哪些模型, - 各自的窗口和思考等级。留空不生成。默认模型、子代理从这里选,所以排在它们前面。 */} - {category !== "official" && canEditCatalog && ( + 各自的窗口和思考等级。留空不生成。第一行是默认模型,子代理也从这里选。 */} + {firstRowIsDefault && (
- - {t("codexConfig.modelListTitle", { - defaultValue: "模型列表", - })} - +
+ + {t("codexConfig.modelListTitle", { + defaultValue: "模型列表", + })} + + + {t("codexConfig.modelListDefaultHelp", { + defaultValue: + "第一行是默认模型,Codex 启动时用它。列表为空时,可以在下方单独填写默认模型。", + })} + +
{renderCatalogActionButtons( handleAddCatalogRow, t("codexConfig.addCatalogModel", { @@ -1858,92 +1915,76 @@ export function CodexFormFields({ onAdd={handleAddFetchedCatalogRows} /> )} + {defaultModelNotInListNotice} {catalogRows.length > 0 && renderCatalogRows()}
)} - {/* 默认模型 —— config.toml 顶层 model,Codex 启动时默认请求的模型。 - 实时写回 TOML;留空则删行(有模型列表时保存回退为列表第一行)。 */} - {category !== "official" && onModelChange && ( -
- - {t("codexConfig.defaultModelLabel", { defaultValue: "默认模型" })} - -
- onModelChange(event.target.value)} - placeholder={ - isGrokBuild - ? t("grokBuild.defaultModelPlaceholder", { - defaultValue: "例如: grok-4.5", - }) - : t("codexConfig.defaultModelPlaceholder", { - defaultValue: "例如: gpt-5.6", - }) - } - className="flex-1" - /> - {/* 有模型列表时用列表上的「获取模型列表」,这里不再放一个。 */} - {!canEditCatalog && ( - - - - )} - {defaultModelSuggestions.length > 0 && ( - onModelChange(id)} + {/* 默认模型 —— config.toml 顶层 model,Codex 启动时默认请求的模型。实时写回 TOML; + 留空则删行。有模型列表时第一行就是它,不显示这个框(Grok Build 没有列表,一直显示)。 */} + {category !== "official" && + onModelChange && + (!firstRowIsDefault || catalogRows.length === 0) && ( +
+ + {t("codexConfig.defaultModelLabel", { defaultValue: "默认模型" })} + +
+ onModelChange(event.target.value)} + placeholder={ + isGrokBuild + ? t("grokBuild.defaultModelPlaceholder", { + defaultValue: "例如: grok-4.5", + }) + : t("codexConfig.defaultModelPlaceholder", { + defaultValue: "例如: gpt-5.6", + }) + } + className="flex-1" /> - )} -
-

- {isGrokBuild - ? t("grokBuild.defaultModelHint", { - defaultValue: - "Grok Build 默认请求的模型,随时可改,无需等待预设更新。", - }) - : t("codexConfig.defaultModelHint", { - defaultValue: - "Codex 默认请求的模型,随时可改,无需等待预设更新。留空时用模型列表第一行。", - })} -

- {isDefaultModelOutsideCatalog && ( -

- {t("codexConfig.defaultModelNotInCatalog", { - defaultValue: - "该模型不在模型列表中,Codex 的 /model 菜单不会列出它(直接请求仍然有效)。", - })} - + {/* 有模型列表时用列表上的「获取模型列表」,这里不再放一个。 */} + {!canEditCatalog && ( + + + + )} + {defaultModelSuggestions.length > 0 && ( + onModelChange(id)} + /> + )} +

+

+ {isGrokBuild + ? t("grokBuild.defaultModelHint", { + defaultValue: + "Grok Build 默认请求的模型,随时可改,无需等待预设更新。", + }) + : t("codexConfig.defaultModelHint", { + defaultValue: + "Codex 默认请求的模型,随时可改,无需等待预设更新。添加模型列表后,列表第一行就是默认模型。", + })}

- )} -
- )} +
+ )} {/* 子代理 —— config.toml 的 [agents] default_subagent_model / default_subagent_reasoning_effort。 留空删键,子代理跟随主模型;聚合模式下不写进 live。 */} diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 22c5987af..f4cb57319 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1912,8 +1912,7 @@ "modelMetadataAdvancedHint": "Overrides Codex metadata for unknown models; clearing a field removes the matching config.toml entry.", "defaultModelLabel": "Default Model", "defaultModelPlaceholder": "e.g. gpt-5.6-sol", - "defaultModelHint": "The model Codex requests by default; change it anytime without waiting for a preset update. If left empty, the first row of the model list is used.", - "defaultModelNotInCatalog": "This model is not in the model list, so the Codex /model menu won't list it (direct requests still work).", + "defaultModelHint": "The model Codex requests by default; change it anytime without waiting for a preset update. Once you add a model list, its first row is the default model.", "subagentModelLabel": "Sub-agent Model", "subagentModelPlaceholder": "Leave empty to follow the main model", "subagentReasoningEffortLabel": "Sub-agent reasoning effort", @@ -1921,11 +1920,12 @@ "subagentModelHint": "The model and reasoning effort Codex sub-agents use by default. When empty, sub-agents use the main agent's; with only a model set, they use that model's default effort. Not applied in Aggregation mode, where sub-agents follow the main model.", "subagentModelNotInCatalog": "This model is not in the model list: Codex can't get its context window, and when routing converts requests to Chat / Anthropic it is replaced by the default model.", "modelListHint": "These models appear in Codex's /model menu with their own context window and reasoning levels. Restart Codex after changes.", + "modelListDefaultHelp": "The first row is the default model Codex uses at startup. With an empty list, you can set the default model separately below.", "addCatalogModel": "Add manually", "modelListTitle": "Models", "stackModelListHint": "These models appear in Codex's /model menu; picking one sends requests straight to this provider. Restart Codex after changes.", "stackModelDefaultHelp": "The first row is this provider's default model: while this provider is the default, Codex uses it by default.", - "stackDefaultNotInList": "The default model {{model}} is not in the list: Codex still requests it by default, but /model won't show it.", + "defaultModelNotInList": "The default model {{model}} is not in the list: Codex still requests it by default, but /model won't show it.", "addToModelList": "Add to list", "modelListEmpty": "No models configured: in Aggregation mode only this provider's default model (the model in config.toml) is published.", "stackAdvancedHint": "Reasoning, Anthropic-specific options, custom User-Agent and request overrides; usually no need to change.", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index cf9d98a8d..290d8f201 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1912,8 +1912,7 @@ "modelMetadataAdvancedHint": "未知のモデル向けに Codex メタデータを上書きします。空にすると対応する config.toml 設定を削除します。", "defaultModelLabel": "デフォルトモデル", "defaultModelPlaceholder": "例: gpt-5.6-sol", - "defaultModelHint": "Codex がデフォルトでリクエストするモデルです。プリセットの更新を待たずにいつでも変更できます。空欄のときはモデル一覧の先頭行が使われます。", - "defaultModelNotInCatalog": "このモデルはモデル一覧にないため、Codex の /model メニューには表示されません(直接リクエストは有効です)。", + "defaultModelHint": "Codex がデフォルトでリクエストするモデルです。プリセットの更新を待たずにいつでも変更できます。モデル一覧を追加すると、その先頭行がデフォルトモデルになります。", "subagentModelLabel": "サブエージェントモデル", "subagentModelPlaceholder": "空欄ならメインモデルに従う", "subagentReasoningEffortLabel": "サブエージェントの推論強度", @@ -1921,11 +1920,12 @@ "subagentModelHint": "Codex が起動するサブエージェントのデフォルトのモデルと推論強度です。空欄ならメインエージェントと同じものを使い、モデルだけ指定するとそのモデルのデフォルト強度を使います。集約モードでは適用されず、サブエージェントはメインモデルに従います。", "subagentModelNotInCatalog": "このモデルはモデル一覧にありません。Codex はコンテキストウィンドウを取得できず、ルーティングで Chat / Anthropic 形式に変換する際はデフォルトモデルに置き換えられます。", "modelListHint": "これらのモデルは、それぞれのコンテキストウィンドウと思考レベル付きで Codex の /model に表示されます。変更後は Codex を再起動してください。", + "modelListDefaultHelp": "先頭の行がデフォルトモデルで、Codex の起動時に使われます。一覧が空のときは、下でデフォルトモデルを個別に設定できます。", "addCatalogModel": "手動で追加", "modelListTitle": "モデル一覧", "stackModelListHint": "これらのモデルは Codex の /model に表示され、選ぶとリクエストがこのプロバイダーへ直接送られます。変更後は Codex を再起動してください。", "stackModelDefaultHelp": "先頭の行がこのプロバイダーのデフォルトモデルです。このプロバイダーがデフォルトのとき、Codex はこれを既定で使います。", - "stackDefaultNotInList": "デフォルトモデル {{model}} は一覧にありません。Codex は既定でこれをリクエストしますが、/model には表示されません。", + "defaultModelNotInList": "デフォルトモデル {{model}} は一覧にありません。Codex は既定でこれをリクエストしますが、/model には表示されません。", "addToModelList": "一覧に追加", "modelListEmpty": "モデル未設定:集約モードではこのプロバイダーのデフォルトモデル(config.toml の model)だけが公開されます。", "stackAdvancedHint": "思考能力、Anthropic 固有の項目、カスタム User-Agent とリクエスト上書き。通常は変更不要です。", diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index fe7739137..78562f7e0 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -1907,8 +1907,7 @@ "promptCacheRoutingHint": "自動模式僅對已確認相容的上游傳送 prompt_cache_key;開啟可用於其他相容閘道,關閉可避免嚴格閘道因未知欄位回傳 400。只使用客戶端提供的穩定工作階段 ID。", "defaultModelLabel": "預設模型", "defaultModelPlaceholder": "例如: gpt-5.6-sol", - "defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。留空時使用模型列表第一行。", - "defaultModelNotInCatalog": "該模型不在模型列表中,Codex 的 /model 選單不會列出它(直接請求仍然有效)。", + "defaultModelHint": "Codex 預設請求的模型,隨時可改,無需等待軟體更新供應商範本。加入模型列表後,列表第一行就是預設模型。", "subagentModelLabel": "子代理模型", "subagentModelPlaceholder": "留空則跟隨主模型", "subagentReasoningEffortLabel": "子代理推理強度", @@ -1916,11 +1915,12 @@ "subagentModelHint": "Codex 派出的子代理預設使用的模型和推理強度。留空時子代理沿用主代理的;只填模型時使用該模型自己的預設強度。聚合模式下不生效,子代理跟隨主模型。", "subagentModelNotInCatalog": "該模型不在模型列表中:Codex 取得不到它的上下文視窗;經路由轉成 Chat / Anthropic 協定時會被換成預設模型。", "modelListHint": "這些模型會出現在 Codex 的 /model 裡,帶上各自的上下文視窗和思考等級。修改後需要重新啟動 Codex。", + "modelListDefaultHelp": "第一行是預設模型,Codex 啟動時使用它。列表為空時,可以在下方單獨填寫預設模型。", "addCatalogModel": "手動新增", "modelListTitle": "模型列表", "stackModelListHint": "這些模型會出現在 Codex 的 /model 裡,選中後請求直達這家。修改後需要重新啟動 Codex。", "stackModelDefaultHelp": "第一行是這家的預設模型:這家被設為預設時,Codex 預設使用它。", - "stackDefaultNotInList": "預設模型 {{model}} 不在清單裡:Codex 預設仍請求它,但 /model 裡沒有這一項。", + "defaultModelNotInList": "預設模型 {{model}} 不在清單裡:Codex 預設仍請求它,但 /model 裡沒有這一項。", "addToModelList": "加入清單", "modelListEmpty": "未設定模型:聚合模式下只發布這家的預設模型(config.toml 的 model)。", "stackAdvancedHint": "思考能力、Anthropic 專有項、自訂 User-Agent 與請求覆寫,一般無需修改。", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 8c7605b9e..9ccf8d6ef 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1912,8 +1912,7 @@ "modelMetadataAdvancedHint": "用于未知模型的 Codex 元数据覆盖;清空字段会从 config.toml 删除对应配置。", "defaultModelLabel": "默认模型", "defaultModelPlaceholder": "例如: gpt-5.6-sol", - "defaultModelHint": "Codex 默认请求的模型,随时可改,无需等待预设更新。留空时用模型列表第一行。", - "defaultModelNotInCatalog": "该模型不在模型列表中,Codex 的 /model 菜单不会列出它(直接请求仍然有效)。", + "defaultModelHint": "Codex 默认请求的模型,随时可改,无需等待预设更新。添加模型列表后,列表第一行就是默认模型。", "subagentModelLabel": "子代理模型", "subagentModelPlaceholder": "留空则跟随主模型", "subagentReasoningEffortLabel": "子代理推理强度", @@ -1921,11 +1920,12 @@ "subagentModelHint": "Codex 派出的子代理默认用的模型和推理强度。留空时子代理沿用主代理的;只填模型时用该模型自己的默认强度。聚合模式下不生效,子代理跟随主模型。", "subagentModelNotInCatalog": "该模型不在模型列表中:Codex 拿不到它的上下文窗口;经路由转成 Chat / Anthropic 协议时会被换成默认模型。", "modelListHint": "这些模型会出现在 Codex 的 /model 里,带上各自的上下文窗口和思考等级。修改后需要重启 Codex。", + "modelListDefaultHelp": "第一行是默认模型,Codex 启动时用它。列表为空时,可以在下方单独填写默认模型。", "addCatalogModel": "手动添加", "modelListTitle": "模型列表", "stackModelListHint": "这些模型会出现在 Codex 的 /model 里,选中后请求直达这家。修改后需要重启 Codex。", "stackModelDefaultHelp": "第一行是这家的默认模型:这家被设为默认时,Codex 默认用它。", - "stackDefaultNotInList": "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。", + "defaultModelNotInList": "默认模型 {{model}} 不在列表里:Codex 默认仍请求它,但 /model 里没有这一项。", "addToModelList": "加入列表", "modelListEmpty": "未配置模型:聚合模式下只发布这家的默认模型(config.toml 的 model)。", "stackAdvancedHint": "思考能力、Anthropic 专有项、自定义 User-Agent 与请求覆盖,一般无需修改。", diff --git a/tests/components/CodexFormFields.subagent.test.tsx b/tests/components/CodexFormFields.subagent.test.tsx index 534434894..9cf074108 100644 --- a/tests/components/CodexFormFields.subagent.test.tsx +++ b/tests/components/CodexFormFields.subagent.test.tsx @@ -107,30 +107,64 @@ describe("CodexFormFields sub-agent defaults", () => { }); }); +const rowModels = () => + screen + .getAllByPlaceholderText("例如: deepseek-v4-flash") + .map((input) => (input as HTMLInputElement).value); + describe("CodexFormFields direct layout", () => { - it("shows the model list outside the advanced section, before the default model", () => { + it("lets the list own the default model: no separate field, list outside the advanced section", () => { renderCodexForm({ catalogModels: [{ model: "gpt-a" }], onCatalogModelsChange: vi.fn(), }); - const listTitle = screen.getByText("模型列表"); - expect(listTitle).toBeVisible(); + expect(screen.getByText("模型列表")).toBeVisible(); + expect(screen.getByRole("button", { name: "默认模型" })).toBeVisible(); expect(screen.queryByText("上游格式")).toBeNull(); - const defaultModel = document.getElementById("codexDefaultModel")!; - expect( - listTitle.compareDocumentPosition(defaultModel) & - Node.DOCUMENT_POSITION_FOLLOWING, - ).toBeTruthy(); - // 列表上已有「获取模型列表」,默认模型旁不再放第二个。 + expect(document.getElementById("codexDefaultModel")).toBeNull(); expect( screen.getAllByRole("button", { name: /providerForm\.fetchModels/ }), ).toHaveLength(1); }); - it("keeps the fetch button on the default model when there is no list (Grok Build)", () => { + it("shows the default model field below an empty list, and seeds the first row with it", () => { + const onCatalogModelsChange = vi.fn(); + renderCodexForm({ catalogModels: [], onCatalogModelsChange }); + + const listTitle = screen.getByText("模型列表"); + const defaultModel = document.getElementById("codexDefaultModel")!; + expect( + listTitle.compareDocumentPosition(defaultModel) & + Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: "手动添加" })); + expect(rowModels()).toEqual(["gpt-a", ""]); + expect(document.getElementById("codexDefaultModel")).toBeNull(); + const rows = onCatalogModelsChange.mock.calls.at(-1)?.[0] ?? []; + expect(rows.map((row: { model: string }) => row.model)).toEqual([ + "gpt-a", + "", + ]); + }); + + it("shows an older card's default row first without reporting a change", () => { + const onCatalogModelsChange = vi.fn(); + renderCodexForm({ + codexModel: "gpt-b", + catalogModels: [{ model: "gpt-a" }, { model: "gpt-b" }], + onCatalogModelsChange, + }); + + expect(rowModels()).toEqual(["gpt-b", "gpt-a"]); + expect(onCatalogModelsChange).not.toHaveBeenCalled(); + }); + + it("keeps the default model field and its fetch button without a list (Grok Build)", () => { renderCodexForm(); expect(screen.queryByText("模型列表")).toBeNull(); + expect(document.getElementById("codexDefaultModel")).not.toBeNull(); expect( screen.getByRole("button", { name: "providerForm.fetchModels" }), ).toBeInTheDocument(); diff --git a/tests/components/ProviderForm.codexCopilot.test.tsx b/tests/components/ProviderForm.codexCopilot.test.tsx index 1f42ec30d..0c7035be3 100644 --- a/tests/components/ProviderForm.codexCopilot.test.tsx +++ b/tests/components/ProviderForm.codexCopilot.test.tsx @@ -212,7 +212,9 @@ describe("Codex Copilot provider form", () => { it("defaults to GPT-6 Astra and shows all five preset model mappings", () => { renderForm(); - expect(screen.getAllByDisplayValue("gpt-6-astra")).toHaveLength(2); + // 有模型列表时第一行就是默认模型,没有单独的默认模型框。 + expect(screen.getAllByDisplayValue("gpt-6-astra")).toHaveLength(1); + expect(document.getElementById("codexDefaultModel")).toBeNull(); for (const model of [ "gpt-5.6-sol", "gpt-5.6-terra", @@ -277,9 +279,6 @@ describe("Codex Copilot provider form", () => { screen.getByRole("button", { name: "select-copilot-account" }), ); await selectFormat("openai_responses"); - fireEvent.change(screen.getByLabelText("默认模型"), { - target: { value: "gpt-5.6-luna" }, - }); fireEvent.change(screen.getByDisplayValue("gpt-5.6-sol"), { target: { value: "custom-copilot-model" }, }); @@ -305,7 +304,7 @@ describe("Codex Copilot provider form", () => { const settings = JSON.parse(saved.settingsConfig); expect(settings.auth).toEqual({}); expect(settings.config).toContain(liveToml); - expect(settings.config).toContain('model = "gpt-5.6-luna"'); + expect(settings.config).toContain('model = "gpt-6-astra"'); expect(settings.config).toContain('wire_api = "responses"'); expect(settings.modelCatalog.models).toEqual( preset.modelCatalog?.map((model) => @@ -416,7 +415,8 @@ describe("Codex Copilot provider form", () => { expect(screen.getByTestId("selected-copilot-account")).toHaveTextContent( accountId ?? "default", ); - fireEvent.change(screen.getByLabelText("默认模型"), { + // 没有模型列表的卡才有默认模型框(「?」按钮也叫默认模型)。 + fireEvent.change(screen.getByRole("textbox", { name: "默认模型" }), { target: { value: "gpt-5.6-luna" }, }); fireEvent.click(screen.getByRole("button", { name: "save" }));