Codex Manager is split into four layers:
frontend/: SolidJS UI for account operations, drag/drop, refresh, login, and settings.src/rpc.zig: authoritative backend state, persistence, OpenAI/Codex integration, and RPC dispatch.src/main.zig:webuiruntime startup and launch-surface policy.src/rpc_webui.zig: bridge glue that exposescm_rpcto the page and pushes completion events back over the websocket.
The backend is the source of truth.
- Frontend sends typed RPC intents.
- Backend validates input, mutates state, persists files, and regenerates bootstrap state.
- Frontend updates local UI from RPC responses and pushed refresh completions.
Managed state is stored in:
accounts.json- active account id
- managed accounts
- persisted auth payloads
bootstrap-state.json- UI preferences
- cached usage snapshots keyed by account id
- save timestamp
Whenever backend state changes, the backend also regenerates the live HTML/bootstrap payload used at startup.
managed_files_mutexguards read-modify-write operations over persisted state.- Network work runs outside the file mutex where practical.
- Usage refreshes are debounced per account.
- OAuth callback listening uses dedicated shared state with mutex + condition variable.
- The
webuiRPC dispatcher stays threaded, while UI work remains on the main thread.
CLI flags define ordered launch-surface preference:
--webview/-w: native webview--browser/-b: browser app window--web/-u: browser tab / printed URL flow
Default order when no flags are supplied:
webview -> browser -> web
RPC responses are direct JSON:
- success: plain JSON payload or
null - failure:
{ "error": "..." }
There is no nested { ok, value } envelope.
The frontend calls:
window.webuiRpc.cm_rpc(requestObject)
Requests are plain JSON objects with an op field and optional typed fields.
Success responses are direct JSON payloads. Error responses are:
{ "error": "message" }The webui bridge itself uses a websocket-backed runtime channel. Usage refresh completion is pushed back to the frontend over that channel instead of being polled.
Example:
{
"op": "invoke:refresh_account_usage",
"accountId": "acct-..."
}Supported request fields currently include:
themeapiKeyissuerclientIdredirectUrioauthStatecodeVerifierurltimeoutSecondsaccountIdtargetBuckettargetIndexswitchAwayFromMovedautoArchiveZeroQuotaautoUnarchiveNonZeroQuotaautoSwitchAwayFromArchivedautoRefreshActiveEnabledautoRefreshActiveIntervalSecusageRefreshDisplayMode
Input:
{ "op": "shell:open_url", "url": "https://example.com" }Output:
null
Input:
{ "op": "invoke:refresh_account_usage", "accountId": "acct-..." }Immediate response:
{
"accountId": "acct-...",
"credits": { "...": "credits payload" },
"email": "user@example.com",
"inFlight": true
}or, if refresh completed immediately:
{
"accountId": "acct-...",
"credits": { "...": "credits payload" },
"email": "user@example.com",
"inFlight": false
}When the backend refresh finishes asynchronously, it pushes the completion payload back through the websocket bridge.
Credits payload shape:
{
"available": 12.34,
"used": 1.0,
"total": null,
"currency": "USD",
"source": "wham_usage",
"mode": "balance",
"unit": "USD",
"planType": "free",
"isPaidPlan": false,
"hourlyRemainingPercent": null,
"weeklyRemainingPercent": null,
"hourlyRefreshAt": null,
"weeklyRefreshAt": null,
"status": "available",
"message": "...",
"checkedAt": 1772000000
}Input:
{ "op": "invoke:switch_account", "accountId": "acct-..." }Output: AccountsView
Input:
{
"op": "invoke:move_account",
"accountId": "acct-...",
"targetBucket": "active",
"targetIndex": 0,
"switchAwayFromMoved": true
}Output:
null
Input:
{ "op": "invoke:remove_account", "accountId": "acct-..." }Output: AccountsView
Input:
{ "op": "invoke:import_current_account" }Output: AccountsView
Input:
{ "op": "invoke:login_with_api_key", "apiKey": "sk-..." }Output: AccountsView
Input is partial. Example:
{
"op": "invoke:update_ui_preferences",
"theme": "dark",
"autoRefreshActiveEnabled": true,
"autoRefreshActiveIntervalSec": 300,
"usageRefreshDisplayMode": "remaining"
}Output:
null
Input:
{
"op": "invoke:start_oauth_callback_listener",
"timeoutSeconds": 180,
"issuer": "https://auth.openai.com",
"clientId": "app_...",
"redirectUri": "http://localhost:1455/auth/callback",
"oauthState": "...",
"codeVerifier": "..."
}Output:
true
Input:
{ "op": "invoke:poll_oauth_callback_listener" }Output is one of:
{ "status": "running" }{ "status": "idle" }{ "status": "ready", "account": { "id": "...", "email": "...", "state": "active" } }{ "status": "error", "error": "..." }Input:
{ "op": "invoke:cancel_oauth_callback_listener" }Output:
true
{
"accounts": [
{ "id": "acct-...", "email": "user@example.com", "state": "active" }
],
"activeAccountId": "acct-...",
"activeDiskAccountId": "acct-...",
"codexAuthExists": true,
"codexAuthPath": "/home/user/.codex/auth.json",
"storePath": "/home/user/.local/share/com.codex.manager/accounts.json"
}Account summaries only expose:
idemailstate
There is no persisted label, and there is no top-level accountId field on account summaries anymore.
- Unknown operations return an error object.
- Payload keys are camelCase.
- OAuth redirect/callback constants are intentionally aligned with Codex CLI.