ivorn 0.1.6

A web-based chat interface for ACP-compatible AI coding agents
Documentation

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

Config File

Configuration is loaded from ~/.config/ivorn/config.toml (XDG spec).

See config.example.toml for all available settings.

[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:

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):

  1. CLI arguments
  2. Environment variables
  3. Config file
  4. 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/*.json or *.toml
  • Project-local: {project}/.ivorn/acp/*.json or *.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):

{
  "name": "kiro-cli",
  "title": "Kiro CLI",
  "command": "/usr/local/bin/kiro-cli",
  "args": ["acp"],
  "description": "Default Kiro CLI agent",
  "env": [{"name": "KEY", "value": "val"}]
}

TOML format (kiro-cli.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"
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:

name = "kiro-cli"
title = "Kiro CLI"
command = "kiro-cli"
args = ["acp"]
description = "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 directoriesuploads, 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):

  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)

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):

team = "platform"
environment = "development"
ci_provider = "github-actions"

JSON format (.ivorn/context.json):

{
  "team": "platform",
  "environment": "development",
  "ci_provider": "github-actions"
}

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
echo "ci_status=passing"
echo "git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"

Parsing rules:

  • Each line should be key=value format
  • 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:

team = "platform"
default_branch = "main"

Dynamic values in .ivorn/context-hook:

#!/bin/sh
echo "git_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
echo "git_dirty=$(git diff --quiet 2>/dev/null && echo false || echo true)"

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/*.json or *.toml
  • Project-local: {project}/.ivorn/commands/*.json or *.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):

[[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)" }

[[commands]]
name = "test"
description = "Run tests"
command = "cargo test {args}"
output = "display"

JSON format (.ivorn/commands/dev.json):

[
  {
    "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)" }
  }
]

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 container
  • file: Save output to a timestamped file, optionally send prompt with filename
  • prompt: 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.

[[commands]]
name = "audit"
description = "Run design quality checks"
output = "prompt"
prompt = "Review the frontend code for design quality issues. Check typography, color, spacing, and accessibility. Focus on: {args}"
input = { hint = "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-scheme and auto-updates when OS theme changes; preference persisted in localStorage
  • Standalone session pages: /projects for 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/load to 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; beforeunload prompt 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 infer crate (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[*].Json and items[*].Text variants) 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 🔌 N badge 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; supports type="config" (change settings), type="message" (send message on user's behalf), and type="prompt" (resolve prompt by name and send its content); mode attribute 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 /cmd and 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/latest API (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/health every 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) and K (previous user message), j (next write tool) and k (previous write tool) when input not focused
  • Frontend performance optimizations for long conversations: CSS content-visibility: auto for 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.toml or .ivorn/context.json) and/or dynamic scripts (.ivorn/context-hook); global static files also supported (~/.config/ivorn/context.toml or .json); values merged with precedence (global static → project static → dynamic hook); all values prefixed with project_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 status
  • terminated-sessions.json — Terminated session metadata for auto-resume
  • project-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
just fmt

# Run CI checks
just ci

# Run locally
just run /path/to/projects

JavaScript Tests

Frontend unit tests use Deno (no Node.js required):

# Run JS tests locally
just test-js

# Or directly with Deno
deno test tests/

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/.