Skip to content

Commit 825f25a

Browse files
committed
feat: group codex config settings
1 parent e2f46f6 commit 825f25a

15 files changed

Lines changed: 626 additions & 121 deletions

File tree

docs-linhay/memory/2026-05-25.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
# 2026-05-25
22

33
- Settings Release Panel 重设计:`frontend/src/features/settings/components/SettingsReleasePanel.tsx` 改为信息矩阵 + 右侧操作区,并为当前版本、最新 release、GetTokens Git Hash、CLIProxyAPI Git Hash 增加 GitHub 外链入口。URL 构造逻辑收敛到 `settingsRelease.ts`,占位 hash(`DEV```)不显示外链;浏览器预览下外链走 `window.open`,Wails 桌面下走 `BrowserOpenURL`。验证:`npm run test:unit``npm run typecheck``npm run build` 通过;Playwright 预览截图落位 `docs-linhay/screenshots/20260525/settings/20260525-settings-release-panel-after-v02.png`。375px 预览暴露的是既有固定侧边栏桌面壳宽度问题,不作为本次 release panel 阻塞。
4+
- Codex `Feature 配置` 页面完成配置项语义分组与控件收敛:root 按启动默认、模型输出、权限沙箱、工作区文档、工具集成、高级兼容分组;features 按推荐稳定、实验性、高级、兼容旧项分组;notice 按安全提示、迁移提示、结构化 notice 分组;model providers 按 provider id 分组。分组只影响展示层,不改变 draft、preview、save 的字段路径和保留式 TOML patch 语义。固定 enum options 已统一渲染为 `SegmentedControl`,bool / boolean 继续使用 `ToggleSwitch`,并按本地 Codex 源码 `7d47056ea4` 的 `codex-rs/core/config.schema.json` 校准 `approvals_reviewer`、`personality`、`model_auto_compact_token_limit_scope`、`model_providers.*.wire_api` 等选项;布尔型缺省假值不会被误显示成 `false` 枚举项。验证:`go test ./internal/wailsapp -run 'TestGetCodexFeatureConfigReturnsTypedRootDefinitionsAndValues|TestGetCodexFeatureConfigReturnsAllModelProviderSchemaFields'`、`node --test frontend/src/features/status/tests/codexFeatureConfig.test.mjs`、`npm --prefix frontend run typecheck`、`npm --prefix frontend run build` 通过;浏览器确认 `http://127.0.0.1:34115/#frame=codex` 已出现实际非空分组标题,且 DOM 命中 `SegmentedControl: 16`、`ToggleSwitch: 79`、`selectCount: 0`。

docs-linhay/spaces/20260502-codex-config-toml-settings/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ GetTokens 已经在状态页支持把本地 relay service 的 `apiKey / baseURL
4040
- [x] 保存后无关顶层键、注释、MCP section、agents section、未知 provider 字段保持不变。
4141
- [x] 当现有 `config.toml` 语法明显无法安全 patch 时,停止写入并给出错误,不生成破坏性覆盖。
4242
- [x] 新增 Go 单元测试覆盖 `[features]` 读取、diff、merge、异常输入、未知字段保留;前端测试覆盖开关派生状态、保存前校验与 Codex 二级路由迁移。
43+
- [x] `Feature 配置` 页面不再以长列表展示,root、features、notice、model providers 均按语义分组,用户能先定位配置域再修改具体键。
4344
- [ ] 与现有“一键应用到本地 Codex 配置”共用后端保留式 TOML patch 工具,不出现两套写入规则。
4445

4546
说明:本期实现已经使用同一类“保留原文件、只 patch 受控字段”的写入语义,但尚未把既有 relay 一键应用链路重构到同一个 helper;该项保留为后续内部收敛任务。
@@ -62,7 +63,7 @@ GetTokens 已经在状态页支持把本地 relay service 的 `apiKey / baseURL
6263

6364
## 当前状态
6465
- 状态:implemented
65-
- 最近更新:2026-05-23
66+
- 最近更新:2026-05-25
6667

6768
## 实施结果
6869
1. 后端新增 `GetCodexFeatureConfig``PreviewCodexFeatureConfig``SaveCodexFeatureConfig`,并通过根层 `main.App` wrapper 暴露到 Wails bindings。
@@ -77,6 +78,8 @@ GetTokens 已经在状态页支持把本地 relay service 的 `apiKey / baseURL
7778
10. Gemini 用量入口暂不暴露,后续 Gemini 能力成型后再单独纳入导航。
7879
11.`#frame=session-management``#frame=vendor-status``#frame=usage-desk` 路由和本地存储值保留兼容迁移,统一进入 `Codex` 对应二级项。
7980
12. 实现截图:[`20260502-codex-config-codex-menu-after-v01.png`](screenshots/20260502/codex-config/20260502-codex-config-codex-menu-after-v01.png)
81+
13. `Feature 配置` 长列表已追加展示分组:root 按启动默认、模型输出、权限沙箱、工作区文档、工具集成、高级兼容分组;features 按推荐稳定、实验性、高级、兼容旧项分组;notice 按安全提示、迁移提示、结构化 notice 分组;model providers 按 provider id 分组。分组仅影响展示,不改变 draft、preview、save 的字段路径和写入语义。
82+
14. 固定枚举值控件已从原生 select 收敛为 `SegmentedControl`;bool / boolean 仍使用 `ToggleSwitch`。当前值不在枚举 options 中时临时并入 segment 头部展示,布尔型缺省假值不会被渲染成 `false` 枚举项。枚举 options 已按本地 Codex 源码 `7d47056ea4``codex-rs/core/config.schema.json` 校准:`approvals_reviewer``cli_auth_credentials_store``mcp_oauth_credentials_store``model_auto_compact_token_limit_scope``personality``model_providers.*.wire_api` 等不再使用旧值。
8083

8184
## 验证记录
8285
1. `go test ./...`
@@ -98,3 +101,11 @@ GetTokens 已经在状态页支持把本地 relay service 的 `apiKey / baseURL
98101
8. 浏览器 MCP 导航被自动审批超时阻塞后,改用 headless Chrome + CDP 验证 `http://127.0.0.1:34115/#frame=codex`;12 秒后页面 `loading=false`,DOM 命中 `Codex Root Settings``Codex Model Providers``Codex Features``Codex Notices``hide_rate_limit_model_nudge``approval_policy``request_max_retries``supports_websockets``multi_agent_v2``apps_mcp_path_override``network_proxy``model_migrations``external_config_migration_prompts`
99102

100103
补充:`npm --prefix frontend run test:unit` 当前 416 项中 415 通过,失败项为 `frontend/src/features/accounts/tests/accountFilters.test.mjs``AccountsToolbar keeps status, resource, and source filters in the new order`,属于 accounts 工具栏既有工作区改动,不在本次 Codex 配置页修改范围。
104+
105+
## 2026-05-25 追加验证
106+
1. `node --test frontend/src/features/status/tests/codexFeatureConfig.test.mjs`
107+
2. `npm --prefix frontend run typecheck`
108+
3. `npm --prefix frontend run build`
109+
4. 浏览器重新加载 `http://127.0.0.1:34115/#frame=codex`,页面命中分组标题 `启动与默认``模型与输出``权限与沙箱``工作区与文档``工具与集成``高级与兼容``推荐与稳定``实验性``兼容与旧项``安全提示``迁移提示`,确认配置项已从长列表收敛为分组列表;空分组不会渲染。
110+
5. 追加控件形态验收:同一页面 DOM 命中 `SegmentedControl: 16``ToggleSwitch: 79``selectCount: 0`,固定枚举已改为 segment,bool 仍为 toggle。
111+
6. 追加 Codex 源码校准:对照 `docs-linhay/references/codex` 当前 `main``7d47056ea4`)的 `codex-rs/core/config.schema.json`,并用 `go test ./internal/wailsapp -run 'TestGetCodexFeatureConfigReturnsTypedRootDefinitionsAndValues|TestGetCodexFeatureConfigReturnsAllModelProviderSchemaFields'` 锁定关键 enum options。

frontend/src/components/ui/SegmentedControl.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,41 @@ import type { SegmentedOption } from '../../types';
33

44
interface SegmentedControlProps<T extends string> {
55
options: ReadonlyArray<SegmentedOption<T>>;
6-
value: T;
6+
value: T | '';
7+
disabled?: boolean;
78
onChange: (value: T) => void;
89
}
910

1011
export default function SegmentedControl<T extends string>({
1112
options,
1213
value,
14+
disabled = false,
1315
onChange,
1416
}: SegmentedControlProps<T>) {
15-
const selectedIndex = Math.max(
16-
options.findIndex((option) => option.id === value),
17-
0,
18-
);
17+
const selectedIndex = options.findIndex((option) => option.id === value);
1918
const segmentCount = Math.max(options.length, 1);
2019
const indicatorStyle: CSSProperties = {
2120
width: `calc(100% / ${segmentCount})`,
22-
transform: `translateX(${selectedIndex * 100}%)`,
21+
opacity: selectedIndex >= 0 ? 1 : 0,
22+
transform: `translateX(${Math.max(selectedIndex, 0) * 100}%)`,
2323
};
2424

2525
return (
2626
<div
2727
data-design-system-component="true"
2828
data-design-system-component-name="SegmentedControl"
29-
className="relative flex w-full max-w-sm overflow-hidden border-[1px] border-[color:color-mix(in_srgb,var(--border-color)_55%,transparent)] bg-[var(--bg-main)]"
29+
className={`relative flex w-full max-w-sm overflow-hidden border-[1px] border-[color:color-mix(in_srgb,var(--border-color)_55%,transparent)] bg-[var(--bg-main)] ${
30+
disabled ? 'opacity-60' : ''
31+
}`}
3032
>
3133
{options.map((option, index) => (
3234
<button
3335
type="button"
3436
key={option.id}
3537
aria-pressed={value === option.id}
38+
disabled={disabled}
3639
onClick={() => onChange(option.id)}
37-
className={`relative flex min-h-[var(--gt-control-segmented-min-height,34px)] flex-1 items-center justify-center px-[var(--gt-control-segmented-padding-inline,10px)] text-[length:var(--gt-control-segmented-font-size,9px)] font-bold leading-none transition-colors focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--border-color)] ${
40+
className={`relative flex min-h-[var(--gt-control-segmented-min-height,34px)] flex-1 items-center justify-center px-[var(--gt-control-segmented-padding-inline,10px)] text-[length:var(--gt-control-segmented-font-size,9px)] font-bold leading-none transition-colors focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[var(--border-color)] disabled:cursor-not-allowed ${
3841
index !== options.length - 1 ? 'border-r-[1px] border-[color:color-mix(in_srgb,var(--border-color)_35%,transparent)]' : ''
3942
} ${
4043
value === option.id

frontend/src/features/status/components/StatusCodexFeaturesSection.tsx

Lines changed: 64 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
CodexFeatureRow,
66
CodexFeatureStageFilter,
77
} from '../model/codexFeatureConfig';
8+
import { groupCodexFeatureRows } from '../model/codexFeatureConfig';
89
import { renderCodexValueEditor } from '../model/codexValueEditor';
910

1011
const codexFeatureStageFilters: CodexFeatureStageFilter[] = [
@@ -72,6 +73,13 @@ export default function StatusCodexFeaturesSection({
7273
const visibleCount = rows.length;
7374
const totalCount = snapshot?.items.filter((item) => item.section === 'features').length || 0;
7475
const isBusy = isLoading || isSaving;
76+
const groupedRows = groupCodexFeatureRows(rows);
77+
78+
function resolveGroupTitle(groupId: string) {
79+
const translationKey = `status.codex_features_group_${groupId}`;
80+
const translated = t(translationKey);
81+
return translated !== translationKey ? translated : groupId;
82+
}
7583

7684
return (
7785
<section className="relative overflow-hidden border-2 border-[var(--border-color)] bg-[var(--bg-surface)]">
@@ -132,50 +140,66 @@ export default function StatusCodexFeaturesSection({
132140
</div>
133141
) : null}
134142

135-
<div className="divide-y-2 divide-[var(--border-color)]">
136-
{rows.map((row) => (
137-
<div
138-
key={row.key}
139-
className={`grid gap-3 px-4 py-3 md:grid-cols-[minmax(0,1fr)_5rem] md:items-center ${
140-
row.stage === 'unknown' || row.stage === 'unsupported' ? 'bg-[var(--bg-main)]' : ''
141-
}`}
142-
>
143-
<div className="min-w-0">
144-
<div className="break-all font-mono text-[length:var(--font-size-ui-md)] font-black tracking-wide text-[var(--text-primary)]">
145-
{row.key}
143+
<div>
144+
{groupedRows.map((group, groupIndex) => (
145+
<div key={group.id} className={`${groupIndex > 0 ? 'border-t-2 border-[var(--border-color)]' : ''}`}>
146+
<div className="flex flex-wrap items-center justify-between gap-2 border-b-2 border-dashed border-[var(--border-color)] bg-[var(--bg-main)] px-4 py-2">
147+
<div className="text-[length:var(--font-size-ui-xs)] font-black italic uppercase tracking-[0.18em] text-[var(--text-primary)]">
148+
{resolveGroupTitle(group.id)}
149+
</div>
150+
<div className="text-[length:var(--font-size-ui-xs)] font-black uppercase tracking-[0.18em] text-[var(--text-muted)]">
151+
{group.rows.length} {t('design_system.items')}
146152
</div>
147-
<div className="mt-1 flex flex-wrap items-center gap-2 text-[length:var(--font-size-ui-sm)] font-bold tracking-wide text-[var(--text-muted)]">
148-
<span
149-
className={`inline-flex shrink-0 border-2 px-2 py-0.5 text-[length:var(--font-size-ui-xs)] font-black tracking-[0.14em] ${
150-
row.stage === 'unknown' || row.stage === 'unsupported' || row.stage === 'removed'
151-
? 'border-[var(--border-color)] bg-[var(--bg-surface)] text-[var(--color-status-danger)]'
152-
: 'border-[var(--border-color)] bg-[var(--text-primary)] text-[var(--bg-main)]'
153+
</div>
154+
<div className="divide-y-2 divide-[var(--border-color)]">
155+
{group.rows.map((row) => (
156+
<div
157+
key={row.key}
158+
className={`grid gap-3 px-4 py-3 md:grid-cols-[minmax(0,1fr)_5rem] md:items-center ${
159+
row.stage === 'unknown' || row.stage === 'unsupported' ? 'bg-[var(--bg-main)]' : ''
153160
}`}
154161
>
155-
{t(`status.codex_features_stage_${row.stage}`)}
156-
</span>
157-
{row.hiddenByDefault ? (
158-
<span className="inline-flex shrink-0 border-2 border-[var(--border-color)] bg-[var(--bg-surface)] px-2 py-0.5 text-[length:var(--font-size-ui-xs)] font-black tracking-[0.14em] text-[var(--text-muted)]">
159-
{t('status.codex_features_hidden_default')}
160-
</span>
161-
) : null}
162-
<span className="min-w-0">{resolveCodexFeatureDescription(t, row)}</span>
163-
</div>
164-
{row.legacyAliases.length > 0 ? (
165-
<div className="mt-2 inline-flex max-w-full border-2 border-dashed border-[var(--border-color)] px-2 py-1 text-[length:var(--font-size-ui-xs)] font-black uppercase tracking-[0.14em] text-[var(--text-primary)]">
166-
<span className="truncate">
167-
{t('status.codex_features_legacy_alias')}: {row.legacyAliases.join(', ')}
168-
</span>
162+
<div className="min-w-0">
163+
<div className="break-all font-mono text-[length:var(--font-size-ui-md)] font-black tracking-wide text-[var(--text-primary)]">
164+
{row.key}
165+
</div>
166+
<div className="mt-1 flex flex-wrap items-center gap-2 text-[length:var(--font-size-ui-sm)] font-bold tracking-wide text-[var(--text-muted)]">
167+
<span
168+
className={`inline-flex shrink-0 border-2 px-2 py-0.5 text-[length:var(--font-size-ui-xs)] font-black tracking-[0.14em] ${
169+
row.stage === 'unknown' || row.stage === 'unsupported' || row.stage === 'removed'
170+
? 'border-[var(--border-color)] bg-[var(--bg-surface)] text-[var(--color-status-danger)]'
171+
: 'border-[var(--border-color)] bg-[var(--text-primary)] text-[var(--bg-main)]'
172+
}`}
173+
>
174+
{t(`status.codex_features_stage_${row.stage}`)}
175+
</span>
176+
{row.hiddenByDefault ? (
177+
<span className="inline-flex shrink-0 border-2 border-[var(--border-color)] bg-[var(--bg-surface)] px-2 py-0.5 text-[length:var(--font-size-ui-xs)] font-black tracking-[0.14em] text-[var(--text-muted)]">
178+
{t('status.codex_features_hidden_default')}
179+
</span>
180+
) : null}
181+
<span className="min-w-0">{resolveCodexFeatureDescription(t, row)}</span>
182+
</div>
183+
{row.legacyAliases.length > 0 ? (
184+
<div className="mt-2 inline-flex max-w-full border-2 border-dashed border-[var(--border-color)] px-2 py-1 text-[length:var(--font-size-ui-xs)] font-black uppercase tracking-[0.14em] text-[var(--text-primary)]">
185+
<span className="truncate">
186+
{t('status.codex_features_legacy_alias')}: {row.legacyAliases.join(', ')}
187+
</span>
188+
</div>
189+
) : null}
190+
{row.unsupported ? (
191+
<div className="mt-2 text-[length:var(--font-size-ui-xs)] font-black uppercase tracking-[0.14em] text-[var(--text-muted)]">
192+
{t('status.codex_features_unsupported_hint')}
193+
</div>
194+
) : null}
195+
</div>
196+
<div className="flex justify-start md:justify-center">
197+
<div className="w-full max-w-[22rem]">
198+
{renderCodexValueEditor(row, row.readOnly || isBusy, onChangeFeature)}
199+
</div>
200+
</div>
169201
</div>
170-
) : null}
171-
{row.unsupported ? (
172-
<div className="mt-2 text-[length:var(--font-size-ui-xs)] font-black uppercase tracking-[0.14em] text-[var(--text-muted)]">
173-
{t('status.codex_features_unsupported_hint')}
174-
</div>
175-
) : null}
176-
</div>
177-
<div className="flex justify-start md:justify-center">
178-
<div className="w-full max-w-[22rem]">{renderCodexValueEditor(row, row.readOnly || isBusy, onChangeFeature)}</div>
202+
))}
179203
</div>
180204
</div>
181205
))}

0 commit comments

Comments
 (0)