Codumentor logo Codumentor

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

URLDescription
/Redirects to the last active conversation (from localStorage) or creates a new one
/loginLogin page (supports ?redirect= for post-login navigation)
/c/:conversationIdView/interact with a specific conversation
/c/:conversationId/t/:turnIdLink to a specific turn/message within a conversation

Features

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:

  1. Injects a <base href="/prefix/"> tag into the HTML
  2. 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:

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

EndpointMethodDescription
/ui/conversationsGETList all conversations
/ui/conversationsPOSTCreate a new conversation
/ui/conversations/{id}/snapshotGETGet materialized conversation state
/ui/conversations/{id}/eventsGETSSE stream of events
/ui/conversations/{id}/turnsPOSTStart a new turn (send message)
/ui/conversations/{id}/turns/{turn_id}/cancelPOSTCancel an in-progress turn
/ui/conversations/{id}DELETESoft-delete a conversation
/ui/conversations/{id}/restorePOSTRestore a deleted conversation

Extensions

EndpointMethodDescription
/ui/extensionsGETList available UI extensions
/ui/extensions/plugins/{plugin_id}/{path}GETServe 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

Assistant Messages

Tool Execution

Plugin Events

Snapshot Resume

The UI implements robust resume support:

  1. On page load, fetch /ui/conversations/{id}/snapshot to get current state
  2. Connect to SSE with ?after_seq={last_seq} to receive only new events
  3. 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

  1. Create component in ui/src/components/
  2. Export from ui/src/components/index.ts
  3. Import and use in other components

Type Safety

Types are defined in two places:

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:

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:

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:

  1. First Event: When an event with event_group_id arrives, it creates a new transcript item
  2. Subsequent Events: Events with the same event_group_id update 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:

StatusMeaning
pendingOperation is in progress
successOperation completed successfully
errorOperation 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:

Custom Element Properties:

Plugin custom elements receive these properties:

PropertyTypeDescription
eventPluginEventThe full plugin event object
eventTypestringCurrent event type (e.g., "plugin.agentic_memory.stored")
statusstringCurrent status: "pending", "success", or "error"
dataobjectMerged event data from all grouped events

Benefits of Event Grouping:

Troubleshooting

UI Not Loading

  1. Check if the UI is built: ls ui/dist/
  2. If empty, run: cd ui && npm run build
  3. Check server logs for "Serving UI from..." message

API Proxy Not Working (Dev Mode)

  1. Ensure backend is running on port 2638
  2. Check Vite proxy logs for connection errors
  3. Verify /ui/* routes respond: curl http://127.0.0.1:2638/ui/conversations

SSE Connection Issues

  1. Check browser DevTools Network tab for EventStream connection
  2. Verify after_seq parameter matches snapshot's last_seq
  3. Check for CORS errors if running UI on different origin

Extensions Not Loading

  1. Check /ui/extensions endpoint returns plugin manifests
  2. Verify ui_enabled: true in manifest
  3. 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

MethodHow
Drag and dropDrag one or more files from your file manager and drop them anywhere on the chat window
Attach File buttonClick +Attach File to open a file picker
Attach Local FolderClick +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 typeLimit
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:

ButtonDescription
DenyBlock this specific request
Allow OnceExecute this specific request
Allow All in this ConversationAuto-approve all similar requests for the current conversation
Create RuleCreate 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:

  1. Deny rules are checked first (highest priority)
  2. Allow rules are checked second
  3. If no rule matches, you're prompted for approval

Rule Syntax

Rules follow the pattern: ToolName or ToolName(specifier)

PatternDescription
ShellMatch 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

WildcardPositionBehaviorExample
:*End onlyPrefix match with word boundaryShell(npm:*) matches npm install but NOT npmx
*AnywhereGlob match (standard wildcard)Shell(git *) matches git status, git commit

Tool Names

Tool NamePermission TypeDescription
Shellshell_commandShell command execution
Writefile_writeWriting/creating files
Readfile_readReading 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:

  1. Choose the rule type: Allow or Deny
  2. Select a suggested pattern or enter a custom one
  3. Click Create Rule to save and apply the decision

The dialog suggests patterns based on the operation:

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