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/feed | GET | SSE list feed: list-relevant, slimmed events of every conversation in the caller's list (open only while the tab is visible) |
/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}/turns/{turn_id}/headlines | GET | The turn's last headlines, oldest first (?limit=1..5, default 5): {turn_id, headlines: [{text, provenance, agent}]} — the list row's hover trail. Owner only (404 unknown conversation, 403 someone else's); an unknown turn yields [] |
/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) - LaTeX math for
$...(inline) and$$...$$(display, delimiters on their own lines), lazy-loaded on first formula
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
Any file up to 50 MB can be attached. Files over the limit are rejected with an alert. Documents (.pdf, .docx, .xlsx) are extracted to text server-side before being sent to the agent; any other file (a zip, say) is staged as-is for the agent to inspect with a shell tool.
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 (executing a shell command, reading a file outside the working directory, or calling an external system such as Gitea, Redmine, a database or Gmail), Codumentor prompts you for approval. File edits inside the working directory are not gated — no mode or rule prompts for them; the workspace boundary and your version control are what contain them. 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 | Save rules, across all conversations, that let this request through (or block it) |
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 reach external systems 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 outside the working directory |
Write(*.log) | Reserved — nothing requests file-write approval yet |
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 | Reserved. No tool requests file-write approval today, so these rules match nothing |
Read | file_read | Reading a file outside the working directory. A read inside it never reaches the gate |
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 a .env outside the working directory
Read(.env.*) # The same for .env.* 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, open Other ways to answer → Create Rule:
- Choose Allow or Block
- For Allow, pick an outcome: a set of rules the server generated and checked with the approval gate's own code — e.g. "
echoandpwd, with any arguments" or "Exactly these commands" — or write your own under Custom… (one rule per line, checked as you type) - Save: the rules are added to your record and the gate re-runs over the waiting request, which is answered by the rules if they cover it (the dialog closes), or stays with a note of which part is still uncovered
A command carrying an escalating badge can never be allowed by a rule; the editor says so instead
of offering a form. See the permissions guide
for the full behaviour, and doc/core-dev/design/permission-rule-dialog.md for the design.
Rule Persistence
Rules live on the server, on your own user record, and the server is what
evaluates them — the browser's localStorage holds a copy only so the settings
dialog can paint before the request comes back, and the server's answer
overwrites it. So your rules follow you to another browser or device, they
apply to every conversation, and a rule you cannot see in the dialog (an
operator's) still applies.
Two layers are evaluated, in this order:
- The operator's rules, from the server's config file. You cannot see or change these from the web UI; an operator deny is a floor nothing you set can lift.
- Your own rules, from this dialog.
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. — but note the reach: a
Read(…)rule covers reads outside the working directory, and a file the assistant can reach withshellneeds aShell(…)deny rule too