Files
openclaw-android-assistant/PROJECT_SPEC.md
2026-02-25 14:12:34 +07:00

343 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# codex-web-local — Project Specification
## Overview
**codex-web-local** is a lightweight, browser-based web UI for [OpenAI Codex](https://github.com/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.
- **Author:** Pavel Voronin
- **License:** MIT
- **Repository:** https://github.com/pavel-voronin/codex-web-local
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Browser (Vue 3 SPA) │
│ ┌────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ App.vue │ │ Composables │ │ API Layer │ │
│ │ (Router) │──│ useDesktop │──│ codexGateway │ │
│ │ │ │ State │ │ codexRpcClient │ │
│ └────────────┘ └──────────────┘ └────────┬─────────┘ │
└─────────────────────────────────────────────┼───────────┘
│ HTTP/SSE
┌─────────────────────────────────────────────┼───────────┐
│ Node.js Server │ │
│ ┌──────────────────────────────────────────┼─────────┐ │
│ │ Express / Vite Middleware │ │ │
│ │ ┌───────────────────┐ ┌───────────────┴───────┐ │ │
│ │ │ Auth Middleware │ │ Codex Bridge │ │ │
│ │ │ (password, cookie) │ │ /codex-api/* │ │ │
│ │ └───────────────────┘ └───────────┬───────────┘ │ │
│ └─────────────────────────────────────┼─────────────┘ │
│ │ stdin/stdout │
│ ┌─────────────────────────────────────┼─────────────┐ │
│ │ codex app-server (child process) │ │ │
│ │ JSON-RPC over newline-delimited I/O │ │ │
│ └───────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
### 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
```
codex-web-local/
├── src/
│ ├── api/ # Backend communication layer
│ │ ├── codexGateway.ts # High-level API (threads, turns, models)
│ │ ├── codexRpcClient.ts # HTTP/SSE transport for /codex-api/*
│ │ ├── codexErrors.ts # Error normalization
│ │ ├── appServerDtos.ts # Raw DTO types from app-server
│ │ └── normalizers/v2.ts # DTO → UI type transformers
│ ├── components/
│ │ ├── content/ # Main content area
│ │ │ ├── ThreadConversation.vue # Message list with scroll management
│ │ │ ├── ThreadComposer.vue # Input + model/reasoning selectors
│ │ │ ├── ComposerDropdown.vue # Reusable dropdown (model, folder)
│ │ │ └── ContentHeader.vue # Page title bar
│ │ ├── sidebar/ # Left panel
│ │ │ ├── SidebarThreadTree.vue # Thread list grouped by project
│ │ │ └── SidebarThreadControls.vue # New thread, auto-refresh toggle
│ │ ├── layout/
│ │ │ └── DesktopLayout.vue # Sidebar + content split layout
│ │ └── icons/ # Tabler icon components
│ ├── composables/
│ │ └── useDesktopState.ts # Central state composable (~2000 LOC)
│ ├── server/ # Node.js server (production + dev)
│ │ ├── codexAppServerBridge.ts # Spawns/proxies codex app-server
│ │ ├── httpServer.ts # Express app for production
│ │ ├── authMiddleware.ts # Password-based auth
│ │ └── password.ts # Password generation + comparison
│ ├── cli/
│ │ └── index.ts # CLI entry point (Commander)
│ ├── types/
│ │ └── codex.ts # UI-layer TypeScript types
│ ├── router/
│ │ └── index.ts # Vue Router config
│ ├── App.vue # Root component
│ ├── main.ts # Vue app bootstrap
│ └── style.css # Global Tailwind styles
├── documentation/ # Codex app-server protocol docs
│ ├── APP_SERVER_DOCUMENTATION.md # Full protocol reference (66 methods, 7 server requests, 34 notifications)
│ └── app-server-schemas/ # Materialized JSON + TypeScript schemas
│ ├── json/ # JSON Schema files (v1, v2, root)
│ └── typescript/ # TypeScript type definitions (v1, v2, root)
├── index.html # SPA entry point
├── vite.config.ts # Vite config (Vue, Tailwind, bridge middleware)
├── tsup.config.ts # CLI build config
└── package.json # Scripts, deps, bin entry
```
## 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 (260620px), 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:
1. Receives HTTP requests from the frontend
2. Translates them into JSON-RPC calls on stdin
3. Reads JSON-RPC responses from stdout
4. Forwards server-initiated requests to the frontend via SSE
5. 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
1. SSE events arrive via `EventSource` on `/codex-api/events`
2. Each event is passed to `applyRealtimeUpdates()` for immediate UI effects (activity labels, live text, in-progress flags)
3. Events are also passed to `queueEventDrivenSync()` which debounces (220ms) a full data refresh
4. 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 `watch`ers 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
```bash
npx codex-web-local [--port 3000] [--password mypass] [--no-password]
```
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
1. **Minimal dependencies**: Only essential packages — no state management library, no component library, no CSS framework beyond Tailwind
2. **Protocol-first**: The UI is designed around the Codex app-server protocol; all features map directly to RPC methods and notifications
3. **Offline-resilient**: localStorage persistence ensures the UI recovers gracefully from disconnections
4. **Reference equality optimization**: Extensive use of identity checks and shallow merging to minimize unnecessary Vue re-renders
5. **Single composable pattern**: All state and logic in one place for discoverability, at the cost of file size (~2000 LOC)