Codex/rebuild web UI v0.5 - #80
Conversation
There was a problem hiding this comment.
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.
| backups.push({ | ||
| id: entry.name, | ||
| path: entry.fullPath, | ||
| sizeBytes: await getDirectorySize(entry.fullPath), |
| export default defineConfig({ | ||
| root, | ||
| plugins: [react()], | ||
| build: { | ||
| outDir: path.join(root, "dist"), | ||
| emptyOutDir: true, | ||
| sourcemap: true | ||
| }, | ||
| server: { | ||
| host: "127.0.0.1", | ||
| port: 5173 | ||
| } | ||
| }); |
| useEffect(() => { | ||
| window.localStorage.setItem(key, JSON.stringify(value)); | ||
| }, [key, value]); |
| 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; | ||
| } |
| 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"); |
| <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> |
There was a problem hiding this comment.
💡 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".
| const existing = await readRuntimeDescriptor(resolvedRuntimeFile); | ||
| if (existing) { | ||
| try { | ||
| const pairingToken = await requestExistingPairing({ | ||
| port: existing.port, | ||
| internalSecret: existing.internalSecret, | ||
| resetAccess | ||
| }); |
There was a problem hiding this comment.
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 👍 / 👎.
| backups.push({ | ||
| id: entry.name, | ||
| path: entry.fullPath, | ||
| sizeBytes: await getDirectorySize(entry.fullPath), |
There was a problem hiding this comment.
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 👍 / 👎.
| 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); |
There was a problem hiding this comment.
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 👍 / 👎.
| setStatus(statusPayload.status); | ||
| setBackups(backupPayload); | ||
| setSelectedProvider((current) => current && providersFromStatus(statusPayload.status).some((provider) => provider.id === current) | ||
| ? current | ||
| : statusPayload.status.currentProvider); |
There was a problem hiding this comment.
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 👍 / 👎.
| tone="danger" | ||
| onCancel={onCancel} | ||
| confirmDisabled={(!restoreConfig && !restoreDatabase && !restoreSessions) || relocationBlocked} | ||
| onConfirm={() => onConfirm({ restoreConfig, restoreDatabase, restoreSessions, allowSqliteHomeRelocation: Boolean(relocates) })} |
There was a problem hiding this comment.
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 👍 / 👎.
| 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}` }); |
There was a problem hiding this comment.
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 👍 / 👎.
| <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> |
There was a problem hiding this comment.
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 👍 / 👎.
|
感谢你在 v0.5 Web UI 这轮重构里投入的工作。配对认证、localhost/Origin 边界、Profile 管理、History 请求竞态、Node 16 兼容和发布文档这次都一起补齐了,覆盖面很完整。 我们审查代码和 CI 后,发现合并前还有三个 P1 需要一起收敛,想请你继续在当前 PR 里处理:
这三项处理并补齐回归测试后,请重新生成 感谢配合,辛苦了。 |
目的 / Why
将本地 Web UI 作为主要交互入口,并完成 Reviewer 提出的安全、兼容性和可用性整改:
关联 Issue / Related issue
无。本 PR 直接响应现有 PR 的
REQUEST_CHANGESreview feedback。改动 / Changes
127.0.0.1,并按实际回环 Host 与端口校验浏览器Origin。codexHome、sqliteHome改为服务端 Profile 管理,操作 API 仅接受profileId。--no-open、--reset-access、已有实例复用和无桌面环境处理。影响范围 / Impact
数据写入 / Data writes
<Codex Home>/provider-sync-web.json,保存服务端 Profile 和设备凭证哈希,不保存明文设备凭证。<Codex Home>/provider-sync-web.runtime.json,用于记录本地 Web UI 运行实例。--reset-access会清除已配对浏览器的凭证哈希。sync、switch、restore和prune仍可能按用户确认修改config.toml、rollout、SQLite 和托管备份;本次改动未扩大这些操作的既有数据范围。验证 / Validation
Automated
npm testnpm run web:build:通过。git diff --check:通过。origin/main恰好包含两个非 merge commits,工作树干净。Manual
Not run
dotnet,因此未执行本地 .NET build、桌面测试和原生 GUI 测试。检查清单 / Checklist