Skip to content

Codex/rebuild web UI v0.5 - #80

Open
tangquanwei wants to merge 2 commits into
Dailin521:mainfrom
tangquanwei:codex/rebuild-web-ui-v0.5
Open

Codex/rebuild web UI v0.5#80
tangquanwei wants to merge 2 commits into
Dailin521:mainfrom
tangquanwei:codex/rebuild-web-ui-v0.5

Conversation

@tangquanwei

Copy link
Copy Markdown

目的 / Why

将本地 Web UI 作为主要交互入口,并完成 Reviewer 提出的安全、兼容性和可用性整改:

  • 首页不再暴露长期 API token,改为一次性自动配对。
  • 收紧本地服务的监听、Origin 校验和存储路径边界。
  • 修复 History 搜索中的请求竞态。
  • 保持 Node.js 16.20.2 兼容性。
  • 统一 v0.5.0 版本元数据,并补充完整使用及发布文档。

关联 Issue / Related issue

无。本 PR 直接响应现有 PR 的 REQUEST_CHANGES review feedback。

改动 / Changes

  • 新增短时、一次性 Web UI 配对凭证;浏览器换取设备凭证,服务端仅保存哈希。
  • 服务固定监听 127.0.0.1,并按实际回环 Host 与端口校验浏览器 Origin
  • codexHomesqliteHome 改为服务端 Profile 管理,操作 API 仅接受 profileId
  • 新增 --no-open--reset-access、已有实例复用和无桌面环境处理。
  • History 搜索增加 300ms 防抖、旧请求取消和最新响应门控。
  • CI 覆盖 Node.js 16.20.2 与 Node.js 24。
  • npm 包及可发布 .NET 项目版本统一为 0.5.0。
  • 增加认证、Origin、回环监听、Profile、History 竞态及启动失败回归测试。
  • 更新 README、多语言文档、CHANGELOG 和 v0.5.0 发布说明。
  • 将历史重组为两个独立提交:功能与测试、文档与发布说明。

影响范围 / Impact

  • Node.js CLI
  • Shared .NET Core(版本元数据)
  • Windows GUI(版本元数据)
  • macOS GUI(版本元数据)
  • WSL / SQLite paths
  • Backup / restore
  • CI / GitHub Actions
  • Documentation

数据写入 / Data writes

  • 新增 <Codex Home>/provider-sync-web.json,保存服务端 Profile 和设备凭证哈希,不保存明文设备凭证。
  • 新增 <Codex Home>/provider-sync-web.runtime.json,用于记录本地 Web UI 运行实例。
  • --reset-access 会清除已配对浏览器的凭证哈希。
  • syncswitchrestoreprune 仍可能按用户确认修改 config.toml、rollout、SQLite 和托管备份;本次改动未扩大这些操作的既有数据范围。

验证 / Validation

Automated

  • Node.js v24.15.0:npm test
    • 224 tests
    • 220 passed
    • 4 platform-specific skipped
    • 0 failed
  • npm run web:build:通过。
  • git diff --check:通过。
  • 功能提交在不包含文档提交的情况下单独通过测试和构建。
  • 最终分支相对 origin/main 恰好包含两个非 merge commits,工作树干净。

Manual

  • 平台:macOS,本地浏览器默认缩放。
  • 验证自动配对成功,URL fragment 在配对后被清除。
  • 验证服务端 Profile 的新增、选择和使用。
  • 验证 History 搜索及 Enter 立即搜索。
  • 验证“忘记此浏览器”后回到配对状态。
  • 浏览器控制台未发现错误。

Not run

  • 最终重组分支未在本机重新执行精确 Node.js 16.20.2;CI 已配置 Node.js 16.20.2 与 Node.js 24 矩阵。
  • 当前本机环境没有可用的 dotnet,因此未执行本地 .NET build、桌面测试和原生 GUI 测试。
  • 未运行 Windows/macOS 原生 GUI 的 headful E2E。
  • 开发依赖中的 Vite/esbuild 审计问题未强制升级;自动修复会升级到破坏 Node.js 16 兼容性的版本。

检查清单 / Checklist

  • PR 只包含相关修改 / This PR contains only related changes
  • 已补充相关测试,或说明不需要测试的原因 / Tests were added or the reason they are unnecessary is explained
  • 如有用户可见变化,已更新相关文档 / Relevant documentation was updated for user-facing changes
  • 未提交未脱敏的凭据、会话、SQLite、备份、日志或个人信息 / No unredacted credentials, sessions, databases, backups, logs, or personal data are included

Copilot AI lite review requested due to automatic review settings August 9, 2026 03:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR rebuilds and hardens the localhost Web UI as the primary interface for codex-provider-sync v0.5.0, adding one-time browser pairing, server-managed storage profiles, and a read-only History browser while aligning runtime/package versions and documentation.

Changes:

  • Add a new localhost-only Web UI (React + Vite) with one-time pairing, device credentials, profiles, backups/restore, and an activity log.
  • Introduce a new read-only History API and UI with debounce + request cancellation to prevent search/detail races.
  • Bump versions/metadata to 0.5.0, raise Node.js floor to 16.20.2, update CI matrix, and expand docs/release notes.

Reviewed changes

Copilot reviewed 40 out of 46 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
web/vite.config.js Vite build/devserver config for the new Web UI.
web/src/styles.css Web UI styling for layout, navigation, modals, history, and toasts.
web/src/main.jsx Web UI entrypoint mounting the React app.
web/src/icons.jsx Inline SVG icon components used across the Web UI.
web/src/hooks.js usePersistentState localStorage helper for UI preferences/state.
web/src/history-requests.js History request gate + debounce helper for race/cancellation control.
web/src/App.jsx Main Web UI application: overview, sync/switch, backups/restore, history, activity, profiles, pairing gate.
web/src/api.js Browser API client + pairing/bootstrap logic and device credential headers.
web/index.html Web UI HTML entry for dev/build.
web/dist/index.html Committed production build entrypoint served by the Node server.
web/dist/assets/index-00e598c3.css Committed production CSS bundle output.
test/web-server.test.js Tests for pairing, Origin validation, profiles, restore restrictions, and Web UI lifecycle behaviors.
test/sync-service.test.js Update Node version guard test expectations to 16.20.2+.
test/release-metadata.test.js Update release metadata test to v0.5.0.
test/history.test.js New tests for History list/detail safety filtering and message limiting.
test/history-requests.test.js New tests for History debounce and “latest request wins” gate.
src/web-state.js New persistent Web UI state store (profiles + credential hash storage).
src/web-server.js New localhost Web UI server: static serving, CSP, pairing, Origin checks, API endpoints, reuse via runtime descriptor.
src/node-version.js Switch from “major only” to minimum semver floor (16.20.2).
src/history.js New History implementation reading rollout JSONL and returning safe message subsets.
src/cli.js Add codex-provider web command and enforce Node version at startup.
src/backup.js Add listBackups() for Web UI backup listing.
scripts/publish-npm.js Add cross-platform npm publish helper (build + test + pack preview + publish).
README.md Rework root README to emphasize Web UI as primary interface, expand docs/usage.
package.json Bump to 0.5.0; add React/Vite deps, web scripts, prepare, include web/dist in package.
docs/WORKING_PRINCIPLE_ZH.md New detailed design/behavior doc for rollout/SQLite/global-state mechanics and safety model.
docs/release-notes/v0.5.0-zh.md New v0.5.0 Chinese release notes.
docs/RELEASE_NOTES_V0.5.0.md New v0.5.0 technical release notes (English).
docs/README_ZH.md New canonical Chinese README aligned with Web UI-first workflow.
docs/README_WEB_UI_ZH.md New Web UI usage/security guide (Chinese).
docs/README_KO.md New Korean README aligned with Web UI-first workflow.
docs/README_JA.md New Japanese README aligned with Web UI-first workflow.
docs/README_EN.md Adjust legacy English README to defer to root README / mark GUI deprecated.
desktop/CodexProviderSync.Mac/CodexProviderSync.Mac.csproj Bump Mac desktop project version metadata to 0.5.0.
desktop/CodexProviderSync.GuiE2E/CodexProviderSync.GuiE2E.csproj Bump GUI E2E project version metadata to 0.5.0.
desktop/CodexProviderSync.GuiE2E.Tests/ApplicationAndDialogContractTests.cs Update expected “no update” dialog text to v0.5.0.
desktop/CodexProviderSync.Core/CodexProviderSync.Core.csproj Bump Core project version metadata to 0.5.0.
desktop/CodexProviderSync.Automation/CodexProviderSync.Automation.csproj Bump Automation project version metadata to 0.5.0.
desktop/CodexProviderSync.Application/CodexProviderSync.Application.csproj Bump Application project version metadata to 0.5.0.
desktop/CodexProviderSync.App/CodexProviderSync.App.csproj Bump Windows desktop app project version metadata to 0.5.0.
CHANGELOG.md Add v0.5.0 changelog entry covering Web UI, pairing, Origin hardening, History fixes, and runtime floor.
.github/workflows/ci.yml Update CI matrix to test Node.js 16.20.2 and Node 24.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/backup.js
backups.push({
id: entry.name,
path: entry.fullPath,
sizeBytes: await getDirectorySize(entry.fullPath),
Comment thread web/vite.config.js
Comment on lines +8 to +20
export default defineConfig({
root,
plugins: [react()],
build: {
outDir: path.join(root, "dist"),
emptyOutDir: true,
sourcemap: true
},
server: {
host: "127.0.0.1",
port: 5173
}
});
Comment thread web/src/hooks.js
Comment on lines +13 to +15
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
Comment thread src/web-state.js
Comment on lines +185 to +196
async persist() {
const serialized = `${JSON.stringify(this.state, null, 2)}\n`;
const target = this.filePath;
this.writeQueue = this.writeQueue.then(async () => {
await fs.mkdir(path.dirname(target), { recursive: true });
const temporary = `${target}.tmp-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
await fs.writeFile(temporary, serialized, { encoding: "utf8", mode: 0o600 });
await fs.rename(temporary, target);
await fs.chmod(target, 0o600).catch(() => {});
});
return this.writeQueue;
}
Comment thread src/history.js
Comment on lines +113 to +118
async function collectHistory(codexHome) {
const sessions = [];
for (const dirName of SESSION_DIRS) {
const files = await listRolloutFiles(path.join(codexHome, dirName));
for (const filePath of files) {
const session = await readRollout(filePath, dirName === "archived_sessions");
Comment thread web/src/App.jsx
Comment on lines +409 to +413
<label className="form-field form-field--short">
<span>保留备份数</span>
<input type="number" min="1" max="100000" value={keepCount} onChange={(event) => setKeepCount(Number(event.target.value))} disabled={busy} />
<small>同步后自动清理</small>
</label>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45953789f6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/web-server.js
Comment on lines +639 to +646
const existing = await readRuntimeDescriptor(resolvedRuntimeFile);
if (existing) {
try {
const pairingToken = await requestExistingPairing({
port: existing.port,
internalSecret: existing.internalSecret,
resetAccess
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply the SQLite override before reusing the server

When a Web UI is already running for this Codex Home, invoking codex-provider web --sqlite-home <new-path> only requests another pairing and returns the existing server, whose default profile still contains the previous SQLite override. A subsequent sync or restore can therefore modify the old database even though the new command explicitly selected another SQLite Home; either update/restart the existing instance or reject reuse when its storage parameters differ.

AGENTS.md reference: AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

Comment thread src/backup.js
backups.push({
id: entry.name,
path: entry.fullPath,
sizeBytes: await getDirectorySize(entry.fullPath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reuse cached backup inventory sizes

For users with several large managed backups, every Web UI refresh recursively traverses every file in every backup even though metadata.json already carries the cached sizeBytes/fileCount inventory and getBackupSummary uses getBackupDirectorySize to consume it. Since the app fetches /api/backups during each refresh and after operations, this can make the UI repeatedly scan many gigabytes; use the cached-size helper here, retaining its legacy/damaged-metadata fallback.

Useful? React with 👍 / 👎.

Comment thread src/history.js
const provider = normalizeText(options.provider);
const archived = options.archived ?? "all";
if (!["all", "active", "archived"].includes(archived)) throw new Error("archived must be all, active, or archived.");
const sessions = await collectHistory(codexHome);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Paginate before loading every rollout message

On installations with substantial history, a request for one 50-session page reads every rollout file to EOF and retains every message before filtering and slicing the requested page. Detail requests repeat the same complete scan, and aborting the browser fetch does not cancel this server-side work, so normal browsing or searching can consume memory proportional to the entire Codex history and overlap multiple full scans. Build lightweight session summaries first and load message bodies only for matching/search-required sessions and the selected detail.

Useful? React with 👍 / 👎.

Comment thread web/src/App.jsx
Comment on lines +775 to +779
setStatus(statusPayload.status);
setBackups(backupPayload);
setSelectedProvider((current) => current && providersFromStatus(statusPayload.status).some((provider) => provider.id === current)
? current
: statusPayload.status.currentProvider);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Discard status responses from previously selected profiles

When the user changes storage profiles while a refresh is in flight, the older profile's requests can finish last and overwrite status, backups, and selectedProvider because refreshes have no cancellation or latest-response guard. The UI then shows and confirms data from the old profile while write requests use the new profileId; this can sync the old Provider into the new profile or show a small prune count before deleting many backups from the new profile. Associate each response with the requested profile and discard it once the selection changes.

AGENTS.md reference: AGENTS.md:L47-L49

Useful? React with 👍 / 👎.

Comment thread web/src/App.jsx
tone="danger"
onCancel={onCancel}
confirmDisabled={(!restoreConfig && !restoreDatabase && !restoreSessions) || relocationBlocked}
onConfirm={() => onConfirm({ restoreConfig, restoreDatabase, restoreSessions, allowSqliteHomeRelocation: Boolean(relocates) })}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require an explicit relocation target in the modal

When a backup's SQLite Home differs from a target resolved from config.toml or CODEX_SQLITE_HOME, the modal enables confirmation and sends allowSqliteHomeRelocation: true, but a default profile submits no explicit sqliteHome. runRestore therefore always rejects this advertised flow because relocation permission requires an explicit target; only users who happen to create a separate profile with the same SQLite path can complete it. Block the confirmation with actionable guidance or require/select an explicit server-managed SQLite Home before offering relocation.

AGENTS.md reference: AGENTS.md:L159-L160

Useful? React with 👍 / 👎.

Comment thread web/src/App.jsx
const payload = plan.mode === "switch"
? await apiRequest("/api/switch", { ...common, model: plan.modelMode === "custom" ? plan.model : undefined, keepRootModel: plan.modelMode === "keep" })
: await apiRequest("/api/sync", common);
setToast({ tone: "success", title: plan.mode === "switch" ? "切换并同步完成" : "同步完成", message: `备份:${payload.result.backupDir}` });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Report skipped rollout files as partial success

When runSync or runSwitch returns skippedLockedRolloutFiles, the UI always displays an unconditional completion toast containing only the backup path. Files can become locked or change after the preceding status refresh, so the existing warning panel does not reliably cover this result and users may believe all history was rewritten even though sessions remain hidden; inspect the returned result, list the skipped files, and tell the user to rerun after those sessions end.

AGENTS.md reference: AGENTS.md:L137-L141

Useful? React with 👍 / 👎.

Comment thread web/src/App.jsx
<div className="backup-date"><strong>{formatDate(backup.metadata.createdAt)}</strong><span>{backup.id}</span></div>
<div className="backup-facts"><span>Provider <strong>{backup.metadata.targetProvider}</strong></span><span>Rollout <strong>{backup.metadata.changedSessionFiles ?? 0}</strong></span><span>SQLite <strong>{backup.metadata.sqliteDbFiles?.length ? "已包含" : "未包含"}</strong></span></div>
<div className="backup-source"><span>SQLite Home</span><code>{backup.metadata.sqliteHome ?? "旧版 metadata 未记录"}</code></div>
<div className="backup-row-actions"><span>{formatBytes(backup.sizeBytes)}</span><button className="button button--secondary button--compact" type="button" disabled={busy} onClick={() => onRestore(backup)}>恢复</button></div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable restore for Windows WSL UNC storage

When status identifies a WSL UNC SQLite Home as unsupported on Windows, the backups page still enables Restore because this button only checks busy. The shared restore service rejects the layout before restoring even sessions-only content, so every restore offered here is guaranteed to fail; disable the restore entry points when status.sqliteAccess.supported === false and direct the user to run the CLI inside WSL with Linux paths.

AGENTS.md reference: AGENTS.md:L108-L110

Useful? React with 👍 / 👎.

Dailin521 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

感谢你在 v0.5 Web UI 这轮重构里投入的工作。配对认证、localhost/Origin 边界、Profile 管理、History 请求竞态、Node 16 兼容和发布文档这次都一起补齐了,覆盖面很完整。

我们审查代码和 CI 后,发现合并前还有三个 P1 需要一起收敛,想请你继续在当前 PR 里处理:

  1. 修复 Windows checkout

    • images/README/2026-08-05T03:53:48.708Z.png 改为 Windows 合法文件名,并更新所有引用。
    • Windows Node 16.20.2、Node 24 和 desktop-test 需要能够完成 checkout 并实际执行测试,最终 ci-gate 通过。
  2. 避免复用旧实例时忽略新的 --sqlite-home

    • 相同的规范化 SQLite Home 可以继续复用。
    • 如果本次显式指定的 SQLite Home 与已有实例不同,不能静默复用旧实例。一个简单可靠的处理方式是拒绝复用,并明确提示关闭旧实例后重新启动。
    • 建议补充“先以 A 启动,再以 B 启动不会复用 A;再次以 A 启动仍可复用”的回归测试。
  3. 修复 Profile 切换时的状态刷新竞态

    • 只有当前 Profile 的最新请求可以更新 statusbackupsselectedProvider、loading 和错误提示。
    • 除了 AbortController,还需要 latest-request/sequence 门控。
    • 写操作期间避免切换 Profile,确保确认界面与实际写入的 profileId 一致。
    • 建议补充确定性测试:A 请求先发出,切换 B 后 B 先完成、A 最后完成,最终界面和后续操作都保持在 B。

这三项处理并补齐回归测试后,请重新生成 web/dist、推送到当前 PR,并重新跑完整 CI。新的 head 推上来后,我们再一起复审;暂时不需要关闭或重开 PR。

感谢配合,辛苦了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants