| name | hermes-ide-terminal |
| description | AI-native terminal emulator & IDE built with Tauri, React, and Rust |
| triggers | ["set up hermes ide","how do I use hermes terminal","configure hermes ide with claude","hermes ide git integration","build hermes from source","hermes ide agent mode","troubleshoot hermes terminal","hermes ide project scanning"] |
Hermes IDE Terminal Skill
Skill by ara.so — Hermes Skills collection.
Overview
Hermes IDE is an AI-native terminal emulator built with Tauri 2, React 18, and Rust. It provides:
- Agent mode for Claude with rich chat interface, image support, and persistent conversations
- Multi-session terminal management with split panes and WebGL rendering
- Git integration with built-in panel for staging, commits, and inline diffs
- AI intelligence including ghost-text suggestions, prompt composer, and error pattern matching
- Project awareness that scans your codebase to build context for AI agents
- Cross-platform support for macOS, Windows, and Linux
Installation
Pre-built Binaries
Download from the official website:
Build from Source
Prerequisites:
node --version
rustc --version
Platform-specific dependencies:
sudo apt install libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
xcode-select --install
Clone and build:
git clone https://github.com/hermes-hq/hermes-ide.git
cd hermes-ide
npm install
npm run tauri dev
npm run tauri build
Architecture
Hermes IDE uses a layered architecture:
React Frontend (TypeScript) ← Tauri IPC Bridge → Rust Backend
UI & State PTY, DB, Scanner
Key directories:
src/ - React/TypeScript frontend (components, hooks, state, terminal)
src-tauri/ - Rust backend (PTY management, SQLite, project scanning)
src/api/ - Tauri IPC command wrappers
src/terminal/ - Terminal pool & AI intelligence engine
Configuration
Claude Agent Setup
Hermes uses your existing claude CLI authentication:
npm install -g @anthropic-ai/claude-cli
claude auth login
claude auth api-key $ANTHROPIC_API_KEY
In Hermes IDE:
- Open Command Palette (Cmd/Ctrl+K)
- Type "Claude"
- Select "Start Claude Agent Session"
- Conversations persist across restarts
Project Scanning
Hermes automatically scans projects to build AI context:
Terminal Configuration
Configure shell and environment in settings:
{
"shell": {
"program": "/bin/zsh",
"args": ["-l"]
},
"environment": {
"TERM": "xterm-256color",
"COLORTERM": "truecolor"
},
"fontSize": 14,
"fontFamily": "JetBrains Mono, monospace"
}
Key Features & Usage
Terminal Sessions
Git Integration
Access via sidebar panel:
git add .
git commit -m "feat: add new feature"
git push origin main
AI Ghost-Text Suggestions
Hermes provides real-time command suggestions:
git com█
Prompt Composer
Use natural language for autonomous execution:
"Create a new React component called UserProfile with TypeScript"
"Fix all eslint errors in src/"
"Run tests and commit if they pass"
Command Palette
Access all features quickly:
Development Workflow
Frontend Development
import React, { useEffect, useRef } from 'react';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
export const TerminalComponent: React.FC = () => {
const terminalRef = useRef<HTMLDivElement>(null);
const xtermRef = useRef<Terminal | null>(null);
useEffect(() => {
if (!terminalRef.current) return;
const term = new Terminal({
fontFamily: 'JetBrains Mono, monospace',
fontSize: 14,
theme: {
background: '#1e1e1e',
foreground: '#d4d4d4',
},
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(terminalRef.current);
fitAddon.fit();
xtermRef.current = term;
return () => {
term.dispose();
};
}, []);
return <div ref={terminalRef} className="terminal-container" />;
};
Backend Development
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use std::sync::Arc;
use tokio::sync::Mutex;
pub struct PtySession {
pub id: String,
pty: Arc<Mutex<Box<dyn portable_pty::MasterPty + Send>>>,
writer: Arc<Mutex<Box<dyn std::io::Write + Send>>>,
}
impl PtySession {
pub fn new(shell: &str, cwd: &str) -> Result<Self, Box<dyn std::error::Error>> {
let pty_system = native_pty_system();
let pair = pty_system.openpty(PtySize {
rows: 24,
cols: 80,
pixel_width: 0,
pixel_height: 0,
})?;
let mut cmd = CommandBuilder::new(shell);
cmd.cwd(cwd);
let _child = pair.slave.spawn_command(cmd)?;
let writer = pair.master.take_writer()?;
Ok(Self {
id: uuid::Uuid::new_v4().to_string(),
pty: Arc::new(Mutex::new(pair.master)),
writer: Arc::new(Mutex::new(writer)),
})
}
pub async fn write(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
let mut writer = self.writer.lock().await;
writer.write_all(data)?;
writer.flush()?;
Ok(())
}
}
Tauri IPC Commands
use tauri::command;
#[command]
pub async fn create_terminal_session(
shell: String,
cwd: String,
) -> Result<String, String> {
let session = PtySession::new(&shell, &cwd)
.map_err(|e| e.to_string())?;
let session_id = session.id.clone();
Ok(session_id)
}
#[command]
pub async fn write_to_terminal(
session_id: String,
data: String,
) -> Result<(), String> {
session.write(data.as_bytes())
.await
.map_err(|e| e.to_string())?;
Ok(())
}
Frontend API Wrappers
import { invoke } from '@tauri-apps/api/tauri';
export interface TerminalSession {
id: string;
cwd: string;
shell: string;
}
export async function createTerminalSession(
shell: string,
cwd: string
): Promise<string> {
return await invoke<string>('create_terminal_session', {
shell,
cwd,
});
}
export async function writeToTerminal(
sessionId: string,
data: string
): Promise<void> {
await invoke('write_to_terminal', {
sessionId,
data,
});
}
export async function resizeTerminal(
sessionId: string,
rows: number,
cols: number
): Promise<void> {
await invoke('resize_terminal', {
sessionId,
rows,
cols,
});
}
Common Patterns
State Management
Hermes uses React Context + useReducer:
import React, { createContext, useReducer } from 'react';
interface AppState {
sessions: TerminalSession[];
activeSessionId: string | null;
projects: Project[];
}
type AppAction =
| { type: 'ADD_SESSION'; session: TerminalSession }
| { type: 'REMOVE_SESSION'; sessionId: string }
| { type: 'SET_ACTIVE_SESSION'; sessionId: string };
const appReducer = (state: AppState, action: AppAction): AppState => {
switch (action.type) {
case 'ADD_SESSION':
return {
...state,
sessions: [...state.sessions, action.session],
};
case 'REMOVE_SESSION':
return {
...state,
sessions: state.sessions.filter(s => s.id !== action.sessionId),
};
case 'SET_ACTIVE_SESSION':
return {
...state,
activeSessionId: action.sessionId,
};
default:
return state;
}
};
export const AppContext = createContext<{
state: AppState;
dispatch: React.Dispatch<AppAction>;
} | null>(null);
export const AppProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, {
sessions: [],
activeSessionId: null,
projects: [],
});
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
Custom Hooks
import { useContext, useCallback } from 'react';
import { AppContext } from '../state/AppContext';
import { createTerminalSession, writeToTerminal } from '../api/terminal';
export const useTerminal = () => {
const context = useContext(AppContext);
if (!context) throw new Error('useTerminal must be used within AppProvider');
const { state, dispatch } = context;
const createSession = useCallback(async (shell: string, cwd: string) => {
const sessionId = await createTerminalSession(shell, cwd);
dispatch({
type: 'ADD_SESSION',
session: { id: sessionId, shell, cwd },
});
dispatch({ type: 'SET_ACTIVE_SESSION', sessionId });
return sessionId;
}, [dispatch]);
const write = useCallback(async (data: string) => {
if (!state.activeSessionId) return;
await writeToTerminal(state.activeSessionId, data);
}, [state.activeSessionId]);
return {
sessions: state.sessions,
activeSessionId: state.activeSessionId,
createSession,
write,
};
};
Project Scanning
use std::path::Path;
use walkdir::WalkDir;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct ProjectContext {
pub languages: Vec<String>,
pub frameworks: Vec<String>,
pub dependencies: Vec<String>,
pub structure: String,
}
pub fn scan_project(path: &Path) -> Result<ProjectContext, Box<dyn std::error::Error>> {
let mut languages = Vec::new();
let mut frameworks = Vec::new();
let mut dependencies = Vec::new();
if path.join("package.json").exists() {
languages.push("JavaScript".to_string());
let package_json = std::fs::read_to_string(path.join("package.json"))?;
let package: serde_json::Value = serde_json::from_str(&package_json)?;
if let Some(deps) = package.get("dependencies") {
if deps.get("react").is_some() {
frameworks.push("React".to_string());
}
if deps.get("vue").is_some() {
frameworks.push("Vue".to_string());
}
}
}
if path.join("Cargo.toml").exists() {
languages.push("Rust".to_string());
}
let mut file_counts = std::collections::HashMap::new();
for entry in WalkDir::new(path).max_depth(3) {
if let Ok(entry) = entry {
if let Some(ext) = entry.path().extension() {
*file_counts.entry(ext.to_string_lossy().to_string()).or_insert(0) += 1;
}
}
}
Ok(ProjectContext {
languages,
frameworks,
dependencies,
structure: format!("{:?}", file_counts),
})
}
Testing
Frontend Tests
npm run test
npm run test:coverage
npm run test:watch
import { render, screen } from '@testing-library/react';
import { TerminalComponent } from '../Terminal';
describe('TerminalComponent', () => {
it('renders terminal container', () => {
render(<TerminalComponent />);
const container = screen.getByClassName('terminal-container');
expect(container).toBeInTheDocument();
});
});
Backend Tests
cd src-tauri
cargo test
cargo test -- --nocapture
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pty_session_creation() {
let session = PtySession::new("/bin/bash", "/tmp");
assert!(session.is_ok());
let session = session.unwrap();
assert!(!session.id.is_empty());
}
#[tokio::test]
async fn test_write_to_pty() {
let session = PtySession::new("/bin/bash", "/tmp").unwrap();
let result = session.write(b"echo test\n").await;
assert!(result.is_ok());
}
}
Troubleshooting
Build Issues
Error: webkit2gtk not found
sudo apt install libwebkit2gtk-4.1-dev
pkg-config --modversion webkit2gtk-4.1
Error: Rust toolchain not found
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
Error: node-gyp failures on Windows
npm install --global windows-build-tools
npm config set msvs_version 2019
Runtime Issues
Terminal not rendering
useEffect(() => {
console.log('Terminal ref:', terminalRef.current);
if (!terminalRef.current) {
console.error('Terminal container not found');
return;
}
}, []);
PTY session hangs
use tokio::time::{timeout, Duration};
let result = timeout(
Duration::from_secs(5),
session.write(data)
).await;
if result.is_err() {
eprintln!("PTY write timed out");
}
Git authentication fails
ssh-add -l
git config --global credential.helper manager
Performance Issues
High CPU usage
Memory leaks
useEffect(() => {
const term = new Terminal();
return () => {
term.dispose();
};
}, []);
Environment Variables
export ANTHROPIC_API_KEY=sk-ant-...
export HERMES_SHELL=/bin/zsh
export RUST_LOG=debug
export HERMES_DEBUG=1
export HERMES_CONFIG_DIR=$HOME/.config/hermes
Contributing
git clone https://github.com/YOUR_USERNAME/hermes-ide.git
cd hermes-ide
git checkout -b feature/my-feature
npm run tauri dev
npm run test
cd src-tauri && cargo test
npx tsc --noEmit
git commit -m "feat: add new terminal feature"
git push origin feature/my-feature
Read CONTRIBUTING.md and DESIGN_PRINCIPLES.md before contributing.
Resources