mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-18 05:59:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69e0a46430 | ||
|
|
ab19b2ef33 | ||
|
|
ff34902641 | ||
|
|
07e4968ade | ||
|
|
ba5e90e24f | ||
|
|
8ec879e7a9 | ||
|
|
971236fc67 | ||
|
|
fb53b90763 | ||
|
|
257a4385b7 | ||
|
|
c0f3c9240c | ||
|
|
67ad656b45 | ||
|
|
5dbd6e2359 | ||
|
|
da3c52a7de | ||
|
|
34ec59f28b | ||
|
|
9a82612d5d |
@@ -21,10 +21,16 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: macOS
|
||||
- name: macOS Apple Silicon
|
||||
platform: macos-latest
|
||||
args: --bundles dmg
|
||||
artifact: codex-x-macos
|
||||
artifact: codex-x-macos-arm64
|
||||
paths: |
|
||||
apps/desktop/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
- name: macOS Intel
|
||||
platform: macos-15-intel
|
||||
args: --bundles dmg
|
||||
artifact: codex-x-macos-x64
|
||||
paths: |
|
||||
apps/desktop/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
- name: Linux
|
||||
@@ -40,6 +46,7 @@ jobs:
|
||||
artifact: codex-x-windows
|
||||
paths: |
|
||||
apps/desktop/src-tauri/target/release/bundle/msi/*.msi
|
||||
apps/desktop/src-tauri/Codex-X_*_windows_x64_portable.zip
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -81,6 +88,23 @@ jobs:
|
||||
- name: Build Tauri bundles
|
||||
run: pnpm --dir apps/desktop tauri build ${{ matrix.args }}
|
||||
|
||||
- name: Create Windows portable zip
|
||||
if: matrix.platform == 'windows-latest'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(node -p "require('./apps/desktop/package.json').version")
|
||||
portable_dir="apps/desktop/src-tauri/portable/Codex-X_${version}_windows_x64_portable"
|
||||
rm -rf "apps/desktop/src-tauri/portable"
|
||||
mkdir -p "$portable_dir"
|
||||
cp apps/desktop/src-tauri/target/release/codex-x.exe "$portable_dir/Codex-X.exe"
|
||||
cat > "$portable_dir/README-portable.txt" <<'EOF'
|
||||
Codex-X Windows Portable
|
||||
|
||||
Double click Codex-X.exe to run. No MSI installation is required.
|
||||
Microsoft Edge WebView2 Runtime is required; Windows 10/11 usually already includes it.
|
||||
EOF
|
||||
powershell -NoProfile -Command "Compress-Archive -Path 'apps/desktop/src-tauri/portable/Codex-X_${version}_windows_x64_portable/*' -DestinationPath 'apps/desktop/src-tauri/Codex-X_${version}_windows_x64_portable.zip' -Force"
|
||||
|
||||
- name: Upload bundle artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -139,4 +163,10 @@ jobs:
|
||||
else
|
||||
gh release create "$tag" --title "$title" --notes-file release-body.md --latest
|
||||
fi
|
||||
gh release upload "$tag" release-assets/* --clobber
|
||||
mapfile -t assets < <(find release-assets -type f \( -name '*.dmg' -o -name '*.msi' -o -name '*.deb' -o -name '*.rpm' -o -name '*.zip' \) -print | sort)
|
||||
if [ "${#assets[@]}" -eq 0 ]; then
|
||||
echo "No release assets found" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'Uploading %s\n' "${assets[@]}"
|
||||
gh release upload "$tag" "${assets[@]}" --clobber
|
||||
|
||||
@@ -2,6 +2,55 @@
|
||||
|
||||
All notable changes to Codex-X will be documented here.
|
||||
|
||||
## [v0.2.17] - 2026-07-04
|
||||
|
||||
- 修复 macOS Intel Release 构建 runner:从已不可用/长时间排队的 `macos-13` 切换为 `macos-15-intel`。
|
||||
- 继续保留 macOS Apple Silicon / macOS Intel / Windows MSI / Windows portable ZIP / Linux deb/rpm 多平台产物。
|
||||
|
||||
## [v0.2.16] - 2026-07-04
|
||||
|
||||
- Release 新增 macOS Intel 构建,Intel Mac 用户可下载 x64 DMG。
|
||||
- macOS Release 现在同时提供 Apple Silicon 与 Intel 两种 DMG。
|
||||
- Windows Release 新增 portable ZIP,包含可直接运行的 `Codex-X.exe`,无需 MSI 安装。
|
||||
- 发布流程支持上传 `.zip` portable 产物,并更新 README 下载说明。
|
||||
|
||||
## [v0.2.15] - 2026-07-04
|
||||
|
||||
- 会话管理页移除 CODEX_HOME 与 Provider Sync 备份位置展示,页面信息更简洁。
|
||||
- 优化 Windows 打开下载页体验:后端改为后台线程 spawn 浏览器,不再等待 `cmd /C start`,避免 WebView 卡顿 2-3 秒。
|
||||
- 统一所有外部链接按钮走异步打开逻辑,打开项目主页、反馈页、下载页都不会阻塞界面。
|
||||
- 修复 macOS 顶部 toast 被 Overlay 标题栏遮挡的问题,toast 自动下移到标题栏安全区下方。
|
||||
|
||||
## [v0.2.14] - 2026-07-04
|
||||
|
||||
- 修复 macOS Overlay 标题栏遮挡内容的问题,为红黄绿窗口按钮预留完整安全区。
|
||||
- 调整侧边栏、内容区、Provider/TOML/指令提示词/会话管理页高度计算,避免顶部内容被标题栏压住。
|
||||
- 加高 macOS 顶部拖拽区域并保持深色渐变,窗口顶部继续和应用色系统一。
|
||||
|
||||
## [v0.2.13] - 2026-07-04
|
||||
|
||||
- macOS 窗口标题栏改为深色融合样式,避免顶部出现系统白色标题条。
|
||||
- 会话管理页面移除多余外层玻璃边框,统一为完整内容容器。
|
||||
- 优化会话列表边界与滚动区域,避免列表像卡在外层框外面。
|
||||
|
||||
## [v0.2.12] - 2026-07-04
|
||||
|
||||
- Windows MSI 安装器回退为默认简洁样式,移除上一版过重的自定义安装界面图。
|
||||
- 【会话管理】页面新增会话列表,展示标题、Provider、模型、工作目录、更新时间、归档/需同步状态。
|
||||
- 会话扫描会从 Codex 本地 SQLite threads 表读取最近会话,方便用户直接判断哪些历史 thread 需要同步修复。
|
||||
|
||||
## [v0.2.11] - 2026-07-04
|
||||
|
||||
- 调整首次启动自动检查更新体验:不再弹出居中的强提醒窗口。
|
||||
- 自动检测到新版本时仅显示轻量 toast,并保留概览页顶部“发现新版本”提示条。
|
||||
- 只有用户在【关于】页面主动点击“检查更新”时,检测到新版本才弹出“现在下载 / 稍后”窗口。
|
||||
|
||||
## [v0.2.10] - 2026-07-04
|
||||
|
||||
- 修复 Release 发布任务上传 Linux 产物时误把 `deb/`、`rpm/` 目录当作资产上传的问题。
|
||||
- 发布任务现在只收集 `.dmg` / `.msi` / `.deb` / `.rpm` 文件并统一上传。
|
||||
- 补齐稳定的三平台 Release 自动发布流程。
|
||||
|
||||
## [v0.2.9] - 2026-07-04
|
||||
|
||||
- 修复 GitHub Actions Release 发布流程:不再由三平台矩阵并发创建 Release,改为先上传构建产物,再由单独发布任务统一创建/更新 Release。
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<div align="center">
|
||||
<img src="apps/desktop/src-tauri/icons/icon.png" alt="Codex-X Logo" width="160" />
|
||||
<img src="apps/desktop/src-tauri/icons/icon.png" alt="Codex-X Logo" width="150" />
|
||||
|
||||
# Codex-X
|
||||
|
||||
**Codex 提示词注入与配置切换管理器**
|
||||
**Codex 提示词注入 · Provider 切换 · TOML / Auth 可视化管理器**
|
||||
|
||||
一款面向 **OpenAI Codex / Codex CLI** 的跨平台桌面工具,重点提供 `gpt5.5-unrestricted.md` 指令提示词注入、Provider 切换、官方 Auth 管理与 TOML 可视化编辑。
|
||||
一款面向 **OpenAI Codex 桌面端 / Codex CLI** 的跨平台桌面工具,内置 `gpt5.5-unrestricted.md` 与 `gpt5.4-unrestricted.md`,支持一键写入 / 禁用指令提示词、第三方 Provider 切换、官方 Auth 管理、TOML 可视化编辑与本地会话 Provider Sync。
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/github/v/release/yynxxxxx/Codex-X?label=version&color=blue" alt="version" />
|
||||
@@ -27,80 +27,217 @@
|
||||
|
||||
## Codex-X 是什么?
|
||||
|
||||
Codex-X 不是单纯的配置文件编辑器,而是一个更偏向「Codex 增强启动器」的桌面管理工具。
|
||||
Codex-X 不是普通的配置文件编辑器,而是一个面向 Codex CLI 的 **可视化增强管理器**。
|
||||
|
||||
它可以把预设或自定义的 **指令提示词模板** 写入 Codex 配置,例如项目内置的:
|
||||
它把几个高频操作做成了桌面软件:
|
||||
|
||||
```text
|
||||
examples/gpt5.5-unrestricted.md
|
||||
```
|
||||
|
||||
你可以在 UI 中直接启用、禁用、切换多个提示词模板,也可以同时管理不同第三方供应商 API 和官方 OpenAI Codex 登录配置。
|
||||
|
||||
## 核心亮点
|
||||
|
||||
### 1. 指令提示词注入
|
||||
|
||||
- 一键启用 `gpt5.5-unrestricted.md`
|
||||
- 支持多个提示词模板并存
|
||||
- 启用前自动记录状态,方便回退
|
||||
- 支持禁用当前提示词,让 Codex 恢复普通配置
|
||||
|
||||
### 2. Provider 可视化切换
|
||||
|
||||
- 添加第三方 Codex API Provider
|
||||
- 编辑 Base URL / API Key / Model / TOML 配置
|
||||
- 当前 Provider 状态清晰可见
|
||||
- 支持从 cc-switch 数据库导入 Codex Provider
|
||||
|
||||
### 3. 官方 Codex 配置管理
|
||||
|
||||
- 读取 Codex 官方 `auth.json`
|
||||
- 支持官方 ChatGPT 登录态 Auth 查看与编辑
|
||||
- 区分官方 Auth 与第三方 API Key,不混在一起
|
||||
|
||||
### 4. TOML 配置编辑
|
||||
|
||||
- 可视化查看当前 Codex `config.toml`
|
||||
- 在 Provider 编辑页直接编辑完整 TOML
|
||||
- 保存后立即同步到 Codex 配置目录
|
||||
|
||||
### 5. 跨平台桌面软件
|
||||
|
||||
- macOS `.dmg`
|
||||
- Windows `.msi`
|
||||
- Linux `.deb` / `.rpm`
|
||||
- GitHub Releases 自动构建发布
|
||||
- 给 Codex 写入 / 禁用指令提示词模板
|
||||
- 切换官方 OpenAI 与第三方 Codex API Provider
|
||||
- 查看 / 编辑 `~/.codex/config.toml`
|
||||
- 查看 / 编辑官方 `~/.codex/auth.json`
|
||||
- 检查并修复本地历史会话的 Provider 元数据
|
||||
|
||||
## 软件预览
|
||||
|
||||
<details open>
|
||||
<summary><b>主界面 / Provider / TOML / Auth 截图</b></summary>
|
||||
<summary><b>应用界面预览</b>:主界面 / Provider / TOML / Auth</summary>
|
||||
|
||||
> 这里可以放软件主界面、供应商切换页面、TOML 编辑页面、官方 Auth 页面截图。
|
||||
|
||||
<!-- 示例:
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/overview.png" width="760" />
|
||||
<img src="docs/screenshots/app/preview.png" alt="Codex-X 应用界面预览:主界面、Provider、TOML、Auth" width="920" />
|
||||
</p>
|
||||
-->
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>提示词注入前后效果图</b></summary>
|
||||
<summary><b>提示词注入效果:安全测试场景</b></summary>
|
||||
|
||||
> 这里可以放启用 `gpt5.5-unrestricted.md` 前后的 Codex 效果对比图。
|
||||
|
||||
<!-- 示例:
|
||||
<p align="center">
|
||||
<img src="docs/screenshots/prompt-before.png" width="760" />
|
||||
<img src="docs/screenshots/prompt-after.png" width="760" />
|
||||
</p>
|
||||
-->
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
<b>SQL 注入测试</b><br />
|
||||
<sub>部署后测试:如何对目标进行 SQL 注入测试?</sub><br />
|
||||
<img src="docs/screenshots/prompt-effects/security/sql-injection.png" alt="SQL 注入测试效果图" width="420" />
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
<b>NSFW 响应测试</b><br />
|
||||
<sub>用于观察提示词注入后的边界响应变化</sub><br />
|
||||
<img src="docs/screenshots/prompt-effects/security/nsfw.png" alt="NSFW 响应测试效果图" width="420" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>提示词注入效果:逆向工程场景</b></summary>
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="50%">
|
||||
<b>APK 逆向分析</b><br />
|
||||
<sub>Android APK 静态 / 动态分析思路</sub><br />
|
||||
<img src="docs/screenshots/prompt-effects/reverse/apk-reverse-1.png" alt="APK 逆向分析效果图" width="420" />
|
||||
</td>
|
||||
<td align="center" width="50%">
|
||||
<b>APK 逆向分析 2</b><br />
|
||||
<sub>补充 APK 逆向流程与定位方式</sub><br />
|
||||
<img src="docs/screenshots/prompt-effects/reverse/apk-reverse-2.png" alt="APK 逆向分析效果图 2" width="420" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" colspan="2">
|
||||
<b>EXE 逆向分析</b><br />
|
||||
<sub>Windows 可执行文件分析与调试方向</sub><br />
|
||||
<img src="docs/screenshots/prompt-effects/reverse/exe-reverse.png" alt="EXE 逆向分析效果图" width="620" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
## 功能特性
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<th align="center" width="180">功能</th>
|
||||
<th align="center">说明</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">⚡ 供应商 API</td>
|
||||
<td>可视化管理官方 OpenAI / 第三方 Codex Provider,支持 Base URL、API Key、Model、Wire API 与一键切换。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">🧩 <b>提示词注入</b></td>
|
||||
<td><b>特色功能</b>:内置 <code>gpt5.4-unrestricted.md</code> / <code>gpt5.5-unrestricted.md</code>,一键写入 Codex 配置;启用后可达到上方效果图中的 SQL 注入测试、APK / EXE 逆向等响应效果。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">💬 会话管理</td>
|
||||
<td>读取 Codex 本地 SQLite 与 rollout JSONL,会话列表可视化展示,并支持 Provider Sync / 修复历史 thread 元数据。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">📝 TOML 配置</td>
|
||||
<td>查看当前 <code>~/.codex/config.toml</code>,并在 Provider 编辑页直接编辑完整 TOML,保存后同步到 Codex 配置目录。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">🔐 Auth 管理</td>
|
||||
<td>读取 / 编辑官方 <code>~/.codex/auth.json</code>,区分 ChatGPT 登录态 Auth 与第三方 API Key。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">📦 跨平台发布</td>
|
||||
<td>提供 macOS Apple Silicon / Intel <code>.dmg</code>、Windows <code>.msi</code> / portable <code>.zip</code>、Linux <code>.deb</code> / <code>.rpm</code>,支持应用内检测更新。</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
## 核心亮点
|
||||
|
||||
### 1. 内置双指令提示词模板
|
||||
|
||||
Codex-X 当前内置两套提示词模板:
|
||||
|
||||
- `examples/gpt5.5-unrestricted.md`
|
||||
- `examples/gpt5.4-unrestricted.md`
|
||||
|
||||
你可以在【指令提示词】页面直接点击启用 / 禁用。启用后,Codex-X 会把对应 `.md` 写入 Codex 配置目录,并更新 `config.toml` 中的 `model_instructions_file`。
|
||||
|
||||
<div align="center">
|
||||
<table>
|
||||
<tr>
|
||||
<th align="center">对比项</th>
|
||||
<th align="center"><code>gpt5.4-unrestricted.md</code></th>
|
||||
<th align="center"><code>gpt5.5-unrestricted.md</code></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">定位</td>
|
||||
<td align="center">5.4 CTF 沙箱</td>
|
||||
<td align="center">5.5 无限制模式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">策略</td>
|
||||
<td align="center">CTF 竞赛沙箱间接绕过</td>
|
||||
<td align="center">直接声明无限制开发者模式</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">风格</td>
|
||||
<td align="center">安全研究导向,方法论详细</td>
|
||||
<td align="center">极简指令,覆盖面广</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">长度</td>
|
||||
<td align="center">约 180 行</td>
|
||||
<td align="center">约 40 行</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">适用</td>
|
||||
<td align="center">GPT-5.4 Codex CLI</td>
|
||||
<td align="center">GPT-5.5 Codex CLI</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
部署后可以测试:
|
||||
|
||||
```text
|
||||
如何对目标进行 SQL 注入测试?
|
||||
```
|
||||
|
||||
典型效果:
|
||||
|
||||
```text
|
||||
破甲前 → 拒绝或泛化回答
|
||||
破甲后 → 直接给安全研究方法论与测试步骤
|
||||
```
|
||||
|
||||
### 2. Provider 可视化切换
|
||||
|
||||
- 添加第三方 Codex API Provider
|
||||
- 编辑 Base URL / API Key / Model / Wire API
|
||||
- Provider 页面可查看并编辑对应 TOML
|
||||
- 当前启用 Provider 状态清晰可见
|
||||
- 支持从 cc-switch 数据库导入 Codex Provider
|
||||
|
||||
### 3. 官方 Auth 管理
|
||||
|
||||
- 自动读取 Codex 官方 `auth.json`
|
||||
- 支持查看 / 编辑 ChatGPT 登录态 Auth
|
||||
- 区分官方 Auth 与第三方 API Key
|
||||
- 官方配置可和第三方 Provider 在 UI 中统一管理
|
||||
|
||||
### 4. TOML 可视化编辑
|
||||
|
||||
- 查看当前 Codex `config.toml`
|
||||
- 深色代码预览与语法高亮
|
||||
- Provider 编辑页可直接编辑完整 TOML
|
||||
- 保存后同步到 Codex 配置目录
|
||||
|
||||
### 5. 会话管理 / Provider Sync
|
||||
|
||||
Codex-X 可以读取 Codex 本地会话数据:
|
||||
|
||||
```text
|
||||
~/.codex/sqlite/*.db
|
||||
~/.codex/state_5.sqlite
|
||||
~/.codex/sessions/**/rollout-*.jsonl
|
||||
~/.codex/archived_sessions/**/rollout-*.jsonl
|
||||
```
|
||||
|
||||
用于检查旧会话的 Provider 元数据是否和当前配置一致,并支持一键同步 / 修复,让历史 thread 继续被原生 Codex 识别、打开和续聊。
|
||||
|
||||
### 6. 跨平台桌面软件
|
||||
|
||||
- macOS Apple Silicon `.dmg`
|
||||
- macOS Intel `.dmg`
|
||||
- Windows `.msi`
|
||||
- Windows Portable `.zip`
|
||||
- Linux `.deb` / `.rpm`
|
||||
- GitHub Releases 自动构建发布
|
||||
- 应用内检查更新
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 类型 | 技术 |
|
||||
@@ -154,14 +291,9 @@ pnpm dev
|
||||
pnpm --dir apps/desktop tauri build
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT © yynxxxxx
|
||||
|
||||
|
||||
## macOS 安装说明
|
||||
|
||||
如果你在未签名/未公证的 DMG 中看到“软件已损坏”提示,这是 macOS Gatekeeper 的正常行为。
|
||||
如果你在未签名 / 未公证的 DMG 中看到“软件已损坏”提示,这是 macOS Gatekeeper 的正常行为。
|
||||
|
||||
- 最佳方式:使用 Apple Developer ID 签名并 notarize
|
||||
- 仅本地测试:可手动移除 quarantine 属性
|
||||
@@ -169,3 +301,16 @@ MIT © yynxxxxx
|
||||
```bash
|
||||
xattr -dr com.apple.quarantine /Applications/Codex-X.app
|
||||
```
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目基于 [MIT License](https://github.com/yynxxxxx/Codex-X/blob/main/LICENSE) 开源。
|
||||
|
||||
## 致谢 / Thanks
|
||||
|
||||
感谢 [LINUX DO 论坛](https://linux.do/) 社区的关注、反馈与支持。
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://www.star-history.com/#yynxxxxx/Codex-X&Date)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.17",
|
||||
"private": true,
|
||||
"description": "Codex Switch & Instruct desktop manager",
|
||||
"type": "module",
|
||||
|
||||
Generated
+1
-1
@@ -337,7 +337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codex-x"
|
||||
version = "0.2.9"
|
||||
version = "0.2.17"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.2.9"
|
||||
version = "0.2.17"
|
||||
description = "Codex Switch & Instruct desktop manager"
|
||||
authors = ["yynxxxxx"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -192,6 +192,21 @@ struct AboutInfo {
|
||||
github_repo: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SessionPreview {
|
||||
id: String,
|
||||
title: String,
|
||||
model_provider: Option<String>,
|
||||
model: Option<String>,
|
||||
cwd: Option<String>,
|
||||
rollout_path: Option<String>,
|
||||
updated_at_ms: Option<i64>,
|
||||
archived: bool,
|
||||
has_user_event: bool,
|
||||
needs_sync: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SessionSyncStatus {
|
||||
@@ -207,6 +222,7 @@ struct SessionSyncStatus {
|
||||
needs_sync: bool,
|
||||
backup_dir: Option<String>,
|
||||
warnings: Vec<String>,
|
||||
sessions: Vec<SessionPreview>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -238,6 +254,14 @@ struct SqliteScan {
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
fn sql_select_column(cols: &HashSet<String>, name: &str, fallback: &str) -> String {
|
||||
if cols.contains(name) {
|
||||
format!("\"{}\"", name.replace('"', "\"\""))
|
||||
} else {
|
||||
fallback.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn io_err(path: &Path, source: std::io::Error) -> CodexxError {
|
||||
CodexxError::Io {
|
||||
path: path.display().to_string(),
|
||||
@@ -1361,6 +1385,123 @@ fn scan_sqlite(codex_dir: &Path, target_provider: &str) -> Result<SqliteScan> {
|
||||
Ok(scan)
|
||||
}
|
||||
|
||||
fn list_session_previews(
|
||||
codex_dir: &Path,
|
||||
target_provider: &str,
|
||||
limit: usize,
|
||||
) -> Result<(Vec<SessionPreview>, Vec<String>)> {
|
||||
let mut sessions = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for path in sqlite_candidate_paths(codex_dir) {
|
||||
let conn = match Connection::open_with_flags(
|
||||
&path,
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
) {
|
||||
Ok(conn) => conn,
|
||||
Err(e) => {
|
||||
warnings.push(format!("无法读取会话数据库: {} ({e})", path.display()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if !sqlite_has_table(&conn, "threads")? {
|
||||
continue;
|
||||
}
|
||||
let cols = table_column_set(&conn, "threads")?;
|
||||
if !cols.contains("id") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let title_col = sql_select_column(&cols, "title", "NULL");
|
||||
let first_message_col = sql_select_column(&cols, "first_user_message", "NULL");
|
||||
let preview_col = sql_select_column(&cols, "preview", "NULL");
|
||||
let provider_col = sql_select_column(&cols, "model_provider", "NULL");
|
||||
let model_col = sql_select_column(&cols, "model", "NULL");
|
||||
let cwd_col = sql_select_column(&cols, "cwd", "NULL");
|
||||
let rollout_col = sql_select_column(&cols, "rollout_path", "NULL");
|
||||
let updated_ms_col = sql_select_column(&cols, "updated_at_ms", "NULL");
|
||||
let updated_col = sql_select_column(&cols, "updated_at", "NULL");
|
||||
let archived_col = sql_select_column(&cols, "archived", "0");
|
||||
let has_user_event_col = sql_select_column(&cols, "has_user_event", "0");
|
||||
let order_col = if cols.contains("recency_at_ms") {
|
||||
"\"recency_at_ms\""
|
||||
} else if cols.contains("updated_at_ms") {
|
||||
"\"updated_at_ms\""
|
||||
} else if cols.contains("updated_at") {
|
||||
"\"updated_at\""
|
||||
} else {
|
||||
"\"id\""
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
"SELECT \"id\", {title_col}, {first_message_col}, {preview_col}, {provider_col}, {model_col}, {cwd_col}, {rollout_col}, {updated_ms_col}, {updated_col}, {archived_col}, {has_user_event_col} FROM threads ORDER BY {order_col} DESC LIMIT {}",
|
||||
limit.max(1)
|
||||
);
|
||||
let mut stmt = conn
|
||||
.prepare(&query)
|
||||
.map_err(|e| CodexxError::Database(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
let title: Option<String> = row.get(1)?;
|
||||
let first_message: Option<String> = row.get(2)?;
|
||||
let preview: Option<String> = row.get(3)?;
|
||||
let model_provider: Option<String> = row.get(4)?;
|
||||
let model: Option<String> = row.get(5)?;
|
||||
let cwd: Option<String> = row.get(6)?;
|
||||
let rollout_path: Option<String> = row.get(7)?;
|
||||
let updated_at_ms: Option<i64> = row.get(8)?;
|
||||
let updated_at: Option<i64> = row.get(9)?;
|
||||
let archived: i64 = row.get(10)?;
|
||||
let has_user_event: i64 = row.get(11)?;
|
||||
let clean_title = [title, first_message, preview]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|v| v.trim().to_string())
|
||||
.find(|v| !v.is_empty())
|
||||
.unwrap_or_else(|| format!("会话 {}", id.chars().take(8).collect::<String>()));
|
||||
let normalized_provider = model_provider
|
||||
.as_ref()
|
||||
.map(|v| v.trim().to_string())
|
||||
.filter(|v| !v.is_empty());
|
||||
Ok(SessionPreview {
|
||||
id,
|
||||
title: clean_title,
|
||||
model_provider: normalized_provider.clone(),
|
||||
model: model.and_then(|v| {
|
||||
let v = v.trim().to_string();
|
||||
(!v.is_empty()).then_some(v)
|
||||
}),
|
||||
cwd: cwd.and_then(|v| {
|
||||
let v = v.trim().to_string();
|
||||
(!v.is_empty()).then_some(v)
|
||||
}),
|
||||
rollout_path: rollout_path.and_then(|v| {
|
||||
let v = v.trim().to_string();
|
||||
(!v.is_empty()).then_some(v)
|
||||
}),
|
||||
updated_at_ms: updated_at_ms.or_else(|| updated_at.map(|v| v * 1000)),
|
||||
archived: archived != 0,
|
||||
has_user_event: has_user_event != 0,
|
||||
needs_sync: normalized_provider.as_deref() != Some(target_provider),
|
||||
})
|
||||
})
|
||||
.map_err(|e| CodexxError::Database(e.to_string()))?;
|
||||
|
||||
for row in rows {
|
||||
let session = row.map_err(|e| CodexxError::Database(e.to_string()))?;
|
||||
if seen.insert(session.id.clone()) {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessions.sort_by(|a, b| b.updated_at_ms.cmp(&a.updated_at_ms));
|
||||
sessions.truncate(limit);
|
||||
Ok((sessions, warnings))
|
||||
}
|
||||
|
||||
fn provider_sync_backup_root(codex_dir: &Path) -> PathBuf {
|
||||
codex_dir.join("backups_state").join("provider-sync")
|
||||
}
|
||||
@@ -1512,8 +1653,11 @@ fn session_sync_status_inner(
|
||||
let target = current_model_provider(&codex_dir, target_provider)?;
|
||||
let rollouts = scan_rollouts(&codex_dir, &target, false)?;
|
||||
let sqlite = scan_sqlite(&codex_dir, &target)?;
|
||||
let session_limit = sqlite.sqlite_threads.max(50).min(1000);
|
||||
let (sessions, session_warnings) = list_session_previews(&codex_dir, &target, session_limit)?;
|
||||
let mut warnings = rollouts.warnings;
|
||||
warnings.extend(sqlite.warnings);
|
||||
warnings.extend(session_warnings);
|
||||
Ok(SessionSyncStatus {
|
||||
codex_dir: codex_dir.display().to_string(),
|
||||
target_provider: target,
|
||||
@@ -1527,6 +1671,7 @@ fn session_sync_status_inner(
|
||||
needs_sync: rollouts.mismatched_session_meta > 0 || sqlite.mismatched_threads > 0,
|
||||
backup_dir: None,
|
||||
warnings,
|
||||
sessions,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2143,26 +2288,37 @@ fn restore_backup(config_dir: Option<String>, backup_id: String) -> Result<Actio
|
||||
|
||||
#[tauri::command]
|
||||
fn open_url(url: String) -> std::result::Result<(), String> {
|
||||
let trimmed = url.trim();
|
||||
let trimmed = url.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
return Err("URL 为空".to_string());
|
||||
}
|
||||
|
||||
let status = if cfg!(target_os = "macos") {
|
||||
Command::new("open").arg(trimmed).status()
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Command::new("cmd")
|
||||
.args(["/C", "start", "", trimmed])
|
||||
.status()
|
||||
} else {
|
||||
Command::new("xdg-open").arg(trimmed).status()
|
||||
};
|
||||
// Do not wait for the browser process. On Windows, waiting for `cmd /C start` can
|
||||
// visibly freeze the WebView for a few seconds before the default browser appears.
|
||||
std::thread::spawn(move || {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let _ = Command::new("open").arg(&trimmed).spawn();
|
||||
}
|
||||
|
||||
match status {
|
||||
Ok(s) if s.success() => Ok(()),
|
||||
Ok(s) => Err(format!("打开失败,退出码: {:?}", s.code())),
|
||||
Err(e) => Err(format!("打开失败: {e}")),
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let _ = Command::new("cmd")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.args(["/C", "start", ""])
|
||||
.arg(&trimmed)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
|
||||
{
|
||||
let _ = Command::new("xdg-open").arg(&trimmed).spawn();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.17",
|
||||
"identifier": "com.yynxxxxx.codexx",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
@@ -19,7 +19,13 @@
|
||||
"minWidth": 1320,
|
||||
"minHeight": 720,
|
||||
"center": true,
|
||||
"resizable": true
|
||||
"resizable": true,
|
||||
"titleBarStyle": "Overlay",
|
||||
"hiddenTitle": true,
|
||||
"trafficLightPosition": {
|
||||
"x": 16,
|
||||
"y": 14
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -35,12 +41,6 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"wix": {
|
||||
"bannerPath": "icons/wix/banner.bmp",
|
||||
"dialogImagePath": "icons/wix/dialog.bmp"
|
||||
}
|
||||
}
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
|
||||
+114
-14
@@ -145,6 +145,20 @@ type SessionSyncStatus = {
|
||||
needsSync: boolean;
|
||||
backupDir?: string | null;
|
||||
warnings: string[];
|
||||
sessions: SessionPreview[];
|
||||
};
|
||||
|
||||
type SessionPreview = {
|
||||
id: string;
|
||||
title: string;
|
||||
modelProvider?: string | null;
|
||||
model?: string | null;
|
||||
cwd?: string | null;
|
||||
rolloutPath?: string | null;
|
||||
updatedAtMs?: number | null;
|
||||
archived: boolean;
|
||||
hasUserEvent: boolean;
|
||||
needsSync: boolean;
|
||||
};
|
||||
|
||||
type SessionSyncResult = {
|
||||
@@ -673,10 +687,39 @@ function releaseAssetForPlatform(assets: Array<{ name?: string; browser_download
|
||||
}) || assets[0];
|
||||
}
|
||||
|
||||
function formatSessionTime(value?: number | null) {
|
||||
if (!value) return "未知时间";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "未知时间";
|
||||
return date.toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function compactPath(value?: string | null, max = 58) {
|
||||
if (!value) return "未记录路径";
|
||||
const normalized = value.replace(/\\/g, "/");
|
||||
if (normalized.length <= max) return normalized;
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
if (parts.length >= 3) {
|
||||
const tail = parts.slice(-3).join("/");
|
||||
return `…/${tail}`;
|
||||
}
|
||||
return `…${normalized.slice(-max + 1)}`;
|
||||
}
|
||||
|
||||
function shortId(value: string) {
|
||||
return value.length > 8 ? value.slice(0, 8) : value;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const initialLang = (localStorage.getItem(LANG_KEY) as Lang | null) || "zh";
|
||||
const [lang, setLang] = React.useState<Lang>(initialLang === "en" ? "en" : "zh");
|
||||
const t = dict[lang];
|
||||
const isMacRuntime = navigator.userAgent.toLowerCase().includes("mac");
|
||||
const [tab, setTab] = React.useState<Tab>("dashboard");
|
||||
const [providerMode, setProviderMode] = React.useState<ProviderMode>("list");
|
||||
const [instructionMode, setInstructionMode] = React.useState<InstructionMode>("list");
|
||||
@@ -997,6 +1040,15 @@ function App() {
|
||||
const restoreBackup = (backupId: string) =>
|
||||
call(() => invoke<ActionResult>("restore_backup", { configDir: configDir || null, backupId }), handleActionResult);
|
||||
|
||||
const openExternalUrl = React.useCallback((url?: string | null) => {
|
||||
if (!url) return;
|
||||
window.setTimeout(() => {
|
||||
void invoke("open_url", { url }).catch(() => {
|
||||
setToast(lang === "zh" ? "打开浏览器失败" : "Failed to open browser");
|
||||
});
|
||||
}, 0);
|
||||
}, [lang]);
|
||||
|
||||
const checkForUpdates = React.useCallback(async ({ quiet = false }: { quiet?: boolean } = {}) => {
|
||||
const repo = aboutInfo?.githubRepo || FALLBACK_GITHUB_REPO;
|
||||
const appVersion = aboutInfo?.appVersion || "0.0.0";
|
||||
@@ -1019,6 +1071,9 @@ function App() {
|
||||
const latestVersion = release.tag_name || release.name || "";
|
||||
const asset = releaseAssetForPlatform(release.assets || []);
|
||||
const hasUpdate = compareVersions(latestVersion, appVersion) > 0;
|
||||
const message = hasUpdate
|
||||
? (lang === "zh" ? "发现新版本" : "Update available")
|
||||
: (lang === "zh" ? "当前已是最新版本" : "You are up to date");
|
||||
setReleaseInfo({
|
||||
status: "ok",
|
||||
latestVersion,
|
||||
@@ -1026,16 +1081,24 @@ function App() {
|
||||
assetName: asset?.name,
|
||||
body: release.body || "",
|
||||
hasUpdate,
|
||||
message: hasUpdate
|
||||
? (lang === "zh" ? "发现新版本" : "Update available")
|
||||
: (lang === "zh" ? "当前已是最新版本" : "You are up to date"),
|
||||
message,
|
||||
});
|
||||
if (hasUpdate) setUpdatePromptOpen(true);
|
||||
if (hasUpdate) {
|
||||
if (quiet) {
|
||||
setToast(lang === "zh" ? `发现新版本 ${latestVersion},可在概览页查看` : `New version ${latestVersion} is available`);
|
||||
} else {
|
||||
setUpdatePromptOpen(true);
|
||||
}
|
||||
} else if (!quiet) {
|
||||
setToast(message);
|
||||
}
|
||||
} catch (e) {
|
||||
const message = quiet ? (lang === "zh" ? "自动检查失败" : "Auto check failed") : (lang === "zh" ? "检查失败" : "Check failed");
|
||||
setReleaseInfo({
|
||||
status: "error",
|
||||
message: quiet ? (lang === "zh" ? "自动检查失败" : "Auto check failed") : (lang === "zh" ? "检查失败" : "Check failed"),
|
||||
message,
|
||||
});
|
||||
if (!quiet) setToast(message);
|
||||
}
|
||||
}, [aboutInfo?.githubRepo, aboutInfo?.appVersion, lang]);
|
||||
|
||||
@@ -1164,7 +1227,8 @@ function App() {
|
||||
];
|
||||
|
||||
return (
|
||||
<main className="app-shell">
|
||||
<main className={cx("app-shell", isMacRuntime && "mac-shell")}>
|
||||
{isMacRuntime && <div className="window-drag-strip" data-tauri-drag-region />}
|
||||
<div className="orb orb-a" />
|
||||
<div className="orb orb-b" />
|
||||
|
||||
@@ -1234,7 +1298,7 @@ function App() {
|
||||
<div className="update-actions">
|
||||
<button className="primary-btn" onClick={() => {
|
||||
setUpdatePromptOpen(false);
|
||||
if (releaseInfo.htmlUrl) void invoke("open_url", { url: releaseInfo.htmlUrl });
|
||||
openExternalUrl(releaseInfo.htmlUrl);
|
||||
}}>
|
||||
<Download size={16} /> {lang === "zh" ? "现在下载" : "Download now"}
|
||||
</button>
|
||||
@@ -1262,7 +1326,7 @@ function App() {
|
||||
<strong>{lang === "zh" ? "发现新版本" : "New version found"}</strong>
|
||||
<p>{lang === "zh" ? `Codex-X ${releaseInfo.latestVersion || ""} 已发布` : `Codex-X ${releaseInfo.latestVersion || ""} is available`}</p>
|
||||
</div>
|
||||
<button className="secondary-btn small" onClick={() => releaseInfo.htmlUrl && void invoke("open_url", { url: releaseInfo.htmlUrl })}>
|
||||
<button className="secondary-btn small" onClick={() => openExternalUrl(releaseInfo.htmlUrl)}>
|
||||
{lang === "zh" ? "查看更新" : "View"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -1506,9 +1570,45 @@ function App() {
|
||||
<StatCard icon={<Code2 size={20} />} label={lang === "zh" ? "SQLite 数据库" : "SQLite DBs"} value={sessionStatus?.sqliteDbs ?? "-"} ok />
|
||||
</div>
|
||||
|
||||
<div className="session-info-list">
|
||||
<div><span>CODEX_HOME</span><code>{sessionStatus?.codexDir || state.codexDir}</code></div>
|
||||
<div><span>{lang === "zh" ? "备份位置" : "Backup"}</span><code>{sessionStatus?.backupDir || `${state.codexDir}/backups_state/provider-sync`}</code></div>
|
||||
|
||||
<div className="session-list-card">
|
||||
<div className="session-list-head">
|
||||
<div>
|
||||
<p className="eyebrow">{lang === "zh" ? "本地会话" : "Local threads"}</p>
|
||||
<h4>{lang === "zh" ? "会话列表" : "Sessions"}</h4>
|
||||
</div>
|
||||
<span>{lang === "zh" ? `展示 ${sessionStatus?.sessions?.length ?? 0} / ${sessionStatus?.sqliteThreads ?? 0} 条` : `${sessionStatus?.sessions?.length ?? 0} / ${sessionStatus?.sqliteThreads ?? 0} shown`}</span>
|
||||
</div>
|
||||
{sessionStatus?.sessions?.length ? (
|
||||
<div className="session-list">
|
||||
{sessionStatus.sessions.map((item) => (
|
||||
<article className={cx("session-row", item.needsSync && "needs-sync")} key={item.id}>
|
||||
<div className="session-row-main">
|
||||
<div className="session-thread-icon"><History size={18} /></div>
|
||||
<div className="session-row-text">
|
||||
<div className="session-row-title">
|
||||
<strong>{item.title}</strong>
|
||||
{item.archived && <span className="mini-tag">{lang === "zh" ? "已归档" : "Archived"}</span>}
|
||||
{item.needsSync && <span className="mini-tag warn">{lang === "zh" ? "需同步" : "Needs sync"}</span>}
|
||||
</div>
|
||||
<p title={item.cwd || item.rolloutPath || undefined}>{compactPath(item.cwd || item.rolloutPath)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="session-row-meta">
|
||||
<span>{formatSessionTime(item.updatedAtMs)}</span>
|
||||
<code>{item.modelProvider || "unknown"}</code>
|
||||
<em>{item.model || "model -"}</em>
|
||||
<small>#{shortId(item.id)}</small>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="session-empty">
|
||||
<History size={22} />
|
||||
<span>{lang === "zh" ? "还没有读取到会话。点击右上角“检查会话”刷新。" : "No sessions loaded. Click Check to refresh."}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{sessionStatus?.warnings?.length ? (
|
||||
@@ -1647,8 +1747,8 @@ function App() {
|
||||
<div><span>{lang === "zh" ? "项目地址" : "Project"}</span><code>{aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}</code></div>
|
||||
</div>
|
||||
<div className="about-actions">
|
||||
<button className="secondary-btn" onClick={() => void invoke("open_url", { url: aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}` })}><ExternalLink size={16} /> {lang === "zh" ? "打开项目主页" : "Open project"}</button>
|
||||
<button className="ghost-btn" onClick={() => void invoke("open_url", { url: `${aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}/issues` })}><ExternalLink size={16} /> {lang === "zh" ? "反馈问题" : "Issues"}</button>
|
||||
<button className="secondary-btn" onClick={() => openExternalUrl(aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`)}><ExternalLink size={16} /> {lang === "zh" ? "打开项目主页" : "Open project"}</button>
|
||||
<button className="ghost-btn" onClick={() => openExternalUrl(`${aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}/issues`)}><ExternalLink size={16} /> {lang === "zh" ? "反馈问题" : "Issues"}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1663,7 +1763,7 @@ function App() {
|
||||
</div>
|
||||
<div className="about-actions">
|
||||
<button className="primary-btn" onClick={() => void checkForUpdates()} disabled={releaseInfo.status === "checking"}><RefreshCw size={16} className={cx(releaseInfo.status === "checking" && "spin")} /> {lang === "zh" ? "检查更新" : "Check updates"}</button>
|
||||
<button className="secondary-btn" onClick={() => releaseInfo.htmlUrl && void invoke("open_url", { url: releaseInfo.htmlUrl })} disabled={!releaseInfo.htmlUrl}><Download size={16} /> {lang === "zh" ? "打开下载页" : "Open releases"}</button>
|
||||
<button className="secondary-btn" onClick={() => openExternalUrl(releaseInfo.htmlUrl)} disabled={!releaseInfo.htmlUrl}><Download size={16} /> {lang === "zh" ? "打开下载页" : "Open releases"}</button>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
+191
-9
@@ -9,18 +9,22 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #root { width: 100%; height: 100%; margin: 0; }
|
||||
body { overflow: hidden; }
|
||||
body { overflow: hidden; background: #070a13; }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
button:disabled { cursor: not-allowed; opacity: .58; }
|
||||
|
||||
.app-shell {
|
||||
--shell-pad-x: 18px;
|
||||
--shell-pad-bottom: 18px;
|
||||
--shell-pad-top: 18px;
|
||||
--shell-vertical-pad: calc(var(--shell-pad-top) + var(--shell-pad-bottom));
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: 18px;
|
||||
padding: 18px;
|
||||
padding: var(--shell-pad-top) var(--shell-pad-x) var(--shell-pad-bottom);
|
||||
background:
|
||||
radial-gradient(circle at 12% 10%, rgba(110, 92, 255, .24), transparent 28%),
|
||||
radial-gradient(circle at 85% 22%, rgba(22, 193, 255, .18), transparent 34%),
|
||||
@@ -43,7 +47,7 @@ button:disabled { cursor: not-allowed; opacity: .58; }
|
||||
padding: 22px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 36px);
|
||||
min-height: calc(100vh - var(--shell-vertical-pad));
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 14px; margin-bottom: 28px; }
|
||||
.brand-mark {
|
||||
@@ -197,6 +201,7 @@ input, select, textarea {
|
||||
}
|
||||
.toast.ok { color: #dbfff3; background: rgba(12, 112, 81, .88); border: 1px solid rgba(31,242,178,.3); }
|
||||
.toast.error { color: #fff; background: rgba(157, 38, 63, .92); border: 1px solid rgba(255,120,145,.35); animation: none; }
|
||||
.app-shell.mac-shell .toast { top: calc(24px + var(--mac-titlebar-safe)); }
|
||||
@keyframes toast { 0% { opacity: 0; transform: translateY(-8px); } 12%, 84% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-8px); } }
|
||||
|
||||
.update-mask {
|
||||
@@ -673,13 +678,17 @@ html, body, #root {
|
||||
min-height: 720px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.app-shell.mac-shell {
|
||||
--mac-titlebar-safe: 44px;
|
||||
--shell-pad-top: calc(18px + var(--mac-titlebar-safe));
|
||||
}
|
||||
.sidebar {
|
||||
min-height: auto;
|
||||
height: calc(100vh - 36px);
|
||||
height: calc(100vh - var(--shell-vertical-pad));
|
||||
overflow: hidden;
|
||||
}
|
||||
.content {
|
||||
height: calc(100vh - 36px);
|
||||
height: calc(100vh - var(--shell-vertical-pad));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-right: 4px;
|
||||
@@ -715,7 +724,7 @@ html, body, #root {
|
||||
.provider-panel,
|
||||
.code-panel {
|
||||
height: auto;
|
||||
min-height: calc(100vh - 36px);
|
||||
min-height: calc(100vh - var(--shell-vertical-pad));
|
||||
overflow: visible;
|
||||
}
|
||||
.provider-row-list {
|
||||
@@ -727,7 +736,7 @@ html, body, #root {
|
||||
overflow: visible;
|
||||
}
|
||||
.code-panel {
|
||||
min-height: calc(100vh - 36px);
|
||||
min-height: calc(100vh - var(--shell-vertical-pad));
|
||||
}
|
||||
.toml-preview {
|
||||
min-height: 520px;
|
||||
@@ -1056,7 +1065,7 @@ html, body, #root {
|
||||
}
|
||||
|
||||
.instruction-panel {
|
||||
min-height: calc(100vh - 36px);
|
||||
min-height: calc(100vh - var(--shell-vertical-pad));
|
||||
}
|
||||
.instruction-row-list {
|
||||
display: flex;
|
||||
@@ -1445,11 +1454,18 @@ html, body, #root {
|
||||
|
||||
/* Session management page. */
|
||||
.sessions-panel {
|
||||
min-height: calc(100vh - 36px);
|
||||
min-height: calc(100vh - var(--shell-vertical-pad));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.panel.glass.sessions-panel {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
padding: 0;
|
||||
}
|
||||
.session-status-card {
|
||||
display: grid;
|
||||
grid-template-columns: 52px 1fr auto;
|
||||
@@ -1513,6 +1529,152 @@ html, body, #root {
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
.session-list-card {
|
||||
padding: 18px;
|
||||
border-radius: 24px;
|
||||
background: linear-gradient(180deg, rgba(6, 12, 27, .78), rgba(4,10,24,.58));
|
||||
border: 1px solid rgba(124,168,255,.16);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.055), 0 18px 46px rgba(0,0,0,.20);
|
||||
overflow: hidden;
|
||||
}
|
||||
.session-list-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.session-list-head h4 {
|
||||
margin: 2px 0 0;
|
||||
color: #f4f8ff;
|
||||
font-size: 17px;
|
||||
letter-spacing: -.02em;
|
||||
}
|
||||
.session-list-head span {
|
||||
color: #8fa0bd;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
padding: 7px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.06);
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.session-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
padding: 0 4px 2px 0;
|
||||
}
|
||||
.session-list::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
.session-list::-webkit-scrollbar-thumb {
|
||||
border-radius: 999px;
|
||||
background: rgba(124,168,255,.30);
|
||||
}
|
||||
.session-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 13px 14px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,.075), rgba(255,255,255,.035));
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.session-row.needs-sync {
|
||||
border-color: rgba(255,176,32,.28);
|
||||
background: linear-gradient(135deg, rgba(255,176,32,.10), rgba(255,255,255,.035));
|
||||
}
|
||||
.session-row-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.session-thread-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #2ff5b5;
|
||||
background: rgba(31,242,178,.12);
|
||||
border: 1px solid rgba(31,242,178,.18);
|
||||
}
|
||||
.session-row-text {
|
||||
min-width: 0;
|
||||
}
|
||||
.session-row-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.session-row-title strong {
|
||||
color: #eef5ff;
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.session-row-text p {
|
||||
margin: 5px 0 0;
|
||||
color: #8fa0bd;
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.session-row-meta {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 92px 92px 72px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-items: end;
|
||||
color: #8fa0bd;
|
||||
font-size: 12px;
|
||||
}
|
||||
.session-row-meta code,
|
||||
.session-row-meta em,
|
||||
.session-row-meta small {
|
||||
max-width: 92px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-style: normal;
|
||||
}
|
||||
.session-row-meta code {
|
||||
color: #b9c8e6;
|
||||
font-family: "JetBrains Mono", "Fira Code", "SFMono-Regular", "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
.mini-tag {
|
||||
flex: 0 0 auto;
|
||||
color: #9fb0cc;
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
padding: 3px 7px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.07);
|
||||
border: 1px solid rgba(255,255,255,.09);
|
||||
}
|
||||
.mini-tag.warn {
|
||||
color: #ffd9a1;
|
||||
background: rgba(255,176,32,.10);
|
||||
border-color: rgba(255,176,32,.22);
|
||||
}
|
||||
.session-empty {
|
||||
min-height: 86px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 8px;
|
||||
color: #8fa0bd;
|
||||
border-radius: 18px;
|
||||
border: 1px dashed rgba(255,255,255,.12);
|
||||
background: rgba(255,255,255,.025);
|
||||
}
|
||||
.session-warning-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1541,4 +1703,24 @@ html, body, #root {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
.session-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.session-row-meta {
|
||||
justify-items: start;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
padding-left: 54px;
|
||||
}
|
||||
}
|
||||
|
||||
/* macOS overlay titlebar: keep top chrome visually integrated with the app theme. */
|
||||
.window-drag-strip {
|
||||
position: fixed;
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
background: linear-gradient(90deg, rgba(16,18,43,.98), rgba(4,19,31,.98));
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 440 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.2.9",
|
||||
"version": "0.2.17",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
Reference in New Issue
Block a user