From 95ce86f69e3869fde32f987111f8fade70c2f610 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 27 Sep 2026 23:52:13 +0900 Subject: [PATCH 01/14] docs: plan release train 4 GUI UX lane --- .../260927_release_train_4/gui-ux/000_plan.md | 48 +++++++++++++++++++ .../gui-ux/001_inventory.md | 17 +++++++ .../gui-ux/002_ux_states.md | 11 +++++ .../gui-ux/003_audit.md | 16 +++++++ .../gui-ux/010_model_settings.md | 21 ++++++++ .../gui-ux/020_combo_sidecar.md | 18 +++++++ .../gui-ux/030_visibility.md | 18 +++++++ .../gui-ux/040_dispositions.md | 15 ++++++ .../gui-ux/050_integration.md | 5 ++ 9 files changed, 169 insertions(+) create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/000_plan.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/001_inventory.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/002_ux_states.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/003_audit.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/020_combo_sidecar.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/030_visibility.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/040_dispositions.md create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/050_integration.md diff --git a/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md b/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md new file mode 100644 index 00000000000..6e144aa2009 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md @@ -0,0 +1,48 @@ +# Release train 4 — GUI UX lane + +The current dashboard exposes routed-model facts that still require editing `config.json`, and a combo with a text-only member still requires a manual Vision Sidecar declaration. This lane reviews those focused GUI changes on the live `dev` shape, verifies their actual browser flows, and lands only changes whose state transitions and CI are proven. `001_inventory.md` records the intake snapshot and PR age; each decade document below gives the file-level implementation or disposition contract. + +## Loop specification + +- **Archetype / trigger / goal:** satisfy the delegated release-train GUI acceptance contract by reviewing seven PRs and three issues, integrating safe lane-owned changes into `dev` with browser, test and CI evidence. +- **Non-goals:** main/preview, release/version changes, live user home/accounts/service, account-pool and picker lane code, a wholesale #5408 adoption, native GitHub stacks, and security findings in tracked files. +- **Tool and write scope:** GitHub reads/writes authorized in the delegation; local source and test commands only in `/Users/jun/.codex/worktrees/t4-gui-ux/opencodex`; docs in this `gui-ux/` directory; isolated proxy homes and distinct local ports; subagents only `gpt-6-sol`; no other-thread messages. No user-imposed token or wall-clock limit. +- **Verifier:** focused Bun tests for changed behavior, `bun run test:changed`, `bun run typecheck`, `bun run lint:gui`, `bun run build:gui`, `cd gui && bun run lint:i18n` for JSX copy, `cd gui && bun test tests/locale-parity.test.ts` for ten-locale key parity, `bun run privacy:scan`, `bun run structure:check`, `bun run skill:surface:check` when capability maps change, and `bun test tests/ci-workflows/file-size-ratchet.test.ts` for capped files; browser click flows and screenshots for UI states; required PR CI at the exact head and post-merge `dev` CI. Script existence is grounded in root and GUI `package.json`; the locale parity test reads `src/i18n/.ts` directly (`gui/tests/locale-parity.test.ts:6,229`). `lint:i18n` does not itself inspect locale dictionaries, so translation meaning also needs manual review and browser proof. `bun run test:changed` follows imports but misses subprocess/source-oracle edges, so explicit regression files remain necessary. Seven concurrent worktrees justify the documented full-local-suite contention exception; CI supplies the broad suite. +- **Stop / outcomes:** DONE when selected PRs are merged with exact-head/post-merge CI and every assigned PR/issue has a linked disposition; NOOP only if current `dev` already satisfies the whole lane; NEEDS_HUMAN for a genuine product/security decision that cannot be inferred; BLOCKED only on a repeated external blocker; UNSAFE when a change fails the security boundary; BUDGET_EXHAUSTED only on an actual user/host bound. +- **Memory artifact:** this numbered unit, PR Verification bodies, exact-head CI links, and local `.tmp/gui-ux/` screenshots/receipts. +- **Escalation:** a changed PR head, another lane changing a shared owner, auth/security objections, irreconcilable CI or browser environment, or an unowned external contract changes the disposition before publication. + +## Design Read + +```yaml +name: opencodex-dashboard +colors: + primary: "var(--accent)" + accent: "var(--amber)" + background: "var(--bg)" +typography: + heading: { fontFamily: "var(--font-ui)", fontSize: "var(--text-subtitle)" } + body: { fontFamily: "var(--font-ui)", fontSize: "var(--text-body)" } +iconography: + system: "existing OpenCodex controls" + weight: "regular" + domain: "existing components" +``` + +This is an operator dashboard for repeated model and provider work. Preserve compact rows, stable navigation, semantic status color and the existing dialog shell from `gui/design-system/`; add actions where a user is already inspecting the affected model. Never apply an editorial or floating-glass kit. **DESIGN_VARIANCE 3 / MOTION_INTENSITY 1 / density D6.** A save needs a clear committed/no-op/unknown outcome, visible loading state, recovery after timeout, focus restoration, keyboard activation, and readable 400px plus long-locale layout. A restore is a separate explicit action because it discards declarations. + +## Ordered work phases and decisions + +| Phase | Decision | Why / acceptance | +|---|---|---| +| `010` | Carry and repair #6058 on current `dev` | Capability editor is a cohesive lane-owned slice with no merge-tree conflict. Fix remaining fractional-window and persistence-failure concerns, prove model settings save/restore in an isolated proxy and browser, and land after exact-head CI. | +| `020` | Reimplement #4932 on current `dev` after `010` | Its model modality declaration shares the new editor's provider map. Resolve `combo-routes.ts` against current code, add Vietnamese copy, and prove a mixed combo's sidecar enrollment plus sibling-axis preservation. | +| `030` | Assess/reimplement #5617 on current `dev` after model changes | Large stale visibility semantic change intersects `model-routes.ts`, `Models.tsx`, catalog, and provider UI. Reimplement only after the account-pool lane finishes the shared provider UI owner, and if global-vs-provider semantics survive current catalog convergence and security review; otherwise leave open with concrete deferral comment. | +| `040` | Defer auth/config/account PRs and resolve issues | Defer #4649/#4644 pending explicit security review and iOS proof; #5932 overlaps account-pool ownership; #2355 is 2,971 commits old with eight conflicts; #5408 is 7,790 added lines and shares the account-pool domain. Keep #3379 open because selector rename is missing, and clarify #4189's intended provider contract. No auth implementation is in this train. | +| `050` | Integration and disposition | Build selected branches sequentially from the preceding accepted `dev` merge SHA, rebase to the current `dev` tip, validate file caps/union/ten locales, attach screenshots by `pr-assets` SHA, merge only after required exact-head checks and review findings, verify `dev`, then close solved issues/original PRs with links and credit. | + +Each phase is its own PABCD cycle. A later P rechecks this plan against the latest `origin/dev` and may narrow or append a slice with a written reason. `structure/INDEX.md` owns the source-document map; relevant SoT targets are `structure/gui-and-management-api.md`, `structure/dashboard-and-usage.md`, `structure/catalog.md`, and `structure/config.md` when their contracts actually change. User-facing changes update the matching `docs-site/` guide or reference. All new tests are registered in both test-layout inventories. PR screenshots live on `pr-assets`, never in the code PR branch. + +## Consultation + +Architect handle `01a0e33b-a8a7-7ab3-8682-461189c7d1c8` proposed GUI-01→GUI-02→GUI-03, conditional auth/config work, account-pool handoff for #5932, and wholesale #5408 deferral. I accept its dependency order, but reject its suggestion that the current OAuth account alias editor resolves #3379's Codex selector rename. `CodexAccountPickerSetting.tsx` still only toggles picker visibility; `codexAccountNamespaces` keys still lack a rename API. The issue remains open. Same-architect reflections first found a #3379 wording gap, #5617 owner/rollback gaps, and then #4932/#5617 decision wording and conditional-path gaps. Those were folded into this revision, `020_combo_sidecar.md` and `030_visibility.md`. The same handle's final response was **ALIGNED**, with no material gap in those amended passages. The independent A reviewer remains separate. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/001_inventory.md b/devlog/_plan/260927_release_train_4/gui-ux/001_inventory.md new file mode 100644 index 00000000000..b1d0c75d178 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/001_inventory.md @@ -0,0 +1,17 @@ +# GUI UX lane — current intake evidence + +Read at `origin/dev` `24b2f39b77` on 2026-09-27. The seven candidate heads were fetched as `t4/pr` for read-only comparison. `git merge-tree --write-tree origin/dev t4/pr` was used to identify textual conflicts without changing the checkout. All candidates target `dev`. + +| PR | Head | Commits behind current dev | Merge-tree conflicts | Initial ownership judgment | +|---|---|---:|---|---| +| #6058 | `0798999c6f` | 51 | none | Routed model capability editor; lane-owned. Current CodeRabbit finding permits a positive fractional context window that floors to zero. | +| #5617 | `f65144160d` | 369 | `app-routing.ts`, `Providers.tsx`, `structure/config.md` | Global visibility and provider-local visibility semantics; potentially lane-owned, but must reconcile with the current catalog and Models UX. | +| #4932 | `99f3abc39f` | 317 | `combo-routes.ts` | Mixed combo Vision Sidecar enrollment; lane-owned. Branch lacks `vi.ts` although current GUI has ten locales. | +| #4649 | `c9b24c20bf` | 317 | none | Remembered admin token for #4644; lane-owned with an auth/security review gate. | +| #5932 | `e4387ea001` | 126 | provider workspace `types.ts` | Antigravity account plan presentation; overlaps account-pool lane's provider workspace files. | +| #2355 | `ec0c68daca` | 2,971 | eight runtime, CLI and GUI files | Config-versus-running-proxy warning; old architecture and broad shared-file collisions. | +| #5408 | `d4112b3e5d` | 108 | six provider/account files | 7,790 added lines and account-pool overlap; release suitability assessment only per lane assignment. | + +Current issue status: #4644, #3379 and #4189 are open. #4644 tracks standalone Safari AutoFill and depends on the security verdict for #4649. #3379 already had journal deletion and custom usage ranges landed; selector naming remains and overlaps the picker/account lane. #4189 conflates ZCode client integration with Z.AI provider login; the existing issue discussion describes both interpretations, but the current code must be checked before disposition. + +The candidate files, comments and later `dev` movement are point-in-time evidence. Recheck heads and merge bases immediately before any carry or merge. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/002_ux_states.md b/devlog/_plan/260927_release_train_4/gui-ux/002_ux_states.md new file mode 100644 index 00000000000..5c4d68b89be --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/002_ux_states.md @@ -0,0 +1,11 @@ +# Dashboard UX state contract + +The lane uses the current OpenCodex design system: one compact row action, the existing dialog/notice primitives, semantic color tokens, no decorative motion. The operator must be able to tell whether a value is inherited, explicitly declared, saved, unsaved, or unknown after a network failure. A default GUI scan cannot prove these states; each is a separate browser or fault-injection activation. + +| Flow | Empty / inherited | Loading | Error / recovery | Keyboard and narrow-width proof | +|---|---|---|---|---| +| Routed model settings | An unticked modality field means no declaration; show the upstream value it follows. No reasoning ladder must be distinct from inherited ladder. | Save/restore disables duplicate submit and exposes busy text; preserve the current fields. | Validation names the field. A confirmed write with failed catalog refresh says the write happened and offers refresh. An unknown save outcome avoids a false “failed” claim and lets the operator reload. | Tab to row Edit, Enter opens; dialog focus is contained, Escape and Close restore focus when idle. At 400px actions stack without horizontal scrolling, and long German/Russian labels wrap. | +| Mixed combo | No target or an unknown catalog row gives an explanation and a next action, not an available Image toggle. Known text-only members are named in the hint before save. | Save shows one in-progress state; the target list remains visible for review. | A rejected sidecar target leaves combo/provider config unchanged and shows the server reason; retry is available. | Toggle and Save work by keyboard. The list and hint wrap at 400px, including long locale/model IDs. | +| Global/provider visibility | A provider with no visible rows distinguishes global hide from local selection and points to the restoring control. | A bounded PUT shows busy state; no switch can become stuck after timeout. | Failed save leaves the previous rendered switch state with retry guidance. | Global scope wording remains clear when rows are grouped by provider; keyboard toggles and 400px filter/action layout remain usable. | + +Screenshots for adopted GUI changes cover the pre-change view and the post-change first action, plus at least one narrow/long-locale and one error/empty/loading state. A screenshot is inspected visually, not merely generated. Text content and focus outcome are checked with browser DOM/keyboard actions. Synthetic credentials and models in an isolated home prevent touching the user's running 10100 proxy. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/003_audit.md b/devlog/_plan/260927_release_train_4/gui-ux/003_audit.md new file mode 100644 index 00000000000..e06d812ab35 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/003_audit.md @@ -0,0 +1,16 @@ +# Roadmap audit and baseline proof + +The independent reviewer (`01a0e349-7a9f-73f3-a3d1-b32cfdf9de9a`, `gpt-6-sol`) audited the docs-first plan at `dev` `24b2f39b77`. Its first verdict was `GO-WITH-FIXES`: the named i18n command was not a root script and did not inspect locale dictionaries, and Phase 030 omitted catalog/GUI consumers of #5617. Both High blockers were folded. Three Medium gaps were also fixed: Phase 040 now explicitly defers auth work, Phase 050 names the real file-size test and treats layout.json as an inventory, and branches are sequential from the previous accepted `dev` merge. A final one-line summary and NEW-vs-MODIFY path correction were rechecked. The last verdict was **PASS** with no blocking issue. + +The same architect (`01a0e33b-a8a7-7ab3-8682-461189c7d1c8`) reflected on the initial plan and later field/consumer and phase-dependency amendments; its final verdict was **ALIGNED**. Main rejected its early inference that an OAuth account alias closes #3379: the requested Codex selector rename remains absent. Explorer findings used for the plan were read-only and tied to PR heads in `001_inventory.md`. + +Baseline command evidence in this worktree: + +| Command | Result | What it actually observes | +|---|---|---| +| `bun run privacy:scan` | pass | Credential/privacy patterns in repository content; rerun after staging docs. | +| `bun test tests/ci-workflows/file-size-ratchet.test.ts` | first attempt failed: missing `zod/v4`; after `bun install --frozen-lockfile`, 9 pass / 0 fail | Repository file-size cap including `gui/src/pages/Models.tsx`. No baseline was updated. | +| `cd gui && bun test tests/locale-parity.test.ts` | after GUI frozen install, 5 pass / 0 fail | Reads locale modules directly and checks key-set parity for ten locales; translation meaning still needs human/browser review. | +| `cd gui && bun run lint:i18n` | not yet run on a changed GUI | Oxlint checks visible JSX copy, not locale dictionary key parity. | + +All installs used the committed lockfiles in the lane worktree. No production home, service, token, account, browser session or `dev` branch was touched in this docs-only phase. The `0.2.39` Codexclaw cache path disappeared during the audit; the now-installed `0.2.36` CLI read the same bound session/source and continued with explicit `--session`. That tool drift is not verification of product behavior. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md new file mode 100644 index 00000000000..e0ce407e303 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md @@ -0,0 +1,21 @@ +# Phase 010 — routed-model capability editing (#6058) + +**Decision:** carry the contributor change from `0798999c6f`, preserve attribution with a `Co-authored-by` trailer, and repair it on a `codex/t4-gui-ux-*` branch based on the newest `origin/dev`. Do not push the contributor fork. + +## Diff-level map + +- **NEW** `gui/src/components/ModelSettingsDialog.tsx`: use the existing narrow dialog shell; seed from declarations rather than observed catalog values; send only changed axes; distinguish confirmed save plus failed refresh from unknown save. The PR currently treats the `unknown` error phase as busy, disabling Cancel and trapping the dialog; busy must mean an in-flight request only. Keep fields and Close/Cancel or retry usable on error. Return focus to the row trigger after close. +- **MODIFY** `gui/src/pages/Models.tsx` and `gui/src/pages/models-shared.ts`: show one Edit control only for routed, non-custom, non-combo rows; load the saved row after a successful mutation; stay under the `Models.tsx` 2,792-line cap by moving logic into sibling files if needed. +- **MODIFY** all ten `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,vi,zh-TW,zh}.ts`: exact same keys, translated semantics, long-string layout proof. `cd gui && bun test tests/locale-parity.test.ts` checks key sets; `cd gui && bun run lint:i18n` checks visible JSX copy; neither proves translation meaning, so inspect long translations in-browser. +- **MODIFY** `src/server/management/model-routes.ts`, `model-rows.ts`, `route-registry.ts`: validate provider, exact model ID and all axes at ingress; reject a positive fractional context value that floors to zero; wrap mutation/persistence in existing `commitProviderPatch` so an unpublished failed save restores live config graph and a published write is not falsely rolled back; avoid false no-op on retry; clear the provider discovery cache only after commit, and converge the catalog. Route projection distinguishes declared and inherited modalities/efforts. Unreleased input-validation findings are kept in ignored scratch until fixed. +- **MODIFY** `src/cli/models-runtime.ts`, `models-runtime-subcommands.ts`, `capabilities.ts`; **REGENERATE** `skills/ocx/references/01_management_surface.md`: CLI and API parity, safe clear spelling. +- **NEW** `tests/server/model-settings-management-api.test.ts`, `tests/cli/cli-models-set.test.ts`, `gui/tests/model-settings-dialog.test.tsx`; **MODIFY** `tests/codex-integration/codex-convergence-contract.test.ts`, both test-layout inventories. Include save-throw-then-retry, no-op, null clear, empty ladder, effective default, duplicate modalities, cache invalidation, and Cancel/Escape after a network error. +- **MODIFY** `structure/gui-and-management-api.md` and `docs-site/src/content/docs/reference/management-api.md`: explain durable-write semantics, clear/no-reasoning distinction and UI behavior. + +## Activation and proof + +With synthetic provider/model entries in an isolated `OPENCODEX_HOME`, inspect the Models row at desktop and 400px, open Edit by pointer and keyboard, save a single axis, reload, restore all, and capture before/after plus loading/empty/error/long-locale views. API tests force save failure, then identical retry: durable config and live rows must agree after the retry. A `contextWindow: 0.5` request must be 400 without mutation. A failed catalog refresh after a confirmed save must say the save occurred and offer recovery, not invite a duplicate write. Verify CLI no-op and clear output against route receipts. Run focused tests, `test:changed`, typecheck, GUI test/lint plus `cd gui && bun test tests/locale-parity.test.ts` and `cd gui && bun run lint:i18n`/build, privacy, structure, skill surface, `bun test tests/ci-workflows/file-size-ratchet.test.ts`, then exact-head CI. Security review checks management admission and no secret exposure. + +## Rollback and boundary + +Existing provider per-model maps remain readable if this editor is reverted. Do not change custom-model ownership or native OpenAI rows. This phase does not edit the user's actual config or Codex home. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/020_combo_sidecar.md b/devlog/_plan/260927_release_train_4/gui-ux/020_combo_sidecar.md new file mode 100644 index 00000000000..db00ea82ef4 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/020_combo_sidecar.md @@ -0,0 +1,18 @@ +# Phase 020 — mixed combo Vision Sidecar enrollment (#4932) + +**Decision:** reimplement the contributor's cohesive behavior after Phase 010, with attribution, against current `combo-routes.ts`. The old branch's GUI work is reusable, but its server route has a 200-success hole for a missing provider and can overwrite an image-capable declaration with text-only. + +## Diff-level map + +- **MODIFY** `gui/src/combo-capabilities.ts`, `combo-workspace-data.ts`, `components/combo-workspace-controls.tsx`, `pages/Combos.tsx`: permit image input when every member is known and text-only members can be declared; reject audio-only rows rather than treating every non-image row as text; name exactly which members receive a sidecar declaration; fail closed for unknown catalog entries; keep the primary save action and clear loading/error states. +- **MODIFY** all ten `gui/src/i18n/` locale modules, including the missing Vietnamese translations; no English-only hint in JSX. +- **MODIFY** `src/server/management/combo-routes.ts`: accept request-only `visionSidecarTargets`, validate exact member ownership, configured provider existence, current modality declaration and image-input gate before any mutation. Reject a missing provider or an already-image-capable submitted target instead of returning 200 after silently skipping/overwriting it. Write each eligible text-only `inputModalities` declaration into the provider map while preserving other capabilities, and do not persist the request-only field under `combos`. +- **MODIFY** `structure/catalog.md`, `structure/config.md`, `structure/dashboard-and-usage.md`, `docs-site/src/content/docs/guides/{combos,sidecars}.md`; **MODIFY** `tests/routing/combo-management-api.test.ts` and `tests/gui/combo-workspace-data.test.ts`; register any new files in both test-layout inventories. + +## Activation and proof + +In an isolated proxy, create a mixed combo from one image-capable and one text-only synthetic member; inspect the hint before save, save, then read back the per-model modality patch and combo behavior. Unknown member, disabled image input, non-member target, missing configured provider, already-image-capable submitted sidecar target and audio-only row each fail without config mutation or a false success receipt. Edit that model's capability through Phase 010 afterward and verify the combo state remains truthful. Browser proof includes desktop/400px, keyboard, empty/loading/error, and at least one long locale; screenshot before/after. Run focused regressions, `test:changed`, typecheck, GUI full tests/lint/build plus `cd gui && bun run lint:i18n` and `cd gui && bun test tests/locale-parity.test.ts`, structure/privacy and exact-head CI. + +## Rollback and boundary + +Do not overwrite sibling per-model context/reasoning axes. If the generated modality declaration cannot be distinguished from an operator's existing declaration, the implementation must preserve the existing one and explain that behavior; rollback must never delete operator-owned facts. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/030_visibility.md b/devlog/_plan/260927_release_train_4/gui-ux/030_visibility.md new file mode 100644 index 00000000000..8478c531372 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/030_visibility.md @@ -0,0 +1,18 @@ +# Phase 030 — global and provider visibility assessment (#5617) + +**Decision under audit:** reimplement the useful global/provider distinction on current `dev` after Phase 010, subject to the account-pool lane finishing the shared provider UI owner. The stale draft is 369 commits behind `dev` and conflicts in `app-routing.ts`, `Providers.tsx`, and `structure/config.md`; a mechanical carry could break the current JEV Auto deep link and provider CTA. If that owner is still changing or a stable config migration cannot be shown, defer with evidence. + +## If adopted: diff-level map + +- **MODIFY** `src/types/config.ts`, `src/codex/catalog.ts`, `src/codex/catalog/model-visibility.ts`, `src/codex/catalog/gather-capture.ts`, `src/codex/catalog/metadata.ts`, `src/codex/catalog/retained-sync.ts`, `src/codex/convergence.ts`, `src/codex/subagent-model-fallback.ts`, `src/config/live-reconcile.ts`: create/serialize/revive a global exact-model-ID hide independently of provider-local selected/hidden rules; apply it to discovery, retained sync, metadata, subagent fallback and catalog convergence; newly added providers obey the global rule while direct routing stays distinct from picker visibility. +- **MODIFY** `src/server/management/model-routes.ts`, `model-rows.ts`, `route-registry.ts`, `agent-settings-routes.ts`, `src/server/index/serve-options.ts`, `src/cli/models-runtime.ts`: expose an explicit global scope through the management, CLI and startup option paths while keeping legacy provider requests stable; avoid a silently changed meaning for existing `disabledModels`. +- **MODIFY** `gui/src/pages/Models.tsx`, `models-shared.ts`, `models-tab.ts`, `Providers.tsx`, `components/provider-workspace/ProviderModels.tsx`, `ProviderModelChip.tsx`, `provider-workspace/model-inventory.ts`, `app-routing.ts`, and all ten locales; **NEW if needed** `gui/src/pages/use-models-provider-selection.ts` (the contributor branch has it, current `dev` does not): separate labels and controls where the user sees the distinction; provider-local empty state explains a global hide and links to the Models control. Keep `JEV_AUTO_CREATE_HASH` and its CTA. Use bounded requests for global visibility so a hung PUT cannot lock future switches. Move new logic into siblings before the 2,792-line Models cap is reached. Preserve dense operator layout and keyboard operation. +- **MODIFY** `structure/catalog.md`, `structure/config.md`, `structure/gui-and-management-api.md` and matching `docs-site` model-routing/dashboard guides; **MODIFY** focused catalog/management/GUI tests and both test-layout inventories for any new files. + +## Acceptance or deferral test + +Prove: globally hide one exact model ID; add a new provider with that ID; it stays hidden in catalog while a direct route still works. Locally hide one provider's ID; another provider's same ID stays visible. Reload, sync and restart preserve these states. An empty provider list explains whether global hide or provider selection caused it. Open the existing JEV Auto CTA and `#models/combos/jev-auto` deep link after the new provider link is added; both must still resolve to the Combos action. Force a global visibility PUT to hang until its timeout, then verify the busy state releases and a late response cannot overwrite a newer switch state. Inspect before/after browser flows at desktop/400px and long locale. Run a dedicated management/security review and exact-head CI. If a stable migration from old `disabledModels` cannot be established, leave #5617 open with a detailed English comment rather than changing semantics mid-train. + +## Rollback + +If adopted, the new persisted global hide field needs an explicit rollback/migration before a UI revert. A revert must either translate it back into the existing visibility representation with a documented precedence or remove it only after the operator is shown the affected exact IDs; leaving invisible models behind an absent control is unacceptable. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/040_dispositions.md b/devlog/_plan/260927_release_train_4/gui-ux/040_dispositions.md new file mode 100644 index 00000000000..94a80d33d11 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/040_dispositions.md @@ -0,0 +1,15 @@ +# Phase 040 — auth, config, account and issue disposition + +## PR decisions + +- **#4649 and #4644 — defer:** The draft changes credential persistence and has not passed the explicit security review required by `MAINTAINERS.md`. Keep the PR and issue open; the current screenshot file also violates repository hygiene. The security analysis and any pre-disclosure repair plan stay in ignored `.tmp/gui-ux/4649-security-review.md`, not this public devlog. This workaround cannot be called Safari AutoFill itself. A later adoption is a new scoped phase after security and real iOS standalone proof. +- **#5932 — defer from this lane:** its `ProviderAuthPanel.tsx`, `useProviderAccountPools.ts`, provider workspace types, and OAuth account DTO overlap the account-pool lane. Do not touch those files here. The account-pool lane should determine whether a plan badge is still missing after its work; leave PR open with an ownership and current-conflict comment. +- **#2355 — defer this old branch; consider a new scoped issue later:** eight conflicts across `src/config.ts`, server lifecycle, CLI and dashboard. A safe version would require a resident-vs-disk config identity, low-privilege status read, warning/retry UI and tests for managed save versus external edit. Its `docs/pr-assets/` screenshot cannot be carried. The config/picker lanes may move those owners during this train; leave the PR open with current file evidence. +- **#5408 — defer wholesale:** 7,790 insertions across 67 files, six current conflicts and account-pool overlap. Do no cherry-pick until individual behavior gaps are identified in the current provider workspace; this lane performs suitability judgment only. Leave a concrete English comment and keep it open. + +## Issue decisions + +- **#3379 — hold open:** journal deletion and custom Usage ranges already landed, but the remaining Codex selector rename is absent. `gui/src/components/CodexAccountPickerSetting.tsx` only toggles picker visibility; `src/server/management/config-routes.ts` has no selector-name edit API. Generic OAuth `alias` is a different field. This touches picker/account ownership, so record the gap and avoid a duplicate implementation in this GUI lane. +- **#4189:** current ZCode client integration and Z.AI provider APIs are separate. The report does not identify a ZCode upstream login/API contract. Ask the issue author in English whether they mean the ZCode client using OpenCodex, Z.AI API-key upstream, or a distinct login; leave open pending answer and do not add a fake `zcode` provider card. + +For every deferred PR or issue, post one evidence-backed English comment with the disposition. Do not close a contributor PR merely for being large or stale. An adopted contributor PR closes only after a replacement lands and a credit trailer plus replacement link are present. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/050_integration.md b/devlog/_plan/260927_release_train_4/gui-ux/050_integration.md new file mode 100644 index 00000000000..0023c878867 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/050_integration.md @@ -0,0 +1,5 @@ +# Phase 050 — merge evidence and post-merge cleanup + +Before each selected PR: fetch the latest `origin/dev` and contributor head; compare diff and merge-base; build the next lane branch from the **previous accepted dev merge SHA** (or current `origin/dev` for the first branch), then rebase that branch to the exact current `dev` tip and rerun tests affected by the rebase. Check the union of prior lane changes with the new diff. Prove the `Models.tsx` cap with `bun test tests/ci-workflows/file-size-ratchet.test.ts`; `scripts/test-layout/layout.json` is an inventory to keep synchronized with `tests/fixtures/test-layout-expected.json`, not a file-size-baseline entry. Review union/locale rosters. Write the PR template's Summary, Verification and Checklist with exact commands, results, full-suite contention exception, current screenshot `pr-assets` commit SHA URLs, and contributor `Co-authored-by` trailer. No screenshot files in the code branch. + +Inspect GitHub required checks against the PR's current `headRefOid`; skipped, cancelled, pending and older-head results are not success. Resolve valid Codex/CodeRabbit findings; merge only after all required checks succeed and MAINTAINERS.md security/review rules are satisfied. Record each merge SHA, CI run URL, source PR close/comment and linked issue close/comment. Fetch `dev` after each merge and verify its current CI; repair any regression owned by this lane before the next batch. The final report names merged PRs, exact SHAs, held work, source credit, browser evidence and remaining risk. From 77769c0ce93941a05b7817f584da2cbba9adc3a5 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:28:38 +0900 Subject: [PATCH 02/14] docs: checkpoint GUI model settings plan and baseline --- .../260927_release_train_4/gui-ux/000_plan.md | 2 +- .../gui-ux/004_ui_baseline.md | 5 +++++ .../gui-ux/010_model_settings.md | 16 ++++++++++------ 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/004_ui_baseline.md diff --git a/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md b/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md index 6e144aa2009..4090ef7f111 100644 --- a/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md +++ b/devlog/_plan/260927_release_train_4/gui-ux/000_plan.md @@ -6,7 +6,7 @@ The current dashboard exposes routed-model facts that still require editing `con - **Archetype / trigger / goal:** satisfy the delegated release-train GUI acceptance contract by reviewing seven PRs and three issues, integrating safe lane-owned changes into `dev` with browser, test and CI evidence. - **Non-goals:** main/preview, release/version changes, live user home/accounts/service, account-pool and picker lane code, a wholesale #5408 adoption, native GitHub stacks, and security findings in tracked files. -- **Tool and write scope:** GitHub reads/writes authorized in the delegation; local source and test commands only in `/Users/jun/.codex/worktrees/t4-gui-ux/opencodex`; docs in this `gui-ux/` directory; isolated proxy homes and distinct local ports; subagents only `gpt-6-sol`; no other-thread messages. No user-imposed token or wall-clock limit. +- **Tool and write scope:** GitHub reads/writes authorized in the delegation; source edits/Git operations in `/Users/jun/.codex/worktrees/t4-gui-ux/opencodex`; docs in this `gui-ux/` directory; focused tests there, with `test:changed`/full suites in a verification-only worktree at `/private/tmp/t4-gui-ux-verify` after the exact commit; isolated proxy homes and distinct local ports via the direct server entrypoint with client integrations OFF; subagents only `gpt-6-sol`; no other-thread messages. No user-imposed token or wall-clock limit. - **Verifier:** focused Bun tests for changed behavior, `bun run test:changed`, `bun run typecheck`, `bun run lint:gui`, `bun run build:gui`, `cd gui && bun run lint:i18n` for JSX copy, `cd gui && bun test tests/locale-parity.test.ts` for ten-locale key parity, `bun run privacy:scan`, `bun run structure:check`, `bun run skill:surface:check` when capability maps change, and `bun test tests/ci-workflows/file-size-ratchet.test.ts` for capped files; browser click flows and screenshots for UI states; required PR CI at the exact head and post-merge `dev` CI. Script existence is grounded in root and GUI `package.json`; the locale parity test reads `src/i18n/.ts` directly (`gui/tests/locale-parity.test.ts:6,229`). `lint:i18n` does not itself inspect locale dictionaries, so translation meaning also needs manual review and browser proof. `bun run test:changed` follows imports but misses subprocess/source-oracle edges, so explicit regression files remain necessary. Seven concurrent worktrees justify the documented full-local-suite contention exception; CI supplies the broad suite. - **Stop / outcomes:** DONE when selected PRs are merged with exact-head/post-merge CI and every assigned PR/issue has a linked disposition; NOOP only if current `dev` already satisfies the whole lane; NEEDS_HUMAN for a genuine product/security decision that cannot be inferred; BLOCKED only on a repeated external blocker; UNSAFE when a change fails the security boundary; BUDGET_EXHAUSTED only on an actual user/host bound. - **Memory artifact:** this numbered unit, PR Verification bodies, exact-head CI links, and local `.tmp/gui-ux/` screenshots/receipts. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/004_ui_baseline.md b/devlog/_plan/260927_release_train_4/gui-ux/004_ui_baseline.md new file mode 100644 index 00000000000..2d4d87626ab --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/004_ui_baseline.md @@ -0,0 +1,5 @@ +# Before-change Models UI observation + +At `a60b078cda` (source code from `dev` `24b2f39b77`), a direct `startServer(18761)` call with synthetic `demo` models and isolated OpenCodex/Codex homes served the packaged dashboard. A separate headless Chrome profile opened Models, dismissed the first-run surface, selected Demo and used Expand all. The rendered two rows had visibility, alias, Name and Price controls. Neither row had capability editing. The inspected desktop screenshot is in ignored `.tmp/gui-ux/before-models-desktop.png`; its eventual PR evidence copy must live on `pr-assets`, not the code branch. + +The baseline browser interaction verified the live proxy reported version 2.69.0 and showed `demo/text-demo` plus `demo/vision-demo`. The synthetic provider used an unreachable loopback upstream and no real key, so no model request was sent. A CLI startup attempted to synchronize an installed local client despite isolated proxy homes; that process was stopped and its one observed real-home managed endpoint was restored. Operational detail and restoration evidence are kept in ignored `.tmp/gui-ux/qa-isolation-incident.md`. Later QA uses only the direct server entrypoint with local-client integrations explicitly OFF. This observation does not prove any changed UI behavior or iOS Safari behavior. diff --git a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md index e0ce407e303..1f364d67c31 100644 --- a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md +++ b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md @@ -4,18 +4,22 @@ ## Diff-level map -- **NEW** `gui/src/components/ModelSettingsDialog.tsx`: use the existing narrow dialog shell; seed from declarations rather than observed catalog values; send only changed axes; distinguish confirmed save plus failed refresh from unknown save. The PR currently treats the `unknown` error phase as busy, disabling Cancel and trapping the dialog; busy must mean an in-flight request only. Keep fields and Close/Cancel or retry usable on error. Return focus to the row trigger after close. -- **MODIFY** `gui/src/pages/Models.tsx` and `gui/src/pages/models-shared.ts`: show one Edit control only for routed, non-custom, non-combo rows; load the saved row after a successful mutation; stay under the `Models.tsx` 2,792-line cap by moving logic into sibling files if needed. -- **MODIFY** all ten `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,vi,zh-TW,zh}.ts`: exact same keys, translated semantics, long-string layout proof. `cd gui && bun test tests/locale-parity.test.ts` checks key sets; `cd gui && bun run lint:i18n` checks visible JSX copy; neither proves translation meaning, so inspect long translations in-browser. -- **MODIFY** `src/server/management/model-routes.ts`, `model-rows.ts`, `route-registry.ts`: validate provider, exact model ID and all axes at ingress; reject a positive fractional context value that floors to zero; wrap mutation/persistence in existing `commitProviderPatch` so an unpublished failed save restores live config graph and a published write is not falsely rolled back; avoid false no-op on retry; clear the provider discovery cache only after commit, and converge the catalog. Route projection distinguishes declared and inherited modalities/efforts. Unreleased input-validation findings are kept in ignored scratch until fixed. +- **NEW** `gui/src/components/ModelSettingsDialog.tsx`: use the existing narrow dialog shell; seed every draft from stored declarations, not effective catalog values; show the effective context window as an inherited hint when no stored declaration exists. Send only changed axes and distinguish confirmed save plus failed refresh from unknown save. The PR currently treats the `unknown` error phase as busy, disabling Cancel and trapping the dialog; busy must mean an in-flight request only. On unknown outcome, focus an enabled Close control, disable Save/Restore, and offer a read-only Retry that reloads the row before another mutation. Return focus to the row trigger after close. +- **MODIFY** `gui/src/pages/Models.tsx` and `gui/src/pages/models-shared.ts`: show one Edit control only for routed, non-custom, non-combo rows; carry exact stored `contextWindowDeclared` separately from effective `contextWindow`; load the saved row after a successful mutation; stay under the `Models.tsx` 2,792-line cap by moving logic into sibling files if needed. +- **MODIFY** all ten `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,vi,zh-TW,zh}.ts`: exact same keys, translated inherited-context and unknown/confirmed-reload semantics, long-string layout proof. `cd gui && bun test tests/locale-parity.test.ts` checks key sets; `cd gui && bun run lint:i18n` checks visible JSX copy; neither proves translation meaning, so inspect long translations in-browser. +- **MODIFY** `src/server/management/model-routes.ts`, `model-rows.ts`, `route-registry.ts`: validate provider, exact model ID, allowed request fields and all axes at ingress; reject a positive fractional context value that floors to zero or exceeds safe integer range. Compute changed/no-op against copied touched maps, then wrap mutation/persistence in existing `commitProviderPatch` so an unpublished failed save restores live config graph and a published write is not falsely rolled back. On a published write with a post-publication error, return a sanitized saved receipt; do not claim catalog convergence when it is skipped or failed. Clear the provider discovery cache only after publication. Route projection distinguishes stored context/modality/reasoning declarations from effective values. Unreleased input-validation findings are kept in ignored scratch until fixed. - **MODIFY** `src/cli/models-runtime.ts`, `models-runtime-subcommands.ts`, `capabilities.ts`; **REGENERATE** `skills/ocx/references/01_management_surface.md`: CLI and API parity, safe clear spelling. -- **NEW** `tests/server/model-settings-management-api.test.ts`, `tests/cli/cli-models-set.test.ts`, `gui/tests/model-settings-dialog.test.tsx`; **MODIFY** `tests/codex-integration/codex-convergence-contract.test.ts`, both test-layout inventories. Include save-throw-then-retry, no-op, null clear, empty ladder, effective default, duplicate modalities, cache invalidation, and Cancel/Escape after a network error. +- **NEW** `tests/server/model-settings-management-api.test.ts`, `tests/cli/cli-models-set.test.ts`, `gui/tests/model-settings-dialog.test.tsx`; **MODIFY** `tests/codex-integration/codex-convergence-contract.test.ts`, both test-layout inventories. Include unpublished save-throw-then-identical-retry, published-postwrite-error receipt, no-op, null clear, empty ladder, effective default, duplicate modalities, context declaration projection, cache invalidation, unknown outcome read-only Retry, and Close/Escape after a network error. Validate rejected inputs cause no save, cache clear or convergence. - **MODIFY** `structure/gui-and-management-api.md` and `docs-site/src/content/docs/reference/management-api.md`: explain durable-write semantics, clear/no-reasoning distinction and UI behavior. ## Activation and proof -With synthetic provider/model entries in an isolated `OPENCODEX_HOME`, inspect the Models row at desktop and 400px, open Edit by pointer and keyboard, save a single axis, reload, restore all, and capture before/after plus loading/empty/error/long-locale views. API tests force save failure, then identical retry: durable config and live rows must agree after the retry. A `contextWindow: 0.5` request must be 400 without mutation. A failed catalog refresh after a confirmed save must say the save occurred and offer recovery, not invite a duplicate write. Verify CLI no-op and clear output against route receipts. Run focused tests, `test:changed`, typecheck, GUI test/lint plus `cd gui && bun test tests/locale-parity.test.ts` and `cd gui && bun run lint:i18n`/build, privacy, structure, skill surface, `bun test tests/ci-workflows/file-size-ratchet.test.ts`, then exact-head CI. Security review checks management admission and no secret exposure. +With synthetic provider/model entries in an isolated `OPENCODEX_HOME`, inspect the Models row at desktop and 400px, open Edit by pointer and keyboard, save a single axis, reload, restore all, and capture before/after plus loading/empty/error/long-locale views. Use a direct `startServer` import with explicit client integrations OFF and an isolated `CODEX_HOME`; the CLI startup path auto-syncs installed clients and is unsuitable for this QA boundary. The `dev` baseline screenshot at ignored `.tmp/gui-ux/before-models-desktop.png` shows demo routed rows with Name and Price actions and no capability Edit. API tests force save failure, then identical retry: durable config and live rows must agree after the retry. A `contextWindow: 0.5` request must be 400 without mutation. A failed catalog refresh after a confirmed save must say the save occurred and offer recovery, not invite a duplicate write. Verify CLI no-op and clear output against route receipts. Run focused tests, typecheck, GUI test/lint plus `cd gui && bun test tests/locale-parity.test.ts` and `cd gui && bun run lint:i18n`/build, privacy, structure, skill surface, `bun test tests/ci-workflows/file-size-ratchet.test.ts`, then exact-head CI. Run `test:changed` and any full suite in a verification-only Git worktree at `/private/tmp/t4-gui-ux-verify` at the exact code commit; tests under `~/.codex` can hit protected cleanup paths unrelated to product behavior. Code edits remain in the lane worktree. Security review checks management admission and no secret exposure. ## Rollback and boundary Existing provider per-model maps remain readable if this editor is reverted. Do not change custom-model ownership or native OpenAI rows. This phase does not edit the user's actual config or Codex home. + +## Architect consultation for this work-phase + +Read-only architect `01a0e369-14f4-7d40-b0de-1ddef731fa98` proposed MS58-01 through MS58-05 against contributor head `0798999c6f`. Main accepted MS58-01 atomic write using the repository's `commitProviderPatch` owner and copied touched maps; accepted MS58-02 exact ingress; accepted MS58-03 with a sanitized publication receipt that distinguishes config save from catalog convergence; accepted MS58-04 read-only recovery and focus behavior; and accepted MS58-05 stored-versus-effective context projection. The alternative of a discovery fetch to prove model-row existence is rejected here because it makes a local config mutation depend on a possibly unavailable upstream; exact ID validation is the admission boundary. The same handle reflected on this amended plan and returned **ALIGNED** with no material gap. The independent A reviewer remains separate. From 3a35fa2d87b8c1035ada922e20ecd835763ecd20 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:29:22 +0900 Subject: [PATCH 03/14] docs: hand off GUI UX release train lane --- devlog/_plan/260927_release_train_4/gui-ux/_handoff.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/_handoff.md diff --git a/devlog/_plan/260927_release_train_4/gui-ux/_handoff.md b/devlog/_plan/260927_release_train_4/gui-ux/_handoff.md new file mode 100644 index 00000000000..15588cd294f --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/_handoff.md @@ -0,0 +1,9 @@ +# GUI UX lane handoff — 2026-09-28 KST + +Stopped at the coordinator's request. Dedicated checkout: `/Users/jun/.codex/worktrees/t4-gui-ux/opencodex`, branch `codex/t4-gui-ux-model-settings`, latest work commit `05ff0a8372` (parent roadmap commit `a60b078cda`); **local only, not pushed**. No lane PR was created or merged, and no GitHub issue/PR comment or closure was made. The seven source PRs and three issues were open at intake; refresh their states before acting. Last verified `origin/dev` was `24b2f39b77`, #6058 head `0798999c6f`. + +Docs-first `wp0` PABCD cycle is complete. The session `01a0e337-f3a7-7380-82dd-4a66bb2da2fd` is at **A** for `wp1` (#6058). The independent A reviewer `01a0e372-f380-7e11-bf2b-661afd132a50` was shut down before a verdict; do not treat it as approval. The `wp1` architect reflected on the amended `010_model_settings.md` and returned ALIGNED. The roadmap, PR/issue disposition and UX state design are in numbered files here. Product code is still unchanged from `dev`. + +The before-change live GUI screenshot is in ignored `.tmp/gui-ux/before-models-desktop.png`. Its synthetic proxy was run from direct `startServer` with isolated OpenCodex/Codex homes and client integrations OFF. An earlier CLI-start attempt rewrote the real Grok managed endpoint to test port 18761; it was stopped and the endpoint restored to the running user proxy on 10100, with readback verification. Details are in ignored `.tmp/gui-ux/qa-isolation-incident.md`. **Use only the direct server entrypoint for future local QA.** Unreleased #6058 and #4649 security notes are in ignored `.tmp/gui-ux/*-security-review.md`, not tracked docs. + +Next: fetch current `dev` and #6058; obtain a fresh `gpt-6-sol` independent A verdict on `010_model_settings.md` and its owning code. If approved, carry #6058 onto this lane branch with contributor credit, repair the documented validation, persistence and dialog recovery defects, then verify focused tests/typecheck/GUI lint/build and real desktop/narrow/long-locale click flows. Run `test:changed` or a full suite only in a verification-only checkout of the exact code commit at `/private/tmp/t4-gui-ux-verify` because tests under `~/.codex` can hit protected cleanup paths. Screenshots for a code PR go to `pr-assets` by SHA, never the code branch. Required exact-head CI and post-merge `dev` CI are still outstanding. Later phases #4932, #5617 and the deferred PR/issue dispositions remain unstarted. From dc25096ec907382978bcdd860482d36dfe25c08c Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:33:21 +0900 Subject: [PATCH 04/14] docs: revalidate GUI model settings plan at resume --- .../_plan/260927_release_train_4/gui-ux/010_model_settings.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md index 1f364d67c31..5afe6fd19e0 100644 --- a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md +++ b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md @@ -23,3 +23,7 @@ Existing provider per-model maps remain readable if this editor is reverted. Do ## Architect consultation for this work-phase Read-only architect `01a0e369-14f4-7d40-b0de-1ddef731fa98` proposed MS58-01 through MS58-05 against contributor head `0798999c6f`. Main accepted MS58-01 atomic write using the repository's `commitProviderPatch` owner and copied touched maps; accepted MS58-02 exact ingress; accepted MS58-03 with a sanitized publication receipt that distinguishes config save from catalog convergence; accepted MS58-04 read-only recovery and focus behavior; and accepted MS58-05 stored-versus-effective context projection. The alternative of a discovery fetch to prove model-row existence is rejected here because it makes a local config mutation depend on a possibly unavailable upstream; exact ID validation is the admission boundary. The same handle reflected on this amended plan and returned **ALIGNED** with no material gap. The independent A reviewer remains separate. + +## Revalidation at resume (2026-09-28) + +The lane resumed in coordinator thread `01a0e37e-639d-7693-a2bc-5e4df49fa656` after the previous thread stopped at A. The previous D (docs-first `wp0`) locked this roadmap and pointed `wp1` at a carry-and-repair of #6058. That direction stands: `origin/dev` is still `24b2f39b77` and #6058 is still `0798999c6f`, so the diff-level map above needs no change. A fresh independent `gpt-6-sol` A verdict replaces the one lost when reviewer `01a0e372-f380-7e11-bf2b-661afd132a50` was shut down. From 701344a05f4cee29cd5d042a06fbe94452f6a00f Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:38:39 +0900 Subject: [PATCH 05/14] docs: fold independent audit into GUI model settings plan --- .../gui-ux/010_model_settings.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md index 5afe6fd19e0..adf10aa3c84 100644 --- a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md +++ b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md @@ -27,3 +27,13 @@ Read-only architect `01a0e369-14f4-7d40-b0de-1ddef731fa98` proposed MS58-01 thro ## Revalidation at resume (2026-09-28) The lane resumed in coordinator thread `01a0e37e-639d-7693-a2bc-5e4df49fa656` after the previous thread stopped at A. The previous D (docs-first `wp0`) locked this roadmap and pointed `wp1` at a carry-and-repair of #6058. That direction stands: `origin/dev` is still `24b2f39b77` and #6058 is still `0798999c6f`, so the diff-level map above needs no change. A fresh independent `gpt-6-sol` A verdict replaces the one lost when reviewer `01a0e372-f380-7e11-bf2b-661afd132a50` was shut down. + +## Independent A verdict at resume + +Reviewer `01a0e380-3554-72c3-935d-9f1ec8a1193b` (`gpt-6-sol`) returned **GO-WITH-FIXES** and confirmed `commitProviderPatch(config, mutate, save)` in `src/server/management/provider-patch-transaction.ts:45-61` restores the live graph after an unpublished failure and keeps it after `ConfigWritePublishedError`. Its three findings are folded into this phase: + +1. **CLI unsafe integers (Medium):** `src/cli/models-runtime.ts` in the PR uses `Number.isInteger`; require `Number.isSafeInteger` with a focused CLI case. The server check remains authoritative. +2. **No-op wording (Medium):** `changed: false` means "nothing changed", not "no overrides stored". CLI and dialog no-op copy become neutral ("No settings changed" / nothing to restore only when the receipt shows every axis null), with tests for an identical write that leaves a declaration stored. +3. **Catalog receipt (Low):** the dialog must read the server's `catalogRefresh` result; a confirmed save with a failed or skipped refresh says the settings were saved and offers reload, covered by a GUI test. + +The only capped file grown is `gui/src/pages/Models.tsx` (cap 2,792; dev 2,780; PR 2,788). The merge has no textual conflict, but locale, management-API doc, structure doc and test inventories need a union review after carry. The CLI is kept: it is small, already tested, and gives API parity. From 3cd78023cfba2709424a9895e1b173d45aab9745 Mon Sep 17 00:00:00 2001 From: ardeyouxipianyi <189708448+ardeyouxipianyi@users.noreply.github.com> Date: Sun, 27 Sep 2026 12:03:16 +0800 Subject: [PATCH 06/14] feat(models): edit a routed model's capability axes in place A routed row's context window, input modalities, reasoning ladder and default effort were previously write-only through config.json. This adds PUT /api/model-settings, the matching 'ocx models set' verb, and an Edit control on the Models page that pre-fills from what the model actually carries. The write goes through the provider-level per-model maps the runtime already reads (modelContextWindows, modelCapabilities..inputModalities, modelReasoningEfforts, modelDefaultReasoningEfforts), so an edited row keeps its discovery provenance instead of being replaced by a custom model. Every field is optional and null clears the declaration back to the registry/catalog answer; an emptied map is removed rather than left as {}. A no-op request answers changed: false, which is what lets the editor say there was no override to restore. Display name is deliberately not part of this route: PUT /api/providers/{provider}/model-display-names already owns that key, and a second control writing it would be two statements that can drift. --- .../content/docs/reference/management-api.md | 24 ++ gui/src/components/ModelSettingsDialog.tsx | 395 ++++++++++++++++++ gui/src/i18n/de.ts | 12 + gui/src/i18n/en.ts | 12 + gui/src/i18n/fr.ts | 12 + gui/src/i18n/ja.ts | 12 + gui/src/i18n/ko.ts | 12 + gui/src/i18n/ru.ts | 12 + gui/src/i18n/tr.ts | 12 + gui/src/i18n/vi.ts | 12 + gui/src/i18n/zh-TW.ts | 12 + gui/src/i18n/zh.ts | 12 + gui/src/pages/Models.tsx | 8 + gui/src/pages/models-shared.ts | 16 +- gui/tests/model-settings-dialog.test.tsx | 212 ++++++++++ scripts/test-layout/layout.json | 4 +- .../ocx/references/01_management_surface.md | 26 +- src/cli/capabilities.ts | 19 + src/cli/models-runtime-subcommands.ts | 1 + src/cli/models-runtime.ts | 81 ++++ src/server/management/model-routes.ts | 191 ++++++++- src/server/management/model-rows.ts | 194 ++++++++- src/server/management/route-registry.ts | 1 + structure/gui-and-management-api.md | 2 +- tests/cli/cli-models-set.test.ts | 136 ++++++ .../codex-convergence-contract.test.ts | 31 +- tests/fixtures/test-layout-expected.json | 4 +- .../model-settings-management-api.test.ts | 364 ++++++++++++++++ 28 files changed, 1812 insertions(+), 17 deletions(-) create mode 100644 gui/src/components/ModelSettingsDialog.tsx create mode 100644 gui/tests/model-settings-dialog.test.tsx create mode 100644 tests/cli/cli-models-set.test.ts create mode 100644 tests/server/model-settings-management-api.test.ts diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index f10c36ba19b..d5866889c07 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -485,6 +485,30 @@ can still fail when a recorded session has no surviving rollout file. | `PUT, DELETE /api/custom-models/{id}` | Edit or delete one custom model | 400 invalid id/fields; 404 not found; 409 duplicate model | | `GET, PUT /api/selected-models` | Read provider allowlists and availability, or replace one allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Read preset summaries or choose preset/all/custom mode | 400 invalid mode or unsupported preset; 404 unknown provider; PUT 409 `initial_model_selection_pending` | +| `PUT /api/model-settings` | Edit one routed model's capability axes in place | 400 missing provider/modelId, unknown or native/combo provider, malformed field, or an invalid default effort for the resulting ladder | + +`PUT /api/model-settings` takes `{ provider, modelId }` plus any of `contextWindow`, +`inputModalities`, `reasoningEfforts`, and `defaultReasoningEffort`. It writes the +provider-level per-model maps the runtime already reads (`modelContextWindows`, +`modelCapabilities..inputModalities`, `modelReasoningEfforts`, +`modelDefaultReasoningEfforts`), so an edited row keeps its discovery provenance instead of +being replaced by a custom model. `inputModalities` accepts `text`, `image`, and `audio`; +`reasoningEfforts` must be a subset of the efforts the runtime knows. + +Every field is optional, and `null` **clears** the declaration rather than writing a default, +which hands the fact back to the registry, the catalog, and the provider. An empty +`inputModalities` array also clears, while an empty `reasoningEfforts` array is stored as an +explicit "this model has no reasoning rungs" override — it does not clear. +An emptied per-model map is removed instead of left as `{}`. A request that changes nothing +answers `changed: false` with the stored state, which is how an editor distinguishes "no +override to restore" from "restored one". A request that does change something persists the +config, drops that provider's cached `/models` result, and reports `catalogRefresh` — the gather +bakes resolved hints into the rows it caches, and a cache hit may only lower a configured window, +so a raised or cleared override would otherwise read back the previous answer for the whole TTL. +Only routed providers are addressable: `openai` is the +native passthrough lane and `combo` is a synthetic row, so neither is a provider whose per-model +maps these facts come from. Display name is not part of this route; use +`PUT /api/providers/{provider}/model-display-names`. A manual model replaces the Models dashboard row with the same provider and model ID. For OpenAI, the manual row keeps `openai/` and supports the same visibility controls diff --git a/gui/src/components/ModelSettingsDialog.tsx b/gui/src/components/ModelSettingsDialog.tsx new file mode 100644 index 00000000000..46e80007f80 --- /dev/null +++ b/gui/src/components/ModelSettingsDialog.tsx @@ -0,0 +1,395 @@ +import { useEffect, useId, useRef, useState } from "react"; +import { confirmAction } from "../action-dialogs"; +import { createBoundedFetch, type BoundedFetch } from "../bounded-fetch"; +import { readJsonOrThrow } from "../fetch-json"; +import { useT, type TKey } from "../i18n/shared"; +import { Notice, Select } from "../ui"; +import { CUSTOM_OPTION, REASONING_EFFORT_LEVELS, type ModelRow } from "../pages/models-shared"; + +/** + * The per-model overrides on ONE routed row, in the shape the row already reports them. + * + * Three axes, and no fourth. Display name is absent on purpose: the row has its own editor for + * it, with its own provenance display, and a second control writing the same key is two + * statements that can drift apart. This dialog owns what nothing else edits — the context + * window, the modality declaration, and the reasoning ladder — so "restore" means "clear the + * capability overrides", which is a promise it can keep. + * + * Every field is optional and clearing one is a distinct intent from writing a value: an empty + * window sends `null`, which hands the fact back to the registry, the catalog and the provider + * default instead of pinning today's answer as an override that outlives its source. + */ + +/** The modalities the API accepts. `video` is absent: the wire carries text and image parts. */ +const MODALITIES = ["text", "image", "audio"] as const; + +const CONTEXT_PRESETS = ["100000", "128000", "200000", "256000", "352000", "500000", "1000000"]; + +const REQUEST_TIMEOUT_MS = 60_000; + +type Phase = "ready" | "saving" | "restoring" | "unknown"; + +function isRecord(value: unknown): value is Record { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +/** What the row reported when the dialog opened: the form's start, and the save diff's baseline. */ +interface SettingsBaseline { + contextWindow: string; + modalities: string[]; + reasoning: boolean; + ladder: string[]; + defaultEffort: string; +} + +function baselineOf(row: ModelRow): SettingsBaseline { + const reasoning = row.reasoningOverridden === true; + return { + contextWindow: row.contextWindow ? String(row.contextWindow) : "", + // The DECLARATION, never the row's own `inputModalities`. That one is the catalog value: + // pre-filling from it made an undeclared model look declared, and because an untouched field + // is not submitted, every save came back as "nothing changed" and the row never moved. + modalities: Array.isArray(row.inputModalitiesDeclared) ? [...row.inputModalitiesDeclared] : [], + reasoning, + // The ladder only counts as the operator's when the config really overrides it: OpenCodex + // writes the registry ladder into the config itself, so key presence would tick this box on + // every model that merely inherits its levels. + ladder: reasoning && Array.isArray(row.reasoningEfforts) ? [...row.reasoningEfforts] : [], + defaultEffort: reasoning ? (row.defaultReasoningEffort ?? "") : "", + }; +} + +function sortedKey(values: readonly string[]): string { + return [...values].sort().join(","); +} + +/** undefined when the draft is not a usable window, null when the operator cleared it. */ +function parseContextWindow(raw: string): number | null | undefined { + const trimmed = raw.trim(); + if (!trimmed) return null; + const value = Number(trimmed.replace(/[_,\s]/g, "")); + return Number.isSafeInteger(value) && value > 0 ? value : undefined; +} + +interface ModelSettingsDialogProps { + row: ModelRow; + apiBase: string; + /** Reload the catalog. Resolves false when the reload failed, so the dialog can say so. */ + onRefresh: (signal: AbortSignal) => Promise; + onFeedback: (ok: boolean, message: string) => void; + onClose: () => void; +} + +export default function ModelSettingsDialog({ row, apiBase, onRefresh, onFeedback, onClose }: ModelSettingsDialogProps) { + const t = useT(); + const id = useId(); + const dialogRef = useRef(null); + const submitRef = useRef(null); + const requestRef = useRef(null); + // State rather than a ref: the form's start values are read during render to seed the fields, + // and a ref read in render is exactly what the React Compiler lint refuses. + const [baseline] = useState(() => baselineOf(row)); + const [phase, setPhase] = useState("ready"); + const [contextDraft, setContextDraft] = useState(() => baseline.contextWindow); + const [customContext, setCustomContext] = useState(() => ( + baseline.contextWindow !== "" && !CONTEXT_PRESETS.includes(baseline.contextWindow) + )); + const [modalities, setModalities] = useState(() => [...baseline.modalities]); + const [reasoning, setReasoning] = useState(() => baseline.reasoning); + const [ladder, setLadder] = useState(() => [...baseline.ladder]); + const [defaultEffort, setDefaultEffort] = useState(() => baseline.defaultEffort); + const [errorKey, setErrorKey] = useState(null); + const busy = phase !== "ready"; + + useEffect(() => { + const dialog = dialogRef.current; + if (dialog && !dialog.open) dialog.showModal(); + return () => { if (dialog?.open) dialog.close(); }; + }, []); + + useEffect(() => { + if (phase === "unknown") submitRef.current?.focus(); + }, [phase]); + + const toggleReasoning = (next: boolean) => { + setReasoning(next); + if (!next) return; + // First enable seeds from what the model advertises rather than the full shared ladder: a + // provider that accepts only a subset would otherwise be handed levels its API rejects. + if (ladder.length > 0) return; + const advertised = Array.isArray(row.reasoningEfforts) ? row.reasoningEfforts : undefined; + setLadder(advertised && advertised.length > 0 ? [...advertised] : [...REASONING_EFFORT_LEVELS]); + }; + + const toggleLevel = (level: string, on: boolean) => { + setLadder(prev => (on ? [...prev, level] : prev.filter(value => value !== level))); + // The default has to stay inside the ladder, so dropping its level drops it too. + if (!on && defaultEffort === level) setDefaultEffort(""); + }; + + const requestClose = () => { + if (!busy) onClose(); + }; + + const send = async (patch: Record, restoring: boolean) => { + const bounded = createBoundedFetch(REQUEST_TIMEOUT_MS); + requestRef.current = bounded; + setPhase(restoring ? "restoring" : "saving"); + setErrorKey(null); + let confirmed = false; + try { + const response = await fetch(apiBase + "/api/model-settings", { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ provider: row.provider, modelId: row.id, ...patch }), + signal: bounded.signal, + }); + const result = await readJsonOrThrow(response); + bounded.signal.throwIfAborted(); + // Validate the receipt against the request: a 200 for another row would otherwise be + // reported as this row's save, and the reload would appear to disagree with the toast. + if (!isRecord(result) || result.ok !== true || result.provider !== row.provider || result.modelId !== row.id) { + throw new Error("invalid model-settings receipt"); + } + if (requestRef.current !== bounded) return; + confirmed = true; + const changed = result.changed === false; + if (!await onRefresh(bounded.signal)) throw new Error("catalog refresh failed"); + bounded.signal.throwIfAborted(); + if (requestRef.current !== bounded) return; + if (restoring) { + onFeedback(true, t(changed ? "models.settingsNothingToRestore" : "models.settingsRestored", { model: row.namespaced })); + } else { + onFeedback(true, t("models.settingsSaved", { model: row.namespaced })); + } + onClose(); + } catch { + if (requestRef.current !== bounded) return; + setPhase("unknown"); + setErrorKey(confirmed ? "models.settingsRefreshFailed" : "models.settingsSaveFailed"); + } finally { + bounded.clear(); + if (requestRef.current === bounded) requestRef.current = null; + } + }; + + const submit = () => { + if (busy) return; + const parsedWindow = parseContextWindow(contextDraft); + if (parsedWindow === undefined) { setErrorKey("models.contextInvalid"); return; } + const base = baseline; + const patch: Record = {}; + if (contextDraft.trim() !== base.contextWindow) patch.contextWindow = parsedWindow; + if (sortedKey(modalities) !== sortedKey(base.modalities)) { + patch.inputModalities = modalities.length > 0 ? [...modalities] : null; + } + const ladderKey = reasoning ? sortedKey(ladder) : ""; + if (reasoning !== base.reasoning || ladderKey !== sortedKey(base.ladder)) { + patch.reasoningEfforts = reasoning ? [...ladder] : null; + } + const nextDefault = reasoning && ladder.includes(defaultEffort) ? defaultEffort : ""; + if (nextDefault !== base.defaultEffort) patch.defaultReasoningEffort = nextDefault || null; + // Only what the operator actually touched is submitted. Writing an untouched field would + // convert a fact the row merely inherits into a stored override, which is the failure this + // whole surface exists to avoid. + if (Object.keys(patch).length === 0) { onClose(); return; } + void send(patch, false); + }; + + const restore = async () => { + if (busy) return; + const confirmed = await confirmAction({ + message: t("models.settingsRestoreConfirm", { model: row.namespaced }), + confirmLabel: t("models.settingsRestore"), + tone: "danger", + }); + if (!confirmed) return; + // Every axis at once, and it writes immediately rather than resetting the form: a form-level + // reset would still need a save, and that save would re-pin the values it had just restored. + void send({ contextWindow: null, inputModalities: null, reasoningEfforts: null, defaultReasoningEffort: null }, true); + }; + + const inheritedModalities = Array.isArray(row.inputModalities) ? row.inputModalities.join(", ") : ""; + + return ( + { event.preventDefault(); requestClose(); }} + > + + + + {errorKey && {t(errorKey)}} + +
+
+ {t("models.customFieldModelId")} + {row.namespaced} +
+ + + +
+ {t("models.customFieldModalities")} +
+ {MODALITIES.map(modality => ( + + ))} +
+ {modalities.length === 0 && ( + // No box ticked is a real state — undeclared — not an empty model. Say what is + // being followed instead, or the blank row reads as "this model accepts nothing". + + {t("models.settingsModalitiesInherit", { + modalities: inheritedModalities || t("models.settingsModalitiesUnknown"), + })} + + )} +
+ +
+ {t("models.customFieldReasoning")} +
+ +
+ {reasoning && ( +
+ {REASONING_EFFORT_LEVELS.map(level => ( + + ))} +
+ )} +
+ + {reasoning && ( + diff --git a/gui/tests/model-settings-dialog.test.tsx b/gui/tests/model-settings-dialog.test.tsx index 6f5a4d9ee4b..93ed111057e 100644 --- a/gui/tests/model-settings-dialog.test.tsx +++ b/gui/tests/model-settings-dialog.test.tsx @@ -182,6 +182,29 @@ describe("Models per-model settings dialog", () => { expect(dialog()!.textContent).toContain("currently 200000"); }); + test("the first modality tick on an undeclared model starts from what it follows", async () => { + delete rows[0]!.inputModalitiesDeclared; + rows[0]!.inputModalities = ["text"]; + await mount(); + await open(); + await act(async () => checkboxes()[1]!.click()); + expect(checkboxes().slice(0, 3).map(box => box.checked)).toEqual([true, true, false]); + await click("Apply"); + expect(mutations).toEqual([{ provider: "vendor-demo", modelId: "chat-large", inputModalities: ["text", "image"] }]); + }); + + test("the context menu opens inside the modal so its options can be clicked", async () => { + await mount(); + await open(); + const combo = dialog()!.querySelector('[role="combobox"]')!; + await act(async () => combo.click()); + // A menu portaled to sits under the modal's top layer and the backdrop takes its clicks. + expect(dialog()!.querySelector('[role="listbox"]')).not.toBeNull(); + const custom = [...dialog()!.querySelectorAll('[role="option"]')].find(node => node.textContent?.startsWith("Custom")); + await act(async () => custom!.click()); + expect(dialog()!.querySelector('input[aria-label="Context window"]')).not.toBeNull(); + }); + test("the context draft comes from the stored declaration", async () => { rows[0]!.contextWindowDeclared = 128000; await mount(); From 6b83c1360206d4229f03793edb1b8fb5448fb49f Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 01:10:00 +0900 Subject: [PATCH 10/14] docs: record GUI model settings build and browser evidence --- .../gui-ux/011_model_settings_evidence.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 devlog/_plan/260927_release_train_4/gui-ux/011_model_settings_evidence.md diff --git a/devlog/_plan/260927_release_train_4/gui-ux/011_model_settings_evidence.md b/devlog/_plan/260927_release_train_4/gui-ux/011_model_settings_evidence.md new file mode 100644 index 00000000000..d19956129c0 --- /dev/null +++ b/devlog/_plan/260927_release_train_4/gui-ux/011_model_settings_evidence.md @@ -0,0 +1,42 @@ +# Phase 010 — build and browser evidence + +The #6058 carry is on `codex/t4-gui-ux-model-settings`. The contributor commit is `bb779ff13d` with its original author, followed by lane repairs `0d2d7ff0f7`, `1a61f6773c` and `03928972b5`, each with a `Co-authored-by` trailer. Every audit item in `010_model_settings.md` is implemented. Browser QA also found and fixed a defect that the unit tests missed. + +## What changed beyond the plan + +- **Menus behind the modal (found in browser QA).** The Context window and Default level menus portaled to ``, under the modal's top layer. The dialog's backdrop button took their clicks, so the context window could not be changed by pointer (`08-pre-fix-menu-behind-modal.png`). Both menus now render inside the dialog (`portal={false}`), matching `gui/src/pages/dashboard-dialogs.tsx:53`. A regression test fails without the fix and passes with it. +- **Modality seeding.** On an undeclared row every modality box starts unticked, meaning "follow upstream". Ticking `image` to add image support used to declare the model image-only. The first tick now starts from the modalities the row already follows. +- **Codex catalog versus dashboard list.** `catalogRefresh` describes the Codex app's catalog, while the dashboard reads the saved config directly. A non-retryable skip (no managed catalog) is therefore a clean save. A failed or retryable refresh is a warning after the save and does not trap the dialog. Only a failed dashboard list reload enters the read-only stale state, which uses the warning tone because the save succeeded. + +## UX state results (isolated proxy, direct `startServer`, port 18761) + +Each state below was clicked through in a real Chrome browser, driven by Playwright with the system Chrome. The config file in the isolated `OPENCODEX_HOME` was read back after each write. + +| State | Observation | +|---|---| +| Row entry | `Edit` appears only on routed rows, after Name and Price. Keyboard Enter opens the dialog and focus lands inside it. | +| Save one axis | Ticking `image` sent only `inputModalities`. Config read back as `["text","image"]`. Focus returned to the row's Edit button. | +| Reopen | The declared boxes are ticked. A saved context window of `128000` reopens as 128k. | +| Invalid input | Custom `0.5` shows "Context windows must be positive whole numbers" and makes no write (config unchanged). | +| Restore | The confirm dialog appears; after confirming, the declaration is removed from config. | +| Unknown outcome (PUT aborted) | Apply and Restore are disabled, while Cancel, Close and Escape work. Focus moves to Reload. Config is unchanged. | +| Saved, list reload failed | The warning notice appears with Apply and Restore disabled. Reload then closes the dialog with "reopen" feedback. | +| Keyboard | Tab cycles through the dialog's controls. The only stop outside is ``, which is Chrome's native modal browser UI stop; the page behind stays inert. | +| 400px ru, de and en; 1360px ja | Page `scrollWidth` equals the viewport. The form's `scrollWidth` equals its `clientWidth` (378). The long title and reasoning labels wrap, and the actions stack full width. | + +The real-home sentinel files (`~/.grok/config.toml`, `~/.codex/config.toml`, Claude settings, Claude Desktop config, `~/.zshrc`, `~/.opencodex/config.json`) hashed identically before and after QA. + +Screenshots are on `pr-assets` at [`f3f98330`](https://github.com/lidge-jun/opencodex/blob/f3f98330421483b49050d65e2e09d6ead4467202/260928-t4-gui-model-settings). + +## Commands + +| Command | Where | Result | +|---|---|---| +| `bun test tests/server/model-settings-management-api.test.ts tests/cli/cli-models-set.test.ts tests/codex-integration/codex-convergence-contract.test.ts` | lane worktree | 48 pass, 0 fail | +| `cd gui && bun test tests/model-settings-dialog.test.tsx tests/locale-parity.test.ts` | lane worktree | 22 pass, 0 fail (17 dialog + 5 parity at final head) | +| `bun run typecheck`, `bun run lint:gui`, `cd gui && bun run lint:i18n`, `bun run build:gui` | lane worktree | all exit 0 | +| `bun test tests/ci-workflows/file-size-ratchet.test.ts` | lane worktree | 9 pass (`Models.tsx` 2,788 of 2,792) | +| `bun run privacy:scan`, `bun run structure:check`, `bun run skill:surface:check` | lane worktree | pass | +| `bun run test:changed` | `/private/tmp/t4-gui-ux-verify` at `03928972b5` | 6,011 pass, 2 skip, 0 fail, 297 files | + +The full local suite was not run because seven lane worktrees share this machine; CI runs the full suite on the PR head. From 35f48e50568263ccbb9b76597ca5f95d61ae5ab0 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 01:15:23 +0900 Subject: [PATCH 11/14] fix(models): sort the settings diff key without mutating a copy --- gui/src/components/ModelSettingsDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/components/ModelSettingsDialog.tsx b/gui/src/components/ModelSettingsDialog.tsx index a3b61439427..4504d0c2a0a 100644 --- a/gui/src/components/ModelSettingsDialog.tsx +++ b/gui/src/components/ModelSettingsDialog.tsx @@ -60,7 +60,7 @@ function baselineOf(row: ModelRow): SettingsBaseline { } function sortedKey(values: readonly string[]): string { - return [...values].sort().join(","); + return values.toSorted().join(","); } /** undefined when the draft is not a usable window, null when the operator cleared it. */ From 80cdb0609c1a656205b4d54fe89b10f4666904b0 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 01:27:35 +0900 Subject: [PATCH 12/14] fix(models): clear legacy modality entries on restore and key catalog ladders by routed slug --- .../content/docs/reference/management-api.md | 4 ++- src/server/management/model-routes.ts | 17 ++++++++++- src/server/management/model-rows.ts | 12 ++++---- .../model-settings-management-api.test.ts | 29 +++++++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index f95c71167f5..3ad8cfc60de 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -502,7 +502,9 @@ Every field is optional, and `null` **clears** the declaration rather than writi which hands the fact back to the registry, the catalog, and the provider. An empty `inputModalities` array also clears, while an empty `reasoningEfforts` array is stored as an explicit "this model has no reasoning rungs" override — it does not clear. -An emptied per-model map is removed instead of left as `{}`. A request that changes nothing +Clearing `inputModalities` also removes the exact legacy `modelInputModalities` entry for that +model, so a restore cannot leave an older declaration in force; family keys shared with other +models stay. An emptied per-model map is removed instead of left as `{}`. A request that changes nothing answers `changed: false` with the stored state. The receipt also reports `saved` (whether this request published config), `hasOverrides` (whether any of the four axes remains stored), and `catalogRefresh` (`committed`, `skipped`, or `failed`). These fields distinguish a no-op with diff --git a/src/server/management/model-routes.ts b/src/server/management/model-routes.ts index c6bddf5d08b..1662815408d 100644 --- a/src/server/management/model-routes.ts +++ b/src/server/management/model-routes.ts @@ -896,10 +896,25 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise = Object.assign(Object.create(null), legacy); + delete next[modelId]; + changed = true; + if (Object.keys(next).length === 0) delete providerConfig.modelInputModalities; + else providerConfig.modelInputModalities = next; + } } const hasOverrides = [providerConfig.modelContextWindows, providerConfig.modelReasoningEfforts, - providerConfig.modelDefaultReasoningEfforts].some(map => map !== undefined && Object.hasOwn(map, modelId)) + providerConfig.modelDefaultReasoningEfforts, providerConfig.modelInputModalities] + .some(map => map !== undefined && Object.hasOwn(map, modelId)) || providerConfig.modelCapabilities?.[modelId]?.inputModalities !== undefined; const state = { ok: true as const, diff --git a/src/server/management/model-rows.ts b/src/server/management/model-rows.ts index 38d9a828db8..28a24d4690a 100644 --- a/src/server/management/model-rows.ts +++ b/src/server/management/model-rows.ts @@ -133,8 +133,10 @@ export function declaredModelInputModalities( */ export type CatalogLadderLookup = ReadonlyMap; -function catalogLadder(ladders: CatalogLadderLookup | undefined, modelId: string): string[] | undefined { - return ladders ? ladders.get(modelId) : catalogModelEfforts([modelId]).get(modelId); +/** Catalog entries for routed rows carry the Codex-facing `provider/model` slug, never the bare id. */ +function catalogLadder(ladders: CatalogLadderLookup | undefined, providerName: string, modelId: string): string[] | undefined { + const slug = routedSlug(providerName, modelId); + return ladders ? ladders.get(slug) : catalogModelEfforts([slug]).get(slug); } export function effectiveModelReasoningEfforts(config: OcxConfig, providerName: string, modelId: string, declaredEfforts?: string[], catalogLadders?: CatalogLadderLookup): string[] | undefined { @@ -159,7 +161,7 @@ export function effectiveModelReasoningEfforts(config: OcxConfig, providerName: const native = nativeReasoningEfforts(modelId); if (native.length > 0) return native; } - const hits = catalogLadder(catalogLadders, modelId); + const hits = catalogLadder(catalogLadders, providerName, modelId); if (hits && hits.length > 0) return hits; return undefined; } @@ -366,8 +368,8 @@ export async function listManagementModelRows( // One catalog read for the whole roster. Every `catalogModelEfforts` call parses the catalog // file and scans it once per requested slug, so resolving these per row (and twice per row, as // the guard and the value) re-read the same file thousands of times on a large roster. - const routedIds = publicModels.filter(m => m.provider !== "combo").map(m => m.id); - const catalogLadders = catalogModelEfforts([...new Set(routedIds)]); + const routedSlugs = publicModels.filter(m => m.provider !== "combo").map(m => routedSlug(m.provider, m.id)); + const catalogLadders = catalogModelEfforts([...new Set(routedSlugs)]); const dedupedRouted = publicModels.map((m): ManagementModelRow | null => { // Codex-facing slug (one "/", slug-codec); disabledModels compares tolerate both forms. const namespaced = catalogModelSlug(m); diff --git a/tests/server/model-settings-management-api.test.ts b/tests/server/model-settings-management-api.test.ts index 6013de24f20..fa2b480494a 100644 --- a/tests/server/model-settings-management-api.test.ts +++ b/tests/server/model-settings-management-api.test.ts @@ -10,6 +10,11 @@ import type { CatalogModel } from "../../src/codex/catalog"; import type { OcxConfig } from "../../src/types"; import { removeTreeWithRetry } from "../helpers/remove-tree"; import { ConfigWritePublishedError } from "../../src/config/persist-unlocked"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname } from "node:path"; +import { readCodexCatalogPath } from "../../src/codex/catalog/parsing"; +import { effectiveModelReasoningEfforts } from "../../src/server/management/model-rows"; +import { routedSlug } from "../../src/providers/slug-codec"; /** * `PUT /api/model-settings` writes per-model overrides onto a routed row. @@ -95,6 +100,30 @@ function harness(config = fixture(), converge?: () => Promise, save?: (sa } describe("per-model settings API", () => { + test("restoring modalities also clears the exact legacy declaration", async () => { + const config = fixture(); + config.providers[PROVIDER]!.modelInputModalities = { [MODEL]: ["text"], "vendor/other": ["text", "image"] }; + const h = harness(config); + const response = await h.call({ provider: PROVIDER, modelId: MODEL, inputModalities: null }); + expect(response.status).toBe(200); + expect(await response.json()).toMatchObject({ changed: true, saved: true, hasOverrides: false, inputModalities: null }); + expect(h.persisted).toHaveLength(1); + // Only the exact entry goes; another model's legacy declaration is not this request's to clear. + expect(h.persisted[0]!.providers[PROVIDER]!.modelInputModalities).toEqual({ "vendor/other": ["text", "image"] }); + const noop = await h.call({ provider: PROVIDER, modelId: MODEL, inputModalities: null }); + expect(await noop.json()).toMatchObject({ changed: false, hasOverrides: false }); + }); + + test("the catalog ladder fallback is looked up by the routed slug", () => { + const config = fixture(); + const catalogPath = readCodexCatalogPath(); + mkdirSync(dirname(catalogPath), { recursive: true }); + writeFileSync(catalogPath, JSON.stringify({ models: [ + { slug: routedSlug(PROVIDER, MODEL), supported_reasoning_levels: [{ effort: "low" }, { effort: "high" }] }, + ] })); + expect(effectiveModelReasoningEfforts(config, PROVIDER, MODEL)).toEqual(["low", "high"]); + }); + test("each axis round-trips, persists once, and converges the catalog once", async () => { const h = harness(); const response = await h.call({ From c219c99aff1e189e94eb95571fb940207cf6ed0a Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 03:24:11 +0900 Subject: [PATCH 13/14] fix(models): keep rejected settings editable and validate CLI modalities A 4xx from PUT /api/model-settings is answered before any write, so the dialog stays editable with translated copy instead of entering the unknown-outcome state. The CLI rejects blank or unknown --modalities members rather than normalizing them into an implicit clear. The Codex catalog warning names the localized Sync now button, French copy is completed, and the API reference lists the 500 save failure. Co-authored-by: ardeyouxipianyi <189708448+ardeyouxipianyi@users.noreply.github.com> --- .../gui-ux/010_model_settings.md | 2 +- .../src/content/docs/reference/management-api.md | 2 +- gui/src/components/ModelSettingsDialog.tsx | 12 ++++++++++++ gui/src/i18n/de.ts | 3 ++- gui/src/i18n/en.ts | 3 ++- gui/src/i18n/fr.ts | 5 +++-- gui/src/i18n/ja.ts | 3 ++- gui/src/i18n/ko.ts | 3 ++- gui/src/i18n/ru.ts | 3 ++- gui/src/i18n/tr.ts | 3 ++- gui/src/i18n/vi.ts | 3 ++- gui/src/i18n/zh-TW.ts | 3 ++- gui/src/i18n/zh.ts | 3 ++- gui/tests/model-settings-dialog.test.tsx | 13 +++++++++++++ src/cli/models-runtime.ts | 14 +++++++++++++- tests/cli/cli-models-set.test.ts | 6 ++++++ 16 files changed, 67 insertions(+), 14 deletions(-) diff --git a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md index adf10aa3c84..42608a5d148 100644 --- a/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md +++ b/devlog/_plan/260927_release_train_4/gui-ux/010_model_settings.md @@ -4,7 +4,7 @@ ## Diff-level map -- **NEW** `gui/src/components/ModelSettingsDialog.tsx`: use the existing narrow dialog shell; seed every draft from stored declarations, not effective catalog values; show the effective context window as an inherited hint when no stored declaration exists. Send only changed axes and distinguish confirmed save plus failed refresh from unknown save. The PR currently treats the `unknown` error phase as busy, disabling Cancel and trapping the dialog; busy must mean an in-flight request only. On unknown outcome, focus an enabled Close control, disable Save/Restore, and offer a read-only Retry that reloads the row before another mutation. Return focus to the row trigger after close. +- **NEW** `gui/src/components/ModelSettingsDialog.tsx`: use the existing narrow dialog shell; seed every draft from stored declarations, not effective catalog values; show the effective context window as an inherited hint when no stored declaration exists. Send only changed axes and distinguish confirmed save plus failed refresh from unknown save. The PR currently treats the `unknown` error phase as busy, disabling Cancel and trapping the dialog; busy must mean an in-flight request only. On unknown outcome, disable Save/Restore, keep Close/Cancel/Escape enabled, and move focus to a read-only Reload that refreshes the list before another mutation. (Implemented focus target: Reload; see `011_model_settings_evidence.md`.) Return focus to the row trigger after close. - **MODIFY** `gui/src/pages/Models.tsx` and `gui/src/pages/models-shared.ts`: show one Edit control only for routed, non-custom, non-combo rows; carry exact stored `contextWindowDeclared` separately from effective `contextWindow`; load the saved row after a successful mutation; stay under the `Models.tsx` 2,792-line cap by moving logic into sibling files if needed. - **MODIFY** all ten `gui/src/i18n/{en,de,fr,ja,ko,ru,tr,vi,zh-TW,zh}.ts`: exact same keys, translated inherited-context and unknown/confirmed-reload semantics, long-string layout proof. `cd gui && bun test tests/locale-parity.test.ts` checks key sets; `cd gui && bun run lint:i18n` checks visible JSX copy; neither proves translation meaning, so inspect long translations in-browser. - **MODIFY** `src/server/management/model-routes.ts`, `model-rows.ts`, `route-registry.ts`: validate provider, exact model ID, allowed request fields and all axes at ingress; reject a positive fractional context value that floors to zero or exceeds safe integer range. Compute changed/no-op against copied touched maps, then wrap mutation/persistence in existing `commitProviderPatch` so an unpublished failed save restores live config graph and a published write is not falsely rolled back. On a published write with a post-publication error, return a sanitized saved receipt; do not claim catalog convergence when it is skipped or failed. Clear the provider discovery cache only after publication. Route projection distinguishes stored context/modality/reasoning declarations from effective values. Unreleased input-validation findings are kept in ignored scratch until fixed. diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 3ad8cfc60de..ffd0bff3855 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -485,7 +485,7 @@ can still fail when a recorded session has no surviving rollout file. | `PUT, DELETE /api/custom-models/{id}` | Edit or delete one custom model | 400 invalid id/fields; 404 not found; 409 duplicate model | | `GET, PUT /api/selected-models` | Read provider allowlists and availability, or replace one allowlist | 400 missing provider/body; 404 unknown provider; PUT 409 `initial_model_selection_pending` | | `GET, PUT /api/model-presets` | Read preset summaries or choose preset/all/custom mode | 400 invalid mode or unsupported preset; 404 unknown provider; PUT 409 `initial_model_selection_pending` | -| `PUT /api/model-settings` | Edit one routed model's capability axes in place | 400 missing provider/modelId, unknown or native/combo provider, malformed field, or an invalid default effort for the resulting ladder | +| `PUT /api/model-settings` | Edit one routed model's capability axes in place | 400 missing provider/modelId, unknown or native/combo provider, malformed field, unknown field, non-exact model id, or an invalid default effort for the resulting ladder; 500 when the config could not be saved (live config left unchanged) | `PUT /api/model-settings` takes `{ provider, modelId }` plus any of `contextWindow`, `inputModalities`, `reasoningEfforts`, and `defaultReasoningEffort`. It writes the diff --git a/gui/src/components/ModelSettingsDialog.tsx b/gui/src/components/ModelSettingsDialog.tsx index 4504d0c2a0a..09a9bd6d26e 100644 --- a/gui/src/components/ModelSettingsDialog.tsx +++ b/gui/src/components/ModelSettingsDialog.tsx @@ -156,6 +156,7 @@ export default function ModelSettingsDialog({ row, apiBase, onRefresh, onFeedbac setPhase(restoring ? "restoring" : "saving"); setErrorKey(null); let confirmed = false; + let rejected = false; try { const response = await fetch(apiBase + "/api/model-settings", { method: "PUT", @@ -163,6 +164,12 @@ export default function ModelSettingsDialog({ row, apiBase, onRefresh, onFeedbac body: JSON.stringify({ provider: row.provider, modelId: row.id, ...patch }), signal: bounded.signal, }); + // A 4xx is answered before anything is written, so the outcome is known: nothing saved. + // The form stays editable, and the server's untranslated text is not shown. + if (response.status >= 400 && response.status < 500) { + rejected = true; + throw new Error("model settings rejected"); + } const result = await readJsonOrThrow(response); bounded.signal.throwIfAborted(); // Validate the receipt against the request: a 200 for another row would otherwise be @@ -198,6 +205,11 @@ export default function ModelSettingsDialog({ row, apiBase, onRefresh, onFeedbac onClose(); } catch { if (requestRef.current !== bounded) return; + if (rejected) { + setPhase("ready"); + setErrorKey("models.settingsRejected"); + return; + } setPhase(confirmed ? "saved-stale" : "unknown"); setErrorKey(confirmed ? "models.settingsRefreshFailed" : "models.settingsSaveFailed"); } finally { diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 69fbb322fc4..cedce555631 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -732,7 +732,8 @@ export const de: Record = { "models.settingsSaved": "Modelleinstellungen für {model} gespeichert", "models.settingsSaveFailed": "Wir konnten nicht bestätigen, ob die Einstellungen gespeichert wurden. Lade die Modellliste neu, bevor du es erneut versuchst.", "models.settingsRefreshFailed": "Gespeichert. Die Modellliste wurde nicht neu geladen, daher kann die Zeile veraltete Werte zeigen. Lade sie zur Prüfung neu.", - "models.settingsSavedCodexStale": "{model} gespeichert, aber der Codex-Modellkatalog wurde nicht aktualisiert. Führe Sync aus, damit Codex die Änderung übernimmt.", + "models.settingsSavedCodexStale": "{model} gespeichert, aber der Codex-Modellkatalog wurde nicht aktualisiert. Klicke im Dashboard auf „Jetzt synchronisieren“, damit Codex die Änderung übernimmt.", + "models.settingsRejected": "Der Server hat diese Einstellungen abgelehnt, daher wurde nichts gespeichert. Prüfe die Werte und versuche es erneut.", "models.settingsDefaultEffort": "Standardstufe", "models.settingsModalitiesInherit": "Nicht überschrieben — folgt derzeit der Upstream-Deklaration: {modalities}", "models.settingsModalitiesUnknown": "nicht deklariert", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 8588ce5b6da..d708ceacb4a 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -762,7 +762,8 @@ export const en = { "models.settingsSaved": "Model settings saved for {model}", "models.settingsSaveFailed": "We could not confirm whether the settings were saved. Reload the model list before trying again.", "models.settingsRefreshFailed": "Saved. The model list did not reload, so the row may show old values. Reload to check.", - "models.settingsSavedCodexStale": "Saved {model}, but the Codex model catalog did not refresh. Run Sync so Codex picks up the change.", + "models.settingsSavedCodexStale": "Saved {model}, but the Codex model catalog did not refresh. Click Sync now on the Dashboard so Codex picks up the change.", + "models.settingsRejected": "The server rejected these settings, so nothing was saved. Check the values and try again.", "models.settingsDefaultEffort": "Default level", "models.settingsModalitiesInherit": "Not overridden — currently following the upstream declaration: {modalities}", "models.settingsModalitiesUnknown": "not declared", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index df91b14a253..78577fe8128 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -747,7 +747,8 @@ export const fr: Record = { "models.settingsSaved": "Paramètres du modèle enregistrés pour {model}", "models.settingsSaveFailed": "Impossible de confirmer si les paramètres ont été enregistrés. Rechargez la liste des modèles avant de réessayer.", "models.settingsRefreshFailed": "Enregistré. La liste des modèles ne s’est pas rechargée et la ligne peut afficher d’anciennes valeurs. Rechargez-la pour vérifier.", - "models.settingsSavedCodexStale": "{model} enregistré, mais le catalogue de modèles Codex ne s’est pas actualisé. Lancez Sync pour que Codex prenne en compte la modification.", + "models.settingsSavedCodexStale": "{model} enregistré, mais le catalogue de modèles Codex ne s’est pas actualisé. Cliquez sur « Synchroniser maintenant » dans le tableau de bord pour que Codex prenne en compte la modification.", + "models.settingsRejected": "Le serveur a refusé ces réglages ; rien n’a été enregistré. Vérifiez les valeurs et réessayez.", "models.settingsDefaultEffort": "Niveau par défaut", "models.settingsModalitiesInherit": "Non remplacé — suit actuellement la déclaration amont : {modalities}", "models.settingsModalitiesUnknown": "non déclaré", @@ -755,7 +756,7 @@ export const fr: Record = { "models.settingsRestoreHint": "Supprimer toutes les substitutions de ce modèle et revenir aux valeurs calculées.", "models.settingsRestoreConfirm": "Réinitialiser {model} aux valeurs calculées ? Cela supprime ses substitutions de fenêtre de contexte, de modalité et de raisonnement.", "models.settingsRestored": "{model} réinitialisé aux valeurs calculées", - "models.settingsNothingToRestore": "{model} n'a aucune substitution — déjà les valeurs calculées", + "models.settingsNothingToRestore": "{model} n’a aucune substitution — les valeurs calculées sont déjà utilisées", "models.customAdded": "Modèle personnalisé ajouté", "models.customUpdated": "Modèle personnalisé mis à jour", "models.customDeleted": "Modèle personnalisé supprimé", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 821155b0ab9..a23b7addfae 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -3027,7 +3027,8 @@ export const ja: Record = { "models.settingsSaved": "{model} のモデル設定を保存しました", "models.settingsSaveFailed": "設定が保存されたか確認できませんでした。再試行する前にモデル一覧を再読み込みしてください。", "models.settingsRefreshFailed": "保存しましたが、モデル一覧を再読み込みできませんでした。行に古い値が表示される可能性があります。再読み込みして確認してください。", - "models.settingsSavedCodexStale": "{model} を保存しましたが、Codex のモデルカタログは更新されませんでした。Codex に反映するには Sync を実行してください。", + "models.settingsSavedCodexStale": "{model} を保存しましたが、Codex のモデルカタログは更新されませんでした。Codex に反映するには、ダッシュボードの「今すぐ同期」を押してください。", + "models.settingsRejected": "サーバーがこの設定を拒否したため、何も保存されていません。値を確認してもう一度お試しください。", "models.settingsDefaultEffort": "既定レベル", "models.settingsModalitiesInherit": "未上書き — 現在は上流の宣言に従います: {modalities}", "models.settingsModalitiesUnknown": "未宣言", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index fdebc31866f..d7df5375960 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -743,7 +743,8 @@ export const ko: Record = { "models.settingsSaved": "{model} 모델 설정을 저장했습니다", "models.settingsSaveFailed": "설정이 저장되었는지 확인할 수 없습니다. 다시 시도하기 전에 모델 목록을 다시 불러오세요.", "models.settingsRefreshFailed": "저장했습니다. 모델 목록을 다시 불러오지 못해 행에 이전 값이 보일 수 있습니다. 다시 불러와 확인하세요.", - "models.settingsSavedCodexStale": "{model}을 저장했지만 Codex 모델 카탈로그는 갱신되지 않았습니다. Codex에 반영하려면 동기화를 실행하세요.", + "models.settingsSavedCodexStale": "{model}을 저장했지만 Codex 모델 카탈로그는 갱신되지 않았습니다. Codex에 반영하려면 대시보드에서 '지금 동기화'를 누르세요.", + "models.settingsRejected": "서버가 이 설정을 거부해 아무것도 저장되지 않았습니다. 값을 확인한 뒤 다시 시도하세요.", "models.settingsDefaultEffort": "기본 수준", "models.settingsModalitiesInherit": "재정의되지 않음 — 현재 업스트림 선언을 따릅니다: {modalities}", "models.settingsModalitiesUnknown": "선언되지 않음", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index ea022cbff37..6bd6b67ae65 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -745,7 +745,8 @@ export const ru: Record = { "models.settingsSaved": "Настройки модели {model} сохранены", "models.settingsSaveFailed": "Не удалось подтвердить сохранение настроек. Перед повторной попыткой обновите список моделей.", "models.settingsRefreshFailed": "Сохранено. Список моделей не обновился, поэтому строка может показывать старые значения. Обновите его для проверки.", - "models.settingsSavedCodexStale": "{model} сохранена, но каталог моделей Codex не обновился. Запустите Sync, чтобы Codex получил изменение.", + "models.settingsSavedCodexStale": "{model} сохранена, но каталог моделей Codex не обновился. Нажмите «Синхронизировать» на панели, чтобы Codex получил изменение.", + "models.settingsRejected": "Сервер отклонил эти настройки, поэтому ничего не сохранено. Проверьте значения и повторите попытку.", "models.settingsDefaultEffort": "Уровень по умолчанию", "models.settingsModalitiesInherit": "Не переопределено — сейчас используется объявление провайдера: {modalities}", "models.settingsModalitiesUnknown": "не объявлено", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 3a7cf9e4878..0faaf29a308 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -748,7 +748,8 @@ export const tr: Record = { "models.settingsSaved": "{model} için model ayarları kaydedildi", "models.settingsSaveFailed": "Ayarların kaydedilip kaydedilmediğini doğrulayamadık. Yeniden denemeden önce model listesini yenileyin.", "models.settingsRefreshFailed": "Kaydedildi. Model listesi yenilenmediği için satır eski değerleri gösterebilir. Kontrol etmek için yenileyin.", - "models.settingsSavedCodexStale": "{model} kaydedildi ancak Codex model kataloğu yenilenmedi. Değişikliğin Codex'e geçmesi için Sync çalıştırın.", + "models.settingsSavedCodexStale": "{model} kaydedildi ancak Codex model kataloğu yenilenmedi. Değişikliğin Codex'e geçmesi için panodaki \"Şimdi senkronize et\" düğmesine tıklayın.", + "models.settingsRejected": "Sunucu bu ayarları reddetti, bu yüzden hiçbir şey kaydedilmedi. Değerleri kontrol edip yeniden deneyin.", "models.settingsDefaultEffort": "Varsayılan düzey", "models.settingsModalitiesInherit": "Geçersiz kılınmadı — şu anda üst kaynak bildirimine uyuluyor: {modalities}", "models.settingsModalitiesUnknown": "bildirilmemiş", diff --git a/gui/src/i18n/vi.ts b/gui/src/i18n/vi.ts index 62f53300264..9082a7e4ba8 100644 --- a/gui/src/i18n/vi.ts +++ b/gui/src/i18n/vi.ts @@ -747,7 +747,8 @@ export const vi: Record = { "models.settingsSaved": "Đã lưu cài đặt model cho {model}", "models.settingsSaveFailed": "Không thể xác nhận cài đặt đã được lưu hay chưa. Hãy tải lại danh sách model trước khi thử lại.", "models.settingsRefreshFailed": "Đã lưu. Danh sách model chưa tải lại nên dòng có thể hiển thị giá trị cũ. Hãy tải lại để kiểm tra.", - "models.settingsSavedCodexStale": "Đã lưu {model}, nhưng danh mục model của Codex chưa được làm mới. Hãy chạy Sync để Codex nhận thay đổi.", + "models.settingsSavedCodexStale": "Đã lưu {model}, nhưng danh mục model của Codex chưa được làm mới. Hãy bấm \"Đồng bộ ngay\" trên bảng điều khiển để Codex nhận thay đổi.", + "models.settingsRejected": "Máy chủ đã từ chối các thiết lập này nên chưa có gì được lưu. Hãy kiểm tra giá trị rồi thử lại.", "models.settingsDefaultEffort": "Mức mặc định", "models.settingsModalitiesInherit": "Chưa ghi đè — hiện theo khai báo từ nhà cung cấp: {modalities}", "models.settingsModalitiesUnknown": "chưa khai báo", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 66268cd7a49..e3f4654a540 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -596,7 +596,8 @@ export const zhTW: Record = { "models.settingsSaved": "已儲存 {model} 的模型設定", "models.settingsSaveFailed": "無法確認設定是否已儲存。再次嘗試前請重新載入模型清單。", "models.settingsRefreshFailed": "已儲存。模型清單未重新載入,因此該列可能顯示舊值。請重新載入確認。", - "models.settingsSavedCodexStale": "已儲存 {model},但 Codex 模型目錄未重新整理。請執行 Sync 讓 Codex 套用變更。", + "models.settingsSavedCodexStale": "已儲存 {model},但 Codex 模型目錄未重新整理。請在儀表板按「立即同步」讓 Codex 套用變更。", + "models.settingsRejected": "伺服器拒絕了這些設定,因此沒有儲存任何內容。請檢查數值後再試一次。", "models.settingsDefaultEffort": "預設檔位", "models.settingsModalitiesInherit": "未覆蓋 — 目前依上游宣告:{modalities}", "models.settingsModalitiesUnknown": "未宣告", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 6471f18a60e..9d4516af97d 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -740,7 +740,8 @@ export const zh: Record = { "models.settingsSaved": "已保存 {model} 的模型设置", "models.settingsSaveFailed": "无法确认设置是否已保存。重试前请重新加载模型列表。", "models.settingsRefreshFailed": "已保存。模型列表未重新加载,因此该行可能显示旧值。请重新加载确认。", - "models.settingsSavedCodexStale": "已保存 {model},但 Codex 模型目录未刷新。请运行 Sync 让 Codex 应用此更改。", + "models.settingsSavedCodexStale": "已保存 {model},但 Codex 模型目录未刷新。请在仪表板点击「立即同步」让 Codex 应用此更改。", + "models.settingsRejected": "服务器拒绝了这些设置,因此没有保存任何内容。请检查数值后重试。", "models.settingsDefaultEffort": "默认档位", "models.settingsModalitiesInherit": "未覆盖 — 当前按上游声明:{modalities}", "models.settingsModalitiesUnknown": "未声明", diff --git a/gui/tests/model-settings-dialog.test.tsx b/gui/tests/model-settings-dialog.test.tsx index 93ed111057e..9e785091f89 100644 --- a/gui/tests/model-settings-dialog.test.tsx +++ b/gui/tests/model-settings-dialog.test.tsx @@ -297,6 +297,19 @@ describe("Models per-model settings dialog", () => { }); test("a failed Codex catalog refresh warns after the save instead of trapping the dialog", async () => { + await mount(); + settingsResponse = () => Response.json({ error: "raw server detail" }, { status: 400 }); + await open(); + await act(async () => checkboxes()[1]!.click()); + await click("Apply"); + // A 4xx means nothing was written: the form stays editable and shows translated copy only. + expect(dialog()!.textContent).toContain("rejected these settings"); + expect(dialog()!.textContent).not.toContain("raw server detail"); + expect(dialogButton("Apply")!.disabled).toBe(false); + expect(dialogButton("Reload")).toBeUndefined(); + }); + + test("a Codex catalog refresh failure is only a warning after the save", async () => { await mount(); settingsResponse = body => Response.json({ ok: true, provider: body.provider, modelId: body.modelId, changed: true, saved: true, hasOverrides: true, diff --git a/src/cli/models-runtime.ts b/src/cli/models-runtime.ts index fde6ae3dda8..c5a0a58c15a 100644 --- a/src/cli/models-runtime.ts +++ b/src/cli/models-runtime.ts @@ -288,7 +288,19 @@ async function setModelSettings(argv: string[], deps: RuntimeApiDeps): Promise item.trim()); + if (members.some(item => !["text", "image", "audio"].includes(item))) { + throw new CliUsageError("--modalities must list text, image or audio (comma-separated), or - to clear", USAGE); + } + patch.inputModalities = [...new Set(members)]; + } + } if (reasoningEffortsRaw !== undefined) { // "-" restores inheritance by clearing the stored ladder (null); "" stores an explicit empty // ladder, which is the "this model does not reason" override. Embedded blank CSV members diff --git a/tests/cli/cli-models-set.test.ts b/tests/cli/cli-models-set.test.ts index 91a67b21094..d542ebdeb9e 100644 --- a/tests/cli/cli-models-set.test.ts +++ b/tests/cli/cli-models-set.test.ts @@ -122,6 +122,12 @@ describe("models per-model settings command", () => { const unsafeWindow = await invoke(["vendor/model", "--context-window", String(2 ** 60)]); expect(unsafeWindow.code).toBe(2); expect(unsafeWindow.calls).toEqual([]); + // Only "-" clears: a blank value or blank member must not normalize to [] and clear silently. + for (const raw of ["", ",", "text,,image", "video"]) { + const blank = await invoke(["vendor/model", "--modalities", raw]); + expect(blank.code).toBe(2); + expect(blank.calls).toEqual([]); + } }); test("an unchanged answer says so instead of claiming a write", async () => { From 84be8f48a6f65ea7eecc935a60f78770c634417f Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 03:24:56 +0900 Subject: [PATCH 14/14] test(models): name the rejected-save dialog case --- gui/tests/model-settings-dialog.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/tests/model-settings-dialog.test.tsx b/gui/tests/model-settings-dialog.test.tsx index 9e785091f89..552aa71a875 100644 --- a/gui/tests/model-settings-dialog.test.tsx +++ b/gui/tests/model-settings-dialog.test.tsx @@ -296,7 +296,7 @@ describe("Models per-model settings dialog", () => { expect(container.textContent).toContain("Reopen vendor-demo/chat-large"); }); - test("a failed Codex catalog refresh warns after the save instead of trapping the dialog", async () => { + test("a rejected save stays editable with translated copy", async () => { await mount(); settingsResponse = () => Response.json({ error: "raw server detail" }, { status: 400 }); await open();