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.
Quick Start
# Build
# Run (provide path to projects directory)
# Or use environment variable
IVORN_PROJECTS_DIR=/path/to/projects
Open http://localhost:8181 in your browser.
Configuration
Config File
Configuration is loaded from ~/.config/ivorn/config.toml (XDG spec).
See config.example.toml for all available settings.
[]
= 8181
= "0.0.0.0"
[]
= "/path/to/projects"
[]
= true # Enable conversation logging to markdown files
[]
= 25 # Maximum upload file size in MB
Environment Variables
All settings support environment variable overrides with IVORN_ prefix:
| Variable | Description | Default |
|---|---|---|
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
Configuration values are resolved in this order (highest to lowest):
- CLI arguments
- Environment variables
- Config file
- Defaults
ACP Agents
ACP (Agent Client Protocol) agents are external executables that implement the ACP protocol over stdio. Ivorn discovers agents from JSON and TOML configuration files.
Configuration Locations
- Global:
~/.config/ivorn/acp/*.jsonor*.toml - Project-local:
{project}/.ivorn/acp/*.jsonor*.toml(takes precedence over global)
When both JSON and TOML files define the same agent name, TOML takes precedence.
Config File Format
JSON format (kiro-cli.json):
TOML format (kiro-cli.toml):
= "kiro-cli"
= "Kiro CLI"
= "/usr/local/bin/kiro-cli"
= ["acp"]
= "Default Kiro CLI agent"
[[]]
= "KEY"
= "val"
| Field | Required | Description |
|---|---|---|
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 |
Example: Kiro CLI
Create ~/.config/ivorn/acp/kiro-cli.toml:
= "kiro-cli"
= "Kiro CLI"
= "kiro-cli"
= ["acp"]
= "Kiro CLI ACP agent"
API Endpoint
GET /api/acp-agents returns the list of discovered agents (global only, project-local requires session context).
GET /api/acp-agents/{name}/config/{project} returns config options for a specific agent+project combination. Spawns a temporary ACP subprocess to probe the agent's capabilities, caches results to avoid redundant probes.
Projects Directory Structure
The projects directory should contain project subdirectories directly:
/path/to/projects/
├── project-a/
├── project-b/
└── project-c/
Filtered Directories
The following directories are automatically filtered from the project list:
- Dotfiles — Any directory starting with
.(e.g.,.git,.ivorn,.kiro) - Infrastructure directories —
uploads,tmp,logs,target,node_modules
This keeps the project list clean by hiding version control, build artifacts, and other non-project directories.
Project Context
Projects can provide custom context that is injected into every message sent to the ACP agent. This enables project-specific metadata like team name, environment, CI status, or feature flags.
Context is loaded from three sources, merged in order (later overrides earlier):
- Global static —
~/.config/ivorn/context.tomlorcontext.json - Project static —
.ivorn/context.tomlor.ivorn/context.json - Dynamic hook —
.ivorn/context-hookscript (highest priority)
All values are prefixed with project_hook_ in the ivorn-context block.
Static Context Files
For simple, unchanging metadata, use static TOML or JSON files. No executable script needed.
TOML format (.ivorn/context.toml):
= "platform"
= "development"
= "github-actions"
JSON format (.ivorn/context.json):
Rules:
- TOML takes precedence if both exist in the same directory
- Only flat key-value pairs are supported (nested tables/objects are ignored)
- Non-string values (int, bool, float) are coerced to strings automatically
- Parse errors produce
project_hook_error=<description>(doesn't block prompting)
Dynamic Context Hooks
For values that change (git branch, CI status), use an executable script.
Create at one of these locations (first found wins):
.ivorn/context-hook(executable).ivorn/context-hook.sh
The script runs with the project directory as its working directory and outputs key=value lines:
#!/bin/sh
Parsing rules:
- Each line should be
key=valueformat - Lines starting with
#are ignored (comments) - Blank lines and lines without
=are ignored - Whitespace around keys and values is trimmed
Timeout and errors:
- Scripts have a 5-second timeout
- On timeout:
project_hook_error=timeout - On non-zero exit:
project_hook_error=exit(N) - On IO error:
project_hook_error=io_error
Example: Combined Static + Dynamic
Static metadata in .ivorn/context.toml:
= "platform"
= "main"
Dynamic values in .ivorn/context-hook:
#!/bin/sh
Result in ivorn-context:
project_hook_team=platform
project_hook_default_branch=main
project_hook_git_branch=feature/new-api
project_hook_git_dirty=true
Project Commands
Projects can define custom slash commands that execute shell commands or send prompts directly to the LLM.
Configuration Locations
- Global:
~/.config/ivorn/commands/*.jsonor*.toml - Project-local:
{project}/.ivorn/commands/*.jsonor*.toml(takes precedence)
Commands are merged with agent-provided commands, with project commands taking precedence over agent commands with the same name.
Config File Format
TOML format (.ivorn/commands/dev.toml):
[[]]
= "clippy"
= "Run clippy and fix issues"
= "cargo clippy {args} 2>&1"
= "prompt"
= "Fix these clippy warnings:\n\n{}"
= { = "clippy args (e.g., --fix)" }
[[]]
= "test"
= "Run tests"
= "cargo test {args}"
= "display"
JSON format (.ivorn/commands/dev.json):
Command Fields
| Field | Required | Description |
|---|---|---|
name |
Yes | Command name (without leading /) |
description |
Yes | Human-readable description shown in command palette |
command |
No | Shell command to execute. Use {args} for user arguments. Required for display and file output modes. |
output |
No | Output handling mode: display (default), file, or prompt |
prompt |
No | Prompt template. Use {} for output content (prompt mode) or filename (file mode). Required for prompt-only commands. |
input |
No | Input hint object with hint field for command palette |
Output Modes
display(default): Stream command output to the user in a terminal-style containerfile: Save output to a timestamped file, optionally send prompt with filenameprompt: Capture output, inject into prompt template, send to LLM
Prompt-Only Commands
Commands with output = "prompt" can omit the command field to send the prompt template directly to the LLM without executing any shell command. Use {args} in the prompt template for user arguments.
[[]]
= "audit"
= "Run design quality checks"
= "prompt"
= "Review the frontend code for design quality issues. Check typography, color, spacing, and accessibility. Focus on: {args}"
= { = "component or area to audit" }
Template Variables
{args}— User-provided arguments after the command name{}— Command output (prompt mode) or generated filename (file mode)
Features
- Session naming: assign custom names to sessions for easier identification; click-to-edit in session header (✏️ icon when unnamed); names displayed on projects page instead of truncated UUIDs; 100 character max, persisted across server restarts
- Light/dark theme toggle with system theme follow: accessible via hamburger menu (☰) on both session and projects pages; three options (Light, Dark, System); System follows OS
prefers-color-schemeand auto-updates when OS theme changes; preference persisted in localStorage - Standalone session pages:
/projectsfor ACP-only project list,/session/{id}for single-session view (opens in new browser tab) - Running sessions visible on project list: each project shows its active sessions with status indicators and clickable links to open in new tab
- Rich activity indicators on session tabs: 5 distinct states (idle, streaming, permission_pending, turn_complete, timeout) with visual icons and colors; timeout triggers after 30s of no SSE events during active turns only (pauses during tool execution and between turns); standalone pages update browser title with state prefix (⟳, ⚠, ✓, ⏳)
- Terminated session auto-resume: "Show history" toggle on projects page reveals terminated sessions; clicking resurrects the session by spawning a new ACP subprocess and calling
session/loadto replay kiro-cli's history - Real-time project page updates: project list subscribes to global SSE stream (
/api/stream) for instant session lifecycle events (created, closed, status changes) without page refresh - Session termination control: ⏻ button explicitly terminates session (kills subprocess); closing browser tab leaves session alive for later reconnection;
beforeunloadprompt reminds user session is still running - Per-tab input state (draft messages preserved when switching tabs)
- Project list sorted by most recently used, then alphabetically
- ACP (Agent Client Protocol) sessions using JSON-RPC 2.0 for structured agent communication
- File upload for ACP sessions: drag-and-drop, clipboard paste, or button; stored in OS data directory with text reference
Attached file: ./uploads/{filename} (mime/type, size) - All file types supported; images (detected via magic bytes) sent as base64 Image content blocks + text reference; non-images sent as text reference only
- File attachment display: images render as clickable thumbnails with modal; non-images render as card-style badges with MIME-based icons (📄 PDF, 📦 archives, 📝 text, 📋 JSON, 📎 other)
- MIME type detection using
infercrate (magic bytes) with extension fallback for text files (txt, md, json, xml, csv, log) - Session-type-aware SSE handlers with shared event infrastructure
- ACP spec-aligned tool call visibility (kind, status, title, raw I/O)
- Tool call cards with purpose display, collapsible Parameters and Output sections, and status badges
- Tool call status-based card tinting: subtle background and border color changes for at-a-glance state visibility (blue=in_progress, green=completed, red=failed, amber=rejected)
- Tool call Parameters and Output sections: both shown when expanded, each independently collapsible with state persisted across page reloads
- Tool call output formatting: structured key-value tree rendering (matching Parameters style); MCP response wrappers (
items[*].Jsonanditems[*].Textvariants) automatically unwrapped; direct JSON objects rendered as tree without unwrapping - Tool call diff display: file changes shown in separate collapsible "Diff" section (outside Output section) with colored inline diff (red/green); edit tools with diff hide the Output section when rawOutput is empty
- Tool call kind-based default expansion: impactful tools (edit, delete, move, execute) expanded by default; read-only tools collapsed
- Tool call expansion state persisted across SSE updates and page reloads via toolCallsById
- Tool call selection for follow-up messages: 📌 pin button on tool cards to select for referencing; selected cards highlighted with blue border; chip bar above input shows selected tools with ✕ deselect; on send, tool references (title + args summary) prepended to message; selections auto-clear after send
- Tool call content display: text (markdown), diff, terminal, and unknown content types rendered in expandable cards
- Tool call error display: failed tool calls show error details in collapsible section with red styling; auto-expands when tool fails
- Interspersed tool cards: tool calls appear inline at their first occurrence position, not grouped at bottom
- Turn completion dividers with elapsed time and timestamps
- Unified inline activity indicator: single indicator appears after the last user message; shows "Waiting for response..." with spinner + timer + cancel during waiting state, transitions to bouncing dots during typing state; timer and cancel button visible in both states
- Bidirectional agent communication (incoming requests, interactive permission UI)
- Interactive permission request UI: inline menu above input area, "View" link to highlight tool card, user approves/rejects tool calls
- Agent info and capability detection (name, version, image prompts, session restore, MCP transports)
- MCP server connection status indicator: session header shows
🔌 Nbadge when MCP servers are connected; click to see server names in dropdown; OAuth URLs automatically open in new browser tab; hidden on mobile (accessible via gear menu) - ACP authentication support (auth method parsing and automatic authenticate flow)
- ACP agent selection: projects page shows collapsible sections with agent cards; click agent to start session; auto-expand projects with active sessions; localStorage persistence for expand/collapse state
- Session mode and config option parsing from session/new response
- Session config option setting via ACP session/set_config_option (generic key-value)
- LLM-suggested actions (ACP only): agent can emit
<ivorn-action>XML tags in responses that render as clickable buttons inline; supportstype="config"(change settings),type="message"(send message on user's behalf), andtype="prompt"(resolve prompt by name and send its content);modeattribute on message/prompt actions auto-expands into config+message group for one-click mode switching;<ivorn-actions>wrapper enables multi-action sequences with single button click; buttons show "✓ Applied" state after use; context block injected to teach the LLM the tag format; buttons with config actions render as split badges showing the config value (e.g.,[developer | Begin implementation]) for visual distinction - Config change system messages: full-width banner in chat history when config options change (e.g., agent, model), showing old→new values; distinguishes user-initiated vs agent-initiated changes
- Config snapshot in message footer: assistant messages display current config values (agent, model) left of timestamp for context
- Config options UI for ACP sessions (model/agent dropdowns with search, keyboard shortcuts: Ctrl+M model, Ctrl+A agent)
- Unified prompt menu: type
@at start of message OR click the@button to search and select prompts (arrow keys, Enter/Tab to select, Esc to dismiss), sorted alphabetically by name; ACP sessions receive prompts via SSE with full content for local/global prompts, auto-refreshed on agent change - Command palette: type
/at start of message to discover and execute slash commands; fuzzy search filters as you type; shows command name, description, and input hint; keyboard navigation (Arrow Up/Down, Enter/Tab to select, Escape to dismiss); smart send behavior (commands without input auto-send, commands with input insert/cmdand keep focus for user to type argument) - Session load/resume via fjall history database (single source of truth for chat history)
- Session resume auto-message: on page reload, automatically sends a message to the agent with time since last activity, asking it to summarize where things left off and suggest next steps
- Config options restored on page reload via
/messages/latestAPI (ACP sessions only) - Real-time streaming via Server-Sent Events (SSE) including stop reason events
- Agent execution plan display (ACP plan events)
- Graceful handling of unknown/future ACP session update types
- Mobile-friendly responsive design with consolidated gear menu (⚙️) for config options, history, prompts, and file upload
- Text selection popup: desktop centers on mouse position, mobile centers on viewport with smart vertical placement
- Markdown rendering with syntax highlighting for both user and agent messages; diff code blocks display with red/green line coloring
- Clickable lines in ACP sessions: list items and headings submit as prompts on click (cursor pointer, green hover border)
- Session persistence across page reloads
- Session isolation: every message stamped with
_sid(session ID), validated on load/tab-switch/periodic, misplaced messages auto-corrected - Server-side chat history storage (fjall embedded DB) for multi-device sync and server restart resilience
- Build info system: compile-time capture of version, git hash, jj change id, build timestamp via
build.rs - About modal: hamburger menu → "About" displays version, git hash, jj change id, build time, hostname, and projects directory
- Server identity display: hostname shown in browser tab title and tab bar (desktop only); helps distinguish multiple ivorn instances
- Version mismatch detection: polls
/api/healthevery 5 min + checks before each message send; non-blocking gold banner with reload button when server version changes - Graceful model unavailability handling (ACP): inline error banner, pulsing model selector highlight, automatic retry on model change
- Session error surfacing (ACP): fatal errors (subprocess death) show red banner with "New Session" button; recoverable errors (JSON-RPC failures) show yellow banner with "Retry" button
- Subprocess monitoring (ACP): per-method request timeouts, process liveness polling every 3s, inactivity watchdog (30s soft warning, 5min hard timeout); health banner shows "Restart Session" button on death/timeout, "Keep Waiting" option on unresponsive
- Scroll-to-bottom button: circular floating button (bottom-right) appears when scrolled up, with unread message count badge
- Loading progress indicator: thin indeterminate progress bar at top of input area during history sync, terminated session load, and SSE connection setup
- Prompt navigation buttons: ↑/↓ buttons on user messages navigate between user messages; ↑/↓ buttons on write tool calls (edit/delete/move) navigate between write tools
- Keyboard shortcuts for navigation:
J(next user message) andK(previous user message),j(next write tool) andk(previous write tool) when input not focused - Frontend performance optimizations for long conversations: CSS
content-visibility: autofor virtual scrolling (95% DOM reduction at 1000 messages), rendered HTML caching on message objects, debounced scroll and copy button operations via requestAnimationFrame - Server-side conversation logging (ACP only): real-time markdown logs saved to OS data directory for conversation recovery after server issues
- Shell command execution (ACP only):
!-prefixed messages execute as shell commands in the project directory; output streamed with ANSI color rendering (standard colors, bright colors, bold, 256-color palette) in dark terminal-style container with copy button; stop button kills process group - Ivorn context injection (ACP only): every user message is prepended with
<ivorn-context>XML block containing project name, working directory, session ID, turn count, last activity timestamp, OS/hostname/timezone, VCS type/root/branch, project type, conversation log paths, uploads directory, recent uploads, project file count, previous sessions, and server version - Project context hooks (ACP only): projects can provide static context files (
.ivorn/context.tomlor.ivorn/context.json) and/or dynamic scripts (.ivorn/context-hook); global static files also supported (~/.config/ivorn/context.tomlor.json); values merged with precedence (global static → project static → dynamic hook); all values prefixed withproject_hook_in ivorn-context block - Typography: Literata serif font for assistant message prose (optimized for long-form reading), Source Sans 3 sans-serif for UI and user messages; fonts vendored as WOFF2 with cache-busting URLs; licensed under SIL OFL 1.1
Data Storage
All application data is stored in the OS data directory, keeping project directories clean:
| Platform | Location |
|---|---|
| Linux | ~/.local/share/ivorn/ |
| macOS | ~/Library/Application Support/ivorn/ |
| Windows | C:\Users\<user>\AppData\Roaming\ivorn\ |
Directory Layout
~/.local/share/ivorn/
├── state/ # Global state files
│ ├── sessions.json # Active session metadata
│ ├── terminated-sessions.json # Terminated session records
│ ├── project-usage.json # Project usage timestamps
│ └── .ivorn-sessions.lock # File lock for atomic updates
└── projects/{project}/ # Per-project data
├── conversations/yyyy/mm/dd/ # Conversation logs
│ ├── {session_id}.md
│ └── {session_id}/ # Tool output attachments
└── uploads/ # Uploaded files
Conversation Log Files
ACP sessions automatically log conversations to markdown files for recovery after server issues.
Logs include:
- User messages (as blockquotes)
- Assistant messages (plain text)
- Tool calls with purpose and parameters
- Tool status updates (✅ completed, ❌ failed, ⚠️ rejected)
- Tool results: short results (<21 lines) inlined, long results saved to
{session_id}/{tool_call_id}.txt - Turn separators
Logs are created lazily on first user message and can be used to resume conversations in new sessions by referencing the file.
Privacy Note: Conversation logs contain full message content. Set conversation_log = false in the [logging] config section to disable.
Session State Files
sessions.json— Active session IDs, projects, types, and clean termination statusterminated-sessions.json— Terminated session metadata for auto-resumeproject-usage.json— Last-used timestamps for project list sorting
Migration
On first run, files are automatically migrated from old locations:
| Old Location | New Location |
|---|---|
{projects_dir}/ivorn-sessions.json |
state/sessions.json |
{projects_dir}/ivorn-terminated-sessions.json |
state/terminated-sessions.json |
{projects_dir}/ivorn-project-usage.json |
state/project-usage.json |
{projects_dir}/{project}/uploads/ |
projects/{project}/uploads/ |
conversations/{project}/ |
projects/{project}/conversations/ |
Migration uses rename() for efficiency. If migration fails (e.g., cross-filesystem), a warning is logged with the manual mv command to run. The application continues working from the old location until manually migrated.
Development
# Format code
# Run CI checks
# Run locally
JavaScript Tests
Frontend unit tests use Deno (no Node.js required):
# Run JS tests locally
# Or directly with Deno
Requirements: Deno must be installed locally (mise install deno or see deno.land).
See design-docs/initial-design.md for architecture details.
License
Copyright (C) 2026 Paul Campbell
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.