# Ivorn
A web-based chat interface for ACP-compatible AI coding agents (like Kiro CLI, Goose, etc.) that enables browser-based interaction with multiple parallel project sessions.
Named after Ivor the Engine, the beloved Welsh stop-motion steam train.
## Tech Stack
| Backend | [Rust](https://www.rust-lang.org/) + [Axum](https://github.com/tokio-rs/axum) | HTTP server, process management, async runtime |
| Frontend | [Alpine.js](https://alpinejs.dev/) | Lightweight reactive UI (~15KB, no build step) |
| Communication | [SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) + POST | Server-to-browser streaming, client requests |
| Protocol | [ACP](https://agentclientprotocol.com/) | JSON-RPC 2.0 over stdio for agent communication |
| Build | cargo only | Single binary, no separate frontend build |
## Architecture
```mermaid
flowchart LR
Browser["Browser<br/>(Alpine.js)"]
Axum["Axum Server"]
ACP["ACP Module"]
Agent["ACP Agent<br/>(e.g., Kiro CLI)"]
Browser -->|"POST /api/..."| Axum
Axum -->|"SSE stream"| Browser
Axum --- ACP
ACP -->|"stdin (JSON-RPC)"| Agent
Agent -->|"stdout (JSON-RPC)"| ACP
```
The browser connects to the Axum server via HTTP. User messages are sent as POST requests. Agent responses stream back via Server-Sent Events (SSE). The ACP module (`src/acp/`) handles JSON-RPC 2.0 communication with agents over stdin/stdout.
## Quick Start
### Prerequisites
- [Rust](https://rustup.rs/) (stable toolchain)
### Build and Run
```bash
# Build
cargo build --release
# Run (provide path to projects directory)
./target/release/ivorn /path/to/projects
# Or use environment variable
IVORN_PROJECTS_DIR=/path/to/projects ./target/release/ivorn
```
Open http://localhost:8181 in your browser.
## Configuration
<details>
<summary><strong>User Config (~/.config/ivorn/config.toml)</strong></summary>
Configuration is loaded from `~/.config/ivorn/config.toml` (XDG spec). See [config.example.toml](config.example.toml) for all available settings.
```toml
[server]
port = 8181
bind_address = "0.0.0.0"
[paths]
projects_dir = "/path/to/projects"
[logging]
conversation_log = true # Enable conversation logging to markdown files
[upload]
max_size_mb = 25 # Maximum upload file size in MB
```
**Environment Variables**
All settings support environment variable overrides with `IVORN_` prefix:
| `IVORN_SERVER_PORT` | Server port | `8181` |
| `IVORN_SERVER_BIND_ADDRESS` | Bind address | `0.0.0.0` |
| `IVORN_PROJECTS_DIR` | Projects directory | Current directory |
| `IVORN_LOGGING_CONVERSATION_LOG` | Enable conversation logging | `true` |
| `IVORN_UPLOAD_MAX_SIZE_MB` | Maximum upload file size in MB | `25` |
**Precedence** (highest to lowest): CLI arguments > Environment variables > Config file > Defaults
</details>
<details>
<summary><strong>ACP Agent Config (~/.config/ivorn/acp/*.toml or *.json)</strong></summary>
ACP agents are external executables that implement the [Agent Client Protocol](https://agentclientprotocol.com/) over stdio. Ivorn discovers agents from configuration files.
**Configuration Locations**
- **Global**: `~/.config/ivorn/acp/*.json` or `*.toml`
- **Project-local**: `{project}/.ivorn/acp/*.json` or `*.toml` (takes precedence)
When both JSON and TOML files define the same agent name, TOML takes precedence.
**TOML format** (`kiro-cli.toml`):
```toml
name = "kiro-cli"
title = "Kiro CLI"
command = "/usr/local/bin/kiro-cli"
args = ["acp"]
description = "Default Kiro CLI agent"
[[env]]
name = "KEY"
value = "val"
```
**JSON format** (`kiro-cli.json`):
```json
{
"name": "kiro-cli",
"title": "Kiro CLI",
"command": "/usr/local/bin/kiro-cli",
"args": ["acp"],
"description": "Default Kiro CLI agent",
"env": [{"name": "KEY", "value": "val"}]
}
```
| `name` | Yes | Unique identifier for the agent |
| `title` | No | Display name (falls back to `name`) |
| `command` | Yes | Path to the executable |
| `args` | No | Command-line arguments |
| `description` | No | Description shown in UI |
| `env` | No | Environment variables to set |
</details>
<details>
<summary><strong>Project Context (.ivorn/context.toml or context-hook)</strong></summary>
Projects can provide custom context injected into every message sent to the ACP agent.
Context is loaded from three sources, merged in order (later overrides earlier):
1. **Global static** — `~/.config/ivorn/context.toml` or `context.json`
2. **Project static** — `.ivorn/context.toml` or `.ivorn/context.json`
3. **Dynamic hook** — `.ivorn/context-hook` script (highest priority)
**Static Context** (`.ivorn/context.toml`):
```toml
team = "platform"
environment = "development"
```
**Dynamic Hook** (`.ivorn/context-hook`):
```bash
#!/bin/sh
echo "git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
```
All values are prefixed with `project_hook_` in the ivorn-context block.
</details>
<details>
<summary><strong>Project Commands (.ivorn/commands/*.toml)</strong></summary>
Projects can define custom slash commands that execute shell commands or send prompts to the LLM.
**Configuration Locations**
- **Global**: `~/.config/ivorn/commands/*.json` or `*.toml`
- **Project-local**: `{project}/.ivorn/commands/*.json` or `*.toml` (takes precedence)
**Example** (`.ivorn/commands/dev.toml`):
```toml
[[commands]]
name = "test"
description = "Run tests"
command = "cargo test {args}"
output = "display"
[[commands]]
name = "clippy"
description = "Run clippy and fix issues"
command = "cargo clippy {args} 2>&1"
output = "prompt"
prompt = "Fix these clippy warnings:\n\n{}"
input = { hint = "clippy args (e.g., --fix)" }
```
| `name` | Yes | Command name (without leading `/`) |
| `description` | Yes | Description shown in command palette |
| `command` | No | Shell command to execute (`{args}` for user arguments) |
| `output` | No | `display` (default), `file`, or `prompt` |
| `prompt` | No | Prompt template (`{}` for output content) |
| `input` | No | Input hint object with `hint` field |
</details>
## Project Structure
```
ivorn/
├── src/
│ ├── main.rs # Entry point, CLI parsing
│ ├── config.rs # User configuration loading
│ ├── state.rs # Application state management
│ ├── acp_agents.rs # ACP agent discovery
│ ├── acp/ # ACP protocol implementation
│ │ ├── jsonrpc.rs # JSON-RPC types and builders
│ │ └── subprocess/ # Agent subprocess management
│ │ ├── lifecycle.rs # Spawn, kill, stdin writer
│ │ ├── protocol.rs # Initialize, session methods
│ │ ├── reader.rs # Background stdout reader
│ │ └── ... # Notifications, permissions, etc.
│ ├── api/ # HTTP API endpoints
│ │ ├── session/ # Session management
│ │ ├── projects.rs # Project listing
│ │ ├── upload.rs # File upload handling
│ │ └── ...
│ ├── history.rs # Chat history (fjall DB)
│ ├── events.rs # SSE event types
│ └── ...
├── static/ # HTML pages (session.html, projects.html)
├── tests/ # JavaScript tests (Deno)
└── Cargo.toml
```
## Features
- Session naming with click-to-edit in session header
- Light/dark theme toggle with system theme follow
- Running sessions visible on project list with status indicators
- Session termination control (explicit terminate vs. leave running)
- File upload (drag-and-drop, clipboard paste, or button)
- Tool call cards with purpose, parameters, output, and diff display
- Tool call status-based card tinting (blue=in_progress, green=completed, red=failed)
- Tool call selection for follow-up messages (pin button)
- Interactive permission request UI
- Config options UI (model/agent dropdowns with search, keyboard shortcuts)
- Unified prompt menu (type `@` or click button)
- Command palette (type `/` to discover slash commands)
- Shell command execution (`!`-prefixed messages)
- Real-time streaming via Server-Sent Events
- Mobile-friendly responsive design
- Markdown rendering with syntax highlighting
- Clickable items (list items and headings submit as prompts)
- Server-side chat history storage for multi-device sync
- Version mismatch detection with reload banner
- Server-side conversation logging to markdown files
- Ivorn context injection (project metadata in every message)
- Project context hooks (static files and dynamic scripts)
## Data Storage
All application data is stored in the OS data directory:
| Linux | `~/.local/share/ivorn/` |
| macOS | `~/Library/Application Support/ivorn/` |
| Windows | `C:\Users\<user>\AppData\Roaming\ivorn\` |
```
~/.local/share/ivorn/
├── state/ # Global state files
│ ├── sessions.json # Active session metadata
│ ├── terminated-sessions.json # Terminated session records
│ └── project-usage.json # Project usage timestamps
└── projects/{project}/ # Per-project data
├── conversations/yyyy/mm/dd/ # Conversation logs
└── uploads/ # Uploaded files
```
## Development
```bash
# Format code
just fmt
# Run CI checks
just ci
# Run locally
just run /path/to/projects
# Run JavaScript tests (requires Deno)
just test-js
```
## License
[AGPL-3.0-or-later](LICENSE)
Copyright (C) 2026 Paul Campbell