TermAl

An operating environment for AI coding agents.

TermAl gives you one place to run, supervise, review, and steer long-running software work performed by AI agents — structured around real agent workflows instead of raw terminal output.

Features

Agent Integrations

TermAl integrates with Claude Code, OpenAI Codex, Gemini CLI, Cursor, and OpenCode. Agent protocol differences are normalized into the same session, message, approval, and streaming model in the UI.

Architecture

Browser (React + TypeScript)        Rust Backend (axum + tokio)
┌──────────────────────┐            ┌──────────────────────────────────┐
│  Workspace           │  SSE       │  AppState                        │
│  ├── Split panes     │◄═══════════╡  ├── Sessions + message history  │
│  ├── Session chat    │  REST      │  ├── Agent child processes       │
│  ├── Diff viewer     │───────────>│  ├── Approval queues             │
│  ├── Source editor   │            │  ├── Remote registry             │
│  ├── Control panel   │            │  ├── Orchestrator templates      │
│  ├── Orchestrator    │            │  ├── Workspace layouts           │
│  ├── Filesystem      │            │  └── Persistence (~/.termal/)    │
│  └── Git status      │            │                                  │
└──────────────────────┘            │  Agent Runtimes                  │
                                    │  ├── Claude (NDJSON stdio)       │
                                    │  ├── Codex (JSON-RPC stdio)      │
                                    │  ├── Gemini (ACP stdio)          │
                                    │  ├── Cursor (ACP stdio)          │
                                    │  └── OpenCode (ACP stdio)        │
                                    └──────────────────────────────────┘

SSH remotes

TermAl can manage agent sessions on remote machines over SSH. The browser always connects to the local backend; remote operations are transparently proxied through persistent SSH tunnels.

SSH authentication setup

TermAl currently launches SSH non-interactively, so the simplest setup is key-based auth with your normal system ssh / ssh-agent flow.

If you do not already have an SSH key on your laptop:

ssh-keygen -t ed25519

Install that public key on the remote machine:

ssh-copy-id user@host

On Windows PowerShell, the equivalent is:

type $env:USERPROFILE\.ssh\id_rsa.pub | ssh {IP-ADDRESS-OR-FQDN} "cat >> .ssh/authorized_keys"

If you generated an Ed25519 key above, use id_ed25519.pub instead of id_rsa.pub.

Then verify normal SSH works before configuring the remote in TermAl:

ssh user@host

Notes:

How it works

Your laptop                              Remote build server
┌──────────────┐        ┌────────────────────────┐        ┌────────────────────────┐
│              │  SSE   │  Local TermAl (:8787)  │  SSH   │  Remote TermAl (:8787) │
│   Browser    │◄═══════╡                        │ tunnel │                        │
│              │  REST  │  RemoteRegistry        │───────>│  Agent Runtimes        │
│              │───────>│  ├── build-box ───────────────> │  ├── Claude            │
│              │        │  └── gpu-server ─ ...  │        │  ├── Codex             │
└──────────────┘        └────────────────────────┘        │  └── Gemini            │
                                                          └────────────────────────┘
  1. Configure a remote in Settings > Remotes — give it a name, SSH host, user, and optional port.
  2. Create a project bound to that remote. Sessions created inside the project run on the remote.
  3. TermAl opens an SSH tunnel (ssh -L {local_port}:127.0.0.1:8787) and optionally starts a TermAl server process on the remote host.
  4. All session operations (messages, approvals, stop/kill) are proxied through the tunnel as regular REST calls.
  5. An SSE event bridge subscribes to the remote’s /api/events stream, merging remote state and delta events into the local state so the UI stays live.

SSH tunnel detail

Local port (47000–56999)                 Remote host
       │                                      │
       ▼                                      ▼
  ┌─────────┐    ssh -L 47001:127.0.0.1:8787 user@build-box      ┌─────────┐
  │ :47001  │ ─────────────────────────────────────────────────> │  :8787  │
  └─────────┘           encrypted tunnel                         └─────────┘
       │                                                              │
  Local TermAl                                                  Remote TermAl
  proxies REST + SSE                                            runs agents
  through this port                                             on remote fs

Each remote gets a dedicated local port from the 47000–56999 range. The tunnel carries all traffic: REST API calls, SSE event streams, and health checks.

Two startup modes:

Mode SSH command When used
Managed server ssh ... user@host termal server Default — starts a TermAl server on the remote
Tunnel only ssh -N ... Fallback — assumes TermAl is already running remotely

TermAl tries Managed server first (15 s timeout). If it fails (e.g. termal not installed on the remote), it falls back to Tunnel only mode automatically.

Project-scoped routing

Remotes are bound at the project level, not per-session or per-action:

Project "backend-api"  ──  remote: build-box
  ├── Session 1  (proxied to build-box)
  ├── Session 2  (proxied to build-box)
  └── Files / Git / Reviews  (proxied to build-box)

Project "frontend"  ──  remote: local
  ├── Session 3  (runs locally)
  └── Files / Git / Reviews  (local filesystem)

When you create a session inside a remote-bound project, TermAl:

  1. Ensures a project binding exists on the remote (creates one via POST /api/projects if needed).
  2. Creates the session on the remote and stores a local proxy record with the mapping.
  3. Starts the SSE event bridge for that remote (if not already running).

All subsequent operations — sending messages, approving actions, browsing files, viewing git status — route through the same tunnel automatically.

State synchronization

Remote TermAl                    Local TermAl                     Browser
     │                                │                              │
     │── SSE state ──────────────────>│                              │
     │   (full snapshot)              │── merge + rewrite IDs ─────>│
     │                                │   (local session IDs)        │
     │── SSE delta ──────────────────>│                              │
     │   (text chunk, command update) │── apply delta + publish ───>│
     │                                │   (forwarded as local delta) │
     │                                │                              │
     │<── REST (proxied) ────────────│<── REST ─────────────────────│
     │   POST /api/sessions/3/messages│   POST /api/sessions/5/messages
     │   (remote session ID)          │   (local session ID)         │

The local backend is the single source of truth for the browser. It rewrites session and project IDs so the frontend sees a unified namespace — remote sessions look identical to local ones.

Remote settings

SSH remotes currently store only the connection settings needed to reach the machine:

When a remote is used, TermAl first tries to start termal server over SSH. If that fails, it falls back to tunnel-only mode and expects a TermAl server to already be running on the remote host.

Configuration reference

Setting Description Default
name Display name for the remote (required)
host SSH hostname or IP (required for SSH)
user SSH username (current user)
port SSH port 22
enabled Whether the remote is available for new projects true

Remotes are configured in Settings > Remotes and persisted in ~/.termal/termal.sqlite alongside session data. A built-in local remote is always present and cannot be removed.

Getting started

Prerequisites

Run the backend

cargo run

The server starts on http://localhost:8787.

Run the frontend

cd ui
npm install
npm run dev

The UI opens on http://localhost:4173. API calls are proxied to the backend automatically.

REPL mode (optional)

cargo run -- --repl

Runs an interactive terminal loop — reads prompts from stdin, runs one agent turn at a time. Useful for testing.

Telegram relay (experimental)

Configure Telegram from Settings -> Telegram:

  1. Create a bot with Telegram’s @BotFather and copy the bot token.
  2. Paste the token, test the connection, choose the subscribed projects, and save.
  3. Enable the relay and save. TermAl starts, stops, or restarts the in-process relay from the saved settings.
  4. Open the bot chat in Telegram and send /start.

The backend runs the relay in-process from saved Settings -> Telegram configuration. Today the relay is bound to one Telegram bot/chat and can control multiple subscribed TermAl projects from that chat. The planned multi-bot model is documented in docs/features/telegram-ui-integration.md: each bot becomes a named profile with its own token, linked chat, project subscriptions, defaults, and relay runtime.

Telegram commands:

Project digests and digest actions are temporarily disabled. Free text is forwarded into the selected session, or into the latest promptable root session in the active project when no session is selected. Assistant replies from the selected session are tailed back to Telegram after they settle only when Forward assistant replies is enabled; that setting can send full assistant output, including code, file paths, file contents, or secrets, to Telegram.

Project structure

termal/
├── src/
│   ├── main.rs              # Entry point and CLI
│   ├── api.rs               # Axum HTTP routes
│   ├── api_*.rs             # Route groups for files, git, review, SSE, etc.
│   ├── state*.rs            # AppState, boot, accessors, and persistence-facing state
│   ├── session_*.rs         # Session lifecycle, messages, config, runtime, and sync
│   ├── claude*.rs           # Claude runtime integration
│   ├── codex*.rs            # Codex runtime, RPC, events, and thread actions
│   ├── gemini.rs / acp.rs   # Gemini and ACP runtime integration
│   ├── remote*.rs           # SSH tunnels, remote routing, proxies, and sync
│   ├── orchestrator*.rs     # Orchestrator templates, instances, and transitions
│   ├── terminal*.rs         # Terminal run/stream support
│   ├── wire*.rs             # HTTP/SSE wire types
│   └── tests/               # Backend test modules
├── ui/
│   ├── src/
│   │   ├── App.tsx                  # App composition
│   │   ├── app-*.ts                 # Focused app hooks and state orchestration
│   │   ├── api.ts / types.ts        # API client and shared TypeScript types
│   │   ├── live-updates.ts          # SSE delta event application
│   │   ├── session-store.ts         # Session slice publication for heavy panels
│   │   ├── workspace*.ts            # Pane/tab/split state and layout persistence
│   │   ├── message-*.tsx            # Message cards, icons, and render helpers
│   │   ├── themes/                  # 21 color themes + chrome style presets
│   │   └── panels/
│   │       ├── AgentSessionPanel.tsx                # Chat session view
│   │       ├── ControlPanelSurface.tsx              # Dockable sidebar with section tabs
│   │       ├── OrchestratorTemplatesPanel.tsx        # Visual canvas editor for workflows
│   │       ├── OrchestratorTemplateLibraryPanel.tsx  # Template library and instance management
│   │       ├── SessionCanvasPanel.tsx               # Session graph overview
│   │       ├── SourcePanel.tsx                      # Source file viewer
│   │       ├── DiffPanel.tsx                        # Diff viewer
│   │       ├── FileSystemPanel.tsx                  # Filesystem browser
│   │       ├── GitStatusPanel.tsx                   # Git status and diff tree
│   │       ├── ResponseBoardPanel.tsx               # Spatial board of pinned agent responses
│   │       ├── MailboxPanel.tsx                     # Durable agent mailbox reader
│   │       ├── TerminalPanel.tsx                    # Scoped terminal command runner
│   │       └── InstructionDebuggerPanel.tsx         # Agent instruction tracing
│   └── vite.config.ts          # Dev proxy: /api → :8787
├── docs/
│   ├── architecture.md      # Full architecture reference
│   ├── vision.md            # Product vision
│   ├── roadmap.md           # Phased roadmap
│   └── features/            # Feature briefs (orchestration, workspaces, agent integrations, etc.)
├── Cargo.toml
└── Cargo.lock

Orchestrator

The orchestrator lets you design reusable multi-agent workflows as directed graphs. Each node is an agent session; each edge is a transition that fires when a session completes.

Template design

Open the orchestrator canvas to build a workflow visually:

Runtime

Launch an orchestrator instance from a template. TermAl creates the sessions, starts the first one, and then:

  1. When a session reaches prompt_ready, the orchestrator evaluates outgoing transitions.
  2. The transition assembles a follow-up prompt (optionally including the source session’s last response or a summary).
  3. The follow-up prompt is delivered to the target session, which starts automatically.

Instances can be paused, resumed, or stopped from the control panel or via REST API.

Transition settings

Setting Options Description
Trigger OnCompletion Fires when the source session finishes its turn
Result mode None, LastResponse, Summary, SummaryAndLastResponse What context to include in the delivered prompt
Input mode Queue, Consolidate How multiple inbound transitions are handled
Prompt template Free text with `` placeholder Custom prompt wrapping the result

Docs