diff --git a/CHANGELOG.md b/CHANGELOG.md index 848f26d..e263b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to Codex Extras are documented in this file. +## Unreleased + +- Add an optional, version-checked developer repair for repeated onboarding in + OpenAI Codex 26.1007.21434, with backup, rollback and regression tests. The + repair is excluded from the Extras VSIX and never runs automatically. + ## 1.1.0 - Add a Codex Chats Activity Bar panel with local chat history, automatic diff --git a/README.md b/README.md index b5c5777..ea0d506 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,11 @@ Codex Extras is an independent, unofficial open-source project. It is not affili ## License [MIT](LICENSE) + +## Local onboarding troubleshooting + +If the official Codex welcome screen repeatedly appears after completion, see +[the optional local onboarding repair](docs/local-onboarding-fix.md) for OpenAI +extension version `26.1007.21434`. This developer script changes the installed +OpenAI extension only when explicitly run, keeps backups, and supports rollback. +It is excluded from the Extras VSIX and does not run automatically. diff --git a/docs/local-onboarding-fix.md b/docs/local-onboarding-fix.md new file mode 100644 index 0000000..08025b3 --- /dev/null +++ b/docs/local-onboarding-fix.md @@ -0,0 +1,61 @@ +# Local OpenAI Codex onboarding repair + +This is a development repair of `openai.chatgpt` version `26.1007.21434`. +It is not part of the Codex Extras runtime or VSIX. Do not publish this as an +Extras feature: the defective onboarding decision belongs to OpenAI's webview. + +The installed extension's `FGi()` checks `isLoading` before reading the saved +completion flag. Failed and paused requests can have `isLoading: false` without +successful data, so they incorrectly select onboarding. The repair requires +`isSuccess` instead. It also makes an already open onboarding page navigate home +when the saved completion flag becomes available and says onboarding is complete. +A failed read remains on the loading screen until a successful refetch; this +repair does not diagnose or repair the underlying request failure. + +Local inspection confirmed both saved completion flags were true. Isolated tests +reproduce the incorrect decision in the original installed code. The particular +request failure triggering the user's repeated welcome screen is not yet proven. +After applying the repair and reloading VS Code, the reporter confirmed that the repeated welcome screen stopped appearing. The exact triggering request failure remains unverified. + +## Apply + +From the repository root, pass the absolute installed extension directory: + +```powershell +node scripts/local-onboarding-fix.cjs "$env:USERPROFILE/.vscode/extensions/openai.chatgpt-26.1007.21434-win32-x64" +``` + +Both target files are validated before writes. Original files are saved alongside +them with `.codex-extras-nux-backup` appended. The script rejects unsupported +versions and unexpected code. Reapplying the repair is safe. + +Wait until all local Codex tasks in the window have finished before running +**Developer: Reload Window** from the VS Code command palette. Reload restarts +the official Codex extension and can interrupt its running tasks. Then open a +new agent through both the Extras toolbar and the Codex Chats plus button, +including while another empty agent is open. Repeat after another window reload, +again waiting for active tasks to finish first. +The welcome screen should not return and existing conversations should still open. + +An OpenAI extension update may replace these files. Do not automatically reapply +the repair to another version; inspect and test its code first. + +## Restore + +```powershell +node scripts/local-onboarding-fix.cjs "$env:USERPROFILE/.vscode/extensions/openai.chatgpt-26.1007.21434-win32-x64" --restore +``` + +Reload the VS Code window again after active tasks have finished. +Restore refuses to overwrite files that changed +after the repair. + +## Tests + +```powershell +node --test test/local-onboarding-fix.test.cjs +npm.cmd test +``` + +The focused tests cover the original regression, legitimate first use, completion, +an already open welcome page, version validation, backup, idempotence and restore. diff --git a/scripts/local-onboarding-fix.cjs b/scripts/local-onboarding-fix.cjs new file mode 100644 index 0000000..da00063 --- /dev/null +++ b/scripts/local-onboarding-fix.cjs @@ -0,0 +1,63 @@ +"use strict"; + +// Local repair for OpenAI's extension, NOT part of the Codex Extras runtime. +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); + +const version = "26.1007.21434"; +const guardBefore = "function FGi(){let{data:e,isLoading:t}=UC(hs.NUX_2025_09_15),{authMethod:n}=Lf();if(!t)return e||n==null?`none`:n===`chatgptAuthTokens`||n===`chatgpt`?`2025-09-15-full-chatgpt-auth`:`2025-09-15-apikey-auth`}"; +const guardAfter = guardBefore.replace("isLoading:t", "isSuccess:t").replace("if(!t)", "if(t)"); +const pageBefore = "let s=h(),l;e[0]!==s||e[1]!==n||e[2]!==t?"; +const pageAfter = "let s=h(),l;X.useEffect(()=>{if(n===`none`)s(`/`)},[n,s]);e[0]!==s||e[1]!==n||e[2]!==t?"; +const patches = [ + { file: "app-initial-c014f9ee4429.js", before: guardBefore, after: guardAfter }, + { file: "first-run-afdf3f60ec3c.js", before: pageBefore, after: pageAfter }, +]; + +function transform(source, before, after) { + if (source.includes(after) && !source.includes(before)) return source; + if (source.split(before).length !== 2 || source.includes(after)) { + throw new Error("Unrecognized extension code; refusing to patch"); + } + return source.replace(before, after); +} + +function apply(extensionDirectory, restore = false) { + const manifest = JSON.parse(fs.readFileSync(path.join(extensionDirectory, "package.json"), "utf8")); + if (manifest.publisher !== "openai" || manifest.name !== "chatgpt" || manifest.version !== version) { + throw new Error(`Repair supports only openai.chatgpt ${version}`); + } + // Validate BOTH files before writing either one. Never overwrite a backup. + const changes = patches.map(({ file, before, after }) => { + const target = path.join(extensionDirectory, "webview", "assets", file); + const backup = `${target}.codex-extras-nux-backup`; + const original = fs.readFileSync(target, "utf8"); + const repaired = transform(original, before, after); + if (restore) { + const saved = fs.readFileSync(backup, "utf8"); + if (transform(saved, before, after) !== original) throw new Error("File changed since repair; refusing restore"); + return { target, content: saved }; + } + if (fs.existsSync(backup)) { + if (transform(fs.readFileSync(backup, "utf8"), before, after) !== repaired) { + throw new Error("Backup does not match installed extension"); + } + } else if (original !== repaired) { + return { target, backup, original, content: repaired }; + } + return { target, content: repaired }; + }); + for (const change of changes) { + if (change.backup) fs.writeFileSync(change.backup, change.original, { flag: "wx" }); + fs.writeFileSync(change.target, change.content); + console.log(`${restore ? "Restored" : "Patched"} ${path.basename(change.target)} sha256=${crypto.createHash("sha256").update(change.content).digest("hex")}`); + } +} + +if (require.main === module) { + if (!process.argv[2]) throw new Error("Usage: node scripts/local-onboarding-fix.cjs EXTENSION_DIRECTORY [--restore]"); + apply(path.resolve(process.argv[2]), process.argv.includes("--restore")); +} + +module.exports = { apply, transform, patches, guardBefore, guardAfter, pageAfter, version }; diff --git a/test/local-onboarding-fix.test.cjs b/test/local-onboarding-fix.test.cjs new file mode 100644 index 0000000..cec3f8b --- /dev/null +++ b/test/local-onboarding-fix.test.cjs @@ -0,0 +1,60 @@ +"use strict"; +const assert = require("node:assert/strict"); +const test = require("node:test"); +const vm = require("node:vm"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { apply, transform, patches, guardBefore, guardAfter, pageAfter, version } = require("../scripts/local-onboarding-fix.cjs"); + +function decide(source, query, authMethod = "chatgpt") { + return vm.runInNewContext(`${source}; FGi()`, { + UC: () => query, hs: { NUX_2025_09_15: "completed" }, Lf: () => ({ authMethod }), + }); +} + +test("regression: a paused or failed read must never reopen onboarding", () => { + for (const query of [ + { data: undefined, isLoading: false, isSuccess: false, isPending: true, fetchStatus: "paused" }, + { data: undefined, isLoading: false, isSuccess: false, isError: true }, + ]) { + assert.equal(decide(guardBefore, query), "2025-09-15-full-chatgpt-auth"); + assert.equal(decide(guardAfter, query), undefined); + } +}); + +test("successful reads preserve genuine first use and completed onboarding", () => { + assert.equal(decide(guardAfter, { data: true, isSuccess: true }), "none"); + assert.equal(decide(guardAfter, { data: false, isSuccess: true }), "2025-09-15-full-chatgpt-auth"); + assert.equal(decide(guardAfter, { data: false, isSuccess: true }, "apikey"), "2025-09-15-apikey-auth"); + assert.equal(decide(guardAfter, { data: false, isSuccess: true }, null), "none"); + assert.equal(decide(guardAfter, { data: undefined, isSuccess: false, isLoading: true }), undefined); +}); + +test("an already open welcome page exits after a successful completion read", () => { + const effect = pageAfter.slice(pageAfter.indexOf("X.useEffect"), pageAfter.indexOf(";e[0]")); + for (const state of [undefined, "2025-09-15-full-chatgpt-auth", "none"]) { + const routes = []; + vm.runInNewContext(effect, { X: { useEffect: (fn) => fn() }, n: state, s: (route) => routes.push(route) }); + assert.deepEqual(routes, state === "none" ? ["/"] : []); + } +}); + +test("local deployment is version checked, idempotent, backed up and reversible", (t) => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "codex-nux-fix-")); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + fs.mkdirSync(path.join(dir, "webview", "assets"), { recursive: true }); + fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ publisher: "openai", name: "chatgpt", version })); + for (const patch of patches) fs.writeFileSync(path.join(dir, "webview", "assets", patch.file), patch.before); + apply(dir); + apply(dir); + for (const patch of patches) assert.equal(fs.readFileSync(path.join(dir, "webview", "assets", patch.file), "utf8"), patch.after); + apply(dir, true); + for (const patch of patches) assert.equal(fs.readFileSync(path.join(dir, "webview", "assets", patch.file), "utf8"), patch.before); + fs.writeFileSync(path.join(dir, "webview", "assets", patches[1].file), "unexpected build"); + assert.throws(() => apply(dir), /Unrecognized/); + assert.equal(fs.readFileSync(path.join(dir, "webview", "assets", patches[0].file), "utf8"), patches[0].before); + assert.throws(() => transform("unknown", patches[0].before, patches[0].after), /Unrecognized/); + fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ publisher: "openai", name: "chatgpt", version: "other" })); + assert.throws(() => apply(dir), /supports only/); +});