Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
61 changes: 61 additions & 0 deletions docs/local-onboarding-fix.md
Original file line number Diff line number Diff line change
@@ -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.
63 changes: 63 additions & 0 deletions scripts/local-onboarding-fix.cjs
Original file line number Diff line number Diff line change
@@ -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 };
60 changes: 60 additions & 0 deletions test/local-onboarding-fix.test.cjs
Original file line number Diff line number Diff line change
@@ -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/);
});