diff --git a/README.md b/README.md index d796b8c..d853d17 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,8 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass ` If the script reports that a chunk or function cannot be found, the Codex version has probably changed. Update the search markers in `tools/build-inplace-asar.mjs` and rebuild. +The builder discovers hashed model-selector and React DOM asset filenames automatically. Minified component names can still change between Codex releases; version-specific mappings live in `tools/selector-compatibility.mjs` and are covered by `npm run check`. + ## Advantages | Area | Benefit | @@ -174,6 +176,10 @@ Quit Codex from the Windows notification-area icon. Closing only the main window Rebuild from the newest official archive. This project does not manufacture a model list: it reuses the catalog exposed by Codex. +### The application shows “An error occurred” after a Codex update + +Run `npm run check`, extract `app.asar` from the current official installation, and rebuild. React error 130 usually means Codex renamed a minified component used by the selector patch. Update the matching profile in `tools/selector-compatibility.mjs`; do not map JSX targets to JSX-runtime or lazy-loader objects. + ### I want to return to official Codex Close the customized copy, then launch Codex from the Start menu. No uninstall or file restoration is required. diff --git a/package.json b/package.json index 4c16c1d..f2cfef1 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,6 @@ "@electron/asar": "4.2.0" }, "scripts": { - "check": "node --check tools/build-inplace-asar.mjs" + "check": "node --check tools/build-inplace-asar.mjs && node --test tools/*.test.mjs" } } diff --git a/tools/build-inplace-asar.mjs b/tools/build-inplace-asar.mjs index b25c4bb..18087a9 100644 --- a/tools/build-inplace-asar.mjs +++ b/tools/build-inplace-asar.mjs @@ -3,6 +3,10 @@ import fs from "node:fs"; import path from "node:path"; import { createRequire } from "node:module"; import { pathToFileURL } from "node:url"; +import { + adaptCompactSelector, + selectSelectorCompatibilityProfile, +} from "./selector-compatibility.mjs"; const require = createRequire(import.meta.url); const asarLib = path.dirname(require.resolve("@electron/asar")); const { readArchiveHeaderSync } = await import(`${pathToFileURL(asarLib).href}/disk.js`); @@ -13,20 +17,30 @@ const destinationArchive = path.resolve( process.argv[3] ?? "outputs/Codex-Native-Selector/app/resources/app.asar", ); const extractedRoot = path.resolve(process.argv[5] ?? "work/asar-extracted"); -const chunkRelativePath = - "webview/assets/model-and-reasoning-dropdown-DFupBzq6.js"; +const assetsRoot = path.join(extractedRoot, "webview/assets"); +const findSingleAsset = (pattern, description) => { + const matches = fs.readdirSync(assetsRoot).filter((name) => pattern.test(name)); + if (matches.length !== 1) { + throw new Error(`Expected one ${description} asset, found ${matches.length}.`); + } + return matches[0]; +}; +const chunkFileName = findSingleAsset( + /^model-and-reasoning-dropdown-.*\.js$/u, + "model selector", +); +const reactDomFileName = findSingleAsset(/^react-dom-.*\.js$/u, "React DOM"); +const chunkRelativePath = `webview/assets/${chunkFileName}`; const chunkSourcePath = path.join(extractedRoot, chunkRelativePath); let source = fs.readFileSync(chunkSourcePath, "utf8"); -source = `import{t as Cp}from"./react-dom-CTTwO1mS.js";${source}`; +source = `import{t as Cp}from"./${reactDomFileName}";${source}`; -const catalogBefore = - "function K(e){let t=q(ce,e);if(t.length>=4)return t;let n=q(le,e);return n.length>=4?n:[]}"; -const catalogAfter = "function K(e){return oe(e)}"; -if (!source.includes(catalogBefore)) { - throw new Error("The original compact catalog function was not found."); -} -source = source.replace(catalogBefore, catalogAfter); +const compatibilityProfile = selectSelectorCompatibilityProfile(source); +source = source.replace( + compatibilityProfile.catalogBefore, + compatibilityProfile.catalogAfter, +); const fieldsBefore = "supportedReasoningEfforts:n})=>{let r="; const fieldsAfter = @@ -39,8 +53,12 @@ if (!source.includes(fieldsBefore) || !source.includes(valueBefore)) { } source = source.replace(fieldsBefore, fieldsAfter).replace(valueBefore, valueAfter); -const componentStart = source.indexOf("function Ve(e){"); -const componentEnd = source.indexOf("function He(e){", componentStart); +const { componentName, nextComponentName } = compatibilityProfile; +const componentStart = source.indexOf(`function ${componentName}(e){`); +const componentEnd = source.indexOf( + `function ${nextComponentName}(e){`, + componentStart, +); if (componentStart < 0 || componentEnd < 0) { throw new Error("The original compact selector component was not found."); } @@ -53,22 +71,29 @@ function Ck(e,t){let n=`gpt-${t}`;return e===n?`full`:e.startsWith(`${n}-`)?e.sl function Ve(e){let{active:t,fastModeEnabled:n,onDragToMax:r,onSelectPower:i,powerSelections:a,selectedPowerSelection:o,shouldReduceMotion:s,transitionsReady:l}=e;if(o==null)return(0,Z.jsx)(`div`,{className:X.SimpleView});Cs();let u=Cf(o.model),d=[...new Set(a.filter(e=>Cf(e.model)===u).map(e=>e.model))],f=a.filter(e=>e.model===o.model),p=f.map(e=>({id:e.id,isMax:e.reasoningEffort===`ultra`,label:e.modelLabel})),m=e=>{let t=a.find(t=>t.id===e.id);t&&i(t)},h=e=>{let t=a.filter(t=>t.model===e),n=t.find(e=>e.reasoningEffort===o.reasoningEffort)??t.find(e=>e.reasoningEffort===e.defaultReasoningEffort)??t[0];n&&i(n)},g=d.length>1?(0,Z.jsx)(`div`,{className:`codex-tabs`,role:`tablist`,children:d.map(e=>(0,Z.jsx)(`button`,{type:`button`,role:`tab`,"aria-selected":e===o.model,"data-s":e===o.model,onPointerDown:e=>e.preventDefault(),onClick:t=>{t.preventDefault(),t.stopPropagation(),h(e)},children:Cv(e,u)},e))}):null;return(0,Z.jsxs)(`div`,{className:`${X.SimpleView} codex-native`,"data-v":Ck(o.model,u),children:[g,(0,Z.jsx)(ye,{active:t,fastModeEnabled:n,keyboardControlFocused:!1,onDragToMax:r,onSelectOption:m,options:p,selectedOptionId:o.id,shouldReduceMotion:s,transitionsReady:l})]})} */} -const compactSelector = fs - .readFileSync(path.resolve(process.argv[4] ?? "tools/selector-v2.js.txt"), "utf8") - .replace(/^\uFEFF/u, ""); +const compactSelector = adaptCompactSelector( + fs + .readFileSync(path.resolve(process.argv[4] ?? "tools/selector-v2.js.txt"), "utf8") + .replace(/^\uFEFF/u, ""), + compatibilityProfile, +); source = source.slice(0, componentStart) + compactSelector + source.slice(componentEnd); -const controlsStart = source.indexOf("function rt(e){"); -const controlsEnd = source.indexOf("function it(e){", controlsStart); +const { controlsName, nextControlsName } = compatibilityProfile; +const controlsStart = source.indexOf(`function ${controlsName}(e){`); +const controlsEnd = source.indexOf( + `function ${nextControlsName}(e){`, + controlsStart, +); if (controlsStart < 0 || controlsEnd < 0) { throw new Error("The original compact selector controls were not found."); } source = source.slice(0, controlsStart) + - "function rt(e){let{ref:t}=e;return(0,Q.jsx)(`div`,{className:`codex-controls-placeholder`,ref:t})}" + + `function ${controlsName}(e){let{ref:t}=e;return(0,Q.jsx)(\`div\`,{className:\`codex-controls-placeholder\`,ref:t})}` + source.slice(controlsEnd); const sliderPropsBefore = @@ -80,15 +105,14 @@ if (!source.includes(sliderPropsBefore)) { } source = source.replace(sliderPropsBefore, sliderPropsAfter); -const advancedStateBefore = "D=(0,ft.useRef)(null),O=i===`advanced`,k="; -const advancedStateAfter = "D=(0,ft.useRef)(null),O=!1,k="; +const { advancedStateBefore } = compatibilityProfile; const menuViewBefore = '"data-view":i,style:N'; const menuViewAfter = '"data-view":`simple`,style:N'; -if (!source.includes(advancedStateBefore) || !source.includes(menuViewBefore)) { +if (!source.includes(menuViewBefore)) { throw new Error("The advanced-view state wiring was not found."); } source = source - .replace(advancedStateBefore, advancedStateAfter) + .replace(advancedStateBefore, advancedStateBefore.replace("i===`advanced`", "!1")) .replace(menuViewBefore, menuViewAfter); const originalChunk = fs.readFileSync(chunkSourcePath); diff --git a/tools/build-portable.ps1 b/tools/build-portable.ps1 index f2a29f2..801fb53 100644 --- a/tools/build-portable.ps1 +++ b/tools/build-portable.ps1 @@ -1,11 +1,14 @@ param( [Parameter(Mandatory = $true)] [string]$InstallApp, - [string]$Destination = (Join-Path (Split-Path -Parent $PSScriptRoot) 'outputs\Codex-Native-Selector') + [string]$Destination ) $ErrorActionPreference = 'Stop' $workspace = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) +if ([string]::IsNullOrWhiteSpace($Destination)) { + $Destination = Join-Path $workspace 'outputs\Codex-Native-Selector' +} $destinationFull = [System.IO.Path]::GetFullPath($Destination) $installFull = [System.IO.Path]::GetFullPath($InstallApp) if (-not (Test-Path -LiteralPath $installFull)) { throw "Codex app directory not found: $installFull" } diff --git a/tools/selector-compatibility.mjs b/tools/selector-compatibility.mjs new file mode 100644 index 0000000..cb009ab --- /dev/null +++ b/tools/selector-compatibility.mjs @@ -0,0 +1,98 @@ +export const selectorCompatibilityProfiles = [ + { + name: "legacy", + catalogBefore: + "function K(e){let t=q(ce,e);if(t.length>=4)return t;let n=q(le,e);return n.length>=4?n:[]}", + catalogAfter: "function K(e){return oe(e)}", + componentName: "Ve", + nextComponentName: "He", + controlsName: "rt", + nextControlsName: "it", + advancedStateBefore: "D=(0,ft.useRef)(null),O=i===`advanced`,k=", + hooksName: "Ge", + fastPredicate: "it", + standardPredicate: "at", + fastIcon: "Qe", + standardIcon: "tt", + sliderComponent: "ye", + runtimeMarkers: [ + "Ge=t(n(),1)", + "function it(e){let{iconKind:t}=e;return t===`fast`}", + "function at(e){let{value:t}=e;return t==null}", + "(0,Q.jsx)(Qe,", + "(0,Q.jsx)(tt,", + "(0,Z.jsx)(ye,{active:", + ], + }, + { + name: "codex-26.707", + catalogBefore: + "function ae(e,t=!1){let n=K(t?[...q,le]:q,e);if(n.length>=4)return n;let r=K(ue,e);return r.length>=4?r:[]}", + catalogAfter: "function ae(e,t=!1){return se(e)}", + componentName: "He", + nextComponentName: "Ue", + controlsName: "it", + nextControlsName: "at", + advancedStateBefore: "D=(0,pt.useRef)(null),O=i===`advanced`,k=", + hooksName: "Ke", + fastPredicate: "at", + standardPredicate: "ot", + fastIcon: "$e", + standardIcon: "nt", + sliderComponent: "be", + runtimeMarkers: [ + "Ke=t(n(),1)", + "function at(e){let{iconKind:t}=e;return t===`fast`}", + "function ot(e){let{value:t}=e;return t==null}", + "(0,Q.jsx)($e,", + "(0,Q.jsx)(nt,", + "(0,Z.jsx)(be,{active:", + ], + }, +]; + +function sourceMarkers(profile) { + return [ + profile.catalogBefore, + `function ${profile.componentName}(e){`, + `function ${profile.nextComponentName}(e){`, + `function ${profile.controlsName}(e){`, + `function ${profile.nextControlsName}(e){`, + profile.advancedStateBefore, + ...profile.runtimeMarkers, + ]; +} + +export function selectSelectorCompatibilityProfile(source) { + const matches = selectorCompatibilityProfiles.filter((profile) => + sourceMarkers(profile).every((marker) => source.includes(marker)), + ); + if (matches.length !== 1) { + throw new Error( + `Expected one selector compatibility profile, found ${matches.length}.`, + ); + } + return matches[0]; +} + +export function adaptCompactSelector(template, profile) { + const replacements = [ + ["function Ve(e){", `function ${profile.componentName}(e){`], + ["(0,Ge.", `(0,${profile.hooksName}.`], + ["l?.find(at)", `l?.find(${profile.standardPredicate})`], + ["l?.find(it)", `l?.find(${profile.fastPredicate})`], + ["(0,Z.jsx)(Qe,", `(0,Z.jsx)(${profile.fastIcon},`], + ["(0,Z.jsx)(tt,", `(0,Z.jsx)(${profile.standardIcon},`], + ["(0,Z.jsx)(ye,", `(0,Z.jsx)(${profile.sliderComponent},`], + ]; + + let selector = template; + for (const [before, after] of replacements) { + if (!selector.includes(before)) { + throw new Error(`Selector template marker not found: ${before}`); + } + selector = selector.replaceAll(before, after); + } + + return selector; +} diff --git a/tools/selector-compatibility.test.mjs b/tools/selector-compatibility.test.mjs new file mode 100644 index 0000000..490d1bd --- /dev/null +++ b/tools/selector-compatibility.test.mjs @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { + adaptCompactSelector, + selectSelectorCompatibilityProfile, + selectorCompatibilityProfiles, +} from "./selector-compatibility.mjs"; + +const template = readFileSync( + new URL("./selector-v2.js.txt", import.meta.url), + "utf8", +).replace(/^\uFEFF/u, ""); + +function sourceFor(profile) { + return [ + profile.catalogBefore, + `function ${profile.componentName}(e){}`, + `function ${profile.nextComponentName}(e){}`, + `function ${profile.controlsName}(e){}`, + `function ${profile.nextControlsName}(e){}`, + profile.advancedStateBefore, + ...profile.runtimeMarkers, + ].join(";"); +} + +test("selects legacy and current profiles only from their complete fingerprints", () => { + for (const profile of selectorCompatibilityProfiles) { + assert.equal(selectSelectorCompatibilityProfile(sourceFor(profile)), profile); + } +}); + +test("rejects unknown and ambiguous bundle fingerprints", () => { + assert.throws( + () => selectSelectorCompatibilityProfile("function Ve(e){}"), + /found 0/u, + ); + assert.throws( + () => + selectSelectorCompatibilityProfile( + selectorCompatibilityProfiles.map(sourceFor).join(";"), + ), + /found 2/u, + ); +}); + +test("rejects symbol-only drift in every runtime marker", () => { + for (const profile of selectorCompatibilityProfiles) { + for (const runtimeMarker of profile.runtimeMarkers) { + const source = sourceFor(profile).replace(runtimeMarker, "renamed-symbol"); + assert.throws( + () => selectSelectorCompatibilityProfile(source), + /found 0/u, + `${profile.name} should reject a changed marker: ${runtimeMarker}`, + ); + } + } +}); + +test("keeps the legacy selector symbols for the legacy profile", () => { + const selector = adaptCompactSelector(template, selectorCompatibilityProfiles[0]); + + assert.match(selector, /function Ve\(e\)\{/u); + assert.match(selector, /\(0,Z\.jsx\)\(Qe,/u); + assert.match(selector, /\(0,Z\.jsx\)\(tt,/u); + assert.match(selector, /\(0,Z\.jsx\)\(ye,/u); +}); + +test("maps Codex 26.707 icons and slider to renderable component symbols", () => { + const selector = adaptCompactSelector(template, selectorCompatibilityProfiles[1]); + + assert.match(selector, /function He\(e\)\{/u); + assert.match(selector, /\(0,Ke\.useState\)/u); + assert.match(selector, /l\?\.find\(at\)/u); + assert.match(selector, /l\?\.find\(ot\)/u); + assert.match(selector, /\(0,Z\.jsx\)\(\$e,/u); + assert.match(selector, /\(0,Z\.jsx\)\(nt,/u); + assert.match(selector, /\(0,Z\.jsx\)\(be,/u); + assert.doesNotMatch(selector, /\(0,Z\.jsx\)\((?:Qe|tt|ye),/u); +}); + +test("fails closed when the selector template no longer matches", () => { + assert.throws( + () => adaptCompactSelector("function changed(){}", selectorCompatibilityProfiles[1]), + /Selector template marker not found/u, + ); +});