mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-18 05:59:23 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3c52a7de | ||
|
|
34ec59f28b | ||
|
|
9a82612d5d | ||
|
|
10c16cba7b | ||
|
|
57075256fb | ||
|
|
529af6d796 |
@@ -9,20 +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:
|
||||
include:
|
||||
- platform: macos-latest
|
||||
- name: macOS
|
||||
platform: macos-latest
|
||||
args: --bundles dmg
|
||||
- platform: ubuntu-22.04
|
||||
artifact: codex-x-macos
|
||||
paths: |
|
||||
apps/desktop/src-tauri/target/release/bundle/dmg/*.dmg
|
||||
- name: Linux
|
||||
platform: ubuntu-22.04
|
||||
args: --bundles deb,rpm
|
||||
- platform: windows-latest
|
||||
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
|
||||
@@ -61,24 +78,71 @@ jobs:
|
||||
- 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
|
||||
|
||||
See `CHANGELOG.md` for highlights.
|
||||
release:
|
||||
name: Publish GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- macOS: `.dmg`
|
||||
- Windows: `.msi`
|
||||
- Linux: `.deb` / `.rpm`
|
||||
- name: Download bundle artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: release-assets
|
||||
merge-multiple: true
|
||||
|
||||
GitHub 会自动附带 Source code (zip/tar.gz)。
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
- 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
|
||||
|
||||
@@ -2,11 +2,43 @@
|
||||
|
||||
All notable changes to Codex-X will be documented here.
|
||||
|
||||
## [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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codex-x",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.12",
|
||||
"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.3"
|
||||
version = "0.2.12"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codex-x"
|
||||
version = "0.2.7"
|
||||
version = "0.2.12"
|
||||
description = "Codex Switch & Instruct desktop manager"
|
||||
authors = ["yynxxxxx"]
|
||||
license = "MIT"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Codex-X",
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.12",
|
||||
"identifier": "com.yynxxxxx.codexx",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
@@ -35,12 +35,6 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"wix": {
|
||||
"bannerPath": "icons/wix/banner.bmp",
|
||||
"dialogImagePath": "icons/wix/dialog.bmp"
|
||||
}
|
||||
}
|
||||
"targets": "all"
|
||||
}
|
||||
}
|
||||
|
||||
+225
-11
@@ -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,6 +687,34 @@ 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");
|
||||
@@ -661,6 +729,7 @@ function App() {
|
||||
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("");
|
||||
@@ -768,14 +837,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);
|
||||
},
|
||||
);
|
||||
@@ -990,6 +1061,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,
|
||||
@@ -997,16 +1071,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]);
|
||||
|
||||
@@ -1102,9 +1184,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} />],
|
||||
@@ -1202,6 +1307,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)} />
|
||||
@@ -1234,6 +1352,7 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{tab === "provider" && (
|
||||
@@ -1396,6 +1515,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" ? (
|
||||
@@ -1532,13 +1748,11 @@ function App() {
|
||||
<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={releaseStatusLabel} />
|
||||
<span className={cx("update-status-pill", releaseInfo.hasUpdate && "available")}>{releaseStatusLabel}</span>
|
||||
</div>
|
||||
<div className="about-kv">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -1383,3 +1383,315 @@ 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;
|
||||
}
|
||||
.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: 16px;
|
||||
border-radius: 24px;
|
||||
background: rgba(4,10,24,.34);
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
|
||||
}
|
||||
.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-right: 4px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "codex-x-workspace",
|
||||
"private": true,
|
||||
"version": "0.2.7",
|
||||
"version": "0.2.12",
|
||||
"scripts": {
|
||||
"dev": "pnpm --dir apps/desktop tauri dev",
|
||||
"build": "pnpm --dir apps/desktop tauri build",
|
||||
|
||||
Reference in New Issue
Block a user