Compare commits

..
19 Commits
Author SHA1 Message Date
yyyyyynnn 257a4385b7 Polish mac titlebar and sessions panel 2026-07-04 15:06:17 +08:00
yyyyyynnn c0f3c9240c Polish README layout and license 2026-07-04 14:56:46 +08:00
yyyyyynnn 67ad656b45 Organize README screenshots 2026-07-04 14:53:34 +08:00
yyyyyynnn 5dbd6e2359 Refresh project README with previews 2026-07-04 14:51:00 +08:00
yyyyyynnn da3c52a7de Restore MSI style and list sessions 2026-07-04 14:32:42 +08:00
yyyyyynnn 34ec59f28b Soften startup update notification 2026-07-04 14:03:52 +08:00
yyyyyynnn 9a82612d5d Fix release asset upload 2026-07-04 13:47:14 +08:00
yyyyyynnn 10c16cba7b Fix release workflow publishing 2026-07-04 13:35:51 +08:00
yyyyyynnn 57075256fb Add session manager and polish updates 2026-07-04 13:28:45 +08:00
yyyyyynnn 529af6d796 Use changelog entries as release notes 2026-07-04 13:02:30 +08:00
yyyyyynnn 1f1100225e Simplify update page and releases link 2026-07-04 13:00:35 +08:00
yyyyyynnn b03b9d881c Add startup update prompt 2026-07-04 12:56:57 +08:00
yyyyyynnn 10d4491f78 Add changelog and fix Windows console 2026-07-04 12:44:05 +08:00
yyyyyynnn 964151d479 Polish Windows installer branding 2026-07-04 12:37:20 +08:00
yyyyyynnn 5f1c510dd2 Fix about page external links 2026-07-04 12:32:31 +08:00
yyyyyynnn 686f2b4e18 Fix macOS icon and speed up releases 2026-07-04 12:21:54 +08:00
yyyyyynnn 7cd783b786 Improve README and release bundles 2026-07-04 12:09:50 +08:00
yyyyyynnn ebf9581535 Add Codex-X app icon and bump release 2026-07-04 11:54:39 +08:00
yyyyyynnn 23c569f09b Fix Codex-X copyright author 2026-07-04 11:45:14 +08:00
39 changed files with 2006 additions and 166 deletions
+95 -17
View File
@@ -9,14 +9,37 @@ on:
permissions:
contents: write
defaults:
run:
shell: bash
jobs:
build:
name: Build ${{ matrix.platform }}
name: Build ${{ matrix.name }}
runs-on: ${{ matrix.platform }}
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-22.04, windows-latest]
include:
- name: macOS
platform: macos-latest
args: --bundles dmg
artifact: codex-x-macos
paths: |
apps/desktop/src-tauri/target/release/bundle/dmg/*.dmg
- name: Linux
platform: ubuntu-22.04
args: --bundles deb,rpm
artifact: codex-x-linux
paths: |
apps/desktop/src-tauri/target/release/bundle/deb/*.deb
apps/desktop/src-tauri/target/release/bundle/rpm/*.rpm
- name: Windows
platform: windows-latest
args: --bundles msi
artifact: codex-x-windows
paths: |
apps/desktop/src-tauri/target/release/bundle/msi/*.msi
steps:
- name: Checkout
@@ -47,24 +70,79 @@ jobs:
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: apps/desktop/src-tauri
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build and upload Tauri bundles
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Tauri bundles
run: pnpm --dir apps/desktop tauri build ${{ matrix.args }}
- name: Upload bundle artifacts
uses: actions/upload-artifact@v4
with:
projectPath: apps/desktop
tagName: ${{ github.ref_name }}
releaseName: Codex-X ${{ github.ref_name }}
releaseBody: |
Codex-X desktop release.
name: ${{ matrix.artifact }}
path: ${{ matrix.paths }}
if-no-files-found: error
retention-days: 7
- macOS: `.dmg`
- Windows: `.msi` / `.exe`
- Linux: `.AppImage` / `.deb` / `.rpm`
release:
name: Publish GitHub Release
needs: build
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
GitHub 会自动附带 Source code (zip/tar.gz)。
releaseDraft: false
prerelease: false
- name: Download bundle artifacts
uses: actions/download-artifact@v4
with:
path: release-assets
merge-multiple: true
- name: Prepare release notes
id: release_notes
run: |
python3 - <<'PY'
from pathlib import Path
import os
import re
tag = os.environ["GITHUB_REF_NAME"]
text = Path("CHANGELOG.md").read_text(encoding="utf-8")
match = re.search(
rf"^## \[{re.escape(tag)}\].*?\n(.*?)(?=^## \[|\Z)",
text,
flags=re.S | re.M,
)
body = match.group(1).strip() if match else ""
if not body:
body = "Codex-X desktop release."
with open("release-body.md", "w", encoding="utf-8") as fh:
fh.write(body + "\n")
PY
- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
title="Codex-X ${tag}"
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" --title "$title" --notes-file release-body.md --latest
else
gh release create "$tag" --title "$title" --notes-file release-body.md --latest
fi
mapfile -t assets < <(find release-assets -type f \( -name '*.dmg' -o -name '*.msi' -o -name '*.deb' -o -name '*.rpm' \) -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
+74
View File
@@ -0,0 +1,74 @@
# Changelog
All notable changes to Codex-X will be documented here.
## [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。
- 修复 `Resource not accessible by integration` 导致 Release 创建失败的问题。
- Release 仍会从 `CHANGELOG.md` 自动读取当前 tag 的更新日志,并上传 macOS / Windows / Linux 安装包。
## [v0.2.8] - 2026-07-04
- 更新页进一步产品化:去掉资源/仓库调试信息,将“有更新”改为更明显的绿色标签。
- 概览页顶部新增轻量“发现新版本”提示条,可直接打开 Releases 页面。
- 新增【会话管理】页面:检查 Codex 本地 sessions / archived_sessions 与 SQLite threads 是否和当前 Provider 同步。
- 新增一键 Provider Sync / 修复历史会话:写入前备份到 `~/.codex/backups_state/provider-sync/`,并保留最近 5 份备份。
## [v0.2.7] - 2026-07-04
- 简化更新检查页展示。
- “打开下载页”改为打开 GitHub Releases 页面。
- 更新弹窗保持简洁,仅提示版本差异。
- 首次启动自动检查更新并弹窗提醒。
## [v0.2.5] - 2026-07-04
- Windows 版双击启动不再额外弹出终端窗口。
- 改进 Windows MSI 安装器品牌展示与图标。
- About 页面外部链接改为系统默认浏览器打开。
- Release 流程加入 Rust cache,后续构建更快。
## [v0.2.4] - 2026-07-04
- 美化 Windows MSI 安装器横幅与对话图。
- Windows 安装包加入应用图标。
## [v0.2.3] - 2026-07-04
- 修复 macOS DMG 中应用图标缺失问题。
- 补充 macOS 安装说明。
- Release 流程加入基础缓存优化。
## [v0.2.2] - 2026-07-04
- macOS / Linux / Windows 首次三平台 Release。
- Linux 产物改为 `deb` / `rpm`,避免 AppImage 图标问题。
## [v0.2.1] - 2026-07-04
- 首次加入应用图标与 GitHub Release 自动发布。
-1
View File
@@ -1,6 +1,5 @@
MIT License
Copyright (c) 2026 li lingbo
Copyright (c) 2026 yynxxxxx
Permission is hereby granted, free of charge, to any person obtaining a copy
+224 -55
View File
@@ -1,22 +1,220 @@
# Codex-X
<div align="center">
<img src="apps/desktop/src-tauri/icons/icon.png" alt="Codex-X Logo" width="150" />
Codex-X 是一个面向 OpenAI Codex / Codex CLI 的跨平台桌面配置管理器,用来可视化管理 Codex 的供应商、`config.toml`、`auth.json` 和指令提示词。
# Codex-X
> 当前桌面端基于 **Tauri 2 + React + TypeScript + Rust + SQLite**。
**Codex 提示词注入 · Provider 切换 · TOML / Auth 可视化管理器**
## 功能
一款面向 **OpenAI Codex / Codex CLI** 的跨平台桌面工具,内置 `gpt5.5-unrestricted.md` 与 `gpt5.4-unrestricted.md`,支持一键写入 / 禁用指令提示词、第三方 Provider 切换、官方 Auth 管理、TOML 可视化编辑与本地会话 Provider Sync。
- **供应商管理**:添加、编辑、切换 Codex 第三方 API Provider。
- **官方配置**:查看/编辑 OpenAI Official 的 `auth.json` 与官方模型配置。
- **TOML 编辑**:在供应商编辑页直接编辑并保存完整 `config.toml`。
- **指令提示词管理**:管理多个 `model_instructions_file` 模板,支持自定义提示词、启用、禁用。
- **cc-switch 导入**:从本机 cc-switch SQLite 数据库导入 Codex Provider。
- **跨平台路径**:默认读取 `CODEX_HOME`,否则使用用户目录下的 `.codex`。
- **更新检查**:通过 GitHub Releases 检测最新版本。
<p>
<img src="https://img.shields.io/github/v/release/yynxxxxx/Codex-X?label=version&color=blue" alt="version" />
<img src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-555" alt="platform" />
<img src="https://img.shields.io/badge/built%20with-Tauri%202-24C8DB" alt="tauri" />
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
</p>
## 配置文件位置
<p>
<img src="https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=white" />
<img src="https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white" />
<img src="https://img.shields.io/badge/Rust-stable-000000?logo=rust&logoColor=white" />
<img src="https://img.shields.io/badge/SQLite-Ready-003B57?logo=sqlite&logoColor=white" />
<img src="https://img.shields.io/badge/Vite-Ready-646CFF?logo=vite&logoColor=white" />
</p>
</div>
Codex-X 默认读写:
---
## Codex-X 是什么?
Codex-X 不是普通的配置文件编辑器,而是一个面向 Codex CLI 的 **可视化增强管理器**。
它把几个高频操作做成了桌面软件:
- 给 Codex 写入 / 禁用指令提示词模板
- 切换官方 OpenAI 与第三方 Codex API Provider
- 查看 / 编辑 `~/.codex/config.toml`
- 查看 / 编辑官方 `~/.codex/auth.json`
- 检查并修复本地历史会话的 Provider 元数据
## 软件预览
<details open>
<summary><b>应用界面预览</b>:主界面 / Provider / TOML / Auth</summary>
<p align="center">
<img src="docs/screenshots/app/preview.png" alt="Codex-X 应用界面预览:主界面、Provider、TOML、Auth" width="920" />
</p>
</details>
<details>
<summary><b>提示词注入效果:安全测试场景</b></summary>
<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>
## 核心亮点
### 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 `.dmg`
- Windows `.msi`
- Linux `.deb` / `.rpm`
- GitHub Releases 自动构建发布
- 应用内检查更新
## 技术栈
| 类型 | 技术 |
| --- | --- |
| 桌面框架 | Tauri 2 |
| 前端 | React 18 / TypeScript / Vite |
| 后端 | Rust |
| 本地数据 | SQLite / rusqlite |
| 配置编辑 | TOML / JSON |
| 发布 | GitHub Actions / GitHub Releases |
## 配置路径
Codex-X 默认读取 Codex 配置目录:
```text
~/.codex/config.toml
@@ -31,71 +229,42 @@ CODEXX_HOME=/path/to/codex-x-data
CC_SWITCH_HOME=/path/to/.cc-switch
```
Codex-X 自身数据默认存放在:
Codex-X 自身数据库默认位于:
```text
~/.codexx/codexx.db
```
## Windows 权限说明
## 下载
Windows 正常情况下 Codex 配置位于:
请前往 Releases 页面下载:
```text
%USERPROFILE%\.codex
```
https://github.com/yynxxxxx/Codex-X/releases
这是用户目录,不需要管理员权限。只有当你手动把 `CODEX_HOME` 指向 `C:\Program Files`、`C:\Windows`、系统盘根目录等受保护目录时,写入才可能因为权限不足失败。Codex-X 不会静默提权,会直接显示具体 IO 错误。
## 开发
## 开发运行
```bash
pnpm install
pnpm dev
```
类型检查:
```bash
pnpm typecheck
```
构建桌面端:
```bash
pnpm --dir apps/desktop tauri build
```
## Release
## macOS 安装说明
本仓库使用 GitHub Actions 在打 tag 时自动构建三平台安装包:
如果你在未签名 / 未公证的 DMG 中看到“软件已损坏”提示,这是 macOS Gatekeeper 的正常行为。
- 最佳方式:使用 Apple Developer ID 签名并 notarize
- 仅本地测试:可手动移除 quarantine 属性
```bash
git tag v0.2.0
git push origin v0.2.0
xattr -dr com.apple.quarantine /Applications/Codex-X.app
```
Release 页面会包含:
## 许可证
- macOS `.dmg`
- Windows `.msi` / `.exe`
- Linux `.AppImage` / `.deb` / `.rpm`
- GitHub 自动生成的 Source code `.zip` / `.tar.gz`
## 技术栈
- Tauri 2
- React 18
- TypeScript
- Vite
- Rust
- SQLite / rusqlite
- toml_edit
## 项目地址
<https://github.com/yynxxxxx/Codex-X>
## License
MIT
本项目基于 [MIT License](https://github.com/yynxxxxx/Codex-X/blob/main/LICENSE) 开源。
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "codex-x",
"version": "0.2.0",
"version": "0.2.13",
"private": true,
"description": "Codex Switch & Instruct desktop manager",
"type": "module",
@@ -25,5 +25,6 @@
"@vitejs/plugin-react": "^4.2.0",
"typescript": "^5.3.0",
"vite": "^7.3.0"
}
},
"author": "yynxxxxx"
}
+1 -1
View File
@@ -337,7 +337,7 @@ dependencies = [
[[package]]
name = "codex-x"
version = "0.2.0"
version = "0.2.13"
dependencies = [
"chrono",
"dirs 5.0.1",
+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "codex-x"
version = "0.2.0"
version = "0.2.13"
description = "Codex Switch & Instruct desktop manager"
authors = ["Codex-X Contributors"]
authors = ["yynxxxxx"]
license = "MIT"
edition = "2021"
rust-version = "1.85.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,3 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
codexx_lib::run()
}
+15 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Codex-X",
"version": "0.2.0",
"version": "0.2.13",
"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": {
@@ -28,6 +34,13 @@
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"targets": "all"
}
}
+284 -20
View File
@@ -6,6 +6,7 @@ import {
Code2,
Download,
ExternalLink,
AlertCircle,
FileCode2,
Globe2,
History,
@@ -29,7 +30,7 @@ import "./styles.css";
type Lang = "zh" | "en";
type ProviderMode = "list" | "form" | "official";
type InstructionMode = "list" | "form";
type Tab = "dashboard" | "provider" | "instruction" | "toml" | "settings" | "about";
type Tab = "dashboard" | "provider" | "sessions" | "instruction" | "toml" | "settings" | "about";
type InstructionTemplate = {
id: string;
@@ -130,6 +131,43 @@ type ReleaseInfo = {
hasUpdate?: boolean;
};
type SessionSyncStatus = {
codexDir: string;
targetProvider: string;
rolloutFiles: number;
sessionMetaCount: number;
mismatchedRollouts: number;
mismatchedSessionMeta: number;
sqliteDbs: number;
sqliteThreads: number;
mismatchedThreads: number;
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 = {
status: SessionSyncStatus;
updatedRollouts: number;
updatedThreads: number;
backupDir: string;
};
const INSTRUCTION_RELATIVE_UI = "./gpt5.5-unrestricted.md";
const LANG_KEY = "codexx.lang";
const FALLBACK_GITHUB_REPO = "yynxxxxx/Codex-X";
@@ -187,6 +225,7 @@ const dict = {
nav: {
dashboard: "概览",
provider: "供应商",
sessions: "会话管理",
instruction: "指令提示词",
toml: "TOML",
settings: "设置",
@@ -293,6 +332,7 @@ const dict = {
nav: {
dashboard: "Overview",
provider: "Provider",
sessions: "Sessions",
instruction: "Prompt",
toml: "TOML",
settings: "Settings",
@@ -647,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");
@@ -660,6 +729,8 @@ function App() {
const [savedPrompts, setSavedPrompts] = React.useState<SavedPrompt[]>([]);
const [aboutInfo, setAboutInfo] = React.useState<AboutInfo | null>(null);
const [releaseInfo, setReleaseInfo] = React.useState<ReleaseInfo>({ status: "idle" });
const [updatePromptOpen, setUpdatePromptOpen] = React.useState(false);
const [sessionStatus, setSessionStatus] = React.useState<SessionSyncStatus | null>(null);
const [state, setState] = React.useState<CodexState | null>(null);
const [backups, setBackups] = React.useState<BackupEntry[]>([]);
const [configDir, setConfigDir] = React.useState("");
@@ -671,9 +742,17 @@ function App() {
const [providerTomlDirty, setProviderTomlDirty] = React.useState(false);
const [promptForm, setPromptForm] = React.useState<SavedPrompt>(blankPromptForm);
const [officialForm, setOfficialForm] = React.useState({ model: "gpt-5.5", authJson: "" });
const autoUpdateCheckedRef = React.useRef(false);
const providerTomlPreview = React.useMemo(() => buildProviderTomlPreview(providerForm, state), [providerForm, state]);
const providerAuthPreview = React.useMemo(() => buildProviderAuthPreview(providerForm), [providerForm]);
const currentInstructionId = instructionIdFromPath(state?.instructionFile);
const releaseStatusLabel = React.useMemo(() => {
if (releaseInfo.status === "checking") return lang === "zh" ? "检查中" : "Checking";
if (releaseInfo.status === "error") return lang === "zh" ? "失败" : "Failed";
if (releaseInfo.hasUpdate) return lang === "zh" ? "有更新" : "Update found";
if (releaseInfo.status === "ok") return lang === "zh" ? "已是最新" : "Up to date";
return lang === "zh" ? "未检查" : "Idle";
}, [lang, releaseInfo.hasUpdate, releaseInfo.status]);
React.useEffect(() => {
localStorage.setItem(LANG_KEY, lang);
@@ -759,14 +838,16 @@ function App() {
const providerList = await invoke<SavedProvider[]>("list_saved_providers");
const promptList = await invoke<SavedPrompt[]>("list_saved_prompts");
const about = await invoke<AboutInfo>("get_about_info", { configDir: configDir || null });
return { next, backupList, providerList, promptList, about };
const sessions = await invoke<SessionSyncStatus>("get_session_sync_status", { configDir: configDir || null, targetProvider: null });
return { next, backupList, providerList, promptList, about, sessions };
},
({ next, backupList, providerList, promptList, about }) => {
({ next, backupList, providerList, promptList, about, sessions }) => {
setState(next);
setBackups(backupList);
setSavedProviders(providerList);
setSavedPrompts(promptList);
setAboutInfo(about);
setSessionStatus(sessions);
if (!configDir) setConfigDir(next.codexDir);
},
);
@@ -959,9 +1040,10 @@ function App() {
const restoreBackup = (backupId: string) =>
call(() => invoke<ActionResult>("restore_backup", { configDir: configDir || null, backupId }), handleActionResult);
const checkForUpdates = async () => {
const checkForUpdates = React.useCallback(async ({ quiet = false }: { quiet?: boolean } = {}) => {
const repo = aboutInfo?.githubRepo || FALLBACK_GITHUB_REPO;
const appVersion = aboutInfo?.appVersion || "0.0.0";
const releasesUrl = `https://github.com/${repo}/releases/`;
setReleaseInfo({ status: "checking" });
try {
const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
@@ -979,24 +1061,43 @@ 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,
htmlUrl: asset?.browser_download_url || release.html_url,
htmlUrl: releasesUrl,
assetName: asset?.name,
body: release.body || "",
hasUpdate: compareVersions(latestVersion, appVersion) > 0,
message: compareVersions(latestVersion, appVersion) > 0
? (lang === "zh" ? "发现新版本" : "Update available")
: (lang === "zh" ? "当前已是最新版本" : "You are up to date"),
hasUpdate,
message,
});
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: lang === "zh" ? `检查失败:${String(e)}` : `Check failed: ${String(e)}`,
message,
});
if (!quiet) setToast(message);
}
};
}, [aboutInfo?.githubRepo, aboutInfo?.appVersion, lang]);
React.useEffect(() => {
if (!state || !aboutInfo || autoUpdateCheckedRef.current) return;
autoUpdateCheckedRef.current = true;
void checkForUpdates({ quiet: true });
}, [state, aboutInfo, checkForUpdates]);
const loadCcSwitchOfficialAuth = async (showToast = true) => {
const candidate = await invoke<OfficialAuthCandidate | null>("read_ccswitch_official_auth", { dbPath: null });
@@ -1084,9 +1185,32 @@ function App() {
);
};
const checkSessions = () =>
call(
() => invoke<SessionSyncStatus>("get_session_sync_status", { configDir: configDir || null, targetProvider: null }),
(status) => {
setSessionStatus(status);
setToast(status.needsSync
? (lang === "zh" ? `发现 ${status.mismatchedSessionMeta + status.mismatchedThreads} 项需要同步` : "Session sync needed")
: (lang === "zh" ? "会话已同步" : "Sessions are in sync"));
},
);
const syncSessions = () =>
call(
() => invoke<SessionSyncResult>("sync_sessions_provider", { configDir: configDir || null, targetProvider: null }),
(result) => {
setSessionStatus(result.status);
setToast(lang === "zh"
? `已修复 ${result.updatedRollouts} 个会话文件、${result.updatedThreads} 条 SQLite 记录`
: `Updated ${result.updatedRollouts} rollout file(s), ${result.updatedThreads} SQLite row(s)`);
},
);
const navItems: Array<[Tab, string, React.ReactNode]> = [
["dashboard", t.nav.dashboard, <Layers3 size={18} />],
["provider", t.nav.provider, <Zap size={18} />],
["sessions", t.nav.sessions, <History size={18} />],
["instruction", t.nav.instruction, <Sparkles size={18} />],
["toml", t.nav.toml, <FileCode2 size={18} />],
["settings", t.nav.settings, <Settings size={18} />],
@@ -1095,6 +1219,7 @@ function App() {
return (
<main className="app-shell">
{isMacRuntime && <div className="window-drag-strip" data-tauri-drag-region />}
<div className="orb orb-a" />
<div className="orb orb-b" />
@@ -1144,6 +1269,37 @@ function App() {
</div>
)}
{error && <div className="toast error">{error}</div>}
{updatePromptOpen && releaseInfo.hasUpdate && (
<div className="update-mask" onClick={() => setUpdatePromptOpen(false)}>
<div className="update-dialog glass" onClick={(e) => e.stopPropagation()}>
<div className="update-head">
<div className="update-icon"><Sparkles size={22} /></div>
<div>
<p className="eyebrow">Codex-X</p>
<h3>{lang === "zh" ? "发现新版本" : "New version available"}</h3>
</div>
</div>
<div className="update-body">
<p>{lang === "zh" ? "检测到新版本,是否立即打开下载页?" : "A new version was found. Open the download page now?"}</p>
<div className="about-kv compact">
<div><span>{lang === "zh" ? "当前版本" : "Current"}</span><strong>{aboutInfo?.appVersion || "-"}</strong></div>
<div><span>{lang === "zh" ? "最新版本" : "Latest"}</span><strong>{releaseInfo.latestVersion || "-"}</strong></div>
</div>
</div>
<div className="update-actions">
<button className="primary-btn" onClick={() => {
setUpdatePromptOpen(false);
if (releaseInfo.htmlUrl) void invoke("open_url", { url: releaseInfo.htmlUrl });
}}>
<Download size={16} /> {lang === "zh" ? "现在下载" : "Download now"}
</button>
<button className="secondary-btn" onClick={() => setUpdatePromptOpen(false)}>
{lang === "zh" ? "稍后" : "Later"}
</button>
</div>
</div>
</div>
)}
{!state ? (
<div className="panel glass center-panel">
@@ -1153,6 +1309,19 @@ function App() {
) : (
<>
{tab === "dashboard" && (
<>
{releaseInfo.status === "ok" && releaseInfo.hasUpdate && (
<div className="update-strip glass">
<div>
<span className="update-dot" />
<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 })}>
{lang === "zh" ? "查看更新" : "View"}
</button>
</div>
)}
<div className="grid dashboard-grid">
<StatCard icon={<TerminalSquare size={20} />} label={t.dashboard.config} value={state.configExists ? t.dashboard.found : t.dashboard.missing} ok={state.configExists} />
<StatCard icon={<Code2 size={20} />} label={t.dashboard.provider} value={currentProvider?.name || state.modelProvider || t.dashboard.officialDefault} ok={Boolean(state.modelProvider)} />
@@ -1185,6 +1354,7 @@ function App() {
</div>
</section>
</div>
</>
)}
{tab === "provider" && (
@@ -1347,6 +1517,103 @@ function App() {
</section>
)}
{tab === "sessions" && (
<section className="panel glass sessions-panel">
<div className="panel-head provider-title-row">
<div>
<p className="eyebrow">Provider Sync</p>
<h3>{lang === "zh" ? "会话管理" : "Session management"}</h3>
<p className="muted-desc">
{lang === "zh"
? "检查并修复 Codex 本地历史会话的 Provider 元数据,让切换供应商后旧 thread 仍能被原生 Codex 识别、打开和续聊。"
: "Check and repair local Codex session provider metadata so old threads stay visible and resumable after provider switching."}
</p>
</div>
<div className="provider-title-actions">
<button className="secondary-btn add-provider-btn" onClick={checkSessions} disabled={loading}>
<RefreshCw size={18} className={cx(loading && "spin")} /> {lang === "zh" ? "检查会话" : "Check"}
</button>
<button className="primary-btn add-provider-btn" onClick={syncSessions} disabled={loading || !sessionStatus?.needsSync}>
<Zap size={18} /> {lang === "zh" ? "同步 / 修复" : "Sync / repair"}
</button>
</div>
</div>
<div className={cx("session-status-card", sessionStatus?.needsSync ? "needs-sync" : "synced")}>
<div className="session-status-icon">
{sessionStatus?.needsSync ? <AlertCircle size={24} /> : <CheckCircle2 size={24} />}
</div>
<div>
<strong>{sessionStatus?.needsSync ? (lang === "zh" ? "发现未同步会话" : "Unsynced sessions found") : (lang === "zh" ? "会话已同步" : "Sessions are in sync")}</strong>
<p>{lang === "zh"
? `目标 Provider:${sessionStatus?.targetProvider || state.modelProvider || "openai"}`
: `Target provider: ${sessionStatus?.targetProvider || state.modelProvider || "openai"}`}</p>
</div>
{sessionStatus?.needsSync && <StatusPill active label={lang === "zh" ? "需要修复" : "Needs repair"} />}
</div>
<div className="session-stat-grid">
<StatCard icon={<FileCode2 size={20} />} label={lang === "zh" ? "rollout 文件" : "Rollout files"} value={sessionStatus?.rolloutFiles ?? "-"} ok />
<StatCard icon={<Sparkles size={20} />} label={lang === "zh" ? "session_meta" : "session_meta"} value={sessionStatus?.sessionMetaCount ?? "-"} ok />
<StatCard icon={<AlertCircle size={20} />} label={lang === "zh" ? "未同步 JSONL" : "Unsynced JSONL"} value={sessionStatus?.mismatchedRollouts ?? "-"} ok={!sessionStatus?.mismatchedRollouts} />
<StatCard icon={<Layers3 size={20} />} label={lang === "zh" ? "SQLite threads" : "SQLite threads"} value={sessionStatus?.sqliteThreads ?? "-"} ok />
<StatCard icon={<AlertCircle size={20} />} label={lang === "zh" ? "未同步记录" : "Unsynced rows"} value={sessionStatus?.mismatchedThreads ?? "-"} ok={!sessionStatus?.mismatchedThreads} />
<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>
<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 ? (
<div className="session-warning-list">
{sessionStatus.warnings.map((item, index) => <p key={index}><AlertCircle size={15} /> {item}</p>)}
</div>
) : null}
</section>
)}
{tab === "instruction" && (
<section className="panel glass instruction-panel simple-instruction-panel">
{instructionMode === "list" ? (
@@ -1475,26 +1742,23 @@ 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={() => window.open(aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`, "_blank")}><ExternalLink size={16} /> {lang === "zh" ? "打开项目主页" : "Open project"}</button>
<button className="ghost-btn" onClick={() => window.open(`${aboutInfo?.projectUrl || `https://github.com/${FALLBACK_GITHUB_REPO}`}/issues`, "_blank")}><ExternalLink size={16} /> {lang === "zh" ? "反馈问题" : "Issues"}</button>
<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>
</div>
</section>
<section className="panel glass about-card">
<div className="panel-head compact">
<div><p className="eyebrow">GitHub Releases</p><h3>{lang === "zh" ? "更新检查" : "Update check"}</h3></div>
<StatusPill active={releaseInfo.status === "ok" && Boolean(releaseInfo.hasUpdate)} label={releaseInfo.status === "checking" ? (lang === "zh" ? "检查中" : "Checking") : releaseInfo.message || (lang === "zh" ? "未检查" : "Idle")} />
<span className={cx("update-status-pill", releaseInfo.hasUpdate && "available")}>{releaseStatusLabel}</span>
</div>
<div className="about-kv">
<div><span>{lang === "zh" ? "状态" : "Status"}</span><strong>{releaseInfo.status}</strong></div>
<div><span>{lang === "zh" ? "状态" : "Status"}</span><strong>{releaseStatusLabel}</strong></div>
<div><span>{lang === "zh" ? "最新版本" : "Latest"}</span><strong>{releaseInfo.latestVersion || "-"}</strong></div>
<div><span>{lang === "zh" ? "资源" : "Asset"}</span><code>{releaseInfo.assetName || "-"}</code></div>
<div><span>{lang === "zh" ? "仓库" : "Repo"}</span><code>{aboutInfo?.githubRepo || FALLBACK_GITHUB_REPO}</code></div>
</div>
{releaseInfo.body && <pre className="release-notes">{releaseInfo.body}</pre>}
<div className="about-actions">
<button className="primary-btn" onClick={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 && window.open(releaseInfo.htmlUrl, "_blank")} disabled={!releaseInfo.htmlUrl}><Download size={16} /> {lang === "zh" ? "打开下载页" : "Open download"}</button>
<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>
</div>
</section>
</section>
+372 -1
View File
@@ -9,7 +9,7 @@
* { 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; }
@@ -198,6 +198,45 @@ 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; }
@keyframes toast { 0% { opacity: 0; transform: translateY(-8px); } 12%, 84% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-8px); } }
.update-mask {
position: fixed;
inset: 0;
z-index: 40;
display: grid;
place-items: center;
background: rgba(3, 7, 16, .62);
backdrop-filter: blur(10px);
}
.update-dialog {
width: min(560px, calc(100vw - 40px));
border-radius: 26px;
padding: 22px;
box-shadow: 0 28px 90px rgba(0, 0, 0, .5);
}
.update-head {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 14px;
}
.update-icon {
width: 46px;
height: 46px;
border-radius: 16px;
display: grid;
place-items: center;
color: white;
background: linear-gradient(135deg, #7c5cff, #12d3ff);
}
.update-body { display: grid; gap: 14px; color: #dfe7f8; }
.update-body p { margin: 0; line-height: 1.6; }
.about-kv.compact { grid-template-columns: 1fr 1fr; }
.update-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 8px; }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@@ -1344,3 +1383,335 @@ html, body, #root {
background: rgba(255,255,255,.035);
border-radius: 999px;
}
/* Productized update surfaces. */
.update-status-pill {
display: inline-flex;
align-items: center;
min-height: 32px;
padding: 7px 12px;
border-radius: 999px;
color: #aab7d1;
background: rgba(255,255,255,.07);
border: 1px solid rgba(255,255,255,.10);
font-size: 12px;
font-weight: 900;
letter-spacing: .01em;
}
.update-status-pill.available {
color: #071713;
background: linear-gradient(135deg, #2ff5b5, #9cffd8);
border-color: rgba(47, 245, 181, .75);
box-shadow: 0 10px 26px rgba(47, 245, 181, .18), inset 0 1px 0 rgba(255,255,255,.35);
}
.update-strip {
grid-column: 1 / -1;
min-height: 58px;
border-radius: 20px;
padding: 12px 14px 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
background: linear-gradient(135deg, rgba(31, 242, 178, .16), rgba(18, 211, 255, .08));
border-color: rgba(31, 242, 178, .26);
}
.update-strip > div {
display: flex;
align-items: center;
min-width: 0;
gap: 10px;
}
.update-strip strong {
color: #eafff7;
white-space: nowrap;
}
.update-strip p {
margin: 0;
color: #9edcc8;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.update-dot {
width: 9px;
height: 9px;
border-radius: 999px;
background: #2ff5b5;
box-shadow: 0 0 0 6px rgba(47, 245, 181, .12), 0 0 20px rgba(47, 245, 181, .72);
flex: 0 0 auto;
}
/* Session management page. */
.sessions-panel {
min-height: calc(100vh - 36px);
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;
gap: 14px;
align-items: center;
padding: 16px 18px;
border-radius: 22px;
background: rgba(7, 15, 31, .52);
border: 1px solid rgba(255,255,255,.10);
}
.session-status-card.synced {
border-color: rgba(31, 242, 178, .22);
background: linear-gradient(135deg, rgba(31, 242, 178, .10), rgba(7, 15, 31, .52));
}
.session-status-card.needs-sync {
border-color: rgba(47, 245, 181, .38);
background: linear-gradient(135deg, rgba(47, 245, 181, .14), rgba(18, 211, 255, .07));
}
.session-status-icon {
width: 50px;
height: 50px;
display: grid;
place-items: center;
border-radius: 17px;
color: #2ff5b5;
background: rgba(31, 242, 178, .12);
border: 1px solid rgba(31, 242, 178, .20);
}
.session-status-card strong {
display: block;
color: #f4f8ff;
margin-bottom: 5px;
font-size: 17px;
}
.session-status-card p {
margin: 0;
color: #9aa9c4;
font-size: 13px;
}
.session-stat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.session-info-list {
display: grid;
gap: 10px;
padding: 16px;
border-radius: 22px;
background: rgba(4,10,24,.34);
border: 1px solid rgba(255,255,255,.08);
}
.session-info-list div {
display: grid;
grid-template-columns: 130px 1fr;
gap: 12px;
align-items: center;
}
.session-info-list span {
color: #91a0bb;
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;
padding: 14px 16px;
border-radius: 18px;
background: rgba(255, 176, 32, .08);
border: 1px solid rgba(255, 176, 32, .18);
}
.session-warning-list p {
display: flex;
align-items: flex-start;
gap: 8px;
margin: 0;
color: #ffd9a1;
font-size: 13px;
line-height: 1.55;
}
@media (max-width: 1320px) {
.session-stat-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.session-status-card {
grid-template-columns: 52px 1fr;
}
.session-status-card .pill {
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: 30px;
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

+3 -2
View File
@@ -1,12 +1,13 @@
{
"name": "codex-x-workspace",
"private": true,
"version": "0.2.0",
"version": "0.2.13",
"scripts": {
"dev": "pnpm --dir apps/desktop tauri dev",
"build": "pnpm --dir apps/desktop tauri build",
"desktop:dev": "pnpm --dir apps/desktop tauri dev",
"desktop:build": "pnpm --dir apps/desktop tauri build",
"typecheck": "pnpm --dir apps/desktop typecheck"
}
},
"author": "yynxxxxx"
}