CodexPlusPlus Documentation

An enhanced tool for CodexApp, striving to make Codex better to use and more comfortable 一个CodexApp的增强工具,努力让Codex变得更好用更舒服

28,739 stars Rust Rust 1 file · ~45,481 tokens
RAW Doc

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 字段后增加:

rust
#[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:

rust
model_list: String::new(),
model_windows: String::new(),
  • 步骤 3:编译检查

运行:

bash
cargo check -p codex-plus-core

预期:通过(此时字段尚未被使用,只是结构体定义)。

  • 步骤 4:Commit
bash
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 末尾新增:

rust
#[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:运行测试验证失败
bash
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 之后添加:

rust
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:运行测试验证通过
bash
cargo test -p codex-plus-core --test model_suffix migrate_model_list_with_suffixes

预期:PASS。

  • 步骤 6:Commit
bash
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:

rust
let entries = collect_catalog_entries(model_list, &HashMap::new(), current_model);

如果测试原本就依赖后缀窗口,改为在 map 中传入:

rust
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:运行测试验证失败
bash
cargo test -p codex-plus-core --test model_suffix

预期:FAIL,签名不匹配。

  • 步骤 3:修改函数签名和实现

把:

rust
pub fn collect_catalog_entries(model_list: &str, current_model: &str) -> Vec<ModelCatalogEntry>

改为:

rust
pub fn collect_catalog_entries(
    model_list: &str,
    model_windows: &HashMap<String, String>,
    current_model: &str,
) -> Vec<ModelCatalogEntry>

函数内部:

  1. 删除 suffix_for_slug 的构建逻辑(不再从 model_list 解析后缀)。
  2. 解析 model_list 时只保留 slug:
rust
let (slug, _) = parse_model_suffix(raw);
  1. 窗口来源改为 model_windows.get(&slug):
rust
let suffix_window = model_windows.get(&slug).and_then(|token| parse_window_token(token));
  1. 处理 current_model 时同样从 model_windows 查找:
rust
if suffix_window.is_none() {
    if let Some(token) = model_windows.get(&slug) {
        suffix_window = parse_window_token(token);
    }
}
  • 步骤 4:编译检查
bash
cargo check -p codex-plus-core

预期:通过。

  • 步骤 5:运行测试验证通过
bash
cargo test -p codex-plus-core --test model_suffix

预期:PASS。

  • 步骤 6:Commit
bash
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 中新增:

rust
#[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:运行测试验证失败
bash
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 函数中:

rust
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:编译检查
bash
cargo check -p codex-plus-core

预期:通过。

  • 步骤 5:运行测试验证通过
bash
cargo test -p codex-plus-core --test relay_config apply_model_catalog_uses_model_windows_map

预期:PASS。

  • 步骤 6:运行全部相关测试
bash
cargo test -p codex-plus-core --test relay_config --test model_suffix

预期:全部通过。

  • 步骤 7:Commit
bash
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 中新增:

rust
#[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:运行测试验证失败
bash
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 之前)添加:

rust
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:运行测试验证通过
bash
cargo test -p codex-plus-core --test relay_config normalize_migrates_model_list_suffixes_to_model_windows

预期:PASS。

  • 步骤 5:Commit
bash
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; 后添加:

tsx
modelList: string;
modelWindows: string;
userAgent: string;
  • 步骤 2:更新所有 default 创建点

搜索所有 modelList: "" 出现的位置,在同对象内添加 modelWindows: "":

bash
grep -n 'modelList: ""' apps/codex-plus-manager/src/App.tsx

应修改的位置(根据当前代码):

  • apps/codex-plus-manager/src/App.tsx:659
  • apps/codex-plus-manager/src/App.tsx:5475
  • apps/codex-plus-manager/src/App.tsx:5531
  • apps/codex-plus-manager/src/App.tsx:5557
  • apps/codex-plus-manager/src/App.tsx:6169
  • apps/codex-plus-manager/src/App.tsx:6198

每个位置改为:

tsx
modelList: "",
modelWindows: "",
  • 步骤 3:更新 normalizeRelayProfile

在 apps/codex-plus-manager/src/App.tsx:5557:

tsx
modelList: profile.modelList || "",
modelWindows: profile.modelWindows || "",
  • 步骤 4:编译检查
bash
cd apps/codex-plus-manager && npm run tauri dev 2>&1 | head -50

或更轻量:

bash
cd apps/codex-plus-manager && npx tsc --noEmit

预期:通过。

  • 步骤 5:Commit
bash
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 函数之后添加:

tsx
/// 把 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:编译检查
bash
cd apps/codex-plus-manager && npx tsc --noEmit

预期:通过。

  • 步骤 3:Commit
bash
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 替换为:

tsx
{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(或使用的样式文件)中添加:

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:编译检查
bash
cd apps/codex-plus-manager && npx tsc --noEmit

预期:通过。

  • 步骤 4:Commit
bash
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); 之后添加:

tsx
const [modelWindowsText, setModelWindowsText] = useState(
  modelWindowsMapToText(profile.modelList, profile.modelWindows || "")
);
  • 步骤 2:同步 modelList 变化时更新窗口文本

使用 useEffect 监听 draft.modelList 和 draft.modelWindows:

tsx
useEffect(() => {
  setModelWindowsText(modelWindowsMapToText(draft.modelList, draft.modelWindows || ""));
}, [draft.modelList, draft.modelWindows]);

注意:当用户主动编辑右侧窗口文本时,不应被这个 effect 覆盖。因此更精确的做法是:只在 draft.modelList 行数变化且 modelWindowsText 尚未由用户编辑过,或从上游获取/加载时同步。

为简化,初始版本可以在 draft.modelList 变化时重新生成(用户编辑右侧会暂时被覆盖,可后续优化)。

  • 步骤 3:编译检查
bash
cd apps/codex-plus-manager && npx tsc --noEmit

预期:通过。

  • 步骤 4:Commit
bash
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 之前添加:

tsx
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:

tsx
const modelWindows = modelWindowsTextToMap(draft.modelList, modelWindowsText);
const normalizedDraft = deriveRelayProfileFromFiles({ ...draft, modelWindows });
  • 步骤 3:编译检查
bash
cd apps/codex-plus-manager && npx tsc --noEmit

预期:通过。

  • 步骤 4:Commit
bash
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:

tsx
if (models?.length) {
  updateDraft({ modelList: models.join("\n"), modelWindowsText: "" });
}
  • 步骤 2:Commit
bash
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 来源

在:

rust
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:运行后端测试
bash
cargo test -p codex-plus-core --test relay_config --test model_suffix --test launcher --test codex_sqlite

预期:全部通过。

  • 步骤 3:Commit
bash
git add crates/codex-plus-core/tests
git commit -m "test(core): 更新测试以适配 model_windows 新签名"

前端测试

  • 步骤 4:新增渲染测试

在 apps/codex-plus-manager/src/App.test.tsx 中新增:

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:新增行数校验测试
tsx
it("prevents save when model list and windows line counts mismatch", async () => {
  // 设置 draft:模型 2 行,窗口 1 行
  // 触发保存
  // 验证保存未执行(如 mock 的 onFormChange 未被调用)
});
  • 步骤 6:运行前端测试
bash
cd apps/codex-plus-manager && npm test

预期:通过。

  • 步骤 7:Commit
bash
git add apps/codex-plus-manager/src/App.test.tsx
git commit -m "test(manager): 添加模型列表双文本框相关测试"

任务 14:端到端验证

  • 步骤 1:构建并运行管理工具
bash
cd apps/codex-plus-manager && npm run build
  • 步骤 2:运行 example 验证后端
bash
cargo run --example apply_profile -p codex-plus-core

预期:config.toml 中 model 无后缀,catalog 窗口正确。

  • 步骤 3:打包 DMG
bash
bash scripts/installer/macos/package-dmg.sh 1.2.18 arm64
  • 步骤 4:手动验证
  1. 安装新 DMG。
  2. 在管理工具中输入:
    • 左侧:deepseek/deepseek-v4-flash
    • 右侧:1M
  3. 保存并启动 Codex。
  4. 检查 ~/.codex/config.toml:model 无后缀。
  5. 检查 ~/.codex/model-catalogs/.json:slug 无后缀,context_window 为 1M。
  6. 在 Codex 中选择模型,确认不报 "is not a valid model ID"。
  • 步骤 5:Commit(如有文档更新)
bash
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)>:

  1. 解析 profile.model_list,提取带后缀的条目;无后缀条目则 no-op(返回 None)
  2. 取 codex 自带 entry 当 template:codex debug models --bundled 输出,或 fallback 用内置 assets/codex-models.json 模板
  3. 每条 clone template 后覆盖:slug(剥离后缀)/ display_name / context_window / max_context_window,保留 priority / supported_in_api / visibility 等字段
  4. 包含 profile 全部模型(解决 #1064 单模型副作用):当前 model + model_list 全部条目都写进 catalog,带后缀的写窗口,无后缀的回落顶层
  5. 写到 <home>/model-catalogs/.json
  6. config.toml 注入 model_catalog_json = "model-catalogs/.json"(相对路径)

3.3 插入点(中间层影响最小)

现有流程:

text
complete_relay_profile_config → merge_common → preserve_unmanaged
→ apply_context_limits_to_config(写顶层单值)
→ 落盘

新增独立可选步骤,插在 apply_context_limits_to_config 之后、落盘之前:

text
→ 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 样本对比字段。

必验项

  1. Mac 相对路径:model-catalogs/.json 在 Mac 是否可行(#931 转义结论是 Windows 的)
  2. 字段名:context_window+max_context_window 是否让 codex 生效(#931/cc-switch 一致,但须实跑确认;不认则试 n)
  3. auto_compact null:1M 窗口是否在合理比例压缩(非 220K 低值);异常则 fallback 写 ×0.85
  4. effective_percent:写 100 / 省略时显示是否为真实 1M
  5. 单模型副作用: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。但存在以下问题:

  1. 历史记录污染:Codex 选择器会显示最近使用过的 model ID。如果用户曾经选择过带后缀的字符串(或旧版本把带后缀字符串写入了 threads.model),选择器里就会继续显示 deepseek-v4-flash[1M] 这类废弃项。
  2. 用户误选报错:选择带后缀的历史项后,Codex 会把 config.toml 的 model 字段更新为带后缀字符串,而 catalog 里只有无后缀 slug,导致 "is not a valid model ID" 报错。
  3. 与 cc-switch 的体验不一致:cc-switch 把「模型名」和「上下文窗口」在 UI 与存储层彻底分开,Codex 客户端永远看不到带后缀字符串,历史记录自然干净。

本设计将模型列表改为左右并排的两个输入框:左侧填模型名(无后缀),右侧填上下文窗口,从根上避免 Codex 接触到带后缀的 model ID。

设计目标

  1. Codex 客户端实际使用的 model ID 永远不带后缀。
  2. 用户输入方式尽量保持简单:仍然是一个模型一行,只是窗口单独放到右侧。
  3. 旧数据(deepseek-v4-flash[1M])自动迁移一次,之后彻底使用新格式。
  4. 不引入表格、加减行按钮等复杂交互。

数据模型

新增字段

在 RelayProfile 中新增 model_windows 字段:

rust
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:

text
deepseek/deepseek-v4-flash[1M]
deepseek/deepseek-v4-pro
nvidia/nemotron-3-super-120b-a12b:free[1M]

迁移后:

  • model_list:

    text
    deepseek/deepseek-v4-flash
    deepseek/deepseek-v4-pro
    nvidia/nemotron-3-super-120b-a12b:free
  • model_windows(JSON):

    json
    {
      "deepseek/deepseek-v4-flash": "1M",
      "nvidia/nemotron-3-super-120b-a12b:free": "1M"
    }

UI 设计

模型列表区域

将当前单文本框拆为左右两个多行文本框:

tsx
<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>

对齐策略

为避免行数多时左右对应关系错位,采用以下策略:

  1. 等宽字体:左右 textarea 均使用 monospace,相同行高。
  2. 高度绑定:保存/渲染时让两个 textarea 高度一致。
  3. 同步滚动:左右滚动时保持 scrollTop 同步。
  4. 行数校验:保存时校验左右行数一致,不一致则阻止保存并提示。

配置模型字段

「配置模型」保持单个输入框,但只接受无后缀 slug:

tsx
<input
  value={draft.configuredModel}
  onChange={handleConfiguredModelChange}
  placeholder="deepseek/deepseek-v4-flash"
/>

保存时自动剥离后缀(兼容用户手误)。

前端状态与保存逻辑

draft 新增字段

tsx
interface RelayProfileDraft {
  // ... 现有字段 ...
  modelList: string;
  modelWindowsText: string; // 新增
  configuredModel: string;
}

从后端 profile 初始化 draft

tsx
const windowsMap = safeJsonParse<Record<string, string>>(profile.model_windows, {});
const modelWindowsText = profile.model_list
  .split('\n')
  .map(line => windowsMap[line.trim()] ?? '')
  .join('\n');

保存时组装

tsx
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 中执行一次性迁移:

rust
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:

rust
pub fn collect_catalog_entries(
    model_list: &str,
    model_windows: &HashMap<String, String>,
) -> Vec<ModelCatalogEntry>

逻辑:

  1. 解析 model_list 为无后缀 slug 列表。
  2. 对每个 slug,从 model_windows 查找窗口 token;未找到则使用 profile 顶层默认窗口。
  3. 生成 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 初始化后显示为分离的两列

兼容性

  1. 旧格式迁移:settings 加载时自动把 model_list 中的 [suffix] 拆出到 model_windows。
  2. Codex 客户端:始终使用无后缀 model ID,不会再产生带后缀历史项。
  3. 手动编辑 settings 文件:即使用户手动写回带后缀字符串,下次加载时也会重新迁移。
  4. 不写窗口的模型:保持 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/

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:

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:

js
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:

  1. Open a conversation with multiple user questions.
  2. Confirm the right-side vertical timeline appears.
  3. Hover each marker and confirm the tooltip shows the expected question summary.
  4. Click a marker and confirm the conversation scrolls to the matching user message.
  5. 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.py stores backend-readable Codex++ settings in ~/.codex-session-delete/settings.json.
  • codex_session_delete/provider_sync.py ports 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:

text
~/.codex-session-delete/settings.json

Initial content is implicit; if the file is missing or malformed, Codex++ uses defaults:

json
{
  "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/get returns merged defaults and saved backend settings.
  • /settings/set accepts a boolean providerSyncEnabled, 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_provider in ~/.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_event to 1 when the column exists.
  • If rollout metadata or events expose a thread working directory, update that SQLite thread row's cwd when the column exists.
  • Normalize .codex-global-state.json workspace paths using the SQLite threads.cwd values when possible.

The first version does not implement provider switching, restore commands, status UI, or a standalone CLI.

Locking and backups

Use lock directory:

text
~/.codex/tmp/provider-sync.lock

If the lock already exists, skip sync and continue launch.

Before writing, create a backup under:

text
~/.codex/backups_state/provider-sync/<timestamp>

The backup includes:

  • config.toml if present.
  • .codex-global-state.json and .codex-global-state.json.bak if present.
  • state_5.sqlite and sidecar -wal / -shm files 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_provider is updated to the current config provider.
  • Backups are created before writes and pruned to 5 managed backups.

Manual verification should cover:

  1. Turn on Provider 同步 in the Codex++ menu.
  2. Close Codex.
  3. Change model_provider in ~/.codex/config.toml.
  4. Launch Codex++.
  5. Confirm older conversations remain visible in Codex Desktop and /resume.
  6. 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.js with 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:

  1. It starts the no-window Rust launcher.
  2. It does not show a Tauri management window.
  3. It launches Codex App with CDP flags.
  4. It starts the local Rust bridge/helper runtime.
  5. It injects renderer-inject.js.
  6. 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:

  1. User double-clicks Codex++.
  2. The no-window Rust launcher starts without a visible management UI.
  3. Rust chooses available loopback ports for CDP and helper/bridge runtime.
  4. If Provider Sync is enabled, Rust updates local Codex metadata before launch.
  5. Rust launches Codex with:
    • --remote-debugging-port=<debug_port>
    • --remote-allow-origins=http://127.0.0.1:<debug_port>
  6. Rust starts the helper/bridge runtime.
  7. Rust discovers the Codex page target through CDP.
  8. Rust installs Runtime.addBinding, injects the bridge script, and injects renderer-inject.js.
  9. Rust evaluates enabled user scripts.
  10. Runtime stays alive until Codex exits, then shuts down helper resources.

Bridge request flow:

  1. Injected UI calls the bridge from renderer-inject.js.
  2. CDP delivers a Runtime.bindingCalled event to Rust.
  3. Rust routes the request by path.
  4. codex-plus-core and codex-plus-data execute the operation.
  5. Rust resolves or rejects the browser-side promise.
  6. The injected UI shows a toast, updates settings, refreshes status, or updates the current UI state.

Management tool flow:

  1. User opens Codex++ 管理工具.
  2. Tauri UI calls Rust commands.
  3. Commands call shared core/data modules.
  4. 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 the Codex++ shortcut.
  • codex-plus-plus-manager.exe: Tauri management console for Codex++ 管理工具.

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:

  1. Add the Rust workspace beside the existing Python package.
  2. Port behavior by domain: models/settings, data operations, CDP/bridge, launch lifecycle, install/update/watcher, Tauri management UI.
  3. Keep Python tests and modules as behavior references during migration.
  4. Add Rust tests and integration fixtures for each migrated domain.
  5. Switch entry points to Rust only after launch, bridge, data operations, install/update, watcher, and management UI are verified.
  6. Remove Python package files, Python packaging metadata, and obsolete tests after parity is established.
  7. 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,结构如下:

json
{
  "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_id
  • version
  • installed
  • source_url
  • homepage

原有字段 key、name、source、enabled、status、error 保持兼容。

Tauri 命令设计

管理工具新增命令:

  • refresh_script_market:拉取市场清单,返回市场条目、加载状态和当前本地 inventory。
  • install_market_script(id):根据市场清单中的 id 下载并安装脚本,返回更新后的市场状态和本地 inventory。

第一版不做后台缓存刷新。页面进入脚本市场时读取 settings 中已有的本地 inventory,再由用户点击刷新市场触发远程加载;如果网络失败,本地脚本列表仍可用。

前端设计

把导航和页面标题从“用户脚本”改为“脚本市场”。页面包含两个区域:

  1. 市场概览和操作区:

    • 市场加载状态
    • 远程脚本数量
    • 已安装市场脚本数量
    • 用户脚本整体启用状态
    • 刷新市场按钮
  2. 市场列表和本地列表:

    • 市场卡片展示名称、描述、版本、作者、标签和状态。
    • 状态包括“未安装”“已安装”“可更新”。
    • 操作包括安装、更新、打开主页。
    • 保留本地脚本列表,展示内置脚本、用户手动脚本和市场安装脚本的当前启用/关闭状态。

第一版不在管理工具里提供删除脚本或启停单个脚本,单脚本启停仍由注入菜单里的现有用户脚本控制保留。

错误处理

  • 市场清单加载失败:前端展示错误消息,保留本地脚本列表。
  • 清单 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 worktree decides 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:

  1. Discover native branch dropdown menus when they open.
  2. Inject an Upstream section containing remote tracking refs such as upstream/main.
  3. 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:

  • repoRoot
  • currentBranch
  • defaultRemote
  • defaultBaseBranch
  • upstreamRefs

upstreamRefs contains display refs like upstream/main plus structured fields:

json
{
  "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-option nodes 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 upstreamRefs for 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_worktree
  • cargo test -p codex-plus-core --test cdp_bridge
  • cargo test -p codex-plus-core --test bridge_routes
  • node --check assets/inject/renderer-inject.js
  • cargo 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:

bash
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.app bundle 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, with origin available only when upstream is absent and the user explicitly chooses or confirms fallback behavior.

The primary action label should make the Git source clear, for example:

text
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:

json
{
  "repoRoot": "/path/to/repo",
  "currentBranch": "feature/demo",
  "defaultBaseBranch": "feature/demo",
  "remotes": ["origin", "upstream"],
  "defaultRemote": "upstream"
}

/upstream-worktree/create accepts:

json
{
  "repoPath": "/path/to/repo",
  "branchName": "feature/demo-codex",
  "worktreePath": "/path/to/worktrees/demo-codex",
  "remote": "upstream",
  "baseBranch": "main",
  "fetch": true
}

It returns either:

json
{
  "status": "ok",
  "repoRoot": "/path/to/repo",
  "worktreePath": "/path/to/worktrees/demo-codex",
  "branchName": "feature/demo-codex",
  "sourceRef": "upstream/main"
}

or:

json
{
  "status": "failed",
  "code": "branch-exists",
  "message": "Branch already exists: feature/demo-codex"
}

Validation Rules

Validate input before running Git:

  • repoPath must resolve inside a Git worktree.
  • branchName must be non-empty and must pass git check-ref-format --branch .
  • worktreePath must be absolute or resolvable to an absolute path; if it exists, it must be empty only if Git accepts it for worktree add.
  • remote must be a configured remote.
  • baseBranch must 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-missing
  • not-git-repo
  • remote-missing
  • base-branch-missing
  • fetch-failed
  • branch-invalid
  • branch-exists
  • path-exists
  • worktree-create-failed
  • ambiguous-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:

  1. Check /upstream-worktree/status.
  2. Show an Upstream worktree setting row and action entry when supported.
  3. Open a compact modal for repo path, branch, worktree path, remote, and base branch.
  4. Call /upstream-worktree/defaults after the repo path changes.
  5. Call /upstream-worktree/create on submit.
  6. 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 upstream when present.
  • origin fallback is not automatic when upstream is 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:

  1. Create a bare remote named upstream.
  2. Push a base branch to it.
  3. Clone or initialize a local repository with a stale local branch.
  4. Call the Rust create function.
  5. Assert the new worktree's HEAD equals upstream/ 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:

  1. A repository with upstream/main newer than local main.
  2. Creating feature/test at a new worktree path through Codex++ menu.
  3. Confirming git -C <worktree-path> rev-parse HEAD matches git -C <repo> rev-parse upstream/main.
  4. Confirming a duplicate branch gives a clear branch-exists error.
  5. Confirming a missing upstream remote gives a clear remote-missing error.
  6. 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.toml context 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
  • 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:

ts
contextSelection: {
  mcpServers: string[]
  skills: string[]
  plugins: string[]
}
contextWindow: string
autoCompactLimit: string

Rust 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 relayCommonConfigContents into 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_window and model_auto_compact_token_limit are 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.toml textarea 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_window and model_auto_compact_token_limit only 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 relayProfiles as 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 relayProfiles automatically.
  • 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:

  1. Provider data is embedded inside a larger settings blob.
  2. Runtime profile concerns and provider identity are coupled.
  3. 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:

  • id
  • name
  • settingsConfig
  • websiteUrl (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:

  1. Read activeProviderId from settings.
  2. Load the referenced provider from provider storage.
  3. Derive the live Codex config, auth config, provider selection, and any runtime relay/provider artifacts from that provider.
  4. 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: String
  • name: String
  • settings_config: serde_json::Value
  • website_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:

  1. clear separation from settings storage
  2. 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:

  1. Read BackendSettings.relayProfiles and activeRelayId.
  2. Convert each relay profile into a provider entity.
  3. Persist the resulting providers in the new provider store.
  4. Map activeRelayId to activeProviderId.
  5. 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 RelayProfile to the new provider model
  • preserve de-duplication behavior based on stable provider identity semantics

Supported import sources in this phase

  1. Automatic migration from legacy local relay profiles
  2. Existing external Codex provider import path
  3. 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 activeProviderId is 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

  1. Add provider domain model and provider store.
  2. Add migration from legacy relay profiles.
  3. Update importers to create providers.
  4. Refactor runtime/launch/config generation to read active provider.
  5. Add Tauri commands for provider CRUD and selection.
  6. Build the manager provider configuration screen.
  7. Remove or demote relay-profile editing paths.
  8. 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.
  • BackendSettings stores global app state and active provider selection, not provider details.
  • Migration from relayProfiles is 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 relayProfiles as 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.json
    • npm run check runs TypeScript checking.
    • npm run vite:build builds the frontend.
    • npm run build locally builds launcher and Tauri manager, but CI can use more explicit steps to stage artifacts.
  • Workspace root Cargo.toml
    • cargo test --workspace validates Rust crates and apps.
    • cargo build --release builds 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

Workflow

Create .github/workflows/pr-build.yml with these triggers:

  • pull_request
  • push to main
  • workflow_dispatch

Use minimal permissions:

yaml
permissions:
  contents: read

The 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:

  1. Checkout the repository.
  2. Set up Node 22.
  3. Set up Rust stable.
  4. Install NSIS with Chocolatey.
  5. Install frontend dependencies in apps/codex-plus-manager using npm install --package-lock=false.
  6. Run npm run check in apps/codex-plus-manager.
  7. Run cargo test --workspace from the repository root.
  8. Run npm run vite:build in apps/codex-plus-manager.
  9. Run cargo build --release from the repository root.
  10. Stage Windows binaries into dist/windows/app:
    • target/release/codex-plus-plus.exe
    • target/release/codex-plus-plus-manager.exe
  11. Build the NSIS installer using scripts/installer/windows/CodexPlusPlus.nsi.
  12. Upload two artifacts:
    • codex-plus-plus-windows-binaries
    • codex-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
  • Apple Silicon:
    • runner: macos-14
    • target: aarch64-apple-darwin
    • arch: arm64

Steps for each matrix entry:

  1. Checkout the repository.
  2. Set up Node 22.
  3. Set up Rust stable with the matrix target.
  4. Install frontend dependencies in apps/codex-plus-manager using npm install --package-lock=false.
  5. Run npm run vite:build in apps/codex-plus-manager.
  6. Run cargo build --release --target <target> from the repository root.
  7. Build the DMG using scripts/installer/macos/package-dmg.sh, passing the package version and architecture.
  8. Upload the generated DMG as an artifact named for the architecture:
    • codex-plus-plus-macos-x64-dmg
    • codex-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: false for 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:

  1. Validate the workflow file is present at .github/workflows/pr-build.yml.
  2. Run a local repository build command that matches the existing project build path:
    • npm --prefix /e/Desktop/CodexPlusPlus/apps/codex-plus-manager run build
  3. 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

  1. Keep the current Codex++ marketplace unlock behavior.

    • The current list-plugins request 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.
  2. 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 authMethod to chatgpt.
    • Force the entry button visible and enabled.
    • Patch React props disabled state 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.
  3. 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.
  4. Do not lose current safeguards.

    • If global enhancementsEnabled is false, all three plugin layers are disabled.
    • In relay/compatibility mode, plugin-related patches remain disabled as they are today.
    • No app.asar patching; all behavior remains runtime injection plus backend settings.
  5. Preserve upgrade compatibility.

    • Existing settings files without the new marketplace field default it to true.
    • Existing codexAppPluginEntryUnlock returns to meaning “legacy entry unlock.”
    • A new codexAppPluginMarketplaceUnlock field controls the current marketplace unlock layer.
    • codexAppForcePluginInstall remains the force install/load field.

Current Context

Current code has these relevant pieces:

  • assets/inject/renderer-inject.js

    • Uses pluginMarketplaceUnlock as the local setting name mapped to backend codexAppPluginEntryUnlock.
    • Contains the current marketplace request/filter patch:
      • patchPluginMarketplaceRequestParams
      • installPluginBuildFlavorFilterPatch
      • installPluginMarketplaceRequestPatch
    • Contains force install button code:
      • pluginInstallCandidates
      • unblockPluginInstallButtons
      • refreshForcePluginInstallUnlockLoop
    • No longer contains the 1.1.9 enablePluginEntry path.
  • v1.1.9:assets/inject/renderer-inject.js

    • Used local setting pluginEntryUnlock mapped to backend codexAppPluginEntryUnlock.
    • Restored functions should include:
      • reactFiberFrom
      • authContextValueFrom
      • spoofChatGPTAuthMethod
      • pluginEntryButton
      • labelUnlockedPluginEntry
      • clearPluginEntryUnlockLabel
      • enablePluginEntry
  • crates/codex-plus-core/src/settings.rs

    • Already has codex_app_plugin_entry_unlock and codex_app_force_plugin_install.
    • Needs codex_app_plugin_marketplace_unlock.

Proposed Data Model

Add a backend setting field:

rust
#[serde(rename = "codexAppPluginMarketplaceUnlock", default = "default_true")]
pub codex_app_plugin_marketplace_unlock: bool,

Defaults:

rust
codex_app_plugin_entry_unlock: true,
codex_app_plugin_marketplace_unlock: true,
codex_app_force_plugin_install: true,

JavaScript local settings:

js
{
  pluginEntryUnlock: true,
  pluginMarketplaceUnlock: true,
  forcePluginInstall: true,
}

Backend map:

js
{
  pluginEntryUnlock: "codexAppPluginEntryUnlock",
  pluginMarketplaceUnlock: "codexAppPluginMarketplaceUnlock",
  forcePluginInstall: "codexAppForcePluginInstall",
}

Runtime Injection Flow

During scanDeferred():

  1. If plugin patches are disabled by relay mode:

    • call clearPluginPatchArtifacts();
    • refresh/stop the force install loop;
    • skip all plugin patch installs.
  2. Otherwise:

    • enablePluginEntry() runs only when pluginEntryUnlock is enabled.
    • installPluginBuildFlavorFilterPatch() and installPluginMarketplaceRequestPatch() run only when pluginMarketplaceUnlock is enabled.
    • unblockPluginInstallButtons() and refreshForcePluginInstallUnlockLoop() run only when forcePluginInstall is enabled.

clearPluginPatchArtifacts() must clear both legacy entry labels and force install labels.

UI Design

Codex++ injected menu shows three plugin rows:

  1. 插件市场解锁

    • Description: API Key 模式下扩展插件市场请求,尽量显示完整插件列表。
    • Setting: pluginMarketplaceUnlock
  2. 强制解锁入口

    • Description: 恢复 1.1.9 的入口解锁方式,强制显示并启用插件入口。
    • Setting: pluginEntryUnlock
  3. 特殊插件强制安装 or 强制加载

    • Description: 解除 App unavailable / 应用不可用导致的前端安装禁用。
    • Setting: forcePluginInstall

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

  1. Rust settings tests:

    • defaults include all three plugin booleans as true;
    • JSON missing codexAppPluginMarketplaceUnlock loads as true;
    • settings update preserves independent values for entry, marketplace, and force install.
  2. Injection static/behavior tests:

    • backend setting map contains both pluginEntryUnlock and pluginMarketplaceUnlock with different backend keys;
    • enablePluginEntry() is gated by pluginEntryUnlock;
    • marketplace request/filter patch functions are gated by pluginMarketplaceUnlock;
    • force install functions are gated by forcePluginInstall;
    • scanDeferred() invokes all three independent paths.
  3. Verification commands:

    • cargo test -p codex-plus-core settings
    • cargo test -p codex-plus-core cdp_bridge -- --nocapture if injection static tests live there
    • cargo check -p codex-plus-manager
    • npm --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-src or 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:

text
同步目标
[ 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:

text
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:

  1. Previously selected provider if it is still present.
  2. Current root model_provider if present.
  3. First provider option.
  4. openai only if no provider can be discovered.

The progress message includes the chosen target:

text
正在同步到 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:

  1. Provider target discovery, matching the reference project's source merge rules.
  2. Explicit-target provider sync, matching codex-provider sync --provider semantics.
  3. 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:

rust
#[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:

rust
pub fn load_provider_sync_targets(codex_home: Option<&Path>) -> ProviderSyncTargetList

It builds a map from provider ID to sources, mirroring the reference project:

  1. Add all provider IDs declared by [model_providers.] in config.toml with source Config.
  2. Add the current root model_provider with source Config.
  3. Add rollout provider IDs from sessions/**/rollout-*.jsonl and archived_sessions/**/rollout-*.jsonl with source Rollout.
  4. Add SQLite provider IDs from state_5.sqlite threads.model_provider, split by archived/non-archived only for counting; source is still Sqlite.
  5. Add saved/manual providers from Codex++ settings with source Manual once those settings fields exist.
  6. Add the default provider openai through the config-provider parser behavior, matching the reference listConfiguredProviderIds() default inclusion.

Provider IDs that are missing, empty, or whitespace-only are ignored. Missing model_provider falls back to openai.

Sort options by:

  1. id == current_provider descending.
  2. id ascending with ordinal comparison.

Settings for saved and manual providers

Extend manager settings with fields equivalent to the reference app state:

ts
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:

rust
pub fn run_provider_sync_with_target(
    codex_home: Option<&Path>,
    target_provider: Option<&str>,
) -> ProviderSyncResult

Preserve the existing function as a wrapper:

rust
pub fn run_provider_sync(codex_home: Option<&Path>) -> ProviderSyncResult {
    run_provider_sync_with_target(codex_home, None)
}

Target resolution matches the reference project:

  1. If target_provider is present and non-empty after trim, use it.
  2. Otherwise use root model_provider from config.toml.
  3. 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:

text
~/.codex/sessions/**/rollout-*.jsonl
~/.codex/archived_sessions/**/rollout-*.jsonl

For 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:

sql
UPDATE threads
SET model_provider = $provider
WHERE COALESCE(model_provider, '') <> $provider

If the has_user_event column exists, set it for threads whose rollout file contains user events:

sql
UPDATE threads
SET has_user_event = 1
WHERE id = $id AND COALESCE(has_user_event, 0) <> 1

If the cwd column exists, update it from rollout metadata by thread ID:

sql
UPDATE threads
SET cwd = $cwd
WHERE id = $id AND COALESCE(cwd, '') <> $cwd

Malformed 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:

text
electron-saved-workspace-roots
project-order
active-workspace-roots
electron-workspace-root-labels
open-in-target-preferences.perPath

Codex++ 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:

text
.codex-global-state.json
.codex-global-state.json.bak

Backup format

Keep the backup root:

text
~/.codex/backups_state/provider-sync/<timestamp>

Back up the same artifacts as the reference project:

text
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.json

session-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:

json
{
  "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:

  1. Open SQLite transaction if the database exists.
  2. Apply SQLite provider/user-event/cwd updates inside the transaction.
  3. Rewrite rollout files and record the actually applied files.
  4. Sync global state.
  5. Commit SQLite transaction only after rollout and global state writes succeed.
  6. On failure before commit, rollback SQLite.
  7. Restore any rollout files already rewritten.
  8. 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:

rust
#[tauri::command]
pub async fn load_provider_sync_targets() -> CommandResult<Value>

Return payload shape:

json
{
  "currentProvider": "apigather",
  "targets": [
    {
      "id": "apigather",
      "sources": ["config"],
      "isCurrentProvider": true,
      "isManual": false,
      "isSaved": true
    }
  ]
}

Modify existing command:

rust
#[tauri::command]
pub async fn sync_providers_now(target_provider: Option<String>) -> CommandResult<Value>

The frontend calls it as:

ts
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.rs continues calling codex_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_now is called without targetProvider, 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 sessions rollout metadata.
  • Provider discovery includes provider IDs from archived_sessions rollout 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_provider to the selected target.
  • Sync updates SQLite threads.model_provider to the selected target.
  • Sync updates has_user_event and cwd consistently with existing behavior.
  • Global state sync handles open-in-target-preferences.perPath.
  • Backup metadata.json includes structured fields and preserves managedBy.
  • Failure during rollout/global state write rolls back SQLite and restores applied files.
  • Tauri sync_providers_now accepts optional targetProvider and returns the selected targetProvider.

Manual verification should cover:

  1. Open Codex++ manager.
  2. Confirm the sync target dropdown lists providers from config, rollout files, and SQLite.
  3. Pick a non-current provider.
  4. Run sync.
  5. Confirm result message says the selected target.
  6. Confirm historical sessions become visible under the selected provider.
  7. Confirm config.toml root model_provider was not changed.
  8. 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

  1. 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.
  2. 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.
  3. Preserve existing safety gates.

    • If enhancementsEnabled is false, plugin unlock work does not run.
    • If launch mode is relay, plugin unlock work stays disabled.
    • No app.asar patching.
  4. 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:

text
PLUGIN_LEGACY_ENTRY_UNLOCK_BEFORE = 26.601.21317

Rules:

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:

json
{
  "codexAppVersion": "26.601.21317"
}

If the version cannot be read:

json
{
  "codexAppVersion": ""
}

Implementation options for locating the app version:

  1. Use the configured Codex App path from settings if present.
  2. Fall back to the same discovery path used by load_overview.
  3. 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:

js
const codexPluginLegacyEntryUnlockBeforeVersion = "26.601.21317";

function parseCodexVersionParts(version) { ... }
function compareCodexVersions(left, right) { ... }
function codexPluginUnlockStrategy() { ... }

Expected behavior:

js
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:

js
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_selected
    • strategy
    • codexAppVersion
    • cutoff

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

  1. Backend route tests:

    • /settings/get response contains codexAppVersion when 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.
  2. Injection static tests:

    • Script contains cutoff 26.601.21317.
    • Script contains compareCodexVersions and codexPluginUnlockStrategy.
    • Legacy strategy gates enablePluginEntry().
    • Modern strategy gates installPluginBuildFlavorFilterPatch() and installPluginMarketplaceRequestPatch().
    • Unknown strategy allows both enabled paths.
  3. 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 forcePluginInstall behavior 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-primary surface 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.py

  • Test: tests/test_settings_store.py

  • Step 1: Write the failing settings tests

Create tests/test_settings_store.py with:

python
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:

bash
python -m pytest -q tests/test_settings_store.py

Expected: FAIL with ModuleNotFoundError: No module named 'codex_session_delete.settings_store'.

  • Step 3: Implement settings store

Create codex_session_delete/settings_store.py with:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 4: Run tests to verify they pass

Run:

bash
python -m pytest -q tests/test_settings_store.py

Expected: 3 passed.

  • Step 5: Commit settings store

Run:

bash
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.py

  • Test: tests/test_provider_sync.py

  • Step 1: Write the failing provider sync tests

Create tests/test_provider_sync.py with:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 2: Run tests to verify they fail

Run:

bash
python -m pytest -q tests/test_provider_sync.py

Expected: 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:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 4: Run provider sync tests

Run:

bash
python -m pytest -q tests/test_provider_sync.py

Expected: 3 passed.

  • Step 5: Commit provider sync core

Run:

bash
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.py

  • Test: tests/test_launcher_cli.py

  • Step 1: Write failing launcher tests

Add these tests after test_launch_and_inject_returns_windows_packaged_process_id in tests/test_launcher_cli.py:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 2: Run tests to verify they fail

Run:

bash
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_disabled

Expected: 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:

python
from codex_session_delete.provider_sync import ProviderSyncStatus, run_provider_sync
from codex_session_delete.settings_store import BackendSettings, SettingsStore

Add this function after user_scripts_config_dir():

python
def backend_settings() -> BackendSettings:
    return SettingsStore().load()
  • Step 4: Call provider sync before launch

In launch_and_inject(), before server = start_helper(...), add:

python
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:

bash
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_disabled

Expected: 2 passed.

  • Step 6: Commit launcher integration

Run:

bash
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.py

  • Test: tests/test_launcher_user_scripts.py

  • Step 1: Write failing bridge tests

Add this import to tests/test_launcher_user_scripts.py:

python
from codex_session_delete.settings_store import SettingsStore

Add these tests after test_handle_bridge_request_reports_and_repairs_backend_status:

python
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:

bash
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_settings

Expected: 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:

python
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:

bash
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_settings

Expected: 2 passed.

  • Step 5: Commit bridge endpoints

Run:

bash
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.js

  • Modify: tests/test_renderer_script.py

  • Step 1: Write failing renderer contract test

Add this test to tests/test_renderer_script.py near the Codex++ menu tests:

python
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:

bash
python -m pytest -q tests/test_renderer_script.py::test_renderer_script_has_backend_provider_sync_toggle

Expected: FAIL with missing Provider 同步.

  • Step 3: Add renderer backend settings functions

In codex_session_delete/inject/renderer-inject.js, after function setCodexPlusSetting(...), add:

javascript
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:

html
<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:

javascript
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:

javascript
loadBackendSettings();
  • Step 6: Run renderer contract test

Run:

bash
python -m pytest -q tests/test_renderer_script.py::test_renderer_script_has_backend_provider_sync_toggle

Expected: 1 passed.

  • Step 7: Commit renderer toggle

Run:

bash
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:

bash
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.py

Expected: 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:

bash
python -m pytest -q

Expected: all tests pass.

  • Step 3: Inspect final diff

Run:

bash
git diff --stat HEAD~5..HEAD

Expected: 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 under crates/ and apps/.
  • 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 the Codex++ shortcut.
  • Create apps/codex-plus-manager/: Tauri management console used by Codex++ 管理工具.
  • Move or copy runtime assets into assets/: icons, sponsor images, and renderer-inject.js source used by Rust packaging.
  • Create tests/fixtures/: shared SQLite, rollout, and settings fixtures for Rust integration tests.
  • Modify README.md and README_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.toml

  • Create: .cargo/config.toml

  • Create: crates/codex-plus-core/Cargo.toml

  • Create: crates/codex-plus-core/src/lib.rs

  • Create: crates/codex-plus-core/src/version.rs

  • Create: crates/codex-plus-data/Cargo.toml

  • Create: crates/codex-plus-data/src/lib.rs

  • Create: apps/codex-plus-launcher/Cargo.toml

  • Create: apps/codex-plus-launcher/src/main.rs

  • Create: apps/codex-plus-manager/package.json

  • Create: apps/codex-plus-manager/src-tauri/Cargo.toml

  • Create: apps/codex-plus-manager/src-tauri/src/lib.rs

  • Create: apps/codex-plus-manager/src-tauri/src/main.rs

  • Create: apps/codex-plus-manager/src/main.tsx

  • Create: apps/codex-plus-manager/index.html

  • Step 1: Write the workspace manifests

Create root Cargo.toml:

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:

toml
[alias]
xtest = "test --workspace --all-targets"
xcheck = "check --workspace --all-targets"
  • Step 2: Add core crate

Create crates/codex-plus-core/Cargo.toml:

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 = true

Create crates/codex-plus-core/src/lib.rs:

rust
pub mod version;

Create crates/codex-plus-core/src/version.rs:

rust
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:

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 = true

Create crates/codex-plus-data/src/lib.rs:

rust
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:

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 = true

Create apps/codex-plus-launcher/src/main.rs:

rust
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:

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:

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:

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:

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:

rust
#[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:

rust
fn main() {
    codex_plus_manager_lib::run();
}
  • Step 6: Run Rust checks

Run:

bash
cargo test --workspace --all-targets

Expected: 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
bash
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.rs

  • Create: crates/codex-plus-core/src/paths.rs

  • Create: crates/codex-plus-core/src/settings.rs

  • Create: crates/codex-plus-core/src/status.rs

  • Modify: crates/codex-plus-core/src/lib.rs

  • Step 1: Write model and settings tests

Create tests inside the new modules for:

rust
#[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:

rust
#[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:

rust
#[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:

rust
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
bash
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.rs

  • Create: crates/codex-plus-data/src/storage.rs

  • Create: crates/codex-plus-data/src/markdown.rs

  • Create: crates/codex-plus-data/src/provider_sync.rs

  • Modify: crates/codex-plus-data/src/lib.rs

  • Create: crates/codex-plus-data/tests/storage_adapter.rs

  • Create: crates/codex-plus-data/tests/provider_sync.rs

  • Step 1: Port fixture behavior from Python tests

Use these Python tests as references:

text
tests/test_storage_adapter.py
tests/test_backup_store.py
tests/test_markdown_exporter.py
tests/test_provider_sync.py

Create Rust integration fixtures that cover:

  • sessions/messages schema deletion and undo.

  • threads schema deletion, related table cleanup, and rollout file backup.

  • Workspace move updating both SQLite cwd and 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, SQLite threads.model_provider, has_user_event, and cwd.

  • Step 2: Implement backup store

Implement backup JSON compatible with current Python backups:

rust
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:

rust
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:

text
codex_session_delete/markdown_exporter.py
codex_session_delete/provider_sync.py

Provider 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
bash
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.rs

  • Create: crates/codex-plus-core/src/bridge.rs

  • Create: crates/codex-plus-core/src/assets.rs

  • Modify: crates/codex-plus-core/src/lib.rs

  • Create: crates/codex-plus-core/tests/cdp_bridge.rs

  • Copy asset: assets/inject/renderer-inject.js

  • Copy assets: assets/images/codex-plus-plus.ico, assets/images/codex-plus-plus.png, sponsor images

  • Step 1: Write bridge script tests

Test that the Rust bridge script defines:

text
window.__codexSessionDeleteBridge
window.__codexSessionDeleteResolve
window.__codexSessionDeleteReject
codexSessionDeleteV2

Test that injection prefix includes:

text
window.__CODEX_SESSION_DELETE_HELPER__
window.__CODEX_PLUS_SPONSOR_IMAGES__
  • Step 2: Implement CDP target discovery

Implement:

rust
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.enable
  • Runtime.removeBinding
  • Runtime.addBinding
  • Page.addScriptToEvaluateOnNewDocument
  • Runtime.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
bash
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.rs

  • Create: crates/codex-plus-core/src/ports.rs

  • Create: crates/codex-plus-core/src/proxy.rs

  • Create: crates/codex-plus-core/src/launcher.rs

  • Modify: apps/codex-plus-launcher/src/main.rs

  • Create: crates/codex-plus-core/tests/launcher.rs

  • Step 1: Port path and port tests

Use these Python tests as references:

text
tests/test_app_paths.py
tests/test_launcher_cli.py
tests/test_cdp.py

Test 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:

text
codex_session_delete/app_paths.py
codex_session_delete/launcher.py::codex_process_environment
codex_session_delete/launcher.py::build_codex_arguments

Keep proxy auto-detection ports:

text
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:

rust
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
bash
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.rs

  • Modify: crates/codex-plus-core/src/bridge.rs

  • Modify: crates/codex-plus-core/Cargo.toml

  • Create: crates/codex-plus-core/tests/bridge_routes.rs

  • Step 1: Write route tests

Create tests covering all current route names:

text
/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:

rust
pub async fn handle_bridge_request(ctx: BridgeContext, path: &str, payload: serde_json::Value) -> serde_json::Value;

Unknown routes must return:

json
{"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
bash
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.ts

  • Create: apps/codex-plus-manager/src/styles.css

  • Modify: apps/codex-plus-manager/src/main.tsx

  • Modify: apps/codex-plus-manager/src-tauri/src/lib.rs

  • Create: apps/codex-plus-manager/src-tauri/src/commands.rs

  • Create: apps/codex-plus-manager/src-tauri/src/install.rs

  • Step 1: Implement Tauri commands

Expose commands:

rust
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_settings

Each command returns serializable result objects with status, message, and relevant payload fields.

  • Step 2: Build workbench UI

Implement the left navigation:

text
Overview
Launch
Install
Update
Settings
Logs
Diagnostics

Use 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

bash
cd apps/codex-plus-manager
npm install
npm run check
npm run build

Expected: TypeScript check passes and Tauri build succeeds.

  • Step 6: Commit management console
bash
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.rs

  • Create: crates/codex-plus-core/src/install/macos.rs

  • Create: crates/codex-plus-core/src/install/mod.rs

  • Create: crates/codex-plus-core/src/update.rs

  • Create: crates/codex-plus-core/src/watcher.rs

  • Modify: crates/codex-plus-core/src/lib.rs

  • Create: crates/codex-plus-core/tests/installers.rs

  • Create: crates/codex-plus-core/tests/updater.rs

  • Step 1: Port installer script tests

Use these Python tests as references:

text
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.py

Test generated Windows shortcut scripts contain both:

text
Codex++.lnk
Codex++ 管理工具.lnk

Test generated macOS app bundle metadata contains both:

text
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:

text
/Applications/Codex++.app
/Applications/Codex++ 管理工具.app

The 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
bash
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.md

  • Modify: README_EN.md

  • Modify: setup.bat

  • Modify: .github release workflow files if present

  • Modify: docs/images only if icon packaging requires path updates

  • Step 1: Update README commands

Replace Python commands with two-entry usage:

text
双击 Codex++:静默启动增强版 Codex。
双击 Codex++ 管理工具:安装、卸载、更新、设置、日志和诊断。

Remove references to:

text
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:

text
codex-plus-plus.exe
codex-plus-plus-manager.exe
Codex++.app
Codex++ 管理工具.app
  • Step 4: Run docs tests and commit
bash
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.toml

  • Delete or replace: tests/test_*.py that only validate Python internals

  • Modify: .gitignore if Python-only entries are obsolete

  • Step 1: Run full Rust verification

bash
cargo test --workspace --all-targets
cd apps/codex-plus-manager
npm run check
npm run build

Expected: all Rust tests pass and Tauri app builds.

  • Step 2: Run remaining Python docs tests
bash
python -m pytest -q tests/test_readme.py tests/test_setup_bat.py

Expected: 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
bash
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 TODO or TBD placeholders 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-launcher for 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.rs

  • Test: crates/codex-plus-core/tests/upstream_worktree.rs

  • Step 1: Write failing test

Add this test to crates/codex-plus-core/tests/upstream_worktree.rs:

rust
#[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:

bash
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_branch

Expected: 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:

rust
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:

rust
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
bash
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.js

  • Test: crates/codex-plus-core/tests/cdp_bridge.rs

  • Step 1: Write failing script-marker test

Add this test to crates/codex-plus-core/tests/cdp_bridge.rs:

rust
#[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:

bash
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_adapter

Expected: fails because adapter names and markers are not present.

  • Step 3: Implement adapter skeleton

In renderer-inject.js, add constants near other upstream constants:

js
const upstreamBranchOptionAttribute = "data-codex-upstream-branch-option";
const upstreamBranchSelectionKey = "codexUpstreamBranchSelection";

Add functions near the upstream worktree helpers:

text
/* 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
bash
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.js

  • Test: crates/codex-plus-core/tests/cdp_bridge.rs

  • Step 1: Write failing marker test

Extend injection_script_installs_upstream_branch_dropdown_adapter with:

rust
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:

js
let upstreamBranchDefaultsCache = null;

Add helpers:

text
/* Detailed source-code truncated for AI context efficiency. */

In the adapter install function, add a mutation observer that schedules injection:

js
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
bash
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.js

  • Test: crates/codex-plus-core/tests/cdp_bridge.rs

  • Step 1: Write failing marker test

Extend the cdp bridge test with:

rust
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:

js
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:

js
const payload = upstreamWorktreePayloadFromSelection(trigger) || upstreamWorktreeNativePayloadFromElement(trigger);

After success, clear selection:

js
writeUpstreamBranchSelection(null);
  • Step 4: Verify GREEN

Run the cdp bridge test. Expected: PASS.

  • Step 5: Commit
bash
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

bash
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
bash
env -u CFLAGS -u CPPFLAGS -u LDFLAGS rustup run "1.95.0" cargo test -p codex-plus-core

Expected: PASS.

  • Step 3: Build and package
bash
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
bash
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:

bash
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()
PY

Then verify git status --short is clean except ignored build outputs.