codex-web-local — Project Specification
Overview
codex-web-local is a lightweight, browser-based web UI for OpenAI Codex. It mirrors the Codex Desktop experience and runs on top of the Codex app-server, allowing remote access to a local Codex instance from any browser.
Architecture
Key Architectural Decisions
- No Pinia / Vuex: All state lives in a single composable (
useDesktopState). Reactive refs + computed properties manage thread, message, model, and UI state.
- No WebSocket: Real-time communication uses SSE (
EventSource) on /codex-api/events for server-to-client notifications, and standard HTTP POST for client-to-server RPC.
- Single child process: The Node server spawns exactly one
codex app-server child process and multiplexes all RPC calls through it via stdin/stdout.
- Shared bridge state: A global singleton (
AppServerProcess + MethodCatalog) survives Vite HMR reloads during development.
Tech Stack
| Layer |
Technology |
Version |
| Frontend framework |
Vue 3 (Composition API, <script setup>) |
^3.5 |
| Routing |
Vue Router 4 |
^4.6 |
| Styling |
Tailwind CSS 4 (via @tailwindcss/vite) |
^4.1 |
| Build tool |
Vite 6 |
^6.1 |
| CLI build |
tsup 8 |
^8.4 |
| Type checking |
TypeScript 5, vue-tsc 2 |
^5.7 / ^2.2 |
| Server |
Express 5 |
^5.1 |
| CLI framework |
Commander 13 |
^13.1 |
| Runtime |
Node.js >= 18 |
— |
Project Structure
Features
Implemented
| Feature |
Description |
| Thread management |
List, create, archive, select threads; resume inactive threads on demand |
| Chat conversation |
Send messages, view full conversation history with user/assistant/system roles |
| Real-time streaming |
SSE-based live updates for agent messages, reasoning text, and turn lifecycle |
| Model selection |
Dropdown to choose from available models (model/list RPC) |
| Reasoning effort |
Configurable reasoning effort level (none → xhigh) |
| Turn interrupt |
Stop in-progress agent turns |
| Server request handling |
Approve/reject server-initiated requests (command approvals, file changes, tool calls) |
| Project grouping |
Threads organized by working directory (project) |
| Project customization |
Rename, reorder, remove projects (persisted to localStorage) |
| Unread indicators |
Track read/unread state per thread |
| Auto-refresh |
Optional 4-second polling with visual countdown |
| Collapsible sidebar |
Resizable (260–620px), toggle with Ctrl/Cmd+B |
| Scroll state persistence |
Remember scroll position per thread across navigation |
| Password auth |
Optional password protection with auto-generated passwords in production |
| New thread creation |
"Let's build" hero view with folder selector |
| Live overlay |
Reasoning text, activity labels, and error messages during agent work |
| Turn duration display |
"Worked for Xm Ys" summary after turn completion |
Not Yet Implemented
Based on the app-server protocol (documentation/APP_SERVER_DOCUMENTATION.md), the following capabilities are available in the protocol but not yet surfaced in the UI:
| Feature |
Relevant RPC Methods |
| Thread forking |
thread/fork |
| Thread rollback |
thread/rollback |
| Thread naming |
thread/name/set |
| Thread unarchiving |
thread/unarchive |
| Context compaction |
thread/compact/start |
| Code review |
review/start |
| Skills management |
skills/list, skills/remote/read, skills/remote/write, skills/config/write |
| Apps management |
app/list |
| MCP server status |
mcpServerStatus/list, config/mcpServer/reload |
| Account management |
account/login/start, account/logout, account/read, account/rateLimits/read |
| Configuration UI |
config/read, config/value/write, config/batchWrite, configRequirements/read |
| Command execution |
command/exec |
| Git diff view |
gitDiffToRemote |
| Fuzzy file search |
fuzzyFileSearch, session-based search |
| Feedback upload |
feedback/upload |
| Collaboration modes |
collaborationMode/list |
| Experimental features |
experimentalFeature/list |
| Turn diff view |
turn/diff/updated notification |
| Turn plan view |
turn/plan/updated notification |
| Token usage display |
thread/tokenUsage/updated notification |
| Command output streaming |
item/commandExecution/outputDelta notification |
| File change output |
item/fileChange/outputDelta notification |
| MCP tool call progress |
item/mcpToolCall/progress notification |
| Terminal interaction |
item/commandExecution/terminalInteraction notification |
Planned: Thread Forking
Forking creates a new thread from an existing thread so users can branch the conversation without mutating the original.
UX Requirements
- Add a
Fork thread action in thread row controls and thread header controls.
- On success, navigate to the new forked thread immediately.
- Show lineage metadata in the forked thread header:
Forked from: <source thread title or id>
- Preserve lineage information in thread list tooltips/details where available.
RPC Contract
- Method:
thread/fork
- Primary input:
threadId of the source thread (preferred over path)
- Optional overrides:
cwd, model, approvalPolicy, sandbox, baseInstructions, developerInstructions
- Expected response:
ThreadForkResponse with a new thread.id
State + Routing Behavior
- Insert the new thread into
sourceGroups / projectGroups immediately after fork returns.
- Set
selectedThreadId to the new forked thread id.
- Route to
/thread/:threadId for the new thread.
- Keep original thread unchanged and still selectable.
Acceptance Criteria
- User can fork from any existing thread.
- Forked thread has a different id from the source thread.
- Source thread history remains intact.
- UI displays the fork origin for the selected forked thread.
Communication Protocol
HTTP Endpoints (Bridge)
| Method |
Path |
Purpose |
| POST |
/codex-api/rpc |
JSON-RPC proxy — forwards { method, params } to app-server |
| POST |
/codex-api/server-requests/respond |
Reply to server-initiated requests |
| GET |
/codex-api/server-requests/pending |
List pending server requests |
| GET |
/codex-api/meta/methods |
Discover available RPC methods |
| GET |
/codex-api/meta/notifications |
Discover available notification types |
| GET |
/codex-api/events |
SSE stream for real-time notifications |
Bridge → App-Server
Communication uses newline-delimited JSON-RPC 2.0 over stdin/stdout of the codex app-server child process. The bridge:
- Receives HTTP requests from the frontend
- Translates them into JSON-RPC calls on stdin
- Reads JSON-RPC responses from stdout
- Forwards server-initiated requests to the frontend via SSE
- Routes client responses back to the app-server
RPC Methods Used by the Frontend
| Method |
Purpose |
initialize |
Handshake with app-server (automatic) |
thread/list |
Fetch all non-archived threads |
thread/read |
Fetch thread detail with turns and items |
thread/start |
Create a new thread |
thread/resume |
Resume an inactive thread |
thread/archive |
Archive a thread |
turn/start |
Send a user message and start agent turn |
turn/interrupt |
Interrupt an in-progress turn |
model/list |
List available models |
config/read |
Read current model and reasoning effort |
Notifications Handled by the Frontend
| Notification |
Action |
turn/started |
Mark thread in-progress, show "Thinking" |
turn/completed |
Mark complete, show duration summary |
item/started |
Update activity label (Thinking / Writing) |
item/completed |
Finalize agent message or reasoning |
item/agentMessage/delta |
Append to live agent message text |
item/reasoning/summaryTextDelta |
Append to live reasoning overlay |
item/reasoning/summaryPartAdded |
Insert reasoning section break |
server/request |
Show pending approval in UI |
server/request/resolved |
Remove resolved request from UI |
error |
Display error notification |
thread/name/updated |
(Queued for thread list refresh) |
State Management
All frontend state is managed by useDesktopState() — a single Vue composable that provides:
Reactive State
projectGroups / sourceGroups — thread list grouped by project
selectedThreadId / selectedThread — currently active thread
persistedMessagesByThreadId — loaded messages from server
liveAgentMessagesByThreadId — streaming agent messages
liveReasoningTextByThreadId — streaming reasoning text
inProgressById — per-thread turn-in-progress flags
availableModelIds / selectedModelId / selectedReasoningEffort
pendingServerRequestsByThreadId — approval requests
turnSummaryByThreadId / turnActivityByThreadId / turnErrorByThreadId
- Loading/sending/interrupting boolean flags
Persistence (localStorage)
| Key |
Data |
codex-web-local.thread-read-state.v1 |
Per-thread read timestamps |
codex-web-local.thread-scroll-state.v1 |
Per-thread scroll positions |
codex-web-local.selected-thread-id.v1 |
Last selected thread |
codex-web-local.project-order.v1 |
Custom project ordering |
codex-web-local.project-display-name.v1 |
Custom project names |
codex-web-local.auto-refresh-enabled.v1 |
Auto-refresh preference |
codex-web-local.sidebar-collapsed.v1 |
Sidebar collapse state |
Event Processing Pipeline
- SSE events arrive via
EventSource on /codex-api/events
- Each event is passed to
applyRealtimeUpdates() for immediate UI effects (activity labels, live text, in-progress flags)
- Events are also passed to
queueEventDrivenSync() which debounces (220ms) a full data refresh
- The debounced
syncFromNotifications() calls loadThreads() and loadMessages() to reconcile server state
Routing
| Route |
Path |
Behavior |
| Home |
/ |
New thread creation view with folder selector |
| Thread |
/thread/:threadId |
Thread conversation view |
| Redirect |
/new-thread |
Redirects to Home |
| Fallback |
/:pathMatch(.*)* |
Redirects to Home |
Bidirectional sync between selectedThreadId state and URL is handled via Vue watchers in App.vue.
Development
Prerequisites
- Node.js >= 18
codex CLI installed and in PATH
Scripts
| Command |
Description |
npm run dev |
Start Vite dev server (port 5173) |
npm run build |
Type-check + build frontend + build CLI |
npm run build:frontend |
vue-tsc --noEmit && vite build |
npm run build:cli |
tsup (builds CLI to dist-cli/) |
npm run preview |
Preview production build |
Dev Mode
npm run dev starts a Vite dev server that includes the codex bridge as middleware. The bridge spawns codex app-server as a child process. The frontend calls /codex-api/* endpoints on the same origin.
Production Mode
The CLI starts an Express server that serves the built frontend from dist/ and uses the same bridge middleware. Password authentication is enabled by default with an auto-generated password printed to the console.
Auth (Production)
- Default: auto-generated password printed to console on startup
- Login: POST
/auth/login with { password } body
- Session: HttpOnly cookie
codex_web_local_token
- Uses constant-time comparison to prevent timing attacks
Design Principles
- Minimal dependencies: Only essential packages — no state management library, no component library, no CSS framework beyond Tailwind
- Protocol-first: The UI is designed around the Codex app-server protocol; all features map directly to RPC methods and notifications
- Offline-resilient: localStorage persistence ensures the UI recovers gracefully from disconnections
- Reference equality optimization: Extensive use of identity checks and shallow merging to minimize unnecessary Vue re-renders
- Single composable pattern: All state and logic in one place for discoverability, at the cost of file size (~2000 LOC)