Plans/2026 06 25 Model List Window Split Plan
模型列表与上下文窗口分离实现计划
面向 AI 代理的工作者: 必需子技能:使用
superpowers:subagent-driven-development(推荐)或superpowers:executing-plans逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。
目标: 将 CodexPlusPlus 的模型列表从单文本框(模型名可带 [1M] 后缀)改造为左右并排双文本框(左侧模型名、右侧上下文窗口),在存储层把 model_list 和 model_windows 彻底分离,使 Codex 客户端永远看不到带后缀的模型名。
架构: 在 RelayProfile 中新增 model_windows JSON map 字段;后端 collect_catalog_entries 改为从 model_list(无后缀 slug 列表)和 model_windows(slug -> 窗口 token)组合生成 catalog;前端把模型列表 UI 拆为左右两个 textarea,保存时按行组装成 model_windows;settings 加载时自动把旧格式 deepseek-v4-flash[1M] 一次性迁移到新格式。
技术栈: Rust(serde、toml_edit)、React + TypeScript。
文件结构
| 文件 | 职责 |
|---|---|
crates/codex-plus-core/src/settings.rs |
RelayProfile 结构体,新增 model_windows 字段 |
crates/codex-plus-core/src/model_suffix.rs |
新增旧格式迁移函数;修改 collect_catalog_entries 签名以接收 model_windows map |
crates/codex-plus-core/src/relay_config.rs |
在 normalize_relay_profile_for_storage 调用迁移;在 apply_model_catalog_to_config 中组装 catalog |
crates/codex-plus-core/tests/model_suffix.rs |
更新现有测试;新增迁移和 model_windows 测试 |
crates/codex-plus-core/tests/relay_config.rs |
更新现有测试以使用新签名 |
apps/codex-plus-manager/src/App.tsx |
前端:类型定义、draft 状态、模型列表 UI、保存组装、配置模型剥离后缀 |
apps/codex-plus-manager/src/App.test.tsx |
新增前端渲染、行数校验、保存组装测试 |
任务 1:在 `RelayProfile` 中添加 `model_windows` 字段
文件:
修改:
crates/codex-plus-core/src/settings.rs:82-89步骤 1:添加字段
在 model_list 字段后增加:
#[serde(rename = "modelList", default)]
pub model_list: String,
#[serde(rename = "modelWindows", default, skip_serializing_if = "String::is_empty")]
pub model_windows: String,- 步骤 2:更新 Default 实现
在 impl Default for RelayProfile 中找到 model_list 的默认值,添加 model_windows:
model_list: String::new(),
model_windows: String::new(),- 步骤 3:编译检查
运行:
cargo check -p codex-plus-core预期:通过(此时字段尚未被使用,只是结构体定义)。
- 步骤 4:Commit
git add crates/codex-plus-core/src/settings.rs
git commit -m "feat(core): 在 RelayProfile 中新增 model_windows 字段"任务 2:添加旧格式迁移函数
文件:
修改:
crates/codex-plus-core/src/model_suffix.rs步骤 1:编写测试
在 crates/codex-plus-core/tests/model_suffix.rs 末尾新增:
#[test]
fn migrate_model_list_with_suffixes_splits_slug_and_window() {
let input = "deepseek-v4-flash[1M]\ndeepseek-v4-pro\nnvidia/...:free[200K]";
let (clean_list, windows) = codex_plus_core::model_suffix::migrate_model_list_with_suffixes(input);
assert_eq!(clean_list, "deepseek-v4-flash\ndeepseek-v4-pro\nnvidia/...:free");
assert_eq!(windows.get("deepseek-v4-flash"), Some(&"1M".to_string()));
assert_eq!(windows.get("deepseek-v4-pro"), None);
assert_eq!(windows.get("nvidia/...:free"), Some(&"200K".to_string()));
}- 步骤 2:运行测试验证失败
cargo test -p codex-plus-core --test model_suffix migrate_model_list_with_suffixes预期:FAIL,提示 migrate_model_list_with_suffixes 未找到。
- 步骤 3:实现迁移函数
在 crates/codex-plus-core/src/model_suffix.rs 中,紧接 parse_model_suffix 之后添加:
use std::collections::HashMap;
/// 一次性迁移:把旧格式 `slug[suffix]` 的 model_list 拆成无后缀列表和窗口 map。
pub fn migrate_model_list_with_suffixes(model_list: &str) -> (String, HashMap<String, String>) {
let mut clean_lines = Vec::new();
let mut windows = HashMap::new();
for raw in model_list.split(['\r', '\n', ',']).map(str::trim).filter(|v| !v.is_empty()) {
let (slug, window) = parse_model_suffix(raw);
clean_lines.push(slug.clone());
if let Some(window) = window {
windows.insert(slug, window.to_string());
}
}
(clean_lines.join("\n"), windows)
}注意:parse_model_suffix 返回的 window 是 Option,需要 to_string()。
- 步骤 4:导出函数
确保 model_suffix.rs 已经 pub fn migrate_model_list_with_suffixes。
- 步骤 5:运行测试验证通过
cargo test -p codex-plus-core --test model_suffix migrate_model_list_with_suffixes预期:PASS。
- 步骤 6:Commit
git add crates/codex-plus-core/src/model_suffix.rs crates/codex-plus-core/tests/model_suffix.rs
git commit -m "feat(core): 添加旧 model_list 后缀迁移函数"任务 3:修改 `collect_catalog_entries` 签名以接收 `model_windows`
文件:
修改:
crates/codex-plus-core/src/model_suffix.rs:64-121修改:
crates/codex-plus-core/src/model_suffix.rs相关调用点步骤 1:更新现有测试以使用新签名
在 crates/codex-plus-core/tests/model_suffix.rs 中,找到所有 collect_catalog_entries(...) 调用,改为传入空 map:
let entries = collect_catalog_entries(model_list, &HashMap::new(), current_model);如果测试原本就依赖后缀窗口,改为在 map 中传入:
let mut windows = HashMap::new();
windows.insert("deepseek-v4-flash".to_string(), "1M".to_string());
let entries = collect_catalog_entries("deepseek-v4-flash", &windows, current_model);- 步骤 2:运行测试验证失败
cargo test -p codex-plus-core --test model_suffix预期:FAIL,签名不匹配。
- 步骤 3:修改函数签名和实现
把:
pub fn collect_catalog_entries(model_list: &str, current_model: &str) -> Vec<ModelCatalogEntry>改为:
pub fn collect_catalog_entries(
model_list: &str,
model_windows: &HashMap<String, String>,
current_model: &str,
) -> Vec<ModelCatalogEntry>函数内部:
- 删除
suffix_for_slug的构建逻辑(不再从 model_list 解析后缀)。 - 解析
model_list时只保留 slug:
let (slug, _) = parse_model_suffix(raw);- 窗口来源改为
model_windows.get(&slug):
let suffix_window = model_windows.get(&slug).and_then(|token| parse_window_token(token));- 处理
current_model时同样从model_windows查找:
if suffix_window.is_none() {
if let Some(token) = model_windows.get(&slug) {
suffix_window = parse_window_token(token);
}
}- 步骤 4:编译检查
cargo check -p codex-plus-core预期:通过。
- 步骤 5:运行测试验证通过
cargo test -p codex-plus-core --test model_suffix预期:PASS。
- 步骤 6:Commit
git add crates/codex-plus-core/src/model_suffix.rs crates/codex-plus-core/tests/model_suffix.rs
git commit -m "feat(core): collect_catalog_entries 改为从 model_windows map 读取窗口"任务 4:更新 `relay_config.rs` 中的 catalog 生成调用
文件:
修改:
crates/codex-plus-core/src/relay_config.rs:1389步骤 1:编写失败测试
在 crates/codex-plus-core/tests/relay_config.rs 中新增:
#[test]
fn apply_model_catalog_uses_model_windows_map() {
let temp = tempfile::tempdir().unwrap();
let home = temp.path().join(".codex");
std::fs::create_dir_all(&home).unwrap();
let mut profile = build_test_relay_profile();
profile.model_list = "deepseek-v4-flash\ndeepseek-v4-pro".to_string();
profile.model_windows = r#"{"deepseek-v4-flash":"1M"}"#.to_string();
profile.context_window = "200000".to_string();
apply_relay_profile_to_home_with_switch_rules_and_computer_use_guard(
&home, &profile, "", false,
)
.unwrap();
let catalog_path = home.join("model-catalogs").join(format!("{}.json", sanitize(&profile.id)));
let catalog: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(catalog_path).unwrap()).unwrap();
let models = catalog["models"].as_array().unwrap();
let flash = models.iter().find(|m| m["slug"].as_str().unwrap() == "deepseek-v4-flash").unwrap();
let pro = models.iter().find(|m| m["slug"].as_str().unwrap() == "deepseek-v4-pro").unwrap();
assert_eq!(flash["context_window"].as_u64().unwrap(), 1_000_000);
assert_eq!(pro["context_window"].as_u64().unwrap(), 200_000);
}- 步骤 2:运行测试验证失败
cargo test -p codex-plus-core --test relay_config apply_model_catalog_uses_model_windows_map预期:FAIL,因为 relay_config.rs 仍按旧签名调用。
- 步骤 3:修改调用点
在 crates/codex-plus-core/src/relay_config.rs 的 apply_model_catalog_to_config 函数中:
let model_windows: std::collections::HashMap<String, String> =
serde_json::from_str(&profile.model_windows).unwrap_or_default();
let entries = crate::model_suffix::collect_catalog_entries(&profile.model_list, &model_windows, &profile.model);- 步骤 4:编译检查
cargo check -p codex-plus-core预期:通过。
- 步骤 5:运行测试验证通过
cargo test -p codex-plus-core --test relay_config apply_model_catalog_uses_model_windows_map预期:PASS。
- 步骤 6:运行全部相关测试
cargo test -p codex-plus-core --test relay_config --test model_suffix预期:全部通过。
- 步骤 7:Commit
git add crates/codex-plus-core/src/relay_config.rs crates/codex-plus-core/tests/relay_config.rs
git commit -m "feat(core): apply_model_catalog_to_config 使用 model_windows map"任务 5:在 settings 加载/保存时迁移旧数据
文件:
修改:
crates/codex-plus-core/src/relay_config.rs中的normalize_relay_profile_for_storage步骤 1:编写失败测试
在 crates/codex-plus-core/tests/relay_config.rs 中新增:
#[test]
fn normalize_migrates_model_list_suffixes_to_model_windows() {
let mut profile = build_test_relay_profile();
profile.model_list = "deepseek-v4-flash[1M]\ndeepseek-v4-pro".to_string();
profile.model_windows = String::new();
codex_plus_core::relay_config::normalize_relay_profile_for_storage(&mut profile).unwrap();
assert_eq!(profile.model_list, "deepseek-v4-flash\ndeepseek-v4-pro");
let windows: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&profile.model_windows).unwrap();
assert_eq!(windows.get("deepseek-v4-flash").unwrap().as_str().unwrap(), "1M");
assert!(!windows.contains_key("deepseek-v4-pro"));
}- 步骤 2:运行测试验证失败
cargo test -p codex-plus-core --test relay_config normalize_migrates_model_list_suffixes_to_model_windows预期:FAIL。
- 步骤 3:实现迁移
在 normalize_relay_profile_for_storage 函数开头(在调用 complete_relay_profile_config 之前)添加:
if profile.model_windows.trim().is_empty() && profile.model_list.contains('[') {
let (clean_list, windows) = crate::model_suffix::migrate_model_list_with_suffixes(&profile.model_list);
profile.model_list = clean_list;
profile.model_windows = serde_json::to_string(&windows).unwrap_or_default();
}- 步骤 4:运行测试验证通过
cargo test -p codex-plus-core --test relay_config normalize_migrates_model_list_suffixes_to_model_windows预期:PASS。
- 步骤 5:Commit
git add crates/codex-plus-core/src/relay_config.rs crates/codex-plus-core/tests/relay_config.rs
git commit -m "feat(core): settings 保存时自动迁移旧 model_list 后缀到新 model_windows"任务 6:前端 TypeScript 类型添加 `modelWindows`
文件:
修改:
apps/codex-plus-manager/src/App.tsx:185步骤 1:添加字段
在 modelList: string; 后添加:
modelList: string;
modelWindows: string;
userAgent: string;- 步骤 2:更新所有 default 创建点
搜索所有 modelList: "" 出现的位置,在同对象内添加 modelWindows: "":
grep -n 'modelList: ""' apps/codex-plus-manager/src/App.tsx应修改的位置(根据当前代码):
apps/codex-plus-manager/src/App.tsx:659apps/codex-plus-manager/src/App.tsx:5475apps/codex-plus-manager/src/App.tsx:5531apps/codex-plus-manager/src/App.tsx:5557apps/codex-plus-manager/src/App.tsx:6169apps/codex-plus-manager/src/App.tsx:6198
每个位置改为:
modelList: "",
modelWindows: "",- 步骤 3:更新
normalizeRelayProfile
在 apps/codex-plus-manager/src/App.tsx:5557:
modelList: profile.modelList || "",
modelWindows: profile.modelWindows || "",- 步骤 4:编译检查
cd apps/codex-plus-manager && npm run tauri dev 2>&1 | head -50或更轻量:
cd apps/codex-plus-manager && npx tsc --noEmit预期:通过。
- 步骤 5:Commit
git add apps/codex-plus-manager/src/App.tsx
git commit -m "feat(manager): TypeScript RelayProfile 类型添加 modelWindows 字段"任务 7:前端添加模型窗口文本辅助函数
文件:
修改:
apps/codex-plus-manager/src/App.tsx(在parseModelSuffix附近)步骤 1:添加辅助函数
在 parseModelSuffix 函数之后添加:
/// 把 model_windows JSON map 按 model_list 行顺序转成文本(每行一个窗口,空行表示默认)。
function modelWindowsMapToText(modelList: string, modelWindows: string): string {
try {
const map = JSON.parse(modelWindows || "{}") as Record<string, string>;
return modelList
.split("\n")
.map((line) => map[line.trim()] ?? "")
.join("\n");
} catch {
return "";
}
}
/// 把左右 textarea 文本组装成 model_windows JSON map。
function modelWindowsTextToMap(modelList: string, modelWindowsText: string): string {
const models = modelList.split("\n").map((s) => s.trim()).filter(Boolean);
const windows = modelWindowsText.split("\n").map((s) => s.trim());
const map: Record<string, string> = {};
models.forEach((model, index) => {
if (windows[index]) {
map[model] = windows[index];
}
});
return JSON.stringify(map);
}- 步骤 2:编译检查
cd apps/codex-plus-manager && npx tsc --noEmit预期:通过。
- 步骤 3:Commit
git add apps/codex-plus-manager/src/App.tsx
git commit -m "feat(manager): 添加 model_windows 文本与 map 互转辅助函数"任务 8:前端模型列表 UI 改为双 textarea
文件:
修改:
apps/codex-plus-manager/src/App.tsx:3900-3925步骤 1:修改 UI 渲染
把当前模型列表 Field 替换为:
{showApiFields ? (
<Field className="relay-field-model-list" label="模型列表">
<div className="relay-model-list-split">
<div className="relay-model-list-column">
<label className="relay-model-list-label">模型名称</label>
<Textarea
value={profile.modelList}
onChange={(event) => updateDraft({ modelList: event.currentTarget.value })}
placeholder="deepseek/deepseek-v4-flash"
style={{ fontFamily: "monospace" }}
/>
</div>
<div className="relay-model-list-column">
<label className="relay-model-list-label">上下文窗口</label>
<Textarea
value={modelWindowsText}
onChange={(event) => setModelWindowsText(event.currentTarget.value)}
placeholder="1M"
style={{ fontFamily: "monospace" }}
/>
</div>
</div>
<div className="relay-model-list-tools">
<Button
onClick={async () => {
const models = await actions.fetchRelayProfileModels(profile);
if (models?.length) updateDraft({ modelList: models.join("\n") });
}}
size="sm"
type="button"
variant="secondary"
>
<Download className="h-4 w-4" />
从上游获取
</Button>
</div>
<p className="field-hint">
每行一个模型;左侧填模型名,右侧填上下文窗口(如 <code>1M</code>、<code>200K</code> 或 <code>1000000</code>)。右侧留空表示使用 Codex 默认长度。
</p>
</Field>
) : null}- 步骤 2:添加/更新 CSS
在 apps/codex-plus-manager/src/App.css(或使用的样式文件)中添加:
.relay-model-list-split {
display: flex;
gap: 12px;
}
.relay-model-list-column {
flex: 1;
display: flex;
flex-direction: column;
}
.relay-model-list-label {
font-size: 0.875rem;
margin-bottom: 4px;
color: var(--muted-foreground);
}
.relay-model-list-column textarea {
min-height: 120px;
resize: vertical;
white-space: pre;
overflow-wrap: normal;
overflow-x: auto;
}- 步骤 3:编译检查
cd apps/codex-plus-manager && npx tsc --noEmit预期:通过。
- 步骤 4:Commit
git add apps/codex-plus-manager/src/App.tsx apps/codex-plus-manager/src/App.css
git commit -m "feat(manager): 模型列表 UI 拆分为模型名和上下文窗口两列"任务 9:前端独立管理 `modelWindowsText` state
文件:
修改:
apps/codex-plus-manager/src/App.tsx中RelayProfileEditor组件步骤 1:添加独立 state
在 const [draft, setDraft] = useState<RelayProfile>(profile); 之后添加:
const [modelWindowsText, setModelWindowsText] = useState(
modelWindowsMapToText(profile.modelList, profile.modelWindows || "")
);- 步骤 2:同步 modelList 变化时更新窗口文本
使用 useEffect 监听 draft.modelList 和 draft.modelWindows:
useEffect(() => {
setModelWindowsText(modelWindowsMapToText(draft.modelList, draft.modelWindows || ""));
}, [draft.modelList, draft.modelWindows]);注意:当用户主动编辑右侧窗口文本时,不应被这个 effect 覆盖。因此更精确的做法是:只在 draft.modelList 行数变化且 modelWindowsText 尚未由用户编辑过,或从上游获取/加载时同步。
为简化,初始版本可以在 draft.modelList 变化时重新生成(用户编辑右侧会暂时被覆盖,可后续优化)。
- 步骤 3:编译检查
cd apps/codex-plus-manager && npx tsc --noEmit预期:通过。
- 步骤 4:Commit
git add apps/codex-plus-manager/src/App.tsx
git commit -m "feat(manager): 独立管理 modelWindowsText state"任务 10:前端保存时组装 `modelWindows` 并校验行数
文件:
修改:
apps/codex-plus-manager/src/App.tsx中saveDraft和switchDraft函数步骤 1:修改
saveDraft
在 saveDraft 函数中,在 if (validationError) return; 之后、调用 deriveRelayProfileFromFiles 之前添加:
const models = draft.modelList.split("\n").map((s) => s.trim()).filter(Boolean);
const windows = modelWindowsText.split("\n").map((s) => s.trim());
if (models.length !== windows.length) {
// 可以用 toast 或 alert,这里先用 alert
alert(`模型名称有 ${models.length} 行,上下文窗口有 ${windows.length} 行,请保持行数一致。`);
return;
}
const modelWindows = modelWindowsTextToMap(draft.modelList, modelWindowsText);
const draftWithWindows = { ...draft, modelWindows };
const normalizedDraft = deriveRelayProfileFromFiles(draftWithWindows);然后 normalizedDraft 会包含 modelWindows。
- 步骤 2:同步
switchDraft函数
switchDraft 中也调用了 deriveRelayProfileFromFiles(draft),需要同样组装 modelWindows:
const modelWindows = modelWindowsTextToMap(draft.modelList, modelWindowsText);
const normalizedDraft = deriveRelayProfileFromFiles({ ...draft, modelWindows });- 步骤 3:编译检查
cd apps/codex-plus-manager && npx tsc --noEmit预期:通过。
- 步骤 4:Commit
git add apps/codex-plus-manager/src/App.tsx
git commit -m "feat(manager): 保存时校验行数并组装 model_windows"任务 11:处理从上游获取模型列表
文件:
修改:
apps/codex-plus-manager/src/App.tsx中「从上游获取」按钮事件步骤 1:清空右侧窗口文本
当点击「从上游获取」时,获取到的模型列表是无后缀 slug,应同时清空 modelWindowsText:
if (models?.length) {
updateDraft({ modelList: models.join("\n"), modelWindowsText: "" });
}- 步骤 2:Commit
git add apps/codex-plus-manager/src/App.tsx
git commit -m "feat(manager): 从上游获取模型列表时清空窗口文本"任务 12:后端 `complete_relay_profile_config` 兼容旧 model 字段后缀
文件:
修改:
crates/codex-plus-core/src/relay_config.rs:1873-1888步骤 1:确认现有逻辑
当前已经调用 parse_model_suffix 剥离 model 后缀。如果 model_list 首条也带后缀,需要确保取到无后缀 slug。
- 步骤 2:修复 fallback model 来源
在:
if model.trim().is_empty() && !profile.model_list.trim().is_empty() {
if let Some(first) = profile.model_list.split(...).find(...) {
model = crate::model_suffix::parse_model_suffix(first).0;
}
}这里 profile.model_list 已经迁移为无后缀,所以 parse_model_suffix 返回原串即可,无需改动。
- 步骤 3:Commit
此任务若无需改动,可与任务 13 合并。
任务 13:添加/更新测试
后端测试
- 步骤 1:更新
collect_catalog_entries调用点
在 crates/codex-plus-core/tests/model_suffix.rs 和 tests/relay_config.rs 中,把所有 collect_catalog_entries(model_list, current_model) 改为 collect_catalog_entries(model_list, &HashMap::new(), current_model) 或传入对应 map。
- 步骤 2:运行后端测试
cargo test -p codex-plus-core --test relay_config --test model_suffix --test launcher --test codex_sqlite预期:全部通过。
- 步骤 3:Commit
git add crates/codex-plus-core/tests
git commit -m "test(core): 更新测试以适配 model_windows 新签名"前端测试
- 步骤 4:新增渲染测试
在 apps/codex-plus-manager/src/App.test.tsx 中新增:
it("renders model list with two textareas", () => {
render(<App />);
// 根据实际路由/入口渲染编辑器,可能需要更具体的测试 helper
expect(screen.getByPlaceholderText("deepseek/deepseek-v4-flash")).toBeInTheDocument();
expect(screen.getByPlaceholderText("1M")).toBeInTheDocument();
});- 步骤 5:新增行数校验测试
it("prevents save when model list and windows line counts mismatch", async () => {
// 设置 draft:模型 2 行,窗口 1 行
// 触发保存
// 验证保存未执行(如 mock 的 onFormChange 未被调用)
});- 步骤 6:运行前端测试
cd apps/codex-plus-manager && npm test预期:通过。
- 步骤 7:Commit
git add apps/codex-plus-manager/src/App.test.tsx
git commit -m "test(manager): 添加模型列表双文本框相关测试"任务 14:端到端验证
- 步骤 1:构建并运行管理工具
cd apps/codex-plus-manager && npm run build- 步骤 2:运行 example 验证后端
cargo run --example apply_profile -p codex-plus-core预期:config.toml 中 model 无后缀,catalog 窗口正确。
- 步骤 3:打包 DMG
bash scripts/installer/macos/package-dmg.sh 1.2.18 arm64- 步骤 4:手动验证
- 安装新 DMG。
- 在管理工具中输入:
- 左侧:
deepseek/deepseek-v4-flash - 右侧:
1M
- 左侧:
- 保存并启动 Codex。
- 检查
~/.codex/config.toml:model无后缀。 - 检查
~/.codex/model-catalogs/.json:slug 无后缀,context_window 为 1M。 - 在 Codex 中选择模型,确认不报 "is not a valid model ID"。
- 步骤 5:Commit(如有文档更新)
git add docs/ CHANGELOG.md README.md README_EN.md
git commit -m "docs: 更新模型列表与窗口分离的使用说明"自检
1. 规格覆盖度:
- 双输入框 UI:任务 8 覆盖。
- 存储层分离
model_list+model_windows:任务 1、4、10 覆盖。 - 旧数据一次性迁移:任务 2、5 覆盖。
- Codex 看不到带后缀字符串:任务 4(catalog slug 无后缀)+ 任务 12(config.toml model 无后缀)覆盖。
- 行数校验:任务 10 覆盖。
- 测试:任务 13 覆盖。
2. 占位符扫描:
- 无 TODO / 待定。
- 所有代码步骤均包含实际代码。
3. 类型一致性:
- Rust:
RelayProfile.model_windows类型一致。 - TypeScript:
RelayProfile.modelWindows和 draft 的modelWindowsText类型一致。 collect_catalog_entries签名在定义与调用处一致。
4. 范围检查:
- 本计划聚焦模型列表与窗口分离,不涉及其他功能。
执行交接
计划已完成并保存到 docs/plans/2026-06-25-model-list-window-split-plan.md。两种执行方式:
1. 子代理驱动(推荐) - 每个任务调度一个新的子代理,任务间进行审查,快速迭代
2. 内联执行 - 在当前会话中使用 executing-plans 执行任务,批量执行并设有检查点
选哪种方式?
Specs/2026 06 23 Model Catalog Prototype Design
设计:按模型上下文配置(model_list 后缀 → catalog 生成)
- 日期:2026-06-23
- 状态:已批准(阶段一原型),待实跑验证后推进
- 对应 issue:#1171 / #931
- 阶段:阶段一(原型验证)
1. 背景与目标
CodexPlusPlus 使用第三方模型(deepseek-v4-pro 1M、Claude 200K)时,上下文窗口被锁 258K。根因见 docs/research/01-调研结果.md:codex 客户端二进制硬编码 272000,custom provider slug 回落默认;CodexPlusPlus 不生成 model_catalog_json,用户被迫手改 config.toml。
目标:让 CodexPlusPlus 应用 profile 时自动生成 catalog + 写指针,按模型配置窗口,规避 #931 的路径转义坑与单模型副作用。
2. 范围(阶段一原型)
- 后缀语法:复用
model_list文本框,deepseek-v4-pro[1M]声明窗口 - catalog 生成:新增一个函数,生成 catalog JSON + 写 config.toml 指针
- 实跑验证:codex 客户端 读原型产物,确认报 1M(对拍 cc-switch 格式)
不在阶段一:解析回传 context_window 到前端、前端结构化展示、auto_compact 按比例 UI(后两个阶段)。
3. 设计
3.1 后缀语法(复用 model_list,不新增字段)
model_list 是纯文本(换行/逗号分隔 slug),不进 config.toml。后缀作为输入糖:
| 输入 | slug | context_window |
|---|---|---|
deepseek-v4-pro[1M] |
deepseek-v4-pro |
1000000 |
claude-sonnet-4[200K] |
claude-sonnet-4 |
200000 |
gpt-5.5[512k] |
gpt-5.5 |
512000 |
gpt-5.5[1000000] |
gpt-5.5 |
1000000(纯数字) |
gpt-5.5 |
gpt-5.5 |
无(回落顶层) |
- 单位:
K/k=1000、M/m=1000000 - 后缀在生成 catalog 时剥离,slug 不带后缀进 catalog 与 codex 请求
- 无后缀条目:catalog 里 context_window 留空或回落顶层
model_context_window
3.2 catalog 生成(抄 cc-switch template 思路)
新增函数 generate_profile_model_catalog(home, profile) -> Option<(catalog_path, config_injection)>:
- 解析
profile.model_list,提取带后缀的条目;无后缀条目则 no-op(返回 None) - 取 codex 自带 entry 当 template:
codex debug models --bundled输出,或 fallback 用内置assets/codex-models.json模板 - 每条 clone template 后覆盖:
slug(剥离后缀)/display_name/context_window/max_context_window,保留priority/supported_in_api/visibility等字段 - 包含 profile 全部模型(解决 #1064 单模型副作用):当前 model + model_list 全部条目都写进 catalog,带后缀的写窗口,无后缀的回落顶层
- 写到
<home>/model-catalogs/.json - config.toml 注入
model_catalog_json = "model-catalogs/.json"(相对路径)
3.3 插入点(中间层影响最小)
现有流程:
complete_relay_profile_config → merge_common → preserve_unmanaged
→ apply_context_limits_to_config(写顶层单值)
→ 落盘新增独立可选步骤,插在 apply_context_limits_to_config 之后、落盘之前:
→ apply_context_limits_to_config
→ [新] apply_model_catalog_for_profile(home, profile, &config_with_limits)
→ 落盘- 签名不变:不修改任何现有 apply 函数签名
- opt-in:靠后缀触发,无后缀 no-op
- 旧 profile 零行为变化:那两个「不写 catalog」测试继续过(无需改它们)
3.4 catalog 字段决策
| 字段 | 值 | 依据 |
|---|---|---|
context_window |
后缀解析值 | #931 / cc-switch |
max_context_window |
同 context_window | cc-switch |
effective_context_window_percent |
待验证 | cc-switch 写 95 致显示 950K;若想显示真实值写 100 或省略 |
auto_compact_token_limit |
null | codex 内置模型即 null(按比例算);详见调研文档第六节 |
4. 兼容性
- 无后缀 → no-op,旧 profile 行为不变
- 用户已手写
model_catalog_json指针 → 检测到则跳过生成,不覆盖(保preserves_user_model_catalog_json测试过) model_list不进 config.toml,后缀不污染 codex 请求- 顶层
model_context_window/model_auto_compact_token_limit保留作回退默认
5. 验证计划(阶段一核心)
A. 预检(手工对拍)
手抄 cc-switch 格式 catalog(deepseek-v4-pro→1000000)+ config.toml 指针,直接起 codex 客户端(PureApi,不经 relay 代理),确认报 ~1M。消除「#931 是别人环境结论」疑虑,拿到已知可用样本。
B. 原型产物对拍
实跑原型生成的 catalog + 指针,同样起 codex 客户端,确认报 1M。与 A 样本对比字段。
必验项
- Mac 相对路径:
model-catalogs/.json在 Mac 是否可行(#931 转义结论是 Windows 的) - 字段名:
context_window+max_context_window是否让 codex 生效(#931/cc-switch 一致,但须实跑确认;不认则试n) - auto_compact null:1M 窗口是否在合理比例压缩(非 220K 低值);异常则 fallback 写
×0.85 - effective_percent:写 100 / 省略时显示是否为真实 1M
- 单模型副作用:catalog 含全部模型时,列表是否完整(解决 #1064)
6. 测试计划(主仓风格)
新增 crates/codex-plus-core/tests/relay_config.rs 用例:
- 有后缀 → 生成 catalog 文件 + config.toml 指针 + slug 剥离后缀
- 无后缀 → 不生成(no-op)
- 手写指针 → 不覆盖
- 相对路径格式正确
- catalog 含 profile 全部模型
7. 风险
| 风险 | 应对 |
|---|---|
| codex 不认原型 catalog 字段 | A 预检对拍;试 n 字段;查 codex 版本差异 |
| Mac 相对路径不生效 | 改绝对路径;路径转义按平台处理 |
| template 取不到(codex debug models 不可用) | fallback 内置 assets/codex-models.json 模板 |
| 单模型副作用未解 | catalog 写全部模型;若仍只显示当前 model,查 codex 版本行为 |
8. 后续阶段方向
- 阶段二:解析回传——
parse_model_catalog_json_models保留context_window返回前端;后端完整化 - 阶段三:前端结构化展示(后缀解析高亮/校验)+ auto_compact 按比例 UI + 全测试 + 提 PR
Specs/2026 06 25 Model List Window Split Design
模型列表与上下文窗口分离设计
背景
当前 CodexPlusPlus 的「模型列表」使用单文本框,每行一个模型,支持在模型名后加 [1M]、[200K] 或 [1000000] 等后缀来声明该模型的上下文窗口。
该方案在功能上正确:后端会剥离后缀生成 catalog,Codex 客户端实际使用的是无后缀 slug。但存在以下问题:
- 历史记录污染:Codex 选择器会显示最近使用过的 model ID。如果用户曾经选择过带后缀的字符串(或旧版本把带后缀字符串写入了
threads.model),选择器里就会继续显示deepseek-v4-flash[1M]这类废弃项。 - 用户误选报错:选择带后缀的历史项后,Codex 会把
config.toml的model字段更新为带后缀字符串,而 catalog 里只有无后缀 slug,导致 "is not a valid model ID" 报错。 - 与 cc-switch 的体验不一致:cc-switch 把「模型名」和「上下文窗口」在 UI 与存储层彻底分开,Codex 客户端永远看不到带后缀字符串,历史记录自然干净。
本设计将模型列表改为左右并排的两个输入框:左侧填模型名(无后缀),右侧填上下文窗口,从根上避免 Codex 接触到带后缀的 model ID。
设计目标
- Codex 客户端实际使用的 model ID 永远不带后缀。
- 用户输入方式尽量保持简单:仍然是一个模型一行,只是窗口单独放到右侧。
- 旧数据(
deepseek-v4-flash[1M])自动迁移一次,之后彻底使用新格式。 - 不引入表格、加减行按钮等复杂交互。
数据模型
新增字段
在 RelayProfile 中新增 model_windows 字段:
pub struct RelayProfile {
// ... 现有字段 ...
/// 每行一个无后缀模型 slug。
pub model_list: String,
/// JSON map:slug -> 窗口 token(如 "1M" / "200K" / "1000000")。
/// 空字符串或缺失表示该模型使用 Codex 默认窗口。
#[serde(default, skip_serializing_if = "String::is_empty")]
pub model_windows: String,
}示例
旧格式 model_list:
deepseek/deepseek-v4-flash[1M]
deepseek/deepseek-v4-pro
nvidia/nemotron-3-super-120b-a12b:free[1M]迁移后:
model_list:textdeepseek/deepseek-v4-flash deepseek/deepseek-v4-pro nvidia/nemotron-3-super-120b-a12b:freemodel_windows(JSON):json{ "deepseek/deepseek-v4-flash": "1M", "nvidia/nemotron-3-super-120b-a12b:free": "1M" }
UI 设计
模型列表区域
将当前单文本框拆为左右两个多行文本框:
<div className="model-list-split">
<div className="model-list-column">
<label>模型名称</label>
<textarea
value={draft.modelList}
onChange={handleModelListChange}
placeholder="deepseek/deepseek-v4-flash"
style={{ fontFamily: 'monospace' }}
/>
</div>
<div className="model-list-column">
<label>上下文窗口</label>
<textarea
value={draft.modelWindowsText}
onChange={handleModelWindowsChange}
placeholder="1M"
style={{ fontFamily: 'monospace' }}
/>
</div>
</div>对齐策略
为避免行数多时左右对应关系错位,采用以下策略:
- 等宽字体:左右 textarea 均使用
monospace,相同行高。 - 高度绑定:保存/渲染时让两个 textarea 高度一致。
- 同步滚动:左右滚动时保持
scrollTop同步。 - 行数校验:保存时校验左右行数一致,不一致则阻止保存并提示。
配置模型字段
「配置模型」保持单个输入框,但只接受无后缀 slug:
<input
value={draft.configuredModel}
onChange={handleConfiguredModelChange}
placeholder="deepseek/deepseek-v4-flash"
/>保存时自动剥离后缀(兼容用户手误)。
前端状态与保存逻辑
draft 新增字段
interface RelayProfileDraft {
// ... 现有字段 ...
modelList: string;
modelWindowsText: string; // 新增
configuredModel: string;
}从后端 profile 初始化 draft
const windowsMap = safeJsonParse<Record<string, string>>(profile.model_windows, {});
const modelWindowsText = profile.model_list
.split('\n')
.map(line => windowsMap[line.trim()] ?? '')
.join('\n');保存时组装
const models = draft.modelList
.split('\n')
.map(s => s.trim())
.filter(Boolean);
const windows = draft.modelWindowsText
.split('\n')
.map(s => s.trim());
if (models.length !== windows.length) {
// 阻止保存,提示行数不一致
return;
}
const windowsMap: Record<string, string> = {};
models.forEach((model, index) => {
if (windows[index]) {
windowsMap[model] = windows[index];
}
});
patch.modelList = models.join('\n');
patch.modelWindows = JSON.stringify(windowsMap);后端 apply 流程
旧数据迁移(一次性)
在 settings 加载或 normalize_relay_profile_for_storage 中执行一次性迁移:
if profile.model_windows.is_empty() && profile.model_list.contains('[') {
let (clean_list, windows) = migrate_model_list_with_suffixes(&profile.model_list);
profile.model_list = clean_list;
profile.model_windows = serde_json::to_string(&windows).unwrap_or_default();
}这是一次性迁移。迁移完成后,model_list 不再包含 [suffix] 语法,后续代码也不再把 [suffix] 作为模型列表的合法格式来解析。新 UI 中模型名和窗口是两个独立字段。
catalog 生成
collect_catalog_entries 签名改为接收 model_list 和 model_windows:
pub fn collect_catalog_entries(
model_list: &str,
model_windows: &HashMap<String, String>,
) -> Vec<ModelCatalogEntry>逻辑:
- 解析
model_list为无后缀 slug 列表。 - 对每个 slug,从
model_windows查找窗口 token;未找到则使用 profile 顶层默认窗口。 - 生成 catalog,slug 永远无后缀。
config.toml 写入
complete_relay_profile_config 保持现有逻辑:
model字段写入前调用parse_model_suffix剥离后缀(兼容旧数据/用户手误)。model_catalog_json指向新生成的 catalog 文件。
历史缓存清理
保留现有的启动时清理逻辑:
- 启动前清理
state_5.sqlite的threads.model。 - 启动后通过 CDP 清理 Electron
localStorage.__codexDailyTokenUsageV1。
迁移完成后,Codex 历史记录里不会再出现新的带后缀项。
测试策略
后端测试
| 测试 | 位置 | 说明 |
|---|---|---|
model_windows 解析 |
tests/model_suffix.rs |
验证 1M / 200K / 1000000 解析为数字窗口 |
旧 model_list 迁移 |
tests/settings.rs 或新文件 |
deepseek-v4-flash[1M] 迁移为 model_list + model_windows |
| catalog 生成 | tests/relay_config.rs |
用分离后的格式生成 catalog,slug 无后缀 |
| 窗口为空使用默认值 | tests/relay_config.rs |
无窗口项使用 Codex 默认窗口 |
| 同名 slug 按有窗口处理 | tests/relay_config.rs |
已有测试需适配新签名 |
前端测试
| 测试 | 说明 |
|---|---|
| 渲染测试 | 两个 textarea 存在,placeholder 正确 |
| 输入同步测试 | 修改左侧不影响右侧,反之亦然 |
| 行数校验测试 | 左右行数不一致时阻止保存并提示 |
| 保存组装测试 | 输入模型列表和窗口列表后,model_windows JSON 正确 |
| 旧数据迁移显示测试 | 给定旧 model_list 含后缀,draft 初始化后显示为分离的两列 |
兼容性
- 旧格式迁移:settings 加载时自动把
model_list中的[suffix]拆出到model_windows。 - Codex 客户端:始终使用无后缀 model ID,不会再产生带后缀历史项。
- 手动编辑 settings 文件:即使用户手动写回带后缀字符串,下次加载时也会重新迁移。
- 不写窗口的模型:保持 Codex 默认行为,与现有逻辑一致。
后续可扩展
- 右侧窗口输入框未来可扩展为下拉选择(常用预设)+ 自由输入的混合形态。
- 可在每行右侧增加「自动检测」按钮,根据模型名从内置表猜测默认窗口。
Superpowers/Specs/2026 05 11 User Scripts Design
User Scripts Design
Goal
Add a Codex++ user script system so users can extend the injected renderer behavior with their own JavaScript files without modifying the project source.
Architecture
Codex++ keeps local file access in Python and browser execution in the existing CDP-injected renderer. Python scans two script directories, stores enablement configuration in the user config directory, injects enabled scripts through CDP, and exposes script inventory/actions through the existing bridge. The renderer only displays the script manager UI and sends configuration/reload requests.
Script locations
- Built-in scripts directory:
codex_session_delete/user_scripts/ - User scripts directory: platform user config directory plus
Codex++/user_scripts/- Windows:
%APPDATA%/Codex++/user_scripts/ - Non-Windows: existing user config home pattern plus
Codex++/user_scripts/
- Windows:
The user directory is created automatically. Missing built-in directory is treated as empty. Only top-level *.js files are loaded in the first version.
Configuration
Script settings are persisted as JSON in the user config directory, e.g. %APPDATA%/Codex++/user_scripts.json:
{
"enabled": true,
"scripts": {
"builtin:example.js": true,
"user:my-script.js": false
}
}Newly discovered scripts default to enabled. The global enabled flag controls whether any script executes. Per-script flags control individual scripts. Disabled scripts remain visible in the UI.
Loading behavior
On launch, Python scans built-in scripts then user scripts, sorted by file name within each source. If the global switch is enabled, only individually enabled scripts are injected. Each script is wrapped so one failure does not stop other scripts and so status is written to window.__codexPlusUserScripts.
Reloading scripts from the menu rescans directories and reinjects currently enabled scripts into the current page. Disabling a script does not undo side effects from code that has already run; the UI states that a page reload/restart may be needed to fully remove an already-executed script effect.
Renderer UI
The Codex++ modal gains a “用户脚本” section:
- Global switch: “启用用户脚本”
- Directory hints for built-in and user script directories
- Script list with name, source, status, and per-script toggle
- “重新加载用户脚本” button
- Short warning that disabling a previously executed script may require page reload/restart
Toggles call bridge routes to update JSON config. Reload calls a bridge route that rescans and injects scripts, then returns inventory/status for rendering.
Bridge routes
Add routes handled by Python:
/user-scripts/list: returns directories, global enabled flag, scripts, and statuses/user-scripts/set-enabled: updates the global enabled flag/user-scripts/set-script-enabled: updates one script flag/user-scripts/reload: rescans and injects enabled scripts into current page, returning updated status
Script author contract
A user script is a normal JavaScript file executed in the Codex renderer page context. Scripts should be idempotent if they may be reloaded:
if (window.__myScriptInstalled) return;
window.__myScriptInstalled = true;Testing
- Python unit tests cover scanning, sorting, config defaults/persistence, enable flags, wrapper generation, and bridge routes.
- CDP tests cover injecting concatenated enabled script wrappers.
- Renderer source-contract tests cover the user script UI, global toggle, per-script toggle, reload button, and warning text.
Superpowers/Specs/2026 05 12 Conversation Timeline Design
Conversation Timeline Design
Goal
Add a lightweight timeline to the conversation page so users can see each question they asked and jump back to it quickly.
User experience
A fixed vertical timeline appears near the right edge of the active conversation page. Each user question is represented by one circular marker. Markers are light gray by default and become darker on hover or when active.
Hovering a marker opens a dark tooltip to the left of the marker. The tooltip shows a single-line question summary capped at 40 characters, with overflow replaced by an ellipsis.
Clicking a marker smoothly scrolls the conversation to the corresponding user message and briefly highlights that message so the jump target is clear.
Architecture
Implement the feature in codex_session_delete/inject/renderer-inject.js as renderer-side DOM injection, matching the existing Codex++ enhancement pattern.
The feature does not add a backend API and does not modify the Codex app bundle. It injects:
- timeline CSS through the existing style injection path
- one fixed timeline container in the conversation page
- one marker per detected user question
- tooltip and active/highlight behavior through DOM event listeners
Data source
The injected script scans the current conversation DOM for user message nodes. For each user message it records:
- the message DOM node used as the scroll target
- plain text content used for the tooltip summary
- the message position used to place the timeline marker
Detection should prefer stable role/message structure inside the Codex conversation content. If no explicit role marker is available, matching should stay inside the main conversation region and conservatively match user-styled message nodes to avoid collecting sidebar, menu, or settings text.
Empty or whitespace-only user messages are skipped.
Update flow
A MutationObserver watches the conversation area and rebuilds the timeline when messages change, the user sends a new question, or the active conversation rerenders. Rebuilds must remove previous timeline markers before inserting new ones so duplicate markers are not created.
If no user questions are detected, the timeline container is hidden or removed. If one question is detected, the timeline still shows one marker.
Marker positioning
Markers are distributed along the fixed right-side vertical track based on each message's relative position in the current conversation scrollable area. This keeps markers aligned with the rough location of each question without requiring complex virtualization support.
When markers are close together they may visually cluster; the first version will not add grouping or aggregation.
Edge cases
- No user questions: do not show the timeline.
- One user question: show one marker.
- Empty user message: skip it.
- Long user question: truncate only the tooltip summary, not the source text or scroll target.
- Conversation rerender: rebuild without duplicating containers or markers.
- Missing conversation container: do nothing and retry on later mutations.
Testing
Automated tests should cover the pure logic where practical:
- 40-character summary truncation with ellipsis
- empty/whitespace questions are skipped
- timeline rebuild removes old markers before inserting new ones
- marker click calls scroll behavior and applies a temporary highlight class to the target message
Manual UI verification is required after implementation:
- Open a conversation with multiple user questions.
- Confirm the right-side vertical timeline appears.
- Hover each marker and confirm the tooltip shows the expected question summary.
- Click a marker and confirm the conversation scrolls to the matching user message.
- Send a new question and confirm a new marker appears without duplicating old markers.
Superpowers/Specs/2026 05 13 Provider Sync Design
Provider Sync Design
Goal
Add an optional Codex++ startup sync that keeps existing Codex conversations visible after the user changes model_provider.
User experience
Codex++ settings gains a Provider 同步 toggle. The toggle is off by default.
When the toggle is off, Codex++ launch behavior is unchanged.
When the toggle is on, Codex++ runs provider sync before launching Codex. The sync targets the current root model_provider from ~/.codex/config.toml; it does not switch providers or edit config.toml.
If sync is skipped because another sync is locked, SQLite is busy, or session files are in use, Codex++ writes the reason to ~/.codex-session-delete/launcher.log and continues launching Codex. If sync starts writing and then cannot safely restore or finish, launch fails and the error is written to the launcher log.
Architecture
Implement the feature inside Codex++ as Python code. Do not depend on the external Node.js CLI from Dailin521/codex-provider-sync, because Codex++ should not require Node 24 or a separate global package.
Add three focused areas:
codex_session_delete/settings_store.pystores backend-readable Codex++ settings in~/.codex-session-delete/settings.json.codex_session_delete/provider_sync.pyports the needed provider sync behavior to Python.- Existing launcher and bridge code call the sync and expose settings to the injected menu.
The renderer menu still uses existing visual patterns, but the new setting is backend-backed rather than only localStorage, because startup sync must be readable before Codex launches.
Settings storage
Store backend settings at:
~/.codex-session-delete/settings.jsonInitial content is implicit; if the file is missing or malformed, Codex++ uses defaults:
{
"providerSyncEnabled": false
}Only backend-required settings should live in this file. Existing renderer-only settings remain in localStorage unless they must be read before launch.
Expose bridge endpoints:
/settings/getreturns merged defaults and saved backend settings./settings/setaccepts a booleanproviderSyncEnabled, writes the file atomically, and returns the merged settings.
Startup flow
launch_and_inject() resolves the Codex app directory and ports as it does today. Before launch_codex_app(), it loads backend settings. If providerSyncEnabled is true, it runs provider sync against the default Codex home ~/.codex.
The watcher uses the same launcher path, so watcher-triggered launches also honor the setting.
Sync behavior
Provider sync reads the current provider from root-level model_provider = "..." in ~/.codex/config.toml. If the root setting is absent, the target provider is openai.
The sync updates these locations to the target provider:
- First-line metadata records in
~/.codex/sessions/**/rollout-*.jsonl. - First-line metadata records in
~/.codex/archived_sessions/**/rollout-*.jsonl. threads.model_providerin~/.codex/state_5.sqlite.
It also preserves the visibility repairs from the external project where practical:
- If a rollout file contains user events for a thread id, set that SQLite thread row's
has_user_eventto1when the column exists. - If rollout metadata or events expose a thread working directory, update that SQLite thread row's
cwdwhen the column exists. - Normalize
.codex-global-state.jsonworkspace paths using the SQLitethreads.cwdvalues when possible.
The first version does not implement provider switching, restore commands, status UI, or a standalone CLI.
Locking and backups
Use lock directory:
~/.codex/tmp/provider-sync.lockIf the lock already exists, skip sync and continue launch.
Before writing, create a backup under:
~/.codex/backups_state/provider-sync/<timestamp>The backup includes:
config.tomlif present..codex-global-state.jsonand.codex-global-state.json.bakif present.state_5.sqliteand sidecar-wal/-shmfiles if present.- A JSON manifest containing original first-line metadata for rollout files that will be changed.
After successful sync, prune managed provider-sync backups to keep the newest 5.
Error handling
Skippable conditions continue launch and log a concise message:
- Sync lock already exists.
- SQLite database is busy before writes begin.
- Rollout files are locked or cannot be opened for rewrite.
Fatal conditions stop launch:
- Backup creation fails after sync has decided it needs to write.
- A write starts and then fails, and rollback/restore also fails.
- The Codex home exists but state files are malformed in a way that prevents safe updates.
Testing
Automated tests should cover:
- Backend settings default to
providerSyncEnabled = false. - Backend settings are saved and reloaded from
~/.codex-session-delete/settings.json. - Bridge endpoints read and write
providerSyncEnabled. - Launcher calls provider sync before launching Codex only when the setting is enabled.
- Lock-existing sync result is skipped and does not block launch.
- Rollout first-line metadata is updated to the current config provider.
- SQLite
threads.model_provideris updated to the current config provider. - Backups are created before writes and pruned to 5 managed backups.
Manual verification should cover:
- Turn on
Provider 同步in the Codex++ menu. - Close Codex.
- Change
model_providerin~/.codex/config.toml. - Launch Codex++.
- Confirm older conversations remain visible in Codex Desktop and
/resume. - Confirm lock-existing behavior still launches Codex and writes a launcher log entry.
Superpowers/Specs/2026 05 16 Rust Tauri Migration Design
Codex++ Rust/Tauri Migration Design
Summary
Codex++ will fully replace the current Python backend with Rust and add a Tauri management console. The existing Codex App enhancement model remains: Codex++ launches Codex with CDP flags, injects a bridge plus renderer-inject.js, and handles local operations such as delete, undo, export, move, settings, status, provider sync, and user scripts.
The final user experience has two desktop entry points:
- Codex++: a no-window silent launcher. Double-clicking it starts enhanced Codex directly.
- Codex++ 管理工具: a visible Tauri management app for install, uninstall, update, settings, logs, diagnostics, shortcut repair, and optional manual launch.
There is no separate user-facing CLI. Launch behavior belongs to the silent launcher, and management features belong to the Tauri console.
Goals
- Replace the Python backend completely with Rust.
- Preserve the existing injected Codex App enhancements and user-facing behavior.
- Add a Tauri management console without making it required for daily launch.
- Generate two desktop entry points: silent launcher and management tool.
- Use one shared Rust core for the silent launcher and Tauri commands.
- Migrate safely by building Rust behavior beside Python first, then removing Python after parity is verified.
Non-Goals
- Replacing
renderer-inject.jswith a full Tauri-native Codex UI. - Moving session management into the Tauri app as a standalone conversation browser.
- Providing a separate user-facing CLI.
- Requiring the management tool to stay open while Codex++ is running.
Architecture
The Rust project will be organized as a workspace:
codex-plus-core- Codex app path resolution.
- Loopback port selection.
- Codex process launch and lifecycle handling.
- CDP target discovery, websocket communication, bridge installation, script injection, and DevTools opening.
- Bridge request routing for injected UI calls.
- Settings, logs, assets, status, and diagnostics primitives.
codex-plus-data- SQLite schema detection.
- Local delete and undo backup.
- Workspace move operations.
- Markdown export.
- Provider sync across rollout files, SQLite rows, and global state metadata.
codex-plus-launcher- No-window silent launcher binary.
- Supports internal launch configuration such as app path, database path, backup path, debug port, and helper port.
- Used by the
Codex++desktop entry point.
codex-plus-tauri- Visible management console.
- Tauri commands call
codex-plus-core; no duplicated business logic. - Provides install, uninstall, update, settings, logs, diagnostics, shortcut repair, and optional manual launch controls.
renderer-inject.js- Remains the injected Codex renderer enhancement script.
- Receives helper/bridge configuration from Rust during injection.
- Keeps the current Codex App in-place enhancements: menu, delete, undo, export, move, settings panel, timeline, plugin unlocks, user scripts, and ads/sponsor assets.
Entry Points
Codex++
The Codex++ desktop entry point is silent:
- It starts the no-window Rust launcher.
- It does not show a Tauri management window.
- It launches Codex App with CDP flags.
- It starts the local Rust bridge/helper runtime.
- It injects
renderer-inject.js. - It stays alive until Codex exits.
Codex++ 管理工具
The management tool opens a Tauri window and owns management workflows:
- Install and uninstall Codex++ entry points.
- Check for updates and perform updates.
- Edit backend settings.
- View latest launch status.
- Open logs and copy diagnostics.
- Repair or recreate the silent
Codex++shortcut. - Optionally launch or repair a running Codex++ session.
The management tool is not required during normal use.
Runtime Flow
Silent launch flow:
- User double-clicks
Codex++. - The no-window Rust launcher starts without a visible management UI.
- Rust chooses available loopback ports for CDP and helper/bridge runtime.
- If Provider Sync is enabled, Rust updates local Codex metadata before launch.
- Rust launches Codex with:
--remote-debugging-port=<debug_port>--remote-allow-origins=http://127.0.0.1:<debug_port>
- Rust starts the helper/bridge runtime.
- Rust discovers the Codex page target through CDP.
- Rust installs
Runtime.addBinding, injects the bridge script, and injectsrenderer-inject.js. - Rust evaluates enabled user scripts.
- Runtime stays alive until Codex exits, then shuts down helper resources.
Bridge request flow:
- Injected UI calls the bridge from
renderer-inject.js. - CDP delivers a
Runtime.bindingCalledevent to Rust. - Rust routes the request by path.
codex-plus-coreandcodex-plus-dataexecute the operation.- Rust resolves or rejects the browser-side promise.
- The injected UI shows a toast, updates settings, refreshes status, or updates the current UI state.
Management tool flow:
- User opens
Codex++ 管理工具. - Tauri UI calls Rust commands.
- Commands call shared core/data modules.
- UI displays results for install, update, settings, logs, diagnostics, repair, and launch actions.
Error Handling And Logging
Silent launch should remain quiet but leave actionable diagnostics.
Automatic recovery:
- Pick another helper or debug port if the requested port is unavailable.
- Retry CDP discovery and injection for a bounded number of attempts.
- Continue detecting common local proxy ports when no proxy environment is configured.
- Avoid killing the current launcher process while cleaning up stale launchers.
Failure recording:
- Write human-readable launch logs.
- Write a latest structured status file for the management tool.
- Include error codes or categories where useful, such as app-not-found, port-unavailable, cdp-target-not-found, injection-failed, sqlite-error, update-error, and shortcut-error.
- Keep operation-level failures visible in the injected Codex UI where the action occurred.
Management diagnostics:
- Show Codex app detection status.
- Show whether shortcuts are installed.
- Show latest launch result and timestamp.
- Show debug/helper ports used by the latest run.
- Show settings file and database path.
- Provide actions to open logs, copy diagnostics, repair shortcuts, retry launch, reset settings, and check updates.
Installation And Update Behavior
Windows install creates two desktop shortcuts:
Codex++.lnk: silent launcher, no management window.Codex++ 管理工具.lnk: opens the Tauri management console.
Windows packaging uses separate entry binaries so window behavior is predictable:
codex-plus-plus.exe: no-window silent launcher for theCodex++shortcut.codex-plus-plus-manager.exe: Tauri management console forCodex++ 管理工具.
Windows uninstall removes both shortcuts and the uninstall registry entry. Optional data removal deletes Codex++-owned data such as logs, settings, and backups.
macOS install provides equivalent two-entry behavior with two app bundles:
Codex++.app: silent launch wrapper.Codex++ 管理工具.app: visible management console.
Updates are initiated from the management console. No separate command-line update entry point is provided.
Management Console UI
The Tauri management console uses a workbench layout rather than a marketing page:
- Left navigation: Overview, Launch, Install, Update, Settings, Logs, Diagnostics.
- Overview: shortcut status, Codex app detection, latest launch result, current version, update status, and quick actions.
- Launch: manual launch button, app path override, debug/helper port settings, and repair backend action.
- Install: install, uninstall, repair shortcuts, and optional remove owned data.
- Update: check update, release summary, download/install progress, and restart guidance.
- Settings: provider sync, Codex command wrapper settings, proxy-related environment summary, and user script enablement.
- Logs: latest launcher/helper logs with copy/open controls.
- Diagnostics: bundled report for issue reporting, including paths, version, OS, shortcuts, ports, settings location, and latest status.
Migration Strategy
Use a parallel Rust replacement strategy:
- Add the Rust workspace beside the existing Python package.
- Port behavior by domain: models/settings, data operations, CDP/bridge, launch lifecycle, install/update/watcher, Tauri management UI.
- Keep Python tests and modules as behavior references during migration.
- Add Rust tests and integration fixtures for each migrated domain.
- Switch entry points to Rust only after launch, bridge, data operations, install/update, watcher, and management UI are verified.
- Remove Python package files, Python packaging metadata, and obsolete tests after parity is established.
- Update README and setup instructions to describe the Rust/Tauri distribution.
Testing Strategy
Core unit tests:
- Settings serialization and persistence.
- Version parsing and update asset selection.
- Path resolution.
- Loopback port selection.
- SQLite schema detection.
- Backup and undo serialization.
- Provider sync rollout transformations.
Integration tests:
- SQLite delete, undo, move, sort key, and markdown export using fixtures.
- Bridge route dispatch.
- CDP target selection and bridge script construction.
- Shortcut/app wrapper generation.
- Updater download and install orchestration with mocked network/process calls.
- Watcher behavior where platform facilities can be safely mocked.
Application checks:
- Tauri management app builds.
- Management window opens and can call status/settings/log commands.
- Silent launcher dry run validates app resolution and configuration.
- Renderer injection remains compatible with Rust bridge responses.
- Windows and macOS packaging produce two visible user entry points.
Python removal gate:
Python files should only be deleted after the Rust implementation passes parity checks for:
- Silent launch.
- Codex App CDP injection.
- Bridge routes.
- Delete and undo.
- Markdown export.
- Session workspace move.
- Provider sync.
- Settings.
- Install and uninstall.
- Update.
- Watcher.
- Management tool.
Release Artifacts
Release artifacts should include platform installers or archives that contain the silent launcher, management app, injected renderer assets, icons, and required metadata. Exact installer technology can be selected during implementation, but the release must install the two user-facing desktop entry points and keep update actions inside the management console.
Superpowers/Specs/2026 05 21 Script Market Design
脚本市场设计
目标
把管理工具里的“用户脚本”页改造为“脚本市场”,基于 https://github.com/BigPizzaV3/CodexPlusPlusScriptMarket 提供的静态清单完成远程脚本浏览、安装、更新和本地状态展示。第一版不引入独立后端服务,市场仓库只需要托管 JSON 清单和脚本文件。
市场仓库格式
默认市场清单地址:
https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/index.json
清单版本为 version: 1,结构如下:
{
"version": 1,
"updated_at": "2026-05-21T00:00:00Z",
"scripts": [
{
"id": "demo-script",
"name": "Demo Script",
"description": "脚本用途说明",
"version": "1.0.0",
"author": "BigPizzaV3",
"tags": ["ui", "productivity"],
"homepage": "https://github.com/BigPizzaV3/CodexPlusPlusScriptMarket",
"script_url": "https://raw.githubusercontent.com/BigPizzaV3/CodexPlusPlusScriptMarket/main/scripts/demo-script.js",
"sha256": ""
}
]
}必填字段是 id、name、version、script_url。description、author、tags、homepage、sha256 可以为空或缺失。sha256 为空时跳过完整性校验;非空时必须匹配下载内容的 SHA-256 十六进制摘要。
后端设计
现有 UserScriptManager 继续负责本地脚本目录、启用状态和注入 bundle。新增脚本市场能力复用同一个用户脚本目录,不改变内置脚本和用户手动脚本的加载方式。
新增行为:
- 拉取远程市场清单并解析为稳定 JSON 结构。
- 过滤无效条目,保留有效条目和市场加载错误信息。
- 安装市场脚本时下载
script_url指向的 JS 内容。 - 安装文件名固定为
market-.js,避免覆盖用户手动放入的脚本。 - 安装成功后在
user_scripts.json记录市场元数据,包括市场脚本 id、版本、来源 URL、homepage 和安装时间。 - 更新判断基于本地记录的版本和市场清单版本。版本字符串不同即显示“可更新”,不做语义化版本比较。
- 如果
sha256校验失败,安装失败且不替换已有脚本。
inventory() 返回的本地脚本条目扩展字段:
market_idversioninstalledsource_urlhomepage
原有字段 key、name、source、enabled、status、error 保持兼容。
Tauri 命令设计
管理工具新增命令:
refresh_script_market:拉取市场清单,返回市场条目、加载状态和当前本地 inventory。install_market_script(id):根据市场清单中的id下载并安装脚本,返回更新后的市场状态和本地 inventory。
第一版不做后台缓存刷新。页面进入脚本市场时读取 settings 中已有的本地 inventory,再由用户点击刷新市场触发远程加载;如果网络失败,本地脚本列表仍可用。
前端设计
把导航和页面标题从“用户脚本”改为“脚本市场”。页面包含两个区域:
市场概览和操作区:
- 市场加载状态
- 远程脚本数量
- 已安装市场脚本数量
- 用户脚本整体启用状态
- 刷新市场按钮
市场列表和本地列表:
- 市场卡片展示名称、描述、版本、作者、标签和状态。
- 状态包括“未安装”“已安装”“可更新”。
- 操作包括安装、更新、打开主页。
- 保留本地脚本列表,展示内置脚本、用户手动脚本和市场安装脚本的当前启用/关闭状态。
第一版不在管理工具里提供删除脚本或启停单个脚本,单脚本启停仍由注入菜单里的现有用户脚本控制保留。
错误处理
- 市场清单加载失败:前端展示错误消息,保留本地脚本列表。
- 清单 JSON 无效:命令返回失败状态和错误消息,不写入任何本地文件。
- 单个市场条目缺必填字段:过滤该条目,不影响其他条目。
- 下载失败:安装失败,不改变已有脚本。
- checksum 不匹配:安装失败,不替换已有脚本。
- 写入失败:返回失败状态,前端展示错误消息。
测试
Rust 测试覆盖:
- 市场清单解析和无效条目过滤。
- 安装市场脚本写入
market-.js。 - 安装后
user_scripts.json记录市场元数据。 - checksum 失败不替换已有脚本。
- inventory 能合并市场安装元数据。
前端验证:
- TypeScript check 通过。
- 构建通过或至少管理工具前端 check 通过。
非目标
- 不做脚本评分、评论、收藏。
- 不接 GitHub API。
- 不实现仓库端自动生成清单。
- 不提供脚本删除功能。
- 不修改 Codex App 原始文件。
Superpowers/Specs/2026 05 22 Upstream Branch Dropdown Design
Upstream Branch Dropdown Design
Goal
Make upstream-based worktree creation feel native in Codex App by adding upstream/* choices to the start-new-chat branch dropdown instead of exposing a separate Codex++ worktree dialog.
User Experience
On the start-new-chat screen, the bottom controls already separate two decisions:
New worktreedecides that the next chat should run in a new worktree.- The branch dropdown decides which branch or ref the worktree should start from.
Codex++ should extend the branch dropdown with an Upstream group. For a repository on main, the user should be able to pick upstream/main from the same branch menu shown in the screenshot. After that, the existing New worktree flow should create the worktree from the selected upstream tracking ref.
The user should not need to open a Codex++ modal or manually enter repo path, branch name, worktree path, remote, or base branch.
Design
Codex++ keeps the existing backend bridge and upstream worktree creation core, then adds a renderer-side adapter for the native branch selector.
The adapter has three responsibilities:
- Discover native branch dropdown menus when they open.
- Inject an
Upstreamsection containing remote tracking refs such asupstream/main. - When an injected upstream item is selected, record the selected upstream ref in page state and mark the native worktree flow so the later create action uses the upstream route.
The first iteration will provide upstream/<current-branch> because it is reliable and matches the user's main use case. If backend defaults can read more branches later, the same UI section can list additional upstream refs without changing the user model.
Backend Contract
/upstream-worktree/defaults should return enough data for the dropdown adapter:
repoRootcurrentBranchdefaultRemotedefaultBaseBranchupstreamRefs
upstreamRefs contains display refs like upstream/main plus structured fields:
{
"remote": "upstream",
"branch": "main",
"label": "upstream/main",
"sourceRef": "refs/remotes/upstream/main"
}The create route continues to receive repoPath, branchName, worktreePath, remote, baseBranch, and fetch.
Renderer Behavior
The renderer adapter should be conservative:
- It only mutates open menus that look like Codex branch dropdown menus.
- It adds identifiable
data-codex-upstream-branch-optionnodes so repeated scans do not duplicate options. - It does not remove or rename native local branch options.
- It does not block the native flow unless a complete upstream selection and create payload are available.
- If Codex DOM changes and the adapter cannot safely detect the native menu, it should leave Codex behavior unchanged.
Selecting an upstream item should visibly update the branch dropdown label to upstream/main when possible. If Codex re-renders and overwrites that label, the selected state still remains in Codex++ runtime until the user changes project/branch or the page reloads.
Error Handling
If upstream is missing, show no injected upstream option or show a disabled explanatory option. Creating from an upstream selection should fail with the existing backend error message if fetch or worktree creation fails.
If Codex++ cannot derive the native new-branch/worktree payload, it should not create a worktree and should show a short toast explaining that the current Codex version's native worktree form could not be recognized.
Tests
Add tests at the script level first:
- The injection script contains the branch dropdown adapter functions and data markers.
- The defaults response returns
upstreamRefsfor the current branch and preferred remote. - Existing create tests still prove
refs/remotes/<remote>/is used internally.
Run these checks:
cargo test -p codex-plus-core --test upstream_worktreecargo test -p codex-plus-core --test cdp_bridgecargo test -p codex-plus-core --test bridge_routesnode --check assets/inject/renderer-inject.jscargo test -p codex-plus-core
Out of Scope
This change will not patch Codex App's bundled source, will not require users to type worktree paths manually, and will not replace the full native branch picker implementation. It only injects remote branch choices and routes upstream selections through the Codex++ bridge.
Superpowers/Specs/2026 05 22 Upstream Worktree Design
Upstream-Based Worktree Creation Design
Summary
Codex++ will add an optional enhancement for Codex App worktree creation. The feature makes new worktrees start from a remote tracking branch such as upstream/main, instead of inheriting a stale local HEAD or unsynced branch state.
The intended Git equivalent is:
git fetch upstream <base-branch>
git worktree add -b <new-branch> <worktree-path> upstream/<base-branch>The implementation will first add a reliable Rust backend capability and a Codex++ menu entry. After that is tested, the renderer injection can enhance Codex App's native worktree creation UI when the current Codex version exposes a recognizable action point. If the native action point cannot be detected, Codex++ must show a clear fallback message and keep the menu entry usable.
Goals
- Create new worktrees from a remote tracking ref, defaulting to
upstream/. - Reduce conflicts caused by Codex App creating worktrees from stale local state.
- Keep the Git operation explicit and testable in Rust, not hidden inside the renderer script.
- Provide a stable Codex++ menu workflow even when Codex App UI internals change.
- Enhance the native Codex App worktree flow when it can be detected safely.
- Report actionable errors for missing remotes, missing base branches, existing branches, existing paths, and invalid repositories.
Non-Goals
- Replacing Git's own branch, worktree, or conflict behavior.
- Force-resetting local branches or deleting existing worktrees.
- Automatically pushing branches or changing upstream tracking for existing branches.
- Modifying
/Applications/Codex.appbundle files directly. - Supporting non-Git repositories.
User Experience
Codex++ settings gains an Upstream worktree enhancement toggle. The toggle is enabled only when backend support is available.
When the user uses the Codex++ menu entry, Codex++ asks for:
- Repository path.
- New branch name.
- Worktree path.
- Base branch, defaulting to the current branch name when available, otherwise
main. - Remote name, defaulting to
upstream, withoriginavailable only whenupstreamis absent and the user explicitly chooses or confirms fallback behavior.
The primary action label should make the Git source clear, for example:
Create from upstream/<base-branch>On success, Codex++ shows the created worktree path and source ref. On failure, Codex++ shows a concise error and keeps the user's entered values so they can fix the issue.
When native Codex App worktree enhancement is active, the user can continue using Codex's normal worktree creation path. Codex++ intercepts or augments the action only when it can identify the repository path, new branch name, worktree path, and base branch. If any required value is ambiguous, Codex++ does not run Git. It shows a toast that says the native flow could not be safely enhanced and points the user to the Codex++ menu entry.
Backend Architecture
Add a focused Rust module in crates/codex-plus-core, tentatively named upstream_worktree.
The module owns:
- Repository discovery with
git -C <repo> rev-parse --show-toplevel. - Current branch detection with
git -C <repo> branch --show-current. - Remote validation with
git -C <repo> remote. - Base ref validation against
<remote>/after fetch. - Safe command construction for
git fetch <remote>. - Safe command construction for
git worktree add -b <new-branch> <worktree-path> <remote>/. - Error classification and structured responses.
The module should avoid shell interpolation. Use std::process::Command or Tokio process APIs with each Git argument passed separately. This preserves paths with spaces and prevents branch names or paths from changing command structure.
Bridge Routes
Extend the existing bridge route system with:
/upstream-worktree/status/upstream-worktree/defaults/upstream-worktree/create
/upstream-worktree/status returns whether Git is available and whether the current platform/backend supports the feature.
/upstream-worktree/defaults accepts a repository path and returns detected values:
{
"repoRoot": "/path/to/repo",
"currentBranch": "feature/demo",
"defaultBaseBranch": "feature/demo",
"remotes": ["origin", "upstream"],
"defaultRemote": "upstream"
}/upstream-worktree/create accepts:
{
"repoPath": "/path/to/repo",
"branchName": "feature/demo-codex",
"worktreePath": "/path/to/worktrees/demo-codex",
"remote": "upstream",
"baseBranch": "main",
"fetch": true
}It returns either:
{
"status": "ok",
"repoRoot": "/path/to/repo",
"worktreePath": "/path/to/worktrees/demo-codex",
"branchName": "feature/demo-codex",
"sourceRef": "upstream/main"
}or:
{
"status": "failed",
"code": "branch-exists",
"message": "Branch already exists: feature/demo-codex"
}Validation Rules
Validate input before running Git:
repoPathmust resolve inside a Git worktree.branchNamemust be non-empty and must passgit check-ref-format --branch.worktreePathmust be absolute or resolvable to an absolute path; if it exists, it must be empty only if Git accepts it forworktree add.remotemust be a configured remote.baseBranchmust be non-empty and must not contain path traversal semantics.- Source ref is always constructed as
<remote>/after validation.
Fetch should be enabled by default. If fetch fails because the network or remote is unavailable, the create step must not run, because the feature's purpose is to avoid stale refs.
Error Handling
Return stable error codes for UI handling:
git-missingnot-git-reporemote-missingbase-branch-missingfetch-failedbranch-invalidbranch-existspath-existsworktree-create-failedambiguous-native-flow
Include stderr in diagnostic logs, but keep renderer-facing messages short and safe.
Renderer Injection
Add the menu workflow first. It should reuse existing Codex++ injected menu patterns instead of creating a visually separate system.
The renderer script should:
- Check
/upstream-worktree/status. - Show an
Upstream worktreesetting row and action entry when supported. - Open a compact modal for repo path, branch, worktree path, remote, and base branch.
- Call
/upstream-worktree/defaultsafter the repo path changes. - Call
/upstream-worktree/createon submit. - Display success or failure through existing toast patterns.
Native Codex App enhancement should be implemented as a guarded adapter:
- Detect known Codex worktree creation controls through stable attributes first, then text/structure as fallback.
- Extract repository path, branch name, target path, and base branch before intercepting.
- If extraction is incomplete, do not prevent the native action unless Codex++ can show a clear fallback.
- Keep the adapter versioned so future Codex App DOM changes can be diagnosed.
Settings
Add a renderer setting key for the injected UI, for example upstreamWorktreeCreate.
If the setting only affects renderer behavior, it can remain in the existing local settings path. If a future version needs launcher-time behavior, move it to backend settings like provider sync.
Testing Strategy
Rust tests should cover:
- Command argument construction uses separate arguments, not shell strings.
- Branch name validation accepts normal branch names and rejects invalid refs.
- Remote selection prefers
upstreamwhen present. originfallback is not automatic whenupstreamis present.- Create flow stops after fetch failure.
- Structured error codes map from common Git failures.
- Bridge route payloads call the upstream worktree runtime correctly.
Integration tests should use temporary Git repositories:
- Create a bare remote named
upstream. - Push a base branch to it.
- Clone or initialize a local repository with a stale local branch.
- Call the Rust create function.
- Assert the new worktree's
HEADequalsupstream/at creation time.
Renderer tests can be lightweight and focused on pure helpers:
- Default form state from
/defaults. - Create payload construction.
- Error message rendering.
- Native adapter refusing ambiguous DOM.
Manual verification should cover:
- A repository with
upstream/mainnewer than localmain. - Creating
feature/testat a new worktree path through Codex++ menu. - Confirming
git -C <worktree-path> rev-parse HEADmatchesgit -C <repo> rev-parse upstream/main. - Confirming a duplicate branch gives a clear
branch-existserror. - Confirming a missing
upstreamremote gives a clearremote-missingerror. - Confirming native Codex worktree enhancement either creates correctly or falls back without corrupting the native flow.
Rollout Plan
Phase 1 adds the Rust backend module, bridge routes, tests, and Codex++ menu entry.
Phase 2 adds native Codex App worktree UI detection and guarded interception.
Phase 3 updates README feature bullets and troubleshooting notes after the behavior is verified against the current Codex App version.
This staged rollout keeps the Git operation reliable before depending on Codex App's internal UI shape.
Superpowers/Specs/2026 05 25 Codex Context Management Design
Codex Context Management Design
Goal
Add Codex-only management for MCP servers, skills, and plugins, using the same switching concept as CCSwitch while staying inside Codex++'s current settings and relay profile model.
This feature manages shared Codex context entries from the existing relayCommonConfigContents TOML snippet and lets each supplier profile choose which entries are active when that supplier is switched on.
Scope
In scope:
- Manage Codex
config.tomlcontext sections:[mcp_servers.*][skills.*][plugins.*]
- Provide UI to add, edit, delete, and enable context entries per supplier.
- Store all reusable context entries in
relayCommonConfigContents. - Store each supplier's selected context ids on that supplier.
- Add supplier-level inputs for:
- context window size, written as
model_context_window - auto compact limit, written as
model_auto_compact_token_limit
- context window size, written as
- Apply only the selected context entries when switching a supplier.
- Preserve existing raw TOML editing for advanced users.
Out of scope:
- Cross-application management for Claude, Gemini, OpenCode, Hermes, or Claude Desktop.
- CCSwitch database migration or database-backed MCP/skill/plugin registries.
- Installing skills/plugins from remote repositories.
- Runtime validation that Codex actually honors context-size settings in every Codex version.
Data Model
BackendSettings keeps the shared TOML in relayCommonConfigContents.
RelayProfile gains supplier-level context configuration:
contextSelection: {
mcpServers: string[]
skills: string[]
plugins: string[]
}
contextWindow: string
autoCompactLimit: stringRust should mirror these fields with serde camelCase defaults so existing settings files continue to load.
Empty contextWindow and autoCompactLimit mean no value is written. Non-empty values must be positive integers before being written.
TOML Handling
Core code should parse TOML structurally with toml_edit.
Needed operations:
- Parse
relayCommonConfigContentsinto typed context entries grouped by kind. - Upsert one context entry back into the common snippet.
- Delete one context entry from the common snippet.
- Build a filtered common snippet for a supplier from
contextSelection. - Apply supplier context-size fields to the supplier config before writing live config.
Filtering rules:
- If a supplier has an explicit
contextSelection, only selected ids are applied. - If a supplier was created before this feature and has no selection field, default to selecting all current common entries to preserve previous behavior.
- Deleted common entries should be silently ignored by supplier selections until the supplier is edited or saved.
Merge rules:
- Selected common entries are merged into supplier config during switch.
- Non-selected common entries are not written to live config by the switch path.
- Supplier-specific provider configuration remains supplier-owned.
model_context_windowandmodel_auto_compact_token_limitare supplier-owned top-level keys.
UI
Add a context management section to the supplier configuration page.
It should be compact and operational, matching the current settings UI:
- A tab or segmented control for MCP, Skills, and Plugins.
- Each row shows id, enabled-for-this-supplier checkbox, and a short summary.
- Row actions: edit and delete.
- Add action per kind.
- Editing can use a small form for id plus raw TOML body. MCP may also expose command/args/env fields if this stays simple.
- Keep the existing raw
公共 config.tomltextarea as the advanced escape hatch. - Context window and auto compact limit inputs live beside supplier model/base URL settings because they are supplier-level behavior.
When adding a new supplier:
- The new supplier starts with all existing common context entries selected.
- The user can uncheck entries before saving or switching.
- Context-size inputs start empty.
Backend Commands
Add Tauri commands that operate on settings rather than directly mutating live Codex files:
- list context entries from a provided settings object or current saved settings
- upsert context entry
- delete context entry
- update supplier context selection
The existing save flow remains the source of persistence.
Switch commands should call a new core function that receives:
- supplier config TOML
- auth JSON
- full common config TOML
- supplier context selection
- supplier context-size fields
The core function produces the effective live config.toml and writes it atomically through the existing backup path.
Error Handling
- Invalid common TOML should surface a clear error and avoid writing live config.
- Invalid context entry TOML should block saving that entry.
- Invalid context-size values should block supplier save or switch with a clear message.
- Deleting a common entry removes it from
relayCommonConfigContents; stale profile selections are harmless. - If raw common TOML is edited manually and contains new entries, supplier UI should discover them on refresh.
Tests
Core tests:
- Parse MCP, skills, and plugins from common TOML.
- Upsert and delete context entries without damaging unrelated TOML.
- Filter common config by supplier selection.
- Apply selected context entries only during full file switch.
- Write
model_context_windowandmodel_auto_compact_token_limitonly when non-empty. - Reject invalid context-size values.
Manager tests:
- Settings serde defaults preserve existing settings.
- Save normalization preserves common entries and supplier selections.
- Switch path passes selection and context-size values into core.
Frontend checks:
- TypeScript checks for new fields.
- New supplier defaults select all common entries.
- Existing suppliers without selection behave as all selected.
Open Decisions
None. This design intentionally keeps the first implementation Codex-only and TOML-backed, with raw TOML still available for advanced cases.
Superpowers/Specs/2026 05 27 Provider Configuration Redesign Design
Provider Configuration Redesign
Summary
CodexPlusPlus will replace the current relay-profile-centered provider management with a cc-switch-style provider system. Providers become the primary persisted entity, the manager UI edits providers directly, and runtime configuration is generated from the active provider instead of reading relayProfiles as the source of truth.
The redesign aims for high compatibility with cc-switch’s provider model and import semantics without copying every peripheral feature. The focus is provider CRUD, migration from existing relay profiles, active-provider selection, runtime config generation, and external import compatibility.
Goals
- Replace
relayProfilesas the main provider data model. - Introduce a standalone provider entity with cc-switch-compatible core fields.
- Let the manager app manage providers through a dedicated provider configuration screen.
- Generate Codex runtime/live configuration from the active provider.
- Migrate existing user data from
relayProfilesautomatically. - Preserve and improve external provider import flows so they create providers directly.
Non-goals
- Full one-to-one replication of all cc-switch provider-adjacent features.
- A full DB/schema clone of cc-switch internals.
- Supporting long-term dual editing of both providers and
relayProfiles. - Broad unrelated refactors outside provider, launch, import, and manager flows.
Current State
Today provider-like data is stored in BackendSettings.relayProfiles and selected by activeRelayId in crates/codex-plus-core/src/settings.rs. Each relay profile mixes persisted provider details with runtime-oriented relay configuration fields such as base URL, API key, protocol, auth/config text, and context-related relay options.
The manager UI exposes settings and relay-related behavior through apps/codex-plus-manager/src/App.tsx, but there is no dedicated standalone provider resource layer. The backend already contains an external import path in apps/codex-plus-manager/src-tauri/src/commands.rs and crates/codex-plus-core/src/ccs_import.rs, but imports currently materialize as relay profiles.
This creates three problems:
- Provider data is embedded inside a larger settings blob.
- Runtime profile concerns and provider identity are coupled.
- Interop with cc-switch-style provider resources requires ad hoc mapping.
Proposed Architecture
1. Provider becomes the primary persisted resource
Add a standalone provider model in the Rust core/data layer. This provider model becomes the only authoritative persisted provider representation.
The provider model should align closely with cc-switch’s core fields:
idnamesettingsConfigwebsiteUrl(optional)notes(optional)meta(optional structured metadata)
settingsConfig is the main payload and carries the provider-specific runtime configuration currently spread across relay-profile fields. The exact JSON/TOML shape should be chosen so current launch/runtime code can deterministically derive the generated Codex config.
2. BackendSettings only stores global state
BackendSettings remains for app-global behavior, but provider details move out of it. After the redesign it should only contain:
activeProviderId- non-provider global feature flags
- launch and wrapper settings that are genuinely global
- other manager-wide preferences unrelated to provider identity
relayProfiles, activeRelayId, and relay-profile-specific editing paths stop being the primary editable model. Temporary compatibility code may read them during migration only.
3. Runtime config is generated from the active provider
All flows that currently depend on relay profiles should switch to the following pipeline:
- Read
activeProviderIdfrom settings. - Load the referenced provider from provider storage.
- Derive the live Codex config, auth config, provider selection, and any runtime relay/provider artifacts from that provider.
- Apply those generated artifacts to the current Codex home/config targets.
This keeps runtime behavior deterministic while decoupling editing/storage from generated live files.
Provider Data Model
Provider structure
The new provider entity should preserve high-value compatibility with cc-switch while staying scoped to CodexPlusPlus needs.
Recommended fields:
id: Stringname: Stringsettings_config: serde_json::Valuewebsite_url: Option<String>notes: Option<String>meta: Option
ProviderMeta should start intentionally small and only cover fields that CodexPlusPlus will consume in this phase. It may include compatibility placeholders for future interop, but implementation should not depend on unused surface area.
Initial meta candidates:
- protocol / API format hints where needed
- optional import provenance
- optional compatibility markers for generated auth/config behavior
- optional UI-only metadata if needed later
Avoid introducing broad metadata like icon packs, partner catalogs, or unrelated marketplace annotations unless the manager UI actually consumes them in this redesign.
settingsConfig mapping
The current relay-profile fields should be migrated into settingsConfig or provider metadata so provider data becomes self-contained.
At minimum, migration must preserve equivalent information for:
- upstream/base URL
- API key or auth payload references
- protocol or wire API mode
- provider-specific TOML config content
- provider-specific auth JSON content
- test-model-related information if still provider-specific after redesign
The storage format can differ from cc-switch internals, but field semantics should remain close enough that import/export mapping is straightforward and not lossy for supported fields.
Storage Design
Introduce a dedicated provider store separate from BackendSettings persistence. The store should live in the Rust core/data layer and expose CRUD operations consumed by Tauri commands.
Requirements:
- list providers
- get provider by id
- create provider
- update provider
- delete provider
- set active provider indirectly via settings update
- load providers during runtime config generation
The provider store may use JSON or SQLite, but it should satisfy two constraints:
- clear separation from settings storage
- easy compatibility mapping for imported cc-switch-like provider records
Recommendation: use a dedicated JSON-backed or structured-file-backed store first unless an existing project persistence abstraction strongly favors SQLite. The key design concern is clean ownership and migration simplicity, not maximizing storage sophistication.
Migration Plan
Automatic one-time migration from relayProfiles
On first load after upgrade, detect whether provider storage is empty while legacy relay-profile data exists.
If true:
- Read
BackendSettings.relayProfilesandactiveRelayId. - Convert each relay profile into a provider entity.
- Persist the resulting providers in the new provider store.
- Map
activeRelayIdtoactiveProviderId. - Save updated settings without relying on relay profiles as the source of truth.
Migration behavior requirements
- Migration must be idempotent.
- Migration must not duplicate providers on repeated startup.
- Migration must preserve the active selection when possible.
- Migration must tolerate partially empty legacy values.
- If migration cannot fully preserve a legacy field, the fallback must still produce a valid provider that can launch.
Post-migration compatibility
After successful migration:
- new UI edits only provider data
- runtime generation reads only provider data plus global settings
- legacy relay-profile data is ignored for normal operation
A temporary compatibility reader may remain for safety, but it should not become a second editing path.
Import Compatibility
Existing external import flow
The current ccs_import path should be upgraded so imports create providers directly instead of relay profiles.
That means:
- keep the current external source parsing logic where it is still useful
- change its output target from
RelayProfileto the new provider model - preserve de-duplication behavior based on stable provider identity semantics
Supported import sources in this phase
- Automatic migration from legacy local relay profiles
- Existing external Codex provider import path
- Optional direct import from cc-switch-compatible provider datasets if the format is already accessible with reasonable effort
The first two are required for this redesign. The third is nice-to-have and should only be included if it does not delay the core refactor.
Manager UI Design
Add a dedicated provider configuration page in the manager app, modeled after the cc-switch interaction pattern.
Layout
- left column: provider list
- right column: provider detail/edit form
- top or inline actions: add, delete, duplicate if needed later, set active, import
Core interactions
- create provider
- edit provider fields
- delete provider
- set provider as active
- import providers from supported external sources
- reflect active provider clearly in the list
Fields exposed in phase one
Expose the core provider fields and the provider configuration payload needed to generate runtime config. The form should prioritize usability over replicating every relay profile field one-to-one.
Recommended visible fields:
- provider name
- base URL / upstream endpoint
- API key or auth content entry
- protocol/API mode
- provider config content
- provider auth content
- optional website URL
- optional notes
If some fields are better represented as raw config editors than deeply structured controls, prefer the smallest UI that keeps the system understandable and editable.
UI behavior changes
- Remove or demote relay-profile-centered editing controls from settings screens.
- Keep settings pages focused on global app configuration.
- Route all provider editing to the dedicated provider page.
Runtime and Launch Integration
The launch pipeline in core code should stop assuming relay profiles are present in settings. Instead, it should request the active provider and build the runtime configuration from it.
This includes any code path that currently reads:
- relay base URL
- relay API key
- relay protocol/mode
- relay provider config TOML
- relay auth contents
The generation step should produce the same effective live files as before for equivalent user configuration, but from the new source model.
Error Handling and Recovery
- If
activeProviderIdis missing or points to a deleted provider, select a deterministic fallback provider and surface that state in the UI. - If provider storage is unreadable, fail with a clear manager/backend message instead of silently reverting to legacy state.
- If imported or migrated data is malformed, keep unaffected providers and report which records were skipped.
- If runtime config generation fails for the active provider, the manager should show a precise message tied to that provider.
Testing Strategy
Rust/backend tests
Add tests for:
- provider store CRUD
- relayProfiles -> providers migration
- active provider mapping from legacy state
- import flow producing providers directly
- runtime config generation from provider data
- fallback behavior when active provider is missing
UI tests
Add tests for:
- provider list rendering
- selecting active provider
- editing and saving provider fields
- creating and deleting providers
- import action updating the list
Regression focus
Pay special attention to preserving current runtime behavior for equivalent configurations. The redesign is structural, but users should still be able to launch with the same effective upstream settings after migration.
Implementation Order
- Add provider domain model and provider store.
- Add migration from legacy relay profiles.
- Update importers to create providers.
- Refactor runtime/launch/config generation to read active provider.
- Add Tauri commands for provider CRUD and selection.
- Build the manager provider configuration screen.
- Remove or demote relay-profile editing paths.
- Add and run regression tests.
Open Decisions Resolved
The following decisions are fixed for this implementation:
- Providers are the only source of truth going forward.
- The redesign should be highly compatible with cc-switch’s provider model, but not a full clone.
BackendSettingsstores global app state and active provider selection, not provider details.- Migration from
relayProfilesis automatic and one-time. - Existing external import support remains, but targets providers directly.
Success Criteria
The redesign is complete when:
- users can manage providers entirely from a dedicated manager page
- existing relay-profile users are migrated automatically without losing launchability
- active-provider selection drives generated runtime config
- external provider import creates usable providers directly
- normal operation no longer depends on
relayProfilesas editable state
Superpowers/Specs/2026 06 02 Pr Build Actions Design
PR Build Actions Design
Goal
Add a GitHub Actions workflow that builds downloadable Windows and macOS test artifacts for pull requests, pushes to main, and manual runs, without changing the existing release publishing workflow.
Existing Context
The repository already has .github/workflows/release-assets.yml. That workflow runs only when a GitHub Release is published and uploads official Windows installer, macOS DMGs, and latest.json release metadata. The new workflow should not replace or broaden that release workflow. It should produce temporary CI artifacts only.
The project build entry points are:
apps/codex-plus-manager/package.jsonnpm run checkruns TypeScript checking.npm run vite:buildbuilds the frontend.npm run buildlocally builds launcher and Tauri manager, but CI can use more explicit steps to stage artifacts.
- Workspace root
Cargo.tomlcargo test --workspacevalidates Rust crates and apps.cargo build --releasebuilds Windows binaries on Windows.cargo build --release --target <mac target>builds macOS binaries on macOS.
- Existing installer scripts:
- Windows:
scripts/installer/windows/CodexPlusPlus.nsi - macOS:
scripts/installer/macos/package-dmg.sh
- Windows:
Workflow
Create .github/workflows/pr-build.yml with these triggers:
pull_requestpushtomainworkflow_dispatch
Use minimal permissions:
permissions:
contents: readThe workflow produces artifacts for testing a specific PR or commit. It does not upload GitHub Release assets and does not require write permissions.
Windows Job
Run on windows-latest.
Steps:
- Checkout the repository.
- Set up Node 22.
- Set up Rust stable.
- Install NSIS with Chocolatey.
- Install frontend dependencies in
apps/codex-plus-managerusingnpm install --package-lock=false. - Run
npm run checkinapps/codex-plus-manager. - Run
cargo test --workspacefrom the repository root. - Run
npm run vite:buildinapps/codex-plus-manager. - Run
cargo build --releasefrom the repository root. - Stage Windows binaries into
dist/windows/app:target/release/codex-plus-plus.exetarget/release/codex-plus-plus-manager.exe
- Build the NSIS installer using
scripts/installer/windows/CodexPlusPlus.nsi. - Upload two artifacts:
codex-plus-plus-windows-binariescodex-plus-plus-windows-installer
The Windows job verifies TypeScript and Rust tests before uploading artifacts.
macOS Job
Run a matrix with fail-fast: false:
- Intel:
- runner:
macos-15-intel - target:
x86_64-apple-darwin - arch:
x64
- runner:
- Apple Silicon:
- runner:
macos-14 - target:
aarch64-apple-darwin - arch:
arm64
- runner:
Steps for each matrix entry:
- Checkout the repository.
- Set up Node 22.
- Set up Rust stable with the matrix target.
- Install frontend dependencies in
apps/codex-plus-managerusingnpm install --package-lock=false. - Run
npm run vite:buildinapps/codex-plus-manager. - Run
cargo build --release --target <target>from the repository root. - Build the DMG using
scripts/installer/macos/package-dmg.sh, passing the package version and architecture. - Upload the generated DMG as an artifact named for the architecture:
codex-plus-plus-macos-x64-dmgcodex-plus-plus-macos-arm64-dmg
The macOS job focuses on producing downloadable DMGs. Rust unit tests remain covered by the Windows job to keep PR runtime reasonable while still validating the workspace.
Artifact Policy
Artifacts are temporary GitHub Actions artifacts scoped to a workflow run. They are for PR/main/manual testing only.
Official release downloads remain controlled by .github/workflows/release-assets.yml and GitHub Releases.
Error Handling
- Use independent Windows and macOS jobs so platform-specific failures are easy to diagnose.
- Use
fail-fast: falsefor macOS matrix builds so one architecture does not cancel the other. - If NSIS or packaging fails, the build should fail rather than silently skipping installer output.
Testing and Verification
After implementation:
- Validate the workflow file is present at
.github/workflows/pr-build.yml. - Run a local repository build command that matches the existing project build path:
npm --prefix /e/Desktop/CodexPlusPlus/apps/codex-plus-manager run build
- Push the workflow to GitHub and verify a PR or manual run uploads Windows and macOS artifacts.
Local execution cannot fully prove GitHub-hosted macOS runner behavior, so the first GitHub Actions run is the final integration verification.
Superpowers/Specs/2026 06 04 Plugin Entry Unlock Design
Plugin Entry Unlock Restoration Design
Goal: Restore the Codex++ 1.1.9 plugin entry unlock path while keeping the current marketplace unlock path, and expose separate switches for entry unlock, marketplace unlock, and force install/load.
Architecture: Split plugin-related injection into three independently controlled layers: current marketplace request/filter patching, restored legacy plugin entry unlocking, and force install button unlocking. Backend settings persist each layer independently, while the injected Codex++ menu and manager settings expose clear toggles.
Tech Stack: Rust settings persistence (codex-plus-core), Tauri manager commands/UI, React/TypeScript manager UI, and runtime JavaScript injection in assets/inject/renderer-inject.js.
Requirements
Keep the current Codex++ marketplace unlock behavior.
- The current
list-pluginsrequest patch must stay available. - The current marketplace/filter patch and marketplace display-name handling must stay available.
- This layer is controlled by a dedicated
插件市场解锁switch.
- The current
Restore the Codex++ 1.1.9 plugin entry unlock behavior.
- Find the left navigation Plugins/插件 button.
- Use the React fiber auth context path from 1.1.9 to temporarily set
authMethodtochatgpt. - Force the entry button visible and enabled.
- Patch React props
disabledstate when present. - Spoof auth again on capture-phase click.
- Label the entry as unlocked, and clear that label when disabled.
- This layer is controlled by a new
强制解锁入口switch.
Restore/keep the force install/load switch as an independent plugin layer.
- Unblock disabled plugin install buttons.
- Keep the existing refresh loop that re-unblocks buttons after React rerenders.
- Keep the “强制安装” label behavior.
- This layer is controlled by the
特殊插件强制安装/强制加载switch.
Do not lose current safeguards.
- If global
enhancementsEnabledis false, all three plugin layers are disabled. - In relay/compatibility mode, plugin-related patches remain disabled as they are today.
- No
app.asarpatching; all behavior remains runtime injection plus backend settings.
- If global
Preserve upgrade compatibility.
- Existing settings files without the new marketplace field default it to
true. - Existing
codexAppPluginEntryUnlockreturns to meaning “legacy entry unlock.” - A new
codexAppPluginMarketplaceUnlockfield controls the current marketplace unlock layer. codexAppForcePluginInstallremains the force install/load field.
- Existing settings files without the new marketplace field default it to
Current Context
Current code has these relevant pieces:
assets/inject/renderer-inject.js- Uses
pluginMarketplaceUnlockas the local setting name mapped to backendcodexAppPluginEntryUnlock. - Contains the current marketplace request/filter patch:
patchPluginMarketplaceRequestParamsinstallPluginBuildFlavorFilterPatchinstallPluginMarketplaceRequestPatch
- Contains force install button code:
pluginInstallCandidatesunblockPluginInstallButtonsrefreshForcePluginInstallUnlockLoop
- No longer contains the 1.1.9
enablePluginEntrypath.
- Uses
v1.1.9:assets/inject/renderer-inject.js- Used local setting
pluginEntryUnlockmapped to backendcodexAppPluginEntryUnlock. - Restored functions should include:
reactFiberFromauthContextValueFromspoofChatGPTAuthMethodpluginEntryButtonlabelUnlockedPluginEntryclearPluginEntryUnlockLabelenablePluginEntry
- Used local setting
crates/codex-plus-core/src/settings.rs- Already has
codex_app_plugin_entry_unlockandcodex_app_force_plugin_install. - Needs
codex_app_plugin_marketplace_unlock.
- Already has
Proposed Data Model
Add a backend setting field:
#[serde(rename = "codexAppPluginMarketplaceUnlock", default = "default_true")]
pub codex_app_plugin_marketplace_unlock: bool,Defaults:
codex_app_plugin_entry_unlock: true,
codex_app_plugin_marketplace_unlock: true,
codex_app_force_plugin_install: true,JavaScript local settings:
{
pluginEntryUnlock: true,
pluginMarketplaceUnlock: true,
forcePluginInstall: true,
}Backend map:
{
pluginEntryUnlock: "codexAppPluginEntryUnlock",
pluginMarketplaceUnlock: "codexAppPluginMarketplaceUnlock",
forcePluginInstall: "codexAppForcePluginInstall",
}Runtime Injection Flow
During scanDeferred():
If plugin patches are disabled by relay mode:
- call
clearPluginPatchArtifacts(); - refresh/stop the force install loop;
- skip all plugin patch installs.
- call
Otherwise:
enablePluginEntry()runs only whenpluginEntryUnlockis enabled.installPluginBuildFlavorFilterPatch()andinstallPluginMarketplaceRequestPatch()run only whenpluginMarketplaceUnlockis enabled.unblockPluginInstallButtons()andrefreshForcePluginInstallUnlockLoop()run only whenforcePluginInstallis enabled.
clearPluginPatchArtifacts() must clear both legacy entry labels and force install labels.
UI Design
Codex++ injected menu shows three plugin rows:
插件市场解锁
- Description:
API Key 模式下扩展插件市场请求,尽量显示完整插件列表。 - Setting:
pluginMarketplaceUnlock
- Description:
强制解锁入口
- Description:
恢复 1.1.9 的入口解锁方式,强制显示并启用插件入口。 - Setting:
pluginEntryUnlock
- Description:
特殊插件强制安装 or 强制加载
- Description:
解除 App unavailable / 应用不可用导致的前端安装禁用。 - Setting:
forcePluginInstall
- Description:
The manager settings UI should expose matching independent switches if it already surfaces these Codex App enhancement settings.
Error Handling and Diagnostics
Add/keep diagnostics for:
- legacy entry unlock installed/succeeded;
- legacy entry button not found, but avoid noisy repeated logging;
- auth context spoof success/failure if available;
- marketplace request patch installed/not found/failed;
- force install buttons unblocked count if useful.
Diagnostics must not include tokens or sensitive config values.
Testing Plan
Rust settings tests:
- defaults include all three plugin booleans as
true; - JSON missing
codexAppPluginMarketplaceUnlockloads astrue; - settings update preserves independent values for entry, marketplace, and force install.
- defaults include all three plugin booleans as
Injection static/behavior tests:
- backend setting map contains both
pluginEntryUnlockandpluginMarketplaceUnlockwith different backend keys; enablePluginEntry()is gated bypluginEntryUnlock;- marketplace request/filter patch functions are gated by
pluginMarketplaceUnlock; - force install functions are gated by
forcePluginInstall; scanDeferred()invokes all three independent paths.
- backend setting map contains both
Verification commands:
cargo test -p codex-plus-core settingscargo test -p codex-plus-core cdp_bridge -- --nocaptureif injection static tests live therecargo check -p codex-plus-managernpm --prefix apps/codex-plus-manager run check
Out of Scope
- Do not patch
app.asar. - Do not remove the current marketplace unlock implementation.
- Do not change relay/proxy launch behavior.
- Do not change plugin marketplace grouping or display labels beyond what is needed for the switches.
- Do not touch
.tmp/cc-switch-srcor test cc-switch code.
Superpowers/Specs/2026 06 04 Provider Sync Target Selection Design
Provider Sync Target Selection Design
Goal
Add a user-selectable sync target to the Codex++ session sync page, and align Codex++ provider discovery and sync behavior with Dailin521/codex-provider-sync.
The feature lets users choose which Provider ID historical sessions should be synchronized to, while keeping the current Codex config.toml provider unchanged unless a future separate switch feature is added.
Reference behavior
The reference project fills provider choices in desktop/CodexProviderSync.Core/ProviderDiscoveryService.cs through BuildProviderOptions(StatusSnapshot status, AppSettings settings). It merges provider IDs from:
- Configured providers in
config.toml. - Provider IDs already present in session rollout files.
- Provider IDs already present in archived session rollout files.
- Provider IDs already present in
state_5.sqlite. - Saved and manually-added providers from app settings.
- The current root
model_provider.
It sorts the current provider first, then the remaining IDs by provider ID.
The reference sync behavior in desktop/CodexProviderSync.Core/CodexSyncService.cs accepts an optional explicit provider. If present, sync targets that provider; otherwise it uses the current root model_provider, falling back to the default provider.
User experience
The existing session sync / historical session repair area gains a target selector above or beside the sync button:
同步目标
[ apigather(配置 / 当前) ▼ ]
[立即同步会话]The selector is a dropdown, not a freeform input in the first UI version. Each option shows the provider ID and source badges in Chinese:
apigather 配置 / 当前
custom 配置 / 会话
openai 配置 / 会话 / 索引
old-provider 会话Source labels map as follows:
| Internal source | UI label |
|---|---|
config |
配置 |
rollout |
会话 |
sqlite |
索引 |
manual |
手动 |
If the target list loads successfully, the selected target defaults to:
- Previously selected provider if it is still present.
- Current root
model_providerif present. - First provider option.
openaionly if no provider can be discovered.
The progress message includes the chosen target:
正在同步到 openai…
正在扫描历史会话与索引…
正在写入修复与备份…
已同步到 openai:修复 3 个会话文件,更新 8 行索引。Architecture
Keep provider sync inside crates/codex-plus-data/src/provider_sync.rs; do not depend on the external Node.js or .NET project at runtime.
Add three focused backend capabilities:
- Provider target discovery, matching the reference project's source merge rules.
- Explicit-target provider sync, matching
codex-provider sync --providersemantics. - More complete sync repair logic for global workspace state, matching the reference project where Codex++ currently differs.
The Tauri manager exposes two commands:
load_provider_sync_targets()returns the dropdown choices.sync_providers_now(target_provider: Option<String>)syncs to the chosen target.
The launcher keeps calling the existing no-argument provider sync behavior so automatic startup sync remains unchanged.
Backend data model
Add serializable provider target types in crates/codex-plus-data/src/provider_sync.rs:
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderSyncTargetSource {
Config,
Rollout,
Sqlite,
Manual,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderSyncTargetOption {
pub id: String,
pub sources: Vec<ProviderSyncTargetSource>,
pub is_current_provider: bool,
pub is_manual: bool,
pub is_saved: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderSyncTargetList {
pub current_provider: String,
pub targets: Vec<ProviderSyncTargetOption>,
}Use serde rename behavior at the Tauri boundary so frontend payload keys remain camelCase where existing commands already expect camelCase.
Provider discovery
Add a public function:
pub fn load_provider_sync_targets(codex_home: Option<&Path>) -> ProviderSyncTargetListIt builds a map from provider ID to sources, mirroring the reference project:
- Add all provider IDs declared by
[model_providers.]inconfig.tomlwith sourceConfig. - Add the current root
model_providerwith sourceConfig. - Add rollout provider IDs from
sessions/**/rollout-*.jsonlandarchived_sessions/**/rollout-*.jsonlwith sourceRollout. - Add SQLite provider IDs from
state_5.sqlitethreads.model_provider, split by archived/non-archived only for counting; source is stillSqlite. - Add saved/manual providers from Codex++ settings with source
Manualonce those settings fields exist. - Add the default provider
openaithrough the config-provider parser behavior, matching the referencelistConfiguredProviderIds()default inclusion.
Provider IDs that are missing, empty, or whitespace-only are ignored. Missing model_provider falls back to openai.
Sort options by:
id == current_providerdescending.idascending with ordinal comparison.
Settings for saved and manual providers
Extend manager settings with fields equivalent to the reference app state:
providerSyncSavedProviders: string[];
providerSyncManualProviders: string[];
providerSyncLastSelectedProvider: string;The first UI version does not need a manual-add control. It should still persist providerSyncLastSelectedProvider after the user selects a dropdown option. When sync succeeds, the selected provider should be recorded into providerSyncSavedProviders if absent.
Manual providers are reserved for a future UI that explicitly adds/removes items, matching the reference project without making the first UI heavier.
Explicit-target sync
Add a public function:
pub fn run_provider_sync_with_target(
codex_home: Option<&Path>,
target_provider: Option<&str>,
) -> ProviderSyncResultPreserve the existing function as a wrapper:
pub fn run_provider_sync(codex_home: Option<&Path>) -> ProviderSyncResult {
run_provider_sync_with_target(codex_home, None)
}Target resolution matches the reference project:
- If
target_provideris present and non-empty after trim, use it. - Otherwise use root
model_providerfromconfig.toml. - Otherwise use default
openai.
Validate explicit targets before writing. Reject values containing control characters, newlines, or path separators. Valid provider IDs should accept the same practical shape as configured provider IDs: letters, digits, underscore, dash, and dot.
This command is equivalent to the reference project's codex-provider sync --provider , not codex-provider switch .
Rollout sync behavior
Keep scanning:
~/.codex/sessions/**/rollout-*.jsonl
~/.codex/archived_sessions/**/rollout-*.jsonlFor each rollout file, read the first line only for session_meta metadata. If the first line is not valid JSON, is not type == "session_meta", or lacks an object payload, skip it.
When the current payload.model_provider differs from the target provider, rewrite only that field in the first-line metadata. Do not change the rest of the JSONL conversation content.
If a rollout contains encrypted_content, count it by its original provider and return the existing warning behavior. Sync must not decrypt, re-encrypt, or rewrite encrypted conversation payloads.
Continue to preserve file modification time after rewriting, matching current Codex++ behavior and the reference project.
SQLite sync behavior
Align SQLite updates with the reference SqliteStateService.UpdateSqliteProviderAsync() behavior.
When state_5.sqlite is absent, sync continues with zero SQLite updates.
When present, update the threads table in a transaction:
UPDATE threads
SET model_provider = $provider
WHERE COALESCE(model_provider, '') <> $providerIf the has_user_event column exists, set it for threads whose rollout file contains user events:
UPDATE threads
SET has_user_event = 1
WHERE id = $id AND COALESCE(has_user_event, 0) <> 1If the cwd column exists, update it from rollout metadata by thread ID:
UPDATE threads
SET cwd = $cwd
WHERE id = $id AND COALESCE(cwd, '') <> $cwdMalformed or busy SQLite errors should return a skipped/failed sync result with a clear message and no partial writes reported as success.
Global state sync behavior
Align Codex++ global state repair with the reference GlobalStateService.SyncWorkspaceRootsAsync().
Read .codex-global-state.json if present. If absent, return zero global state updates.
Use SQLite threads.cwd stats when possible to resolve workspace root paths. The effective path normalization should repair Desktop-visible workspace roots, not only dedupe existing strings.
Handle these fields:
electron-saved-workspace-roots
project-order
active-workspace-roots
electron-workspace-root-labels
open-in-target-preferences.perPathCodex++ currently handles the first four groups partially; this design adds open-in-target-preferences.perPath and uses SQLite cwd stats to resolve stored paths in the same spirit as the reference project.
When global state changes, write both:
.codex-global-state.json
.codex-global-state.json.bakBackup format
Keep the backup root:
~/.codex/backups_state/provider-sync/<timestamp>Back up the same artifacts as the reference project:
config.toml
.codex-global-state.json
.codex-global-state.json.bak
db/state_5.sqlite
db/state_5.sqlite-wal
db/state_5.sqlite-shm
session-meta-backup.json
metadata.jsonsession-meta-backup.json records original first-line metadata for rollout files that will be changed, including path, original first line, separator, and original last-write timestamp when available.
Expand metadata.json to include structured fields while preserving Codex++'s existing managedBy marker for pruning:
{
"version": 1,
"namespace": "provider-sync",
"codexHome": "...",
"targetProvider": "...",
"createdAt": "...",
"dbFiles": ["state_5.sqlite"],
"changedSessionFiles": 3,
"managedBy": "Codex++ provider sync"
}After successful sync, keep pruning managed backups to the newest 5.
Failure recovery
Improve Codex++ recovery semantics to match the reference project more closely.
Before writes, create a backup. Then:
- Open SQLite transaction if the database exists.
- Apply SQLite provider/user-event/cwd updates inside the transaction.
- Rewrite rollout files and record the actually applied files.
- Sync global state.
- Commit SQLite transaction only after rollout and global state writes succeed.
- On failure before commit, rollback SQLite.
- Restore any rollout files already rewritten.
- Restore global state files from backup if they were changed before failure.
If restore itself fails, return a clear skipped/failed result explaining both the original write error and restore error.
Tauri command changes
Add command:
#[tauri::command]
pub async fn load_provider_sync_targets() -> CommandResult<Value>Return payload shape:
{
"currentProvider": "apigather",
"targets": [
{
"id": "apigather",
"sources": ["config"],
"isCurrentProvider": true,
"isManual": false,
"isSaved": true
}
]
}Modify existing command:
#[tauri::command]
pub async fn sync_providers_now(target_provider: Option<String>) -> CommandResult<Value>The frontend calls it as:
call("sync_providers_now", {
targetProvider: selectedProviderSyncTarget,
});The returned payload continues to include targetProvider, update counts, skipped locked files, backup directory, and encrypted content warning.
Frontend changes
In apps/codex-plus-manager/src/App.tsx:
- Add types for provider sync target list and target options.
- Load targets when the app initializes and when the sessions page or sync area refreshes.
- Add state for selected sync target.
- Render a dropdown in the existing sync area.
- Persist the last selected provider in backend settings.
- Pass the selected target into
sync_providers_now. - Include the target in progress messages and completion notices.
If target loading fails, the dropdown shows a disabled fallback option and sync uses current backend behavior only when no explicit target is selected.
Compatibility
Launcher behavior remains compatible:
apps/codex-plus-launcher/src/main.rscontinues callingcodex_plus_data::run_provider_sync(None).- Startup sync still targets current root
model_provider. - No launcher UI selection is added.
Existing manager manual sync remains compatible:
- If
sync_providers_nowis called withouttargetProvider, it behaves exactly as before.
Non-goals
This feature does not implement provider switching. It does not rewrite config.toml root model_provider when the user chooses a dropdown target.
This feature does not add a restore UI, standalone CLI, or manual provider add/remove UI in the first version.
This feature does not inspect or modify .tmp/cc-switch-src.
Testing
Automated tests should cover:
- Provider discovery includes default
openai. - Provider discovery includes current root
model_provider. - Provider discovery includes
[model_providers.]entries. - Provider discovery includes provider IDs from
sessionsrollout metadata. - Provider discovery includes provider IDs from
archived_sessionsrollout metadata. - Provider discovery includes provider IDs from SQLite
threads.model_provider. - Provider discovery merges sources for duplicate provider IDs.
- Provider discovery sorts current provider first, then by provider ID.
- Explicit target overrides current config provider during sync.
- Missing explicit target preserves existing current-config behavior.
- Invalid explicit target is rejected before writes.
- Sync updates rollout first-line
payload.model_providerto the selected target. - Sync updates SQLite
threads.model_providerto the selected target. - Sync updates
has_user_eventandcwdconsistently with existing behavior. - Global state sync handles
open-in-target-preferences.perPath. - Backup
metadata.jsonincludes structured fields and preservesmanagedBy. - Failure during rollout/global state write rolls back SQLite and restores applied files.
- Tauri
sync_providers_nowaccepts optionaltargetProviderand returns the selectedtargetProvider.
Manual verification should cover:
- Open Codex++ manager.
- Confirm the sync target dropdown lists providers from config, rollout files, and SQLite.
- Pick a non-current provider.
- Run sync.
- Confirm result message says the selected target.
- Confirm historical sessions become visible under the selected provider.
- Confirm
config.tomlrootmodel_providerwas not changed. - Confirm startup sync still uses current config provider when enabled.
Superpowers/Specs/2026 06 05 Version Gated Plugin Unlock Design
Version-Gated Plugin Unlock Strategy Design
Goal: Automatically choose the old or new plugin unlock strategy based on the installed Codex App version, while preserving manual switches for plugin marketplace unlock, force entry unlock, and force install/load.
Architecture: Reuse backend Codex App version detection and expose the detected version to the injected runtime settings payload. The injection script compares the Codex App version against a fixed cutoff (26.601.21317) and chooses a legacy or modern plugin unlock strategy at scan time.
Tech Stack: Rust backend settings/launcher helper routes, Codex++ runtime JavaScript injection, static injection tests in codex-plus-core, React/Tauri manager settings UI only if a status hint is added.
Requirements
Use Codex App version to select plugin unlock strategy.
- If Codex App version is lower than
26.601.21317, prefer the legacy 1.1.9 plugin entry unlock path. - If Codex App version is greater than or equal to
26.601.21317, prefer the current modern marketplace unlock path. - If Codex App version cannot be read or parsed, preserve current manual-switch behavior.
- If Codex App version is lower than
Preserve existing manual switches.
插件市场解锁controls the modern marketplace request/filter patch.强制解锁入口controls the restored legacy plugin entry unlock path.特殊插件强制安装controls disabled install button unblocking.- All three remain visible and independently configurable.
Preserve existing safety gates.
- If
enhancementsEnabledis false, plugin unlock work does not run. - If launch mode is
relay, plugin unlock work stays disabled. - No
app.asarpatching.
- If
Do not persist runtime-only Codex App version into settings JSON.
- Version is read dynamically from the configured/discovered Codex App path.
- The injected settings response may include
codexAppVersion, but saving settings must not write that field.
Version Cutoff
Use this fixed cutoff:
PLUGIN_LEGACY_ENTRY_UNLOCK_BEFORE = 26.601.21317Rules:
| Codex App version | Strategy | Main behavior |
|---|---|---|
< 26.601.21317 |
legacy |
Run legacy enablePluginEntry() when entry unlock is enabled. |
>= 26.601.21317 |
modern |
Run modern marketplace request/filter patches when marketplace unlock is enabled. |
| unknown/unparseable | unknown |
Keep current manual-switch behavior: run enabled marketplace and entry paths independently. |
Backend Data Flow
The launcher helper currently serves /settings/get to the injected script. Extend that response with a runtime-only field:
{
"codexAppVersion": "26.601.21317"
}If the version cannot be read:
{
"codexAppVersion": ""
}Implementation options for locating the app version:
- Use the configured Codex App path from settings if present.
- Fall back to the same discovery path used by
load_overview. - Use
codex_plus_core::app_paths::codex_app_version(path)to parse the version.
The field is merged into the helper response only. BackendSettings does not need to persist codexAppVersion unless the existing route implementation requires serializing the full settings struct; in that case, use a separate response object instead of adding a serializable persisted field.
Injection Script Strategy
Add constants and helpers to assets/inject/renderer-inject.js:
const codexPluginLegacyEntryUnlockBeforeVersion = "26.601.21317";
function parseCodexVersionParts(version) { ... }
function compareCodexVersions(left, right) { ... }
function codexPluginUnlockStrategy() { ... }Expected behavior:
function codexPluginUnlockStrategy() {
const version = String(codexPlusBackendSettings.codexAppVersion || "").trim();
const comparison = compareCodexVersions(version, codexPluginLegacyEntryUnlockBeforeVersion);
if (comparison == null) return "unknown";
return comparison < 0 ? "legacy" : "modern";
}Scan behavior in the non-relay branch:
const strategy = codexPluginUnlockStrategy();
const settings = codexPlusSettings();
if ((strategy === "legacy" || strategy === "unknown") && settings.pluginEntryUnlock) {
enablePluginEntry();
}
if ((strategy === "modern" || strategy === "unknown") && settings.pluginMarketplaceUnlock) {
installPluginBuildFlavorFilterPatch();
installPluginMarketplaceRequestPatch();
}
unblockPluginInstallButtons();
refreshForcePluginInstallUnlockLoop();This means:
- Low versions do not automatically run the modern marketplace patch just because the marketplace switch is on.
- New versions do not automatically run the legacy entry spoof just because the entry switch defaults to on.
- Unknown versions keep the current behavior: both enabled plugin layers can run.
UI/Diagnostics
Add lightweight diagnostics so runtime behavior is debuggable:
plugin_unlock_strategy_selectedstrategycodexAppVersioncutoff
Avoid logging this on every scan if it becomes noisy; either log only when the strategy changes or store the last logged strategy in window.__codexPluginUnlockStrategyLogged.
Optional UI hint in the injected Codex++ menu:
- Legacy:
检测到旧版 Codex App,自动优先使用旧入口解锁。 - Modern:
检测到新版 Codex App,自动优先使用插件市场解锁。 - Unknown:
未读取到 Codex App 版本,按手动开关执行。
The hint is helpful but not required for the first implementation if tests cover the behavior.
Testing Plan
Backend route tests:
/settings/getresponse containscodexAppVersionwhen a Codex App version is discoverable.- Saving settings does not persist
codexAppVersion. - If version discovery fails, response contains an empty string rather than failing the route.
Injection static tests:
- Script contains cutoff
26.601.21317. - Script contains
compareCodexVersionsandcodexPluginUnlockStrategy. - Legacy strategy gates
enablePluginEntry(). - Modern strategy gates
installPluginBuildFlavorFilterPatch()andinstallPluginMarketplaceRequestPatch(). - Unknown strategy allows both enabled paths.
- Script contains cutoff
Existing regression tests:
- Three plugin switches default to true.
- Legacy entry unlock code remains present.
- Modern marketplace unlock code remains present.
- Relay mode still skips plugin patch work.
Out of Scope
- Do not remove any of the three plugin switches.
- Do not change
forcePluginInstallbehavior by version. - Do not patch
app.asar. - Do not change provider/session sync behavior.
- Do not create a new release tag unless explicitly requested after implementation.
Superpowers/Specs/2026 07 23 Dream Skin Auxiliary Panels Design
Dream Skin Auxiliary Panel Background Design
Goal
Extend the Windows Dream Skin wide-art treatment to Codex's right-side work panel and bottom panel so they visually share the active wallpaper instead of rendering as opaque native surfaces.
Scope
- Cover the outer surface of the right-side work panel.
- Cover the outer surface of the bottom panel, including the terminal container.
- Apply only while
codex-dream-skin,dream-art-wide, and a supported task presentation mode are active. - Preserve the existing left sidebar, main task surface, composer, and companion behavior.
Design
The renderer will identify the current right and bottom auxiliary panel roots from stable structural or accessibility attributes and add Dream Skin-owned marker classes. The existing ensure() lifecycle will reconcile those marker classes whenever Codex changes panel visibility or layout.
The Windows Dream Skin stylesheet will use those marker classes to replace only the panel roots' opaque backgrounds with coordinated translucent surfaces derived from the existing task immersive variables. Panel headers and structural wrappers may be made transparent where necessary so the root treatment remains visible. Editors, terminal content, input controls, menus, dialogs, cards, and other functional content surfaces will retain their native or existing themed backgrounds for readability.
When a panel closes or its DOM node is replaced, stale marker classes will be removed during the next reconciliation. Disabling or cleaning up Dream Skin will also remove all auxiliary panel marker classes.
Alternatives Rejected
- Clearing every
bg-token-main-surface-primarysurface globally would also affect menus, dialogs, and other content that needs an opaque background. - Painting a new full-window overlay would create stacking, clipping, and pointer-event risks and would duplicate the wallpaper layer already owned by
body.
Testing
- Add a regression test that requires the Windows renderer to discover and reconcile both auxiliary panel roles.
- Add a regression test that requires scoped right-panel and bottom-panel selectors in the Dream Skin stylesheet.
- Run the manager test suite, TypeScript check, production frontend build, JavaScript syntax checks, Rust formatting, and the upstream asset hash test in CI.
- Visually verify with both panels open that the wallpaper treatment reaches their outer surfaces while text, terminal content, and controls remain readable.
Success Criteria
- The right-side work panel no longer appears as a solid unrelated block when Dream Skin wide-art task mode is active.
- The bottom terminal panel no longer appears as a solid unrelated block in the same mode.
- Closing, reopening, resizing, or docking either panel does not disable the skin or leave stale styling behind.
- Menus, dialogs, editors, terminal content, and input controls remain legible and interactive.
Superpowers/Plans/2026 05 13 Provider Sync
Provider Sync Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add an optional default-off Codex++ provider sync that runs before Codex launches and keeps historical conversations visible after model_provider changes.
Architecture: Add backend settings in ~/.codex-session-delete/settings.json, expose those settings through the existing CDP bridge/menu, and implement provider sync as a focused Python module called by launcher.launch_and_inject() before launch_codex_app(). The sync module reads ~/.codex/config.toml, backs up writable state, locks with ~/.codex/tmp/provider-sync.lock, updates rollout first-line metadata and SQLite provider metadata, logs skippable lock/busy conditions, and only blocks launch on unsafe write/restore failures.
Tech Stack: Python 3.11 stdlib (json, sqlite3, shutil, pathlib, dataclasses, time), existing renderer injection JavaScript, pytest.
File Structure
- Create
codex_session_delete/settings_store.py: backend-readable Codex++ settings store with defaults and atomic writes. - Create
codex_session_delete/provider_sync.py: provider sync implementation and result model. - Modify
codex_session_delete/launcher.py: load settings, expose bridge endpoints, call provider sync before launch, and log skipped/fatal sync results. - Modify
codex_session_delete/inject/renderer-inject.js: add backend-backed Provider sync toggle to Codex++ menu. - Modify
tests/test_launcher_user_scripts.py: bridge settings endpoint tests. - Modify
tests/test_launcher_cli.py: launch pre-sync tests. - Create
tests/test_settings_store.py: backend settings tests. - Create
tests/test_provider_sync.py: provider sync unit tests. - Modify
tests/test_renderer_script.py: renderer contract for Provider sync menu UI and backend endpoints.
Task 1: Backend Settings Store
Files:
Create:
codex_session_delete/settings_store.pyTest:
tests/test_settings_store.pyStep 1: Write the failing settings tests
Create tests/test_settings_store.py with:
import json
from codex_session_delete.settings_store import BackendSettings, SettingsStore
def test_settings_store_defaults_provider_sync_disabled(tmp_path):
store = SettingsStore(tmp_path / "settings.json")
settings = store.load()
assert settings == BackendSettings(provider_sync_enabled=False)
assert settings.to_dict() == {"providerSyncEnabled": False}
def test_settings_store_saves_and_reloads_provider_sync(tmp_path):
store = SettingsStore(tmp_path / "settings.json")
saved = store.save(BackendSettings(provider_sync_enabled=True))
assert saved == BackendSettings(provider_sync_enabled=True)
assert json.loads((tmp_path / "settings.json").read_text(encoding="utf-8")) == {"providerSyncEnabled": True}
assert store.load() == BackendSettings(provider_sync_enabled=True)
def test_settings_store_ignores_malformed_json(tmp_path):
path = tmp_path / "settings.json"
path.write_text("not json", encoding="utf-8")
store = SettingsStore(path)
assert store.load() == BackendSettings(provider_sync_enabled=False)- Step 2: Run tests to verify they fail
Run:
python -m pytest -q tests/test_settings_store.pyExpected: FAIL with ModuleNotFoundError: No module named 'codex_session_delete.settings_store'.
- Step 3: Implement settings store
Create codex_session_delete/settings_store.py with:
/* Detailed source-code truncated for AI context efficiency. */- Step 4: Run tests to verify they pass
Run:
python -m pytest -q tests/test_settings_store.pyExpected: 3 passed.
- Step 5: Commit settings store
Run:
git add codex_session_delete/settings_store.py tests/test_settings_store.py
git commit -m "Add backend settings store"Task 2: Provider Sync Core
Files:
Create:
codex_session_delete/provider_sync.pyTest:
tests/test_provider_sync.pyStep 1: Write the failing provider sync tests
Create tests/test_provider_sync.py with:
/* Detailed source-code truncated for AI context efficiency. */- Step 2: Run tests to verify they fail
Run:
python -m pytest -q tests/test_provider_sync.pyExpected: FAIL with ModuleNotFoundError: No module named 'codex_session_delete.provider_sync'.
- Step 3: Implement provider sync module
Create codex_session_delete/provider_sync.py with:
/* Detailed source-code truncated for AI context efficiency. */- Step 4: Run provider sync tests
Run:
python -m pytest -q tests/test_provider_sync.pyExpected: 3 passed.
- Step 5: Commit provider sync core
Run:
git add codex_session_delete/provider_sync.py tests/test_provider_sync.py
git commit -m "Add provider sync core"Task 3: Launcher Startup Integration
Files:
Modify:
codex_session_delete/launcher.pyTest:
tests/test_launcher_cli.pyStep 1: Write failing launcher tests
Add these tests after test_launch_and_inject_returns_windows_packaged_process_id in tests/test_launcher_cli.py:
/* Detailed source-code truncated for AI context efficiency. */- Step 2: Run tests to verify they fail
Run:
python -m pytest -q tests/test_launcher_cli.py::test_launch_and_inject_runs_provider_sync_before_launch_when_enabled tests/test_launcher_cli.py::test_launch_and_inject_skips_provider_sync_when_disabledExpected: FAIL with AttributeError for backend_settings or run_provider_sync.
- Step 3: Modify launcher imports and helper
In codex_session_delete/launcher.py, add imports near existing imports:
from codex_session_delete.provider_sync import ProviderSyncStatus, run_provider_sync
from codex_session_delete.settings_store import BackendSettings, SettingsStoreAdd this function after user_scripts_config_dir():
def backend_settings() -> BackendSettings:
return SettingsStore().load()- Step 4: Call provider sync before launch
In launch_and_inject(), before server = start_helper(...), add:
if backend_settings().provider_sync_enabled:
sync_result = run_provider_sync()
if sync_result.status == ProviderSyncStatus.SKIPPED:
print(f"Provider sync skipped: {sync_result.message}")- Step 5: Run launcher tests
Run:
python -m pytest -q tests/test_launcher_cli.py::test_launch_and_inject_runs_provider_sync_before_launch_when_enabled tests/test_launcher_cli.py::test_launch_and_inject_skips_provider_sync_when_disabledExpected: 2 passed.
- Step 6: Commit launcher integration
Run:
git add codex_session_delete/launcher.py tests/test_launcher_cli.py
git commit -m "Run provider sync before Codex launch"Task 4: Bridge Settings Endpoints
Files:
Modify:
codex_session_delete/launcher.pyTest:
tests/test_launcher_user_scripts.pyStep 1: Write failing bridge tests
Add this import to tests/test_launcher_user_scripts.py:
from codex_session_delete.settings_store import SettingsStoreAdd these tests after test_handle_bridge_request_reports_and_repairs_backend_status:
def test_handle_bridge_request_gets_backend_settings(monkeypatch, tmp_path):
store = SettingsStore(tmp_path / "settings.json")
store.update({"providerSyncEnabled": True})
monkeypatch.setattr("codex_session_delete.launcher.SettingsStore", lambda: store)
manager = UserScriptManager(tmp_path / "builtin", tmp_path / "user", tmp_path / "config.json")
runtime = FakeRuntime(manager)
result = handle_bridge_request(FakeDeleteService(), FakeExportService(), "/settings/get", {}, runtime)
assert result == {"providerSyncEnabled": True}
def test_handle_bridge_request_sets_backend_settings(monkeypatch, tmp_path):
store = SettingsStore(tmp_path / "settings.json")
monkeypatch.setattr("codex_session_delete.launcher.SettingsStore", lambda: store)
manager = UserScriptManager(tmp_path / "builtin", tmp_path / "user", tmp_path / "config.json")
runtime = FakeRuntime(manager)
result = handle_bridge_request(FakeDeleteService(), FakeExportService(), "/settings/set", {"providerSyncEnabled": True}, runtime)
assert result == {"providerSyncEnabled": True}
assert store.load().provider_sync_enabled is True- Step 2: Run tests to verify they fail
Run:
python -m pytest -q tests/test_launcher_user_scripts.py::test_handle_bridge_request_gets_backend_settings tests/test_launcher_user_scripts.py::test_handle_bridge_request_sets_backend_settingsExpected: FAIL because /settings/get and /settings/set are not handled.
- Step 3: Add bridge endpoint handling
In codex_session_delete/launcher.py, add these branches in handle_bridge_request() before /user-scripts/list:
if path == "/settings/get" and runtime:
return SettingsStore().load().to_dict()
if path == "/settings/set" and runtime:
return SettingsStore().update(payload).to_dict()- Step 4: Run bridge tests
Run:
python -m pytest -q tests/test_launcher_user_scripts.py::test_handle_bridge_request_gets_backend_settings tests/test_launcher_user_scripts.py::test_handle_bridge_request_sets_backend_settingsExpected: 2 passed.
- Step 5: Commit bridge endpoints
Run:
git add codex_session_delete/launcher.py tests/test_launcher_user_scripts.py
git commit -m "Expose backend settings bridge"Task 5: Provider Sync Menu Toggle
Files:
Modify:
codex_session_delete/inject/renderer-inject.jsModify:
tests/test_renderer_script.pyStep 1: Write failing renderer contract test
Add this test to tests/test_renderer_script.py near the Codex++ menu tests:
def test_renderer_script_has_backend_provider_sync_toggle():
text = Path("codex_session_delete/inject/renderer-inject.js").read_text(encoding="utf-8")
assert "Provider 同步" in text
assert "data-codex-backend-setting=\"providerSyncEnabled\"" in text
assert "/settings/get" in text
assert "/settings/set" in text
assert "loadBackendSettings" in text
assert "setBackendSetting" in text- Step 2: Run test to verify it fails
Run:
python -m pytest -q tests/test_renderer_script.py::test_renderer_script_has_backend_provider_sync_toggleExpected: FAIL with missing Provider 同步.
- Step 3: Add renderer backend settings functions
In codex_session_delete/inject/renderer-inject.js, after function setCodexPlusSetting(...), add:
let codexPlusBackendSettings = { providerSyncEnabled: false };
async function loadBackendSettings() {
try {
const settings = await bridgeCall("/settings/get", {});
codexPlusBackendSettings = { ...codexPlusBackendSettings, ...settings };
refreshCodexPlusBackendToggles();
} catch (_) {
refreshCodexPlusBackendToggles();
}
}
async function setBackendSetting(key, value) {
codexPlusBackendSettings = { ...codexPlusBackendSettings, [key]: value };
refreshCodexPlusBackendToggles();
try {
const settings = await bridgeCall("/settings/set", { [key]: value });
codexPlusBackendSettings = { ...codexPlusBackendSettings, ...settings };
} finally {
refreshCodexPlusBackendToggles();
}
}
function refreshCodexPlusBackendToggles() {
document.querySelectorAll(".codex-plus-toggle[data-codex-backend-setting]").forEach((button) => {
const key = button.getAttribute("data-codex-backend-setting");
button.dataset.enabled = String(!!codexPlusBackendSettings[key]);
});
}- Step 4: Add Provider sync row to settings menu
In the existing settings rows near conversationTimeline, add:
<div class="codex-plus-row">
<div><div class="codex-plus-row-title">Provider 同步</div><div class="codex-plus-row-description">启动 Codex 前同步历史会话到当前 model_provider。</div></div>
<button type="button" class="codex-plus-toggle" data-codex-backend-setting="providerSyncEnabled"><span></span></button>
</div>- Step 5: Wire click handler and initial load
In the Codex++ menu click handler, after existing [data-codex-plus-setting] handling, add:
const backendToggle = target?.closest("[data-codex-backend-setting]");
if (backendToggle) {
const key = backendToggle.getAttribute("data-codex-backend-setting");
setBackendSetting(key, !codexPlusBackendSettings[key]);
return;
}In installCodexPlusMenu(), after the menu is installed and normal toggles refresh, call:
loadBackendSettings();- Step 6: Run renderer contract test
Run:
python -m pytest -q tests/test_renderer_script.py::test_renderer_script_has_backend_provider_sync_toggleExpected: 1 passed.
- Step 7: Commit renderer toggle
Run:
git add codex_session_delete/inject/renderer-inject.js tests/test_renderer_script.py
git commit -m "Add provider sync settings toggle"Task 6: Final Verification
Files:
Verify all changed files.
Step 1: Run focused tests
Run:
python -m pytest -q tests/test_settings_store.py tests/test_provider_sync.py tests/test_launcher_user_scripts.py tests/test_launcher_cli.py tests/test_renderer_script.pyExpected: all focused tests pass. If tests/test_renderer_script.py fails on the pre-existing right: 140px assertion, update that assertion to accept right: var(--codex-plus-menu-right, 140px) because the current source uses the CSS variable from commit 9ab834f.
- Step 2: Run full test suite
Run:
python -m pytest -qExpected: all tests pass.
- Step 3: Inspect final diff
Run:
git diff --stat HEAD~5..HEADExpected: only provider sync/settings/menu/test files are changed.
- Step 4: Report completion status
Report exact commands run and pass/fail counts. Do not claim completion unless the full test suite passes.
Superpowers/Plans/2026 05 16 Rust Tauri Migration
Rust/Tauri Migration Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the Python backend with a Rust core, a no-window silent launcher, and a Tauri management tool while preserving the existing Codex App injection behavior.
Architecture: Add a Rust workspace beside the current Python package, port behavior in verifiable slices, then switch the installed entry points to Rust and remove Python only after parity is proven. The only user-facing entry points are Codex++ silent launch and Codex++ 管理工具; there is no separate CLI.
Tech Stack: Rust 1.95, Cargo workspace, Tauri 2, TypeScript/Vite, rusqlite, serde, serde_json, tokio, reqwest, tungstenite or tokio-tungstenite, Windows PowerShell shortcut generation, macOS app bundle generation, existing renderer-inject.js, existing pytest suite as behavior reference during migration.
Scope Check
The design covers several subsystems: Rust workspace setup, data operations, CDP bridge, silent launcher, management UI, install/update packaging, watcher behavior, documentation, and Python removal. This plan is intentionally staged. Each task below should produce working, testable software and should be committed independently. If a task becomes large during execution, split it into a sub-plan before writing production code.
File Structure
- Create
Cargo.toml: workspace root with members undercrates/andapps/. - Create
crates/codex-plus-core/: shared launch, CDP, bridge, settings, logs, diagnostics, path resolution, install/update primitives. - Create
crates/codex-plus-data/: SQLite, backup, markdown export, provider sync, and filesystem data operations. - Create
apps/codex-plus-launcher/: no-window silent launcher binary used by theCodex++shortcut. - Create
apps/codex-plus-manager/: Tauri management console used byCodex++ 管理工具. - Move or copy runtime assets into
assets/: icons, sponsor images, andrenderer-inject.jssource used by Rust packaging. - Create
tests/fixtures/: shared SQLite, rollout, and settings fixtures for Rust integration tests. - Modify
README.mdandREADME_EN.md: replace Python usage with Rust/Tauri installation and two-entry behavior. - Modify
setup.bat: eventually bootstrap or run the management tool instead of Python setup. - Remove
pyproject.toml,codex_session_delete/, and Python tests only after Rust parity is complete.
Task 1: Rust Workspace Skeleton
Files:
Create:
Cargo.tomlCreate:
.cargo/config.tomlCreate:
crates/codex-plus-core/Cargo.tomlCreate:
crates/codex-plus-core/src/lib.rsCreate:
crates/codex-plus-core/src/version.rsCreate:
crates/codex-plus-data/Cargo.tomlCreate:
crates/codex-plus-data/src/lib.rsCreate:
apps/codex-plus-launcher/Cargo.tomlCreate:
apps/codex-plus-launcher/src/main.rsCreate:
apps/codex-plus-manager/package.jsonCreate:
apps/codex-plus-manager/src-tauri/Cargo.tomlCreate:
apps/codex-plus-manager/src-tauri/src/lib.rsCreate:
apps/codex-plus-manager/src-tauri/src/main.rsCreate:
apps/codex-plus-manager/src/main.tsxCreate:
apps/codex-plus-manager/index.htmlStep 1: Write the workspace manifests
Create root Cargo.toml:
[workspace]
resolver = "2"
members = [
"crates/codex-plus-core",
"crates/codex-plus-data",
"apps/codex-plus-launcher",
"apps/codex-plus-manager/src-tauri",
]
[workspace.package]
version = "1.0.8"
edition = "2024"
license = "MIT"
repository = "https://github.com/BigPizzaV3/CodexPlusPlus"
[workspace.dependencies]
anyhow = "1"
base64 = "0.22"
directories = "6"
fs2 = "0.4"
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls"], default-features = false }
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tempfile = "3"
thiserror = "2"
tokio = { version = "1", features = ["macros", "process", "rt-multi-thread", "time"] }
tokio-tungstenite = { version = "0.26", features = ["rustls-tls-webpki-roots"] }
url = "2"
uuid = { version = "1", features = ["v4"] }Create .cargo/config.toml:
[alias]
xtest = "test --workspace --all-targets"
xcheck = "check --workspace --all-targets"- Step 2: Add core crate
Create crates/codex-plus-core/Cargo.toml:
[package]
name = "codex-plus-core"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
anyhow.workspace = true
directories.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = trueCreate crates/codex-plus-core/src/lib.rs:
pub mod version;Create crates/codex-plus-core/src/version.rs:
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(test)]
mod tests {
use super::VERSION;
#[test]
fn exposes_workspace_version() {
assert!(!VERSION.is_empty());
}
}- Step 3: Add data crate
Create crates/codex-plus-data/Cargo.toml:
[package]
name = "codex-plus-data"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
anyhow.workspace = true
base64.workspace = true
rusqlite.workspace = true
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = trueCreate crates/codex-plus-data/src/lib.rs:
pub fn crate_ready() -> bool {
true
}
#[cfg(test)]
mod tests {
#[test]
fn crate_is_ready() {
assert!(super::crate_ready());
}
}- Step 4: Add silent launcher binary skeleton
Create apps/codex-plus-launcher/Cargo.toml:
[package]
name = "codex-plus-launcher"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[[bin]]
name = "codex-plus-plus"
path = "src/main.rs"
[dependencies]
anyhow.workspace = true
codex-plus-core = { path = "../../crates/codex-plus-core" }
tokio.workspace = trueCreate apps/codex-plus-launcher/src/main.rs:
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
println!("Codex++ launcher {}", codex_plus_core::version::VERSION);
Ok(())
}- Step 5: Add Tauri manager skeleton
Create apps/codex-plus-manager/package.json:
{
"name": "codex-plus-manager",
"version": "1.0.8",
"private": true,
"type": "module",
"scripts": {
"dev": "tauri dev",
"build": "tauri build",
"check": "tsc --noEmit"
},
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/cli": "^2.0.0",
"typescript": "^5.8.0",
"vite": "^6.0.0"
},
"devDependencies": {}
}Create apps/codex-plus-manager/index.html:
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Codex++ 管理工具</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>Create apps/codex-plus-manager/src/main.tsx:
const app = document.getElementById("app");
if (app) {
app.innerHTML = `
<main style="font-family: system-ui, sans-serif; padding: 24px">
<h1>Codex++ 管理工具</h1>
<p>Rust/Tauri migration shell is ready.</p>
</main>
`;
}Create apps/codex-plus-manager/src-tauri/Cargo.toml:
[package]
name = "codex-plus-manager"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "codex_plus_manager_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[[bin]]
name = "codex-plus-plus-manager"
path = "src/main.rs"
[dependencies]
codex-plus-core = { path = "../../../crates/codex-plus-core" }
serde.workspace = true
serde_json.workspace = true
tauri = { version = "2", features = [] }Create apps/codex-plus-manager/src-tauri/src/lib.rs:
#[tauri::command]
fn backend_version() -> &'static str {
codex_plus_core::version::VERSION
}
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![backend_version])
.run(tauri::generate_context!())
.expect("failed to run Codex++ manager");
}Create apps/codex-plus-manager/src-tauri/src/main.rs:
fn main() {
codex_plus_manager_lib::run();
}- Step 6: Run Rust checks
Run:
cargo test --workspace --all-targetsExpected: Rust crates compile and the two skeleton tests pass. If Tauri context files are missing, add the generated tauri.conf.json and icons in the same task before committing.
- Step 7: Commit skeleton
git add Cargo.toml .cargo/config.toml crates apps
git commit -m "feat: add Rust Tauri workspace skeleton"Task 2: Shared Models, Settings, And Status Files
Files:
Create:
crates/codex-plus-core/src/models.rsCreate:
crates/codex-plus-core/src/paths.rsCreate:
crates/codex-plus-core/src/settings.rsCreate:
crates/codex-plus-core/src/status.rsModify:
crates/codex-plus-core/src/lib.rsStep 1: Write model and settings tests
Create tests inside the new modules for:
#[test]
fn settings_default_matches_python_behavior() {
let settings = BackendSettings::default();
assert!(!settings.provider_sync_enabled);
assert!(!settings.cli_wrapper_enabled);
assert_eq!(settings.cli_wrapper_api_key_env, "CUSTOM_OPENAI_API_KEY");
}
#[test]
fn settings_deserialize_uses_existing_json_keys() {
let settings: BackendSettings = serde_json::from_str(
r#"{"providerSyncEnabled":true,"cliWrapperEnabled":true,"cliWrapperBaseUrl":"https://example.test","cliWrapperApiKey":"sk-test","cliWrapperApiKeyEnv":""}"#,
)
.unwrap();
assert!(settings.provider_sync_enabled);
assert!(settings.cli_wrapper_enabled);
assert_eq!(settings.cli_wrapper_base_url, "https://example.test");
assert_eq!(settings.cli_wrapper_api_key, "sk-test");
assert_eq!(settings.cli_wrapper_api_key_env, "CUSTOM_OPENAI_API_KEY");
}- Step 2: Implement models and settings
Implement these public types:
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SessionRef {
pub session_id: String,
pub title: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DeleteStatus {
ServerDeleted,
LocalDeleted,
Partial,
Failed,
Undone,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct DeleteResult {
pub status: DeleteStatus,
pub session_id: String,
pub message: String,
pub undo_token: Option<String>,
pub backup_path: Option<String>,
}Implement BackendSettings with serde field names matching the current JSON keys:
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct BackendSettings {
#[serde(rename = "providerSyncEnabled", default)]
pub provider_sync_enabled: bool,
#[serde(rename = "cliWrapperEnabled", default)]
pub cli_wrapper_enabled: bool,
#[serde(rename = "cliWrapperBaseUrl", default)]
pub cli_wrapper_base_url: String,
#[serde(rename = "cliWrapperApiKey", default)]
pub cli_wrapper_api_key: String,
#[serde(rename = "cliWrapperApiKeyEnv", default = "default_api_key_env", deserialize_with = "empty_as_default_api_key_env")]
pub cli_wrapper_api_key_env: String,
}Add SettingsStore::load, save, and update with atomic temp-file writes to ~/.codex-session-delete/settings.json.
- Step 3: Implement latest status file
Create LaunchStatus with fields:
pub struct LaunchStatus {
pub status: String,
pub message: String,
pub started_at_ms: u64,
pub debug_port: Option<u16>,
pub helper_port: Option<u16>,
pub codex_app: Option<String>,
}Implement StatusStore::save_latest and StatusStore::load_latest at ~/.codex-session-delete/latest-status.json.
- Step 4: Run tests and commit
cargo test -p codex-plus-core settings status models
git add crates/codex-plus-core
git commit -m "feat: add Rust settings and status models"Task 3: Data Layer Parity
Files:
Create:
crates/codex-plus-data/src/backup.rsCreate:
crates/codex-plus-data/src/storage.rsCreate:
crates/codex-plus-data/src/markdown.rsCreate:
crates/codex-plus-data/src/provider_sync.rsModify:
crates/codex-plus-data/src/lib.rsCreate:
crates/codex-plus-data/tests/storage_adapter.rsCreate:
crates/codex-plus-data/tests/provider_sync.rsStep 1: Port fixture behavior from Python tests
Use these Python tests as references:
tests/test_storage_adapter.py
tests/test_backup_store.py
tests/test_markdown_exporter.py
tests/test_provider_sync.pyCreate Rust integration fixtures that cover:
sessions/messagesschema deletion and undo.threadsschema deletion, related table cleanup, and rollout file backup.Workspace move updating both SQLite
cwdand rollout first-line metadata.Sort key lookup for one and many sessions.
Markdown export from rollout JSONL.
Provider sync rewrite of rollout first-line
model_provider, SQLitethreads.model_provider,has_user_event, andcwd.Step 2: Implement backup store
Implement backup JSON compatible with current Python backups:
pub struct BackupStore {
root: PathBuf,
}
impl BackupStore {
pub fn new(root: impl Into<PathBuf>) -> Self;
pub fn write_backup(&self, session_id: &str, source_db: &Path, tables: serde_json::Value) -> anyhow::Result<String>;
pub fn read_backup(&self, token: &str) -> anyhow::Result<serde_json::Value>;
pub fn path_for(&self, token: &str) -> PathBuf;
}- Step 3: Implement storage adapter
Implement a SQLiteStorageAdapter with methods matching the current bridge needs:
pub fn delete_local(&self, session: &SessionRef) -> DeleteResult;
pub fn undo(&self, token: &str) -> DeleteResult;
pub fn find_archived_thread_by_title(&self, title: &str) -> Option<SessionRef>;
pub fn move_codex_thread_workspace(&self, session: &SessionRef, target_cwd: &str) -> serde_json::Value;
pub fn codex_thread_sort_key(&self, session: &SessionRef) -> serde_json::Value;
pub fn codex_thread_sort_keys(&self, sessions: &[SessionRef]) -> serde_json::Value;- Step 4: Implement markdown export and provider sync
Mirror behavior in:
codex_session_delete/markdown_exporter.py
codex_session_delete/provider_sync.pyProvider sync must lock with ~/.codex/tmp/provider-sync.lock, backup writable state before changes, restore rollout first lines on failure, and return skipped rather than failing launch for lock or SQLite busy conditions.
- Step 5: Run data tests and commit
cargo test -p codex-plus-data --all-targets
git add crates/codex-plus-data
git commit -m "feat: port data operations to Rust"Task 4: CDP Bridge And Injection Runtime
Files:
Create:
crates/codex-plus-core/src/cdp.rsCreate:
crates/codex-plus-core/src/bridge.rsCreate:
crates/codex-plus-core/src/assets.rsModify:
crates/codex-plus-core/src/lib.rsCreate:
crates/codex-plus-core/tests/cdp_bridge.rsCopy asset:
assets/inject/renderer-inject.jsCopy assets:
assets/images/codex-plus-plus.ico,assets/images/codex-plus-plus.png, sponsor imagesStep 1: Write bridge script tests
Test that the Rust bridge script defines:
window.__codexSessionDeleteBridge
window.__codexSessionDeleteResolve
window.__codexSessionDeleteReject
codexSessionDeleteV2Test that injection prefix includes:
window.__CODEX_SESSION_DELETE_HELPER__
window.__CODEX_PLUS_SPONSOR_IMAGES__- Step 2: Implement CDP target discovery
Implement:
pub async fn list_targets(debug_port: u16) -> anyhow::Result<Vec<CdpTarget>>;
pub fn pick_page_target(targets: &[CdpTarget]) -> anyhow::Result<CdpTarget>;Selection must prefer page targets whose title or URL contains codex, then fall back to the first page target.
- Step 3: Implement evaluate and bridge install
Implement websocket calls for:
Runtime.enableRuntime.removeBindingRuntime.addBindingPage.addScriptToEvaluateOnNewDocumentRuntime.evaluate
Add a bridge loop that receives Runtime.bindingCalled, routes (path, payload) to a Rust handler, and resolves/rejects the browser promise by evaluating the resolve/reject expression.
- Step 4: Implement asset loading
Use include_str! and include_bytes! for injected JS and sponsor images. The injected JS should remain sourced from one canonical file path during the migration to avoid asset drift.
- Step 5: Run tests and commit
cargo test -p codex-plus-core cdp bridge assets
git add crates/codex-plus-core assets
git commit -m "feat: port CDP bridge injection to Rust"Task 5: Silent Launcher Runtime
Files:
Create:
crates/codex-plus-core/src/app_paths.rsCreate:
crates/codex-plus-core/src/ports.rsCreate:
crates/codex-plus-core/src/proxy.rsCreate:
crates/codex-plus-core/src/launcher.rsModify:
apps/codex-plus-launcher/src/main.rsCreate:
crates/codex-plus-core/tests/launcher.rsStep 1: Port path and port tests
Use these Python tests as references:
tests/test_app_paths.py
tests/test_launcher_cli.py
tests/test_cdp.pyTest Windows packaged Codex path detection, macOS .app executable construction, debug argument construction, and loopback port fallback.
- Step 2: Implement app path resolution and process environment
Port:
codex_session_delete/app_paths.py
codex_session_delete/launcher.py::codex_process_environment
codex_session_delete/launcher.py::build_codex_argumentsKeep proxy auto-detection ports:
7897, 7890, 10809, 10808, 1080- Step 3: Implement Windows packaged app activation
Port activate_packaged_app with Rust Windows APIs. Protect this behind cfg(windows). Tests should validate command construction without requiring a real packaged app.
- Step 4: Implement launch lifecycle
Implement:
pub async fn launch_and_inject(options: LaunchOptions) -> anyhow::Result<LaunchHandle>;It must:
Select ports.
Load settings.
Run provider sync if enabled.
Start helper/bridge runtime.
Launch Codex.
Retry injection.
Write latest status on success/failure.
Shut down helper resources when Codex exits.
Step 5: Implement no-window launcher binary
apps/codex-plus-launcher/src/main.rs should call launch_and_inject. On Windows release builds, configure the binary as no-console if packaging requires it.
- Step 6: Run launcher tests and commit
cargo test -p codex-plus-core launcher ports app_paths proxy
cargo build -p codex-plus-launcher
git add crates/codex-plus-core apps/codex-plus-launcher
git commit -m "feat: add Rust silent launcher runtime"Task 6: Bridge Route Parity
Files:
Create:
crates/codex-plus-core/src/routes.rsModify:
crates/codex-plus-core/src/bridge.rsModify:
crates/codex-plus-core/Cargo.tomlCreate:
crates/codex-plus-core/tests/bridge_routes.rsStep 1: Write route tests
Create tests covering all current route names:
/settings/get
/settings/set
/user-scripts/list
/user-scripts/set-enabled
/user-scripts/set-script-enabled
/user-scripts/reload
/devtools/open
/backend/status
/backend/repair
/ads
/delete
/undo
/export-markdown
/archived-thread
/move-thread-workspace
/thread-sort-key
/thread-sort-keys- Step 2: Implement route dispatcher
Implement a typed dispatcher:
pub async fn handle_bridge_request(ctx: BridgeContext, path: &str, payload: serde_json::Value) -> serde_json::Value;Unknown routes must return:
{"status":"failed","session_id":"","message":"Unknown bridge path"}- Step 3: Wire routes to data and runtime services
Connect delete/export/move/sort/provider/settings/status routes to Rust implementations. Keep user script inventory behavior compatible with current UserScriptManager.
- Step 4: Run route tests and commit
cargo test -p codex-plus-core bridge_routes
git add crates/codex-plus-core
git commit -m "feat: port bridge route handling to Rust"Task 7: Tauri Management Console
Files:
Create:
apps/codex-plus-manager/src/App.tsCreate:
apps/codex-plus-manager/src/styles.cssModify:
apps/codex-plus-manager/src/main.tsxModify:
apps/codex-plus-manager/src-tauri/src/lib.rsCreate:
apps/codex-plus-manager/src-tauri/src/commands.rsCreate:
apps/codex-plus-manager/src-tauri/src/install.rsStep 1: Implement Tauri commands
Expose commands:
backend_version
load_overview
launch_codex_plus
load_settings
save_settings
install_entrypoints
uninstall_entrypoints
repair_shortcuts
check_update
perform_update
read_latest_logs
copy_diagnostics
reset_settingsEach command returns serializable result objects with status, message, and relevant payload fields.
- Step 2: Build workbench UI
Implement the left navigation:
Overview
Launch
Install
Update
Settings
Logs
DiagnosticsUse a compact operational layout. Avoid landing-page hero composition. Do not put cards inside cards. Keep controls dense and predictable.
- Step 3: Implement Overview
Show:
Codex app found/missing.
Silent shortcut installed/missing.
Management shortcut installed/missing.
Latest launch status.
Current version.
Update status.
Quick actions: launch, repair shortcuts, open logs.
Step 4: Implement management screens
Implement:
Launch: manual launch, app path override, debug/helper port fields, repair backend.
Install: install, uninstall, repair shortcuts, optional remove owned data.
Update: check update, release summary, progress, install update.
Settings: provider sync, Codex command wrapper settings, user scripts summary.
Logs: latest log viewer with refresh and copy.
Diagnostics: generated report with copy button.
Step 5: Build and smoke test
cd apps/codex-plus-manager
npm install
npm run check
npm run buildExpected: TypeScript check passes and Tauri build succeeds.
- Step 6: Commit management console
git add apps/codex-plus-manager crates/codex-plus-core
git commit -m "feat: add Tauri management console"Task 8: Install, Uninstall, Update, And Watcher In Rust
Files:
Create:
crates/codex-plus-core/src/install/windows.rsCreate:
crates/codex-plus-core/src/install/macos.rsCreate:
crates/codex-plus-core/src/install/mod.rsCreate:
crates/codex-plus-core/src/update.rsCreate:
crates/codex-plus-core/src/watcher.rsModify:
crates/codex-plus-core/src/lib.rsCreate:
crates/codex-plus-core/tests/installers.rsCreate:
crates/codex-plus-core/tests/updater.rsStep 1: Port installer script tests
Use these Python tests as references:
tests/test_windows_installer.py
tests/test_macos_installer.py
tests/test_installers.py
tests/test_setup_bat.py
tests/test_updater.py
tests/test_watcher.pyTest generated Windows shortcut scripts contain both:
Codex++.lnk
Codex++ 管理工具.lnkTest generated macOS app bundle metadata contains both:
Codex++.app
Codex++ 管理工具.app- Step 2: Implement Windows install/uninstall
Generate two shortcuts and one uninstall registry entry. The silent shortcut points to codex-plus-plus.exe; the management shortcut points to codex-plus-plus-manager.exe.
- Step 3: Implement macOS install/uninstall
Generate two app bundles:
/Applications/Codex++.app
/Applications/Codex++ 管理工具.appThe silent bundle launches the no-window launcher. The management bundle launches the Tauri manager.
- Step 4: Implement update in management core
Port GitHub Release checking, version parsing, asset selection, download, and install orchestration. Expose it only through Tauri commands.
- Step 5: Implement watcher behavior
Port existing watcher behavior so management UI can install/remove/enable/disable it. Watcher management is not exposed as a separate CLI.
- Step 6: Run tests and commit
cargo test -p codex-plus-core installers updater watcher
git add crates/codex-plus-core
git commit -m "feat: port install update and watcher management to Rust"Task 9: Switch Runtime Assets And Documentation
Files:
Modify:
README.mdModify:
README_EN.mdModify:
setup.batModify:
.githubrelease workflow files if presentModify:
docs/imagesonly if icon packaging requires path updatesStep 1: Update README commands
Replace Python commands with two-entry usage:
双击 Codex++:静默启动增强版 Codex。
双击 Codex++ 管理工具:安装、卸载、更新、设置、日志和诊断。Remove references to:
python -m codex_session_delete launch
python -m codex_session_delete setup
python -m codex_session_delete update- Step 2: Update setup.bat
Make setup.bat open or install the Rust/Tauri management tool. It should not call Python modules.
- Step 3: Update release docs
Document release artifacts:
codex-plus-plus.exe
codex-plus-plus-manager.exe
Codex++.app
Codex++ 管理工具.app- Step 4: Run docs tests and commit
python -m pytest -q tests/test_readme.py tests/test_setup_bat.py
git add README.md README_EN.md setup.bat .github docs
git commit -m "docs: document Rust Tauri entry points"Task 10: Python Removal Gate
Files:
Delete:
codex_session_delete/Delete:
pyproject.tomlDelete or replace:
tests/test_*.pythat only validate Python internalsModify:
.gitignoreif Python-only entries are obsoleteStep 1: Run full Rust verification
cargo test --workspace --all-targets
cd apps/codex-plus-manager
npm run check
npm run buildExpected: all Rust tests pass and Tauri app builds.
- Step 2: Run remaining Python docs tests
python -m pytest -q tests/test_readme.py tests/test_setup_bat.pyExpected: pass, or replace these tests with Rust/Node equivalents before deleting Python test infrastructure.
- Step 3: Manual smoke checks
Verify:
Codex++shortcut starts Codex without opening management UI.Codex App shows the Codex++ injected menu.
Delete/undo/export/move/settings routes work from the injected UI.
Codex++ 管理工具opens and shows Overview status.Management tool can repair shortcuts and read latest logs.
Update check works with mocked or real release metadata.
Step 4: Remove Python backend
Delete Python package and obsolete Python tests only after the checks above pass.
- Step 5: Commit removal
git add -A
git commit -m "chore: remove Python backend after Rust migration"Self-Review
- Spec coverage: the plan covers Rust core, data operations, CDP bridge, silent launcher, Tauri management tool, two desktop entry points, install/uninstall/update, watcher, docs, verification, and Python removal.
- Placeholder scan: no
TODOorTBDplaceholders are left. Tasks that are intentionally broad identify exact files, reference tests, expected APIs, and verification commands. - Type consistency: the plan consistently uses
codex-plus-launcherfor the no-window launcher and does not introduce a separate user-facing CLI.
Superpowers/Plans/2026 05 22 Upstream Branch Dropdown
Upstream Branch Dropdown Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add upstream branch choices to Codex App's native start-new-chat branch dropdown so New worktree can start from upstream/ without a separate Codex++ dialog.
Architecture: Extend the existing upstream worktree backend defaults with structured upstream refs, then add a conservative renderer adapter that injects Upstream options into native branch menus and records the selected upstream ref for the existing native-create interception path. Keep the manual Codex++ dialog as fallback for now, but make the native dropdown path the primary UX.
Tech Stack: Rust codex-plus-core, JSON bridge routes, plain JavaScript renderer injection, cargo test, node --check.
Task 1: Backend defaults expose upstream refs
Files:
Modify:
crates/codex-plus-core/src/upstream_worktree/defaults.rsTest:
crates/codex-plus-core/tests/upstream_worktree.rsStep 1: Write failing test
Add this test to crates/codex-plus-core/tests/upstream_worktree.rs:
#[test]
fn defaults_response_lists_preferred_upstream_ref_for_current_branch() {
let temp = tempfile::tempdir().unwrap();
let remote = temp.path().join("remote.git");
git(temp.path(), &["init", "--bare", remote.to_str().unwrap()]);
let repo = temp.path().join("repo");
git(temp.path(), &["clone", remote.to_str().unwrap(), repo.to_str().unwrap()]);
git(&repo, &["config", "user.email", "[email protected]"]);
git(&repo, &["config", "user.name", "Test User"]);
std::fs::write(repo.join("README.md"), "hello").unwrap();
git(&repo, &["add", "README.md"]);
git(&repo, &["commit", "-m", "initial"]);
git(&repo, &["branch", "-M", "main"]);
git(&repo, &["push", "origin", "main"]);
git(&repo, &["remote", "rename", "origin", "upstream"]);
git(&repo, &["fetch", "upstream", "main"]);
let result = defaults_response(&json!({ "repoPath": repo }));
assert_eq!(result["status"], "ok");
assert_eq!(result["defaultRemote"], "upstream");
assert_eq!(result["upstreamRefs"][0]["remote"], "upstream");
assert_eq!(result["upstreamRefs"][0]["branch"], "main");
assert_eq!(result["upstreamRefs"][0]["label"], "upstream/main");
assert_eq!(result["upstreamRefs"][0]["sourceRef"], "refs/remotes/upstream/main");
}- Step 2: Verify RED
Run:
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-core --test upstream_worktree defaults_response_lists_preferred_upstream_ref_for_current_branchExpected: fails because upstreamRefs is missing.
- Step 3: Implement minimal backend defaults
In defaults.rs, add a small helper that emits one structured upstream ref using default_remote_name(&remotes) and default_base_branch:
fn upstream_refs(remote: &str, base_branch: &str) -> Vec<Value> {
if remote.trim().is_empty() || base_branch.trim().is_empty() {
return Vec::new();
}
vec![json!({
"remote": remote,
"branch": base_branch,
"label": format!("{remote}/{base_branch}"),
"sourceRef": format!("refs/remotes/{remote}/{base_branch}"),
})]
}Then include it in defaults_for_repo:
let default_remote = default_remote_name(&remotes);
Ok(json!({
"status": "ok",
"repoRoot": root.to_string_lossy(),
"currentBranch": branch,
"defaultBaseBranch": default_base_branch,
"remotes": remotes,
"defaultRemote": default_remote,
"upstreamRefs": upstream_refs(&default_remote, &default_base_branch),
}))- Step 4: Verify GREEN
Run the same single test. Expected: PASS.
- Step 5: Commit
git add crates/codex-plus-core/src/upstream_worktree/defaults.rs crates/codex-plus-core/tests/upstream_worktree.rs
git commit -m "feat(worktree): expose upstream refs in defaults"Task 2: Renderer script exposes branch dropdown adapter markers
Files:
Modify:
assets/inject/renderer-inject.jsTest:
crates/codex-plus-core/tests/cdp_bridge.rsStep 1: Write failing script-marker test
Add this test to crates/codex-plus-core/tests/cdp_bridge.rs:
#[test]
fn injection_script_installs_upstream_branch_dropdown_adapter() {
let script = assets::injection_script(57321);
assert!(script.contains("installUpstreamBranchDropdownAdapter"));
assert!(script.contains("data-codex-upstream-branch-option"));
assert!(script.contains("codexUpstreamBranchSelection"));
assert!(script.contains("/upstream-worktree/defaults"));
}- Step 2: Verify RED
Run:
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-core --test cdp_bridge injection_script_installs_upstream_branch_dropdown_adapterExpected: fails because adapter names and markers are not present.
- Step 3: Implement adapter skeleton
In renderer-inject.js, add constants near other upstream constants:
const upstreamBranchOptionAttribute = "data-codex-upstream-branch-option";
const upstreamBranchSelectionKey = "codexUpstreamBranchSelection";Add functions near the upstream worktree helpers:
/* Detailed source-code truncated for AI context efficiency. */Call installUpstreamBranchDropdownAdapter(); during boot next to installUpstreamWorktreeNativeAdapter();.
This is only the skeleton; Task 3 adds menu injection.
- Step 4: Verify GREEN
Run the single cdp bridge marker test. Expected: PASS.
- Step 5: Commit
git add assets/inject/renderer-inject.js crates/codex-plus-core/tests/cdp_bridge.rs
git commit -m "feat(worktree): install upstream branch dropdown adapter"Task 3: Inject upstream options into native branch menus
Files:
Modify:
assets/inject/renderer-inject.jsTest:
crates/codex-plus-core/tests/cdp_bridge.rsStep 1: Write failing marker test
Extend injection_script_installs_upstream_branch_dropdown_adapter with:
assert!(script.contains("injectUpstreamBranchOptions"));
assert!(script.contains("Upstream"));
assert!(script.contains("data-base-branch"));
assert!(script.contains("MutationObserver"));- Step 2: Verify RED
Run the same cdp bridge test. Expected: fails because injection function is missing.
- Step 3: Implement menu injection
Add a runtime cache:
let upstreamBranchDefaultsCache = null;Add helpers:
/* Detailed source-code truncated for AI context efficiency. */In the adapter install function, add a mutation observer that schedules injection:
let upstreamBranchInjectTimer = null;
const schedule = () => {
clearTimeout(upstreamBranchInjectTimer);
upstreamBranchInjectTimer = setTimeout(() => {
injectUpstreamBranchOptions().catch((error) => reportDiagnostic("upstream_branch_inject_failed", { error: error?.message || String(error) }));
}, 80);
};
new MutationObserver(schedule).observe(document.body || document.documentElement, { childList: true, subtree: true });
schedule();- Step 4: Verify GREEN
Run the cdp bridge test. Expected: PASS.
- Step 5: Commit
git add assets/inject/renderer-inject.js crates/codex-plus-core/tests/cdp_bridge.rs
git commit -m "feat(worktree): inject upstream branch menu options"Task 4: Route native create through selected upstream ref
Files:
Modify:
assets/inject/renderer-inject.jsTest:
crates/codex-plus-core/tests/cdp_bridge.rsStep 1: Write failing marker test
Extend the cdp bridge test with:
assert!(script.contains("upstreamWorktreePayloadFromSelection"));
assert!(script.contains("readUpstreamBranchSelection"));
assert!(script.contains("writeUpstreamBranchSelection(null)"));- Step 2: Verify RED
Run the cdp bridge test. Expected: fails because payload-from-selection is missing.
- Step 3: Implement selected upstream create path
Add helper:
function upstreamWorktreePayloadFromSelection(trigger) {
const selection = readUpstreamBranchSelection();
if (!selection?.repoPath || !selection?.remote || !selection?.baseBranch) return null;
const nativePayload = upstreamWorktreeNativePayloadFromElement(trigger);
if (!nativePayload?.branchName || !nativePayload?.worktreePath) return null;
return {
...nativePayload,
repoPath: selection.repoPath,
remote: selection.remote,
baseBranch: selection.baseBranch,
fetch: true,
};
}Update handleUpstreamWorktreeNativeCreate so it first tries selected upstream payload:
const payload = upstreamWorktreePayloadFromSelection(trigger) || upstreamWorktreeNativePayloadFromElement(trigger);After success, clear selection:
writeUpstreamBranchSelection(null);- Step 4: Verify GREEN
Run the cdp bridge test. Expected: PASS.
- Step 5: Commit
git add assets/inject/renderer-inject.js crates/codex-plus-core/tests/cdp_bridge.rs
git commit -m "feat(worktree): route native create from upstream selection"Task 5: Validate and install locally
Files:
No source edits expected.
Step 1: Run focused checks
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo fmt --check
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-core --test upstream_worktree
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-core --test cdp_bridge
node --check "assets/inject/renderer-inject.js"Expected: all PASS.
- Step 2: Run full core tests
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-coreExpected: PASS.
- Step 3: Build and package
npm install --prefix "apps/codex-plus-manager"
npm run --prefix "apps/codex-plus-manager" check
npm run --prefix "apps/codex-plus-manager" vite:build
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo build --release
bash "scripts/installer/macos/package-dmg.sh" "1.1.5-local-upstream-dropdown" "$(uname -m)"Expected: DMG created under dist/macos/.
- Step 4: Install and sign local app
hdiutil attach "dist/macos/CodexPlusPlus-1.1.5-local-upstream-dropdown-macos-$(uname -m).dmg" -nobrowse -readonly
ditto "/Volumes/Codex++/Codex++.app" "/Applications/Codex++.app"
ditto "/Volumes/Codex++/Codex++ 管理工具.app" "/Applications/Codex++ 管理工具.app"
codesign --force --deep --sign - "/Applications/Codex++.app"
codesign --force --deep --sign - "/Applications/Codex++ 管理工具.app"
codesign --verify --deep --strict --verbose=2 "/Applications/Codex++.app"
codesign --verify --deep --strict --verbose=2 "/Applications/Codex++ 管理工具.app"
hdiutil detach "/Volumes/Codex++"Expected: both apps verify.
- Step 5: Clean temporary package manager files
If npm install created untracked files, remove only untracked generated dependency files:
python3 - <<'PY'
from pathlib import Path
import shutil
root = Path('/Users/carson/.codex/worktrees/b4e7/CodexPlusPlus')
for rel in ['apps/codex-plus-manager/node_modules', 'apps/codex-plus-manager/package-lock.json']:
path = root / rel
if path.is_dir():
shutil.rmtree(path)
elif path.exists():
path.unlink()
PYThen verify git status --short is clean except ignored build outputs.