Web UI
Codumentor includes a protocol-native web UI built with React that provides a chat interface with real-time streaming, tool execution visualization, and plugin support.
Quick Start
Development Mode
Start the backend API server and the Vite dev server in separate terminals:
# Terminal 1: Start the API server (web UI is the default surface)
python -m codumentor.cli.main api --host 127.0.0.1 --port 2638
# Terminal 2: Start the Vite dev server (with hot reload)
cd ui
npm install # First time only
npm run dev
Open http://localhost:5173 in your browser. The Vite dev server proxies API calls to the backend.
Production Mode
Build the UI and serve it from FastAPI:
# Build the UI
cd ui
npm run build
# Start the API server (UI is served at root by default)
python -m codumentor.cli.main api --host 127.0.0.1 --port 2638
Open http://127.0.0.1:2638/ in your browser.
Enabling the legacy OpenAI-compatible API alongside the web UI
By default the api command only serves the protocol-native /ui/ API and the React SPA. If you also need the legacy /v1/ OpenAI-compatible surface:
python -m codumentor.cli.main api --openai-api
Or set api.enable_openai_api: true in your codumentor.yaml. To suppress the web UI entirely (API-only deployment), combine with --no-web-ui.
Executable mode: The UI can also be served from the PyInstaller-built executable. Build the UI before running the build script so that
ui/dist/is bundled into the executable. See building.md for details.
URL Routing
The UI uses client-side routing with react-router for shareable conversation URLs and proper browser history navigation.
URL Structure
| URL | Description |
|---|---|
/ | Redirects to the last active conversation (from localStorage) or creates a new one |
/login | Login page (supports ?redirect= for post-login navigation) |
/c/:conversationId | View/interact with a specific conversation |
/c/:conversationId/t/:turnId | Link to a specific turn/message within a conversation |
Features
- Shareable URLs: Each conversation has a unique URL like
/c/abc123that can be bookmarked or shared - Browser History: Back/forward buttons navigate between previously viewed conversations
- Deep Linking: Opening a conversation URL loads that specific conversation directly
- Turn Links: Link directly to a specific message with
/c/abc123/t/msg456- scrolls to and highlights the message - Login Redirect:
/login?redirect=/c/abc123remembers where to go after successful login - 404 Handling: Invalid conversation URLs show a friendly error page with options to create new or go home
Reverse Proxy Support
The UI supports running behind a reverse proxy with path rewriting. When the proxy sets the X-Forwarded-Prefix header, the server automatically:
- Injects a
<base href="/prefix/">tag into the HTML - The React app and API client detect this and adjust their paths accordingly
Example nginx configuration:
location /codumentor/ {
proxy_pass http://127.0.0.1:2638/;
proxy_set_header X-Forwarded-Prefix /codumentor;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
This allows multiple Codumentor instances on different paths (e.g., /codumentor/, /project-a/, etc.).
Architecture
Protocol-Native Design
The web UI uses a custom protocol layer (/ui/ API endpoints) rather than the OpenAI-compatible /v1/ endpoints. This provides:
- Structured event streaming (SSE) with typed events
- Append-only event log with sequence numbers for resume
- Snapshot materialization for fast page load
- Plugin event support for extensibility
Component Stack
┌─────────────────────────────────────────┐
│ React App (ui/src/) │
│ - Chat transcript │
│ - Tool execution cards │
│ - Plugin event rendering │
├─────────────────────────────────────────┤
│ UI Protocol API (/ui/*) │
│ - Conversations (CRUD, SSE events) │
│ - Extensions (plugin manifests) │
├─────────────────────────────────────────┤
│ Event Store (SQLite) │
│ - Append-only event log │
│ - Per-conversation sequence numbers │
├─────────────────────────────────────────┤
│ Agent Runtime │
│ - Streaming responses │
│ - Tool execution │
│ - Plugin hooks │
└─────────────────────────────────────────┘
API Endpoints
Conversations
| Endpoint | Method | Description |
|---|---|---|
/ui/conversations | GET | List all conversations |
/ui/conversations | POST | Create a new conversation |
/ui/conversations/{id}/snapshot | GET | Get materialized conversation state |
/ui/conversations/{id}/events | GET | SSE stream of events |
/ui/conversations/{id}/turns | POST | Start a new turn (send message) |
/ui/conversations/{id}/turns/{turn_id}/cancel | POST | Cancel an in-progress turn |
/ui/conversations/{id} | DELETE | Soft-delete a conversation |
/ui/conversations/{id}/restore | POST | Restore a deleted conversation |
Extensions
| Endpoint | Method | Description |
|---|---|---|
/ui/extensions | GET | List available UI extensions |
/ui/extensions/plugins/{plugin_id}/{path} | GET | Serve plugin UI bundles |
Event Types
Events are streamed via SSE and persisted in the event store. Each event has a seq (sequence number) for ordering and resume.
Turn Lifecycle
turn.started- Turn began processingturn.completed- Turn finished successfullyturn.failed- Turn encountered an errorturn.cancelled- Turn was cancelled by user
Assistant Messages
assistant.message.started- Message generation startedassistant.message.delta- Streaming text chunkassistant.message.completed- Message finished
Tool Execution
tool.start- Tool execution begantool.progress- Tool progress updatetool.done- Tool finished successfullytool.error- Tool encountered an error
Plugin Events
plugin.<plugin_id>.*- Plugin-specific events (e.g.,plugin.agentic_memory.stored)
Snapshot Resume
The UI implements robust resume support:
- On page load, fetch
/ui/conversations/{id}/snapshotto get current state - Connect to SSE with
?after_seq={last_seq}to receive only new events - If connection drops, reconnect with exponential backoff using last known
seq
This ensures deterministic state reconstruction without replaying all events.
Development
Project Structure
ui/
├── src/
│ ├── components/ # React components
│ │ ├── MessageList.tsx
│ │ ├── ToolItem.tsx
│ │ ├── PluginItem.tsx
│ │ └── ExtensionsPage.tsx
│ ├── hooks/ # Custom React hooks
│ │ └── useExtensions.ts
│ ├── services/ # API and utilities
│ │ ├── api.ts
│ │ └── extensionLoader.ts
│ ├── types/ # TypeScript types
│ └── main.tsx # Entry point
├── dist/ # Production build output
├── vite.config.ts # Vite configuration
└── package.json
Vite Configuration
The Vite config handles both development and production:
// vite.config.ts
export default defineConfig(({ command }) => ({
// Production uses /app base path (served by FastAPI)
// Development uses root (Vite dev server)
base: command === "build" ? "/app" : "/",
server: {
proxy: {
"/ui": { target: "http://127.0.0.1:2638", changeOrigin: true },
"/health": { target: "http://127.0.0.1:2638", changeOrigin: true },
"/v1": { target: "http://127.0.0.1:2638", changeOrigin: true }
}
}
}));
Adding Components
- Create component in
ui/src/components/ - Export from
ui/src/components/index.ts - Import and use in other components
Type Safety
Types are defined in two places:
ui/src/types/api.ts- API request/response typesui/src/types/messages.ts- Chat message and transcript types
Keep these in sync with backend Pydantic schemas in src/codumentor/api/ui/schemas.py.
Plugin UI Extensions
Plugins can provide custom UI components for rendering their events in the transcript.
Extension Discovery
The /ui/extensions endpoint returns manifests for all loaded plugins:
{
"extensions": [
{
"plugin_id": "agentic_memory",
"name": "Agentic Memory",
"version": "1.0.0",
"event_types": ["plugin.agentic_memory.stored", "plugin.agentic_memory.retrieved"],
"ui": {
"bundle_url": "/ui/extensions/plugins/agentic_memory/index.js",
"sdk_version": "1.0",
"custom_elements": ["x-agentic-memory-card"],
"placements": ["transcript-card"]
},
"ui_enabled": true
}
],
"sdk_version": "1.0"
}
Plugin UI Bundles
Plugin UI bundles are ESM modules that register Custom Elements:
// ui/plugins/agentic_memory/index.js
class AgenticMemoryCard extends HTMLElement {
connectedCallback() {
const data = this.event?.data || {};
this.innerHTML = `<div class="memory-card">Memory stored: ${data.summary}</div>`;
}
}
customElements.define('x-agentic-memory-card', AgenticMemoryCard);
Place bundles in ui/plugins/<plugin_id>/index.js. They are served via /ui/extensions/plugins/<plugin_id>/....
Markdown code-block renderers
In addition to custom elements, extensions can register markdown fenced code-block renderers through ui.markdown_renderers in the extension manifest.
Each renderer declaration includes:
id- unique renderer identifierlanguages- fence languages/aliases handled by the rendererexport_name- named export in the ESM bundle to loadpriority(optional) - higher priority wins when multiple renderers matchoverride_existing(optional) - allow replacement of an existing language renderer
The host validates and registers these renderers after compatibility and origin checks pass. Invalid renderer registrations are logged and isolated to the offending extension.
Built-in renderer plugins:
- Syntax highlighting for standard code fences
- Mermaid for fenced blocks with language
mermaid(lazy-loaded on first Mermaid block)
Fallback Rendering
When a plugin has no UI bundle or is disabled, the PluginItem component renders a generic fallback card showing the event type and status.
Event Grouping
Plugin events can be grouped together using the event_group_id field. When multiple events share the same event_group_id, they are merged into a single transcript item, with the latest event updating the existing item. This is useful for plugins that emit events in phases (e.g., "started" → "completed").
How Event Grouping Works:
- First Event: When an event with
event_group_idarrives, it creates a new transcript item - Subsequent Events: Events with the same
event_group_idupdate the existing item: - The event type, status, and timestamp are updated to the new values - Event data is merged (previous fields kept, new fields added/overwritten)
Explicit Status (Required):
Plugins must provide an explicit status field in their event data:
| Status | Meaning |
|---|---|
pending | Operation is in progress |
success | Operation completed successfully |
error | Operation failed |
If status is not provided, it defaults to "pending" and a warning is logged to help plugin developers catch the issue during development.
Example Event Flow:
// First event - creates transcript item
{
type: "plugin.agentic_memory.storage_started",
data: { status: "pending", event_group_id: "run-123", text_length: 1000 }
}
// Second event - updates the same transcript item
{
type: "plugin.agentic_memory.stored",
data: { status: "success", event_group_id: "run-123", stored_items: [...] }
}
The resulting transcript item will have:
eventType: "plugin.agentic_memory.stored" (latest)status: "success" (from explicit status field)data: merged data from both events (includestext_lengthfrom first event)
Custom Element Properties:
Plugin custom elements receive these properties:
| Property | Type | Description |
|---|---|---|
event | PluginEvent | The full plugin event object |
eventType | string | Current event type (e.g., "plugin.agentic_memory.stored") |
status | string | Current status: "pending", "success", or "error" |
data | object | Merged event data from all grouped events |
Benefits of Event Grouping:
- Single Card: Multiple events appear as one card that updates in place
- State Transitions: Cards can show transitions (pending → success)
- Data Accumulation: Early events can provide context for later events
Troubleshooting
UI Not Loading
- Check if the UI is built:
ls ui/dist/ - If empty, run:
cd ui && npm run build - Check server logs for "Serving UI from..." message
API Proxy Not Working (Dev Mode)
- Ensure backend is running on port 2638
- Check Vite proxy logs for connection errors
- Verify
/ui/*routes respond:curl http://127.0.0.1:2638/ui/conversations
SSE Connection Issues
- Check browser DevTools Network tab for EventStream connection
- Verify
after_seqparameter matches snapshot'slast_seq - Check for CORS errors if running UI on different origin
Extensions Not Loading
- Check
/ui/extensionsendpoint returns plugin manifests - Verify
ui_enabled: truein manifest - Check browser console for extension loader errors
Attaching Files
When the Client Filesystem plugin is enabled, you can share local files with the AI agent directly from the chat interface.
Methods
| Method | How |
|---|---|
| Drag and drop | Drag one or more files from your file manager and drop them anywhere on the chat window |
| Attach File button | Click + → Attach File to open a file picker |
| Attach Local Folder | Click + → Attach Local Folder to give the agent read/write access to an entire folder |
Drag and Drop
Drag files onto the chat from your OS file manager. A "Drop files to attach" overlay confirms the target. On drop, each file is uploaded and appears as an attachment pill above the input box. The files are automatically included as context when you send your next message.
You can drop multiple files at once if your file manager supports multi-select drag.
Attachment Pills
Attached files appear as pills above the input area. Click × on a pill to remove an attachment before sending.
File Size Limits
| File type | Limit |
|---|---|
Text files (.py, .ts, .md, etc.) | 200 KB |
Documents (.pdf, .docx) | 20 MB |
Files over the limit are rejected with an alert. Binary formats (PDF, DOCX) are extracted to text server-side before being sent to the agent.
Browser support: The Attach Local Folder feature requires a Chromium-based browser (Chrome, Edge) and a secure context (HTTPS or localhost). Drag-and-drop and single-file upload work in all browsers.
Tool Approvals
When tools perform potentially dangerous operations (like executing shell commands or writing files), Codumentor prompts you for approval. You can configure automatic approval rules to streamline your workflow.
Permission Dialog
When a tool requests permission, you'll see a dialog with several options:
| Button | Description |
|---|---|
| Deny | Block this specific request |
| Allow Once | Execute this specific request |
| Allow All in this Conversation | Auto-approve all similar requests for the current conversation |
| Create Rule | Create a persistent rule for this type of operation |
Keyboard shortcuts: Press Enter to Allow Once, Esc to Deny.
Settings
Access Tool Approvals settings via the Settings dialog (gear icon in the header).
Auto-approve All Tools
Enable this toggle to skip all permission prompts. Warning: This allows tools to execute commands and modify files without confirmation. Deny rules still apply when this is enabled.
Permission Rules
Rules provide fine-grained control over which operations are automatically approved or blocked.
Rule evaluation order:
- Deny rules are checked first (highest priority)
- Allow rules are checked second
- If no rule matches, you're prompted for approval
Rule Syntax
Rules follow the pattern: ToolName or ToolName(specifier)
| Pattern | Description |
|---|---|
Shell | Match all shell commands |
Shell(npm run:*) | Match commands starting with "npm run " (prefix match) |
Read(.env) | Match reading a specific file |
Write(*.log) | Match writing to .log files |
Wildcards
| Wildcard | Position | Behavior | Example |
|---|---|---|---|
:* | End only | Prefix match with word boundary | Shell(npm:*) matches npm install but NOT npmx |
* | Anywhere | Glob match (standard wildcard) | Shell(git *) matches git status, git commit |
Tool Names
| Tool Name | Permission Type | Description |
|---|---|---|
Shell | shell_command | Shell command execution |
Write | file_write | Writing/creating files |
Read | file_read | Reading files |
Examples
Common allow rules:
Shell(npm run:*) # Allow npm run commands (test, build, etc.)
Shell(git status) # Allow git status (exact match)
Shell(git diff:*) # Allow git diff commands
Shell(python -m pytest:*) # Allow pytest commands
Common deny rules:
Shell(rm -rf:*) # Block recursive force delete
Shell(curl:*) # Block curl commands
Read(.env) # Block reading .env file
Read(.env.*) # Block reading .env.* files
Write(*.exe) # Block writing executable files
Full example configuration:
{
"allow": [
"Shell(npm run:*)",
"Shell(git status)",
"Shell(git diff:*)",
"Shell(python -m unittest:*)"
],
"deny": [
"Shell(rm -rf:*)",
"Read(.env)",
"Read(.env.*)"
]
}
Creating Rules from Permission Dialog
When a permission prompt appears, click Create Rule to quickly create a rule based on the current request:
- Choose the rule type: Allow or Deny
- Select a suggested pattern or enter a custom one
- Click Create Rule to save and apply the decision
The dialog suggests patterns based on the operation:
- For shell commands: prefix patterns like
Shell(npm run:*) - For file paths: directory patterns like
Write(/path/to/dir/*) - For file extensions: patterns like
Read(*.json)
Rule Persistence
Rules are stored in your browser's localStorage and synced to the server. They persist across sessions and apply to all conversations.
Security Notes
- Deny rules always win: Even with "Auto-approve all" enabled, deny rules still block matching operations
- Be specific with allow rules: Broad rules like
Shellallow ALL shell commands - Review rules periodically: Remove rules you no longer need
- Use deny rules for sensitive files: Block access to credential files, secrets, etc.