with one click
codex-app-parity
// Use when implementing or changing user-visible behavior/UI in this repository and parity with the installed Codex desktop app must be validated before coding.
// Use when implementing or changing user-visible behavior/UI in this repository and parity with the installed Codex desktop app must be validated before coding.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | codex-app-parity |
| description | Use when implementing or changing user-visible behavior/UI in this repository and parity with the installed Codex desktop app must be validated before coding. |
Use this skill for any feature work or user-visible behavior/UI change in this repository. Do not use it for purely internal refactors that do not affect behavior.
Ensure behavior is implemented with Codex.app as the source of truth, then verified with headless Playwright and screenshots.
For user-visible Directory, Skills, Apps, Plugins, MCP, or Composio changes in this repo:
tests.md with manual verification steps, including light and dark theme checks.llm-wiki/raw/... source and corresponding llm-wiki/wiki/... concept page.whatToTest.md as a short pending-only checklist; remove items that were actually executed successfully.For every new feature and every behavior/UI change, treat the installed desktop app as the source of truth:
/Applications/Codex.app/Applications/Codex.app/Contents/Resources/app.asarDo not implement first and compare later. Compare first, then implement.
Extract the app bundle once (reuse if already extracted):
mkdir -p /tmp/codex-app-extracted
npx asar extract "/Applications/Codex.app/Contents/Resources/app.asar" /tmp/codex-app-extracted
| Directory | Contents |
|---|---|
/tmp/codex-app-extracted/webview/assets/ | Main frontend bundle (index-*.js) + locale files |
/tmp/codex-app-extracted/.vite/build/ | Electron main process (main.js, main-*.js, preload.js, worker.js) |
/tmp/codex-app-extracted/package.json | App metadata, version, entry point |
The main UI bundle is a single large minified JS file at webview/assets/index-*.js. Use Python to search since grep -o with large repeat counts fails on macOS:
python3 -c "
with open('/tmp/codex-app-extracted/webview/assets/index-<hash>.js', 'r') as f:
content = f.read()
idx = content.find('YOUR_SEARCH_TERM')
if idx >= 0:
print(content[max(0, idx-200):idx+500])
"
i18n keys: Search locale files (webview/assets/zh-TW-*.js, webview/assets/en-*.js, etc.) for human-readable labels. Keys follow the pattern component.feature.property (e.g., composer.dictation.tooltip).
Component functions: Minified React components follow patterns like function X4n({prop1:t,prop2:e,...}). Search for the feature's i18n key to find the component that renders it.
API calls and endpoints: Search main process files (.vite/build/main-*.js) for endpoint URLs, auth handling, and IPC channels. Key patterns:
prodApiBaseUrl → production API base (e.g., https://chatgpt.com/backend-api)devApiBaseUrl → dev API base (e.g., http://localhost:8000/api)fetch-request / fetch-response → IPC-proxied HTTP calls from renderer to main processIcon names: Search for icon imports like audiowave-dark.svg, book-open-dark.svg. Icon mapping is in the main bundle around the Hwn=Object.assign({ pattern.
Keyboard shortcuts: Search for CmdOrCtrl+, Cmd+, keydown, keyCode, or specific key names.
For every feature UI or user-visible fix, inspect the live Codex.app frontend over Chrome DevTools Protocol before implementing. Bundle search is still useful, but it is not enough by itself when a visual/interaction surface exists.
output/playwright/ with a task-specific filename.If the exact UI cannot be reached, capture the closest relevant Codex.app surface and state the gap.
For every feature UI or user-visible fix, compare Codex.app against the web UI before and after implementation.
Required artifacts:
codex-reference: Codex.app CDP screenshot of the target feature UI or closest equivalent.web-before: current web UI screenshot before code changes, showing the existing gap or missing behavior.web-after: web UI screenshot after implementation, showing the proposed parity result.Required comparison notes:
codex-reference vs web-before.web-after against codex-reference.fixed: matched or acceptably alignedintentional deviation: documented reasonneeds follow-up: not fixed in this taskweb-after reveals a fixable mismatch in layout, copy, visibility, interaction, or state handling, do another implementation iteration and capture a new web-after screenshot.Use task-specific screenshot names under output/playwright/, for example:
output/playwright/<task>-codex-reference.pngoutput/playwright/<task>-web-before.pngoutput/playwright/<task>-web-after.pngBefore launching anything new, first check whether a Codex.app CDP endpoint is already available and reusable. Avoid creating additional Codex instances when an existing CDP-enabled instance already exposes a usable app://-/index.html page target.
Preferred reuse check:
for port in 3434 3435 9222 9223; do
if curl -fsS "http://127.0.0.1:$port/json/list" >/tmp/codex-cdp-list.json 2>/dev/null; then
python3 - <<'PY'
import json
from pathlib import Path
rows = json.loads(Path('/tmp/codex-cdp-list.json').read_text())
page = next((row for row in rows if row.get('type') == 'page' and str(row.get('url', '')).startswith('app://-/index.html')), None)
if page:
print(page['webSocketDebuggerUrl'])
PY
if [ -s /tmp/codex-cdp-list.json ]; then
echo "Reusing CDP on port $port"
break
fi
fi
done
If a usable target is found, reuse it and do not launch another Codex instance.
Only if no reusable CDP target exists, prefer running a separate Codex.app debug instance so the user's normal Codex session is not interrupted and the CDP target can stay alive after tests.
In this repo, prefer the maintained helper script first:
bash /Users/igor/Git-projects/codex-web-local/scripts/run-codex-unpacked-debug.sh
The script:
app.asar under external Electronelectron@41.2.0Use --verify-only when you only need to confirm whether the current endpoints are still alive.
Use a fresh app instance with its own profile directory:
CDP_PORT=3434
while lsof -i :"$CDP_PORT" >/dev/null 2>&1; do
CDP_PORT=$((CDP_PORT + 1))
done
CDP_PROFILE_DIR="/tmp/codex-cdp-$CDP_PORT"
mkdir -p "$CDP_PROFILE_DIR"
open -na "Codex" --args \
--remote-debugging-port="$CDP_PORT" \
--user-data-dir="$CDP_PROFILE_DIR"
until curl -fsS "http://127.0.0.1:$CDP_PORT/json/list" >/tmp/codex-cdp-list.json; do
sleep 1
done
If Codex.app is already running without CDP, open -a "Codex" --args --remote-debugging-port=3434 usually does not enable CDP because Electron reuses the existing app instance. Restart Codex.app with the port enabled.
Fallback only when a separate instance cannot be used: restart all Codex.app processes and launch the binary with nohup.
pkill -TERM -f "/Applications/Codex.app" 2>/dev/null || true
sleep 2
if pgrep -f "/Applications/Codex.app" >/dev/null 2>&1; then
pkill -KILL -f "/Applications/Codex.app" 2>/dev/null || true
sleep 1
fi
nohup "/Applications/Codex.app/Contents/MacOS/Codex" \
--remote-debugging-port="$CDP_PORT" \
>/tmp/codex-cdp.log 2>&1 &
Pick the page target from /json/list where type == "page" and url starts with app://-/index.html. For Playwright screenshots, prefer chromium.connectOverCDP("http://127.0.0.1:$CDP_PORT"), select that page, wait briefly for React/app-server hydration, and save the screenshot.
Important caveats:
3434.open -na "Codex" is required for a true separate instance; open -a "Codex" reuses an existing app process and often does not enable CDP flags.--user-data-dir for the debug instance to avoid profile lock contention and cross-session side effects.nohup or a long-lived shell; short one-shot launches can drop the CDP listener when the shell exits.browser.close() when the Codex.app session should remain open.browser.disconnect() is unavailable for CDP sessions, connect, inspect/capture, and exit the test process without close(); this preserves the running Codex.app instance./Applications/Codex.app processes is more reliable than only pkill -x Codex.open -na "Codex" or starting a fresh debug profile, probe common local ports and reuse an existing endpoint when it already serves a valid app://-/index.html page target./tmp/codex-cdp-*.bash /Users/igor/Git-projects/codex-web-local/scripts/run-codex-unpacked-debug.sh/Applications/Codex.app/Contents/MacOS/Codex, because that preserves the generic Electron-style process/icon behavior some parity workflows expect while still launching the installed Codex app.asar.pnpm dlx electron can break startup because Codex.app expects Electron-41-era native resources; the current helper pins the runtime to electron@41.2.0./Applications/Codex.app/Contents/Resources/native/sparkle.nodepnpm dlx Electron bundle before launch.--remote-debugging-port--inspectjson/listcurl /json/list; also confirm a real WebSocket connect to the returned webSocketDebuggerUrl.Uu HTTP client class that sends fetch-request IPC messages to the main process. The main process class tle handles these, adds auth tokens, and uses electron.net.fetch to make actual HTTP calls.getAuthStatus RPC method (ChatGPT backend auth).codex app-server child process communicating via JSON-RPC over stdin/stdout. Our bridge middleware proxies RPC calls to it.R7 = prodApiBaseUrl (https://chatgpt.com/backend-api), I7 = devApiBaseUrl (http://localhost:8000/api), C7 = originator (Codex Desktop).app.asar (extract and search built assets as needed).For feature tasks, include:
Codex.app analysis: what was inspected (files/areas/patterns).Codex.app CDP evidence: target URL/title, screenshot path, and visual behavior confirmed.Before/after comparison: screenshot paths, gap list, and fix iteration result.Parity result: matched items and any explicit deviations.Fallback note only if Codex.app could not be inspected or had no equivalent.If Codex.app cannot be inspected (missing app, extraction/search failure) or has no equivalent pattern:
After each feature implementation session that uses this skill:
## Findings: section documenting any newly discovered Codex.app internals (state keys, API endpoints, component patterns, auth flows, etc.).electron-saved-workspace-roots (order source of truth)electron-workspace-root-labelsactive-workspace-roots~/.codex/.codex-global-state.json~/Library/Application Support/Codex/.codex-global-state.jsonitem/commandExecution/requestApproval and item/fileChange/requestApproval, but the generated event typings also include newer approval event names such as exec_approval_request and apply_patch_approval_request.turn_id, call_id, grant_root) or camelCase fields (conversationId, callId, grantRoot) instead of the older threadId / itemId request metadata.codex-cli 0.118.0 also includes JSON-RPC server requests for mcpServer/elicitation/request and item/permissions/requestApproval. The checked-in schema snapshot in this repo can lag behind the installed CLI, so for approval/request UI bugs it is worth generating fresh schemas locally via codex app-server generate-json-schema --out <dir> before deciding the app-server contract.false or the first enum option changes the meaning of the user’s response.url elicitation mode, treat the server-provided URL as untrusted input and only render clickable links for safe schemes such as http: and https:.~/.codex/.codex-global-state.json) under key thread-pinned-ids.GET /codex-api/thread-pins -> { data: { threadIds: string[] } }PUT /codex-api/thread-pins with body { threadIds: string[] }thread-pinned-ids via the bridge endpoint.localStorage persistence is used for pinned-thread state.openai/codex app-server protocol exposes per-thread context telemetry via thread/tokenUsage/updated with:
tokenUsage.totaltokenUsage.lasttokenUsage.modelContextWindowlast_token_usage, not cumulative total_token_usage.BASELINE_TOKENS = 12000 before computing remaining context percentage, so early turns do not look artificially "used".X% leftY usedZ windowcodex-rs/app-server-protocol/schema/typescript/v2/ThreadTokenUsage*.tscodex-rs/tui/src/chatwidget.rscodex-rs/protocol/src/protocol.rsopenai/codex confirm that:
turn/diff/updated carries { threadId, turnId, diff } as the latest aggregated unified diff for the whole turn.fileChange thread items carry { id, changes, status }.changes entry is { path, kind, diff }.fileChange thread items over reconstructing state from deltas:
kind maps to add/delete/update.update may include move_path for rename/move handling.item/completed is the authoritative final state for whether edits actually applied.turn/diff/updated as a supplemental aggregated diff source, not the only source:
fileChange items are the more reliable source for per-file operation labels.codex.composer.searchBranches), which aligns with searchable branch selection controls in header/composer surfaces.jumpToLatest() immediately and again over the next animation frames so the viewport stays pinned after the keyboard resizethread/fork support in v2, so UI work should call the RPC directly instead of simulating a new thread locally.ThreadForkParams.path is documented as an unstable rollout-path override, while threadId remains the preferred stable entry point for IDE clients.thread/fork for the source threadthread/rollback on the new thread for trailing turns after the chosen answername/title over preview; otherwise renamed forked threads will still look identical to the source thread in the header and sidebar.ThreadConversation.vue uses a custom Markdown block parser rather than a standard Markdown library.orderedList blocks.orderedList block needs the original marker value persisted and rendered via the HTML <ol start=\"...\"> attribute.composer.dictation.* — tooltip is "Hold to dictate", aria is "Dictate".M4n React hook handles recording state, audio capture, and transcription.navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 } }) → MediaRecorder → chunks → Blob → multipart POST./transcribe via the IPC fetch proxy. The main process (tle class) prepends the prodApiBaseUrl (https://chatgpt.com/backend-api) and attaches ChatGPT auth bearer tokens. Full URL: https://chatgpt.com/backend-api/transcribe.----codex-transcribe-<uuid>, fields: file (audio blob) and optional language. Body is base64-encoded and sent with X-Codex-Base64: 1 header.{ text: "transcribed text" }.audiowave-dark.svg / audiowave-light.svg (custom SVG, not from icon library)./codex-api/transcribe to the ChatGPT backend using auth tokens from the app-server getAuthStatus RPC. Frontend uses useDictation composable with MediaRecorder API.image({href,title,text}) emits <img src="...">), consistent with inline markdown image rendering in assistant/user text./Users/... as local files./codex-local-image?path=...) is required for parity-like rendering of absolute filesystem image paths in browser-delivered UI.sendFile must allow dot-directory segments (dotfiles: 'allow') or paths under ~/.codex/... return 404 despite existing files.ProseMirror-based), not single-line.enterBehavior):
enter submits by default.newline inserts a newline on Enter.cmdIfMultiline inserts newline when multiline, otherwise submits.Shift-Enter inserts newline.Alt-Enter inserts newline.Mod-Enter submits.@ Mentions (2026-03-05)@ with pattern /(^|\s)(@[^\s@]*)$/, so mentions activate at word boundaries and stop on whitespace or a second @.mention-ui node with attrs { label, path, fsPath }, rendered with data attributes at-mention-label, at-mention-path, and at-mention-fs-path.Escape closes mention UI.Enter and Tab commit the highlighted mention.Ask Codex anything, @ to add files, / for commands.sidebarElectron.renameThreadsidebarElectron.renameThreadDialogTitlesidebarElectron.renameThreadDialogSubtitlesidebarElectron.renameThreadDialogPlaceholdersidebarElectron.renameThreadDialogSavesidebarElectron.renameThreadDialogCancelsidebarElectron.renameThreadDialogAriaLabelthread/name/set with params { threadId, name } (not threadName).thread/name/updated realtime notification carries { threadId, threadName }, so parity implementations should handle both request/response naming differences (name on write, threadName on notification)./Applications/Codex.app/Contents/Resources/app.asar was not present, so Codex.app-first inspection could not run.link[rel="apple-touch-icon"] in index.html and the PNG entries in public/manifest.webmanifest; updating only the manifest is not enough for iPhone-style add-to-home-screen flows.Image + drawImage + canvas.toDataURL()), which preserves the real SVG bounds and avoids dark corner contamination from the page background.src/main.tsmanifest.webmanifestpublic/icons//Applications/Codex.app was unavailable, so PWA packaging should follow the fallback path and avoid speculative UX changes./codex-api/* and local file proxy endpoints/turn/start.collaborationMode unless the client advertises initialize.capabilities.experimentalApi = true.turn/start.collaborationMode.settings.model must be a non-empty concrete model id. Sending "" can leave a plan-mode thread stuck or fail without rendering plan output.collaborationMode/list returns Plan with mode: "plan" and reasoning_effort: "medium", but model is null, so the client must source the actual model from current config or available models before starting the turn.account/rateLimits/read and pushes live updates with account/rateLimits/updated.rateLimitsByLimitId.codex; if absent, fall back to the legacy top-level rateLimits bucket.limitId, limitName, planType, primary, secondary, creditsusedPercent, windowDurationMins, resetsAthasCredits, unlimited, balanceprimary/secondary windows without forcing a full account panel.windowDurationMins is 10080 (or the nearest longer weekly-like window) and formatting its resetsAt timestamp into a calendar date for the tooltip.title attribute are effectively invisible. Weekly refresh information needs to be rendered as visible text in the composer quota badge, not only in hover-only tooltip content.Mar 28 / 3月28日) and appended on the same line as the quota summary instead of using a separate explanatory label.App.vue) by combining:
document.visibilitychange to record background entrywindow.pageshow to catch BFCache restoreswindow.focus as a final foreground fallback<768px mobile breakpoint avoids forcing reloads on tablet/desktop layouts during tab focus changes.electron-saved-workspace-rootselectron-workspace-root-labelsactive-workspace-rootsorderGroupsByProjectOrder(...), which materializes { projectName, threads: [] } when a persisted project name has no matching incoming thread group./Applications/Codex.app is unavailable.ThreadConversation.vue.- item, 1. item, **bold**).# ... ######)> quote)- [ ], - [x])---, ***, ___) inside fenced code can be misparsed as a real image block./Applications/Codex.app is unavailable, so plan-mode behavior was aligned to the shipped app-server protocol instead of renderer-bundle parity.TurnStartParams.collaborationModeModeKind = "plan" | "default"turn/plan/updateditem/plan/deltaThreadItem.type = "plan"item/tool/requestUserInputcodex-web-local.collaboration-mode.v1collaborationMode: { mode: "plan", settings: { model, reasoning_effort, developer_instructions: null } } only when plan mode is selectedcollaborationMode entirely for default mode to disable plan mode cleanlycollaborationMode/list can be treated as advisory rather than authoritative for web fallback:
default / planDefault and Plan options available so the feature remains usable against servers that lag the preset-list endpointrequest_user_input questions need broader handling than the original approval-like UI:
options is null or emptyisSecret is true16px commonly triggers viewport auto-zoom.text-sm (14px computed on mobile), which is sufficient to trigger that browser behavior.16px on mobile widths, instead of disabling pinch zoom globally.ThreadConversation.vue, matching :root.dark overrides must also be added in style.css; otherwise the new nodes inherit light-theme slate colors and become low-contrast in dark mode..message-heading, .message-heading-h6).message-bold-text, .message-italic-text, .message-strikethrough-text).message-blockquote, .message-list, .message-task-checkbox).message-code-block, .message-code-language, .message-divider)zinc-100/zinc-200) and move structural surfaces to darker zinc backgrounds, so markdown blocks remain legible without deviating from the current dark theme.index.html.authMiddleware.ts, so branding changes need to update both places to stay consistent.right-hover slot of SidebarMenuRow; if the row stops being hovered and no explicit open-state override is applied, that slot can collapse and make the menu hard to interact with.openThreadMenuId in the shared dismiss-listener condition. Otherwise thread menus fall back to ad-hoc closing logic and can behave inconsistently.forceRightHover mode while its menu is openthread/delete method in v2 docs/schemas; thread removal from active list is handled through thread/archive.thread/archive.build-badge, WT, and worktree UI markers; no explicit build badge or worktree/version label found in renderer bundle./Applications/Codex.app may be absent while /tmp/codex-app-extracted still exists as an empty directory from a prior session.ThreadConversation.vue and shared dark-theme overrides in style.css; removing only the button markup leaves dead hover/dark styles behind..message-action* selectors together in the same change.git worktree add --detach <worktreePath> <startRef>/tmp/codex-app-extracted/.vite/build/worker.js (Xq(...) flow)26.417.41555 renderer bundle contains first-class automation UI strings and routes:
inbox.automations.*/automations?automationId=<id>Automation unavailablethreadHeader.archiveConfirmHeartbeatTitlethread.fileCommandMenu.searchFilesthread.fileCommandMenu.filesGroupcomposer.backgroundTerminals.runningLabelcomposer.backgroundTerminals.stopcomposer.backgroundTerminals.stopTooltipthread/backgroundTerminals/clean near the composer/header and disable while pending.app/list/updated by refreshing cached app list data.app/listmcpServerStatus/listconfig/mcpServer/reloadmcpServer/oauth/loginthread/compact/startfeedback/uploadfuzzyFileSearchAdd automation… and Edit automation…sidebarTaskRow.heartbeatAutomation.nextRunArchive chat and remove automation?<heartbeat><automation_id><current_time_iso><instructions>$CODEX_HOME/automations/<id>/automation.toml; heartbeat records include kind = "heartbeat" and target_thread_id.26.417.41555 ships node-pty@1.1.0 in /tmp/codex-app-extracted/package.json.threadPage.toggleTerminal -> Toggle terminalterminal.bottomPanel.new -> New terminalterminal.bottomPanel.close -> Closecodex.command.toggleTerminal -> Toggle terminaltoggleTerminal: CmdOrCtrl+J./tmp/codex-app-extracted/.vite/build/main-CUDSf52Z.js:
node-pty.spawn16 * 1024 bytesTERM=xterm-256color{ cwd, shell, buffer, truncated }terminal-data, terminal-init-log, terminal-attached, terminal-exit, and terminal-error/codex-api/ws and HTTP endpoints instead of Electron IPC, but preserve the same event names and snapshot shape where practical.v-if / v-else pair; otherwise the composer can disappear when the terminal is open.en_US.UTF-8 on macOS) and remove TERMINFO / TERMINFO_DIRS to avoid visible shell startup warnings.node-pty spawn-helper at runtime when pnpm ignores native package build scripts.plugins-page-*.jsplugins-cards-grid-*.jsplugins-settings-*.jscodex app-server generate-json-schema exposes slash-style methods for this surface:
plugin/list, plugin/read, plugin/install, plugin/uninstallapp/listmcpServerStatus/listconfig/mcpServer/reloadid, name, installed, enabled, installPolicy, authPolicy, source, and an optional interface.summary, apps, skills, and mcpServers. Install responses include authPolicy and appsNeedingAuth./codex-api/meta/methods and degrade gracefully when older Codex CLI versions do not expose them.Authenticate action when an MCP server authStatus is notLoggedIn.mcpServer/oauth/login with { name }; the response is { authorizationUrl }.open-in-browser bridge.mcpServer/oauth/login/completed notifications carry { name, success, error? }; after success, invalidate/refetch MCP status.mcpServers from plugin/read should be cross-referenced with mcpServerStatus/list and display auth state:
oAuth -> logged inbearerToken -> bearer tokennotLoggedIn -> login required + authenticate actionunsupported -> auth unsupported26.417.41555 renderer derives chat content into renderable turn entries such as visibleTurnEntries before rendering, rather than parsing every message directly in JSX on each stream tick.item/agentMessage/delta, item/plan/delta, reasoning deltas), while unchanged turn/item render output is protected by React memo-cache patterns.payload.output[].image_url, payload.result, payload.content[].image_url, payload.images[], and payload.replacement_history[].content[].image_url.thread/read, thread/resume, thread/fork, and thread/rollback should sanitize turns before returning JSON to the client./codex-local-image?path=....jsonl files written by Codex app-server remain unchanged unless a separate compaction tool is introduced.projectless-thread-cwd main-process handler before starting the conversation.~/Documents/Codex, with one date subdirectory per local date formatted as YYYY-MM-DD.-, capped at 80 characters, and falls back to new-chat.slug-2, slug-3, and so on for up to 100 attempts.cwd and outputDirectory, with workspaceRoot set to ~/Documents/Codex.No chats even if many project threads exist.thread/list, otherwise they disappear after the optimistic row is replaced by server state~/Documents/Codex/YYYY-MM-DD/<slug> should remain in the sidebar Chats section after title generation and thread-list refreshes.