### 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<u64>`，需要 `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);
```

3. 窗口来源改为 `model_windows.get(&slug)`：

```rust
let suffix_window = model_windows.get(&slug).and_then(|token| parse_window_token(token));
```

4. 处理 `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/<profile>.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/<profile-id>.json`
6. config.toml 注入 `model_catalog_json = "model-catalogs/<id>.json"`（**相对路径**）

### 3.3 插入点（中间层影响最小）

现有流程：
```
complete_relay_profile_config → merge_common → preserve_unmanaged
→ apply_context_limits_to_config（写顶层单值）
→ 落盘
```

新增独立可选步骤，插在 `apply_context_limits_to_config` 之后、落盘之前：
```
→ apply_context_limits_to_config
→ [新] apply_model_catalog_for_profile(home, profile, &config_with_limits)
→ 落盘
```

- **签名不变**：不修改任何现有 apply 函数签名
- **opt-in**：靠后缀触发，无后缀 no-op
- **旧 profile 零行为变化**：那两个「不写 catalog」测试继续过（无需改它们）

### 3.4 catalog 字段决策

| 字段 | 值 | 依据 |
| --- | --- | --- |
| `context_window` | 后缀解析值 | #931 / cc-switch |
| `max_context_window` | 同 context_window | cc-switch |
| `effective_context_window_percent` | 待验证 | cc-switch 写 95 致显示 950K；若想显示真实值写 100 或省略 |
| `auto_compact_token_limit` | null | codex 内置模型即 null（按比例算）；详见调研文档第六节 |

## 4. 兼容性

- 无后缀 → no-op，旧 profile 行为不变
- 用户已手写 `model_catalog_json` 指针 → 检测到则跳过生成，不覆盖（保 `preserves_user_model_catalog_json` 测试过）
- `model_list` 不进 config.toml，后缀不污染 codex 请求
- 顶层 `model_context_window` / `model_auto_compact_token_limit` 保留作回退默认

## 5. 验证计划（阶段一核心）

### A. 预检（手工对拍）
手抄 cc-switch 格式 catalog（deepseek-v4-pro→1000000）+ config.toml 指针，**直接起 codex 客户端（PureApi，不经 relay 代理）**，确认报 ~1M。消除「#931 是别人环境结论」疑虑，拿到已知可用样本。

### B. 原型产物对拍
实跑原型生成的 catalog + 指针，同样起 codex 客户端，确认报 1M。与 A 样本对比字段。

### 必验项
1. **Mac 相对路径**：`model-catalogs/<id>.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-<id>.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-<id>.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>/<branch>` 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/<base-branch>`.
- 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>/<base-branch>` after fetch.
- Safe command construction for `git fetch <remote> <base-branch>`.
- Safe command construction for `git worktree add -b <new-branch> <worktree-path> <remote>/<base-branch>`.
- 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 <branchName>`.
- `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>/<baseBranch>` 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/<base-branch>` 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>`

`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 <id>` 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.<id>]` 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 <id>`, not `codex-provider switch <id>`.

## 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.<id>]` 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:

```
/* 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:

```
/* 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:

```
/* 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`:

```
/* 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/<base>` 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", "test@example.com"]);
    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:

```
/* 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:

```
/* 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.

---