### Install Tailscale on Linux Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Install Tailscale on Linux by piping the official install script to sh. Sign in to Tailscale afterwards. ```bash curl -fsSL https://tailscale.com/install.sh | sh ``` -------------------------------- ### Remote Access Setup Guide Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Provides instructions for setting up remote access to Hermes UI using Tailscale. Emphasizes security and ease of use without port forwarding. ```javascript
{'๐ŸŒ'} Access Hermes from anywhere
Use Tailscale to securely access Hermes UI from your phone, tablet, or any device โ€” no port forwarding, no exposing your machine to the internet.
Setup Guide
1
Install Tailscale
``` -------------------------------- ### Install and Configure Tailscale Source: https://github.com/pyrate-llama/hermes-ui/blob/main/README.md Commands to install Tailscale on the server and expose the Hermes UI service. ```bash brew install tailscale # macOS # or: curl -fsSL https://tailscale.com/install.sh | sh # Linux tailscale up ``` ```text http://100.x.x.x:3333/hermes-ui.html ``` ```bash tailscale serve --bg 3333 # Accessible at https://your-machine.tail1234.ts.net ``` -------------------------------- ### Install Tailscale on macOS Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Use Homebrew to install Tailscale on macOS. Ensure you are signed in to Tailscale afterwards. ```bash brew install tailscale ``` -------------------------------- ### Remote Access with Tailscale Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Commands to install, start, and expose the Hermes UI securely over a Tailscale VPN. ```bash # Install Tailscale on your server brew install tailscale # macOS # or: curl -fsSL https://tailscale.com/install.sh | sh # Linux # Start Tailscale tailscale up # Get your Tailscale IP tailscale ip # Output: 100.x.x.x # Access from any Tailscale-connected device # http://100.x.x.x:3333/hermes-ui.html # Optional: Enable HTTPS with Tailscale Serve tailscale serve --bg 3333 # Now accessible at: https://your-machine.tail1234.ts.net ``` -------------------------------- ### Start Hermes UI Gateway Proxy Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Clone the repository and start the gateway proxy server. You can specify a custom port if needed. Open the provided URL in your browser to access the UI. ```bash git clone https://github.com/pyrate-llama/hermes-ui.git cd hermes-ui python3 serve.py # Or specify a custom port python3 serve.py 8080 # Open in browser open http://localhost:3333/hermes-ui.html ``` -------------------------------- ### Sign in to Tailscale Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html After installation on any device, run this command to sign in to your Tailscale account. ```bash tailscale up ``` -------------------------------- ### Start Streaming Chat Session Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Initiate a chat session with the AI agent using a two-step SSE flow. The first step starts the chat and returns a stream ID, and the second step connects to the SSE stream to receive responses. ```bash # Step 1: Start chat and get stream_id curl -X POST http://localhost:3333/api/chat/start \ -H "Content-Type: application/json" \ -d '{ "messages": [{"role": "user", "content": "Hello, what can you do?"}], "session_id": "my_session_123" }' # Response: { "stream_id": "abc123def456", "session_id": "my_session_123" } # Step 2: Connect to SSE stream curl -N "http://localhost:3333/api/chat/stream?stream_id=abc123def456" # SSE Events received: event: token data: {"text": "Hello"} event: token data: {"text": "! I'm"} event: tool data: {"name": "web_search", "preview": "Searching...", "args": {"query": "weather"}} event: tool_complete data: {"name": "web_search", "duration": 1.2} event: reasoning data: {"text": "Let me think about this..."} event: done data: {"session": {"session_id": "my_session_123", "messages": [...]}, "usage": {"input_tokens": 150, "output_tokens": 89}} ``` -------------------------------- ### Handle Tool Event Started Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Processes the start of a tool event, updating the UI and logging the event. Resets the stall timer and adds the tool call to the state. ```javascript function(toolEvent) { if (toolEvent.type === 'started') { if (!thinkingGotResponse) { thinkingGotResponse = true; } lastChunkTime = Date.now(); // Reset stall timer โ€” tools count as activity addLocalLog('INFO [tool] ' + (toolEvent.label || toolEvent.toolName) + ' started'); toolCalls.push({ toolName: toolEvent.toolName, label: toolEvent.label, timestamp: new Date(), done: false, }); // Update the thinking/streaming message with tool calls updateStreamMessages(sessionId, function(prev) { var updated = [...prev]; var last = updated[updated.length - 1]; if (last && last.role === 'assistant') { updated[updated.length - 1] = { ...last, thinking: false, toolCalls: toolCalls.slice() }; } return updated; }); setActivityLog(function(prev) { return [...prev, { id: Date.now() + Math.random(), emoji: '\u2699\uFE0F', label: toolEvent.label || toolEvent.toolName, timestamp: new Date(), done: false, }]; }); } else if (toolEvent.type === 'completed') { var resultPreview = ''; if (toolEvent.result) { var rs = typeof toolEvent.result === 'string' ? toolEvent.result : JSON.stringify(toolEvent.result); rs = rs.replace(/\\n/g, ' ').replace(/\n/g, ' ').replace(/\s+/g, ' ').trim(); if (rs.length > 200) rs = rs.slice(0, 200) + '...'; resultPreview = ' => ' + rs; } addLocalLog('INFO [tool] ' + (toolEvent.toolName || 'tool') + ' completed' + resultPreview); var lastTc = toolCalls[toolCalls.length - 1]; if (lastTc) { lastTc.result = toolEvent.result; lastTc.done = true; } setActivityLog(function(prev) { const updated = [...prev]; const lastEntry = updated[updated.length - 1]; if (lastEntry) lastEntry.done = true; return updated; }); } } ``` -------------------------------- ### Skills API - List All Skills Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Lists all installed Hermes skills, including their name, category, description, and disabled status. ```bash # List all skills curl http://localhost:3333/api/skills ``` ```json { "skills": [ { "name": "web-search", "category": "tools", "description": "Search the web using DuckDuckGo", "disabled": false }, { "name": "code-review", "category": "development", "description": "Review code for best practices", "disabled": false } ], "total": 42 } ``` -------------------------------- ### Clone and Run Hermes UI Source: https://github.com/pyrate-llama/hermes-ui/blob/main/README.md Clone the repository, navigate to the directory, and start the Python proxy server. You can specify a custom port if needed. Open the provided URL in your browser to access the UI. ```bash git clone https://github.com/pyrate-llama/hermes-ui.git cd hermes-ui python3 serve.py # Or specify a custom port python3 serve.py 8080 ``` -------------------------------- ### Skills API Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Lists and manages installed Hermes skills. ```APIDOC ## Skills API ### Description Lists and manages installed Hermes skills. #### GET /api/skills ### Method GET ### Endpoint `/api/skills` ### Response #### Success Response (200) - **skills** (array) - A list of installed skills. - **name** (string) - The name of the skill. - **category** (string) - The category of the skill. - **description** (string) - A brief description of the skill. - **disabled** (boolean) - Indicates if the skill is disabled. - **total** (integer) - The total number of skills available. ### Response Example ```json { "skills": [ { "name": "web-search", "category": "tools", "description": "Search the web using DuckDuckGo", "disabled": false }, { "name": "code-review", "category": "development", "description": "Review code for best practices", "disabled": false } ], "total": 42 } ``` #### GET /api/skills/{skill_name} ### Method GET ### Endpoint `/api/skills/{skill_name}` #### Path Parameters - **skill_name** (string) - Required - The name of the skill to retrieve details for. ### Response #### Success Response (200) - **name** (string) - The name of the skill. - **category** (string) - The category of the skill. - **success** (boolean) - Indicates if the skill details were retrieved successfully. - **content** (string) - Detailed content or description of the skill. - **files** (array of strings) - List of files associated with the skill. ### Response Example ```json { "name": "web-search", "category": "tools", "success": true, "content": "# Web Search Skill\n\nThis skill allows searching the web...", "files": ["SKILL.md", "search.py"] } ``` #### GET /skills/dates ### Method GET ### Endpoint `/skills/dates` ### Response #### Success Response (200) - **dates** (object) - An object containing skill names as keys and their modification timestamps as values. ### Response Example ```json { "dates": { "web-search": 1705312345.123, "code-review": 1705298765.456 } } ``` ``` -------------------------------- ### Skills API - Get Skill Details Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Retrieves detailed information for a specific skill, including its content and associated files. ```bash # Get skill details curl http://localhost:3333/api/skills/web-search ``` ```json { "name": "web-search", "category": "tools", "success": true, "content": "# Web Search Skill\n\nThis skill allows searching the web...", "files": ["SKILL.md", "search.py"] } ``` -------------------------------- ### Start Streaming and Activity Logging Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Initiates a streaming request to Hermes and logs the action. This function is typically called after sending a user message. ```javascript const abortCtrl = new AbortController(); startStreaming(sessionId, abortCtrl); setActivityLog([]); addLocalLog('INFO Sending message to session ' + sessionId); ``` -------------------------------- ### Chat API - Start Streaming Session Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Initiates a chat session and allows for real-time streaming of responses via Server-Sent Events (SSE). ```APIDOC ## POST /api/chat/start ### Description Starts a new chat session with the AI agent and returns a `stream_id` for subsequent interactions. This is the first step in a two-step SSE streaming flow. ### Method POST ### Endpoint `/api/chat/start` ### Parameters #### Request Body - **messages** (array) - Required - An array of message objects, each with 'role' and 'content'. - **session_id** (string) - Optional - An identifier for the chat session. ### Request Example ```json { "messages": [{"role": "user", "content": "Hello, what can you do?"}], "session_id": "my_session_123" } ``` ### Response #### Success Response (200) - **stream_id** (string) - A unique identifier for the streaming session. - **session_id** (string) - The identifier for the chat session. #### Response Example ```json { "stream_id": "abc123def456", "session_id": "my_session_123" } ``` ## GET /api/chat/stream ### Description Connects to the Server-Sent Events (SSE) stream for a given chat session using the `stream_id` obtained from `/api/chat/start`. This endpoint streams tokens, tool calls, reasoning, and session completion events. ### Method GET ### Endpoint `/api/chat/stream` ### Query Parameters - **stream_id** (string) - Required - The ID of the streaming session to connect to. ### Response #### Success Response (200 - SSE Stream) Events are streamed in Server-Sent Events format. Common event types include: - **token**: Contains a chunk of text generated by the AI. - **tool**: Indicates a tool call with its arguments. - **tool_complete**: Signals the completion of a tool execution. - **reasoning**: Provides the AI's thought process. - **done**: Marks the end of the session, including final messages and usage statistics. #### Response Example (SSE Events) ``` event: token data: {"text": "Hello"} event: token data: {"text": "! I'm"} event: tool data: {"name": "web_search", "preview": "Searching...", "args": {"query": "weather"}} event: tool_complete data: {"name": "web_search", "duration": 1.2} event: reasoning data: {"text": "Let me think about this..."} event: done data: {"session": {"session_id": "my_session_123", "messages": [...]}, "usage": {"input_tokens": 150, "output_tokens": 89}} ``` ``` -------------------------------- ### Memory API - Get Entries Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Retrieves memory entries for both general memory and user profile. Displays entries and their character usage. ```bash # Get memory entries curl http://localhost:3333/api/memory ``` ```json { "targets": [ { "target": "memory", "entries": ["User prefers concise responses", "Works on Python projects"], "usage": "156/4000 chars" }, { "target": "user", "entries": ["Name: Alex", "Timezone: UTC-5"], "usage": "45/1375 chars" } ] } ``` -------------------------------- ### Start Hermes UI Direct Mode Server Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Run Hermes UI in direct mode, connecting directly to the AIAgent without a gateway. You can specify a custom port for the server. ```bash # Start lite server on default port python3 serve_lite.py # Or specify custom port python3 serve_lite.py --port 8080 # Example output: # โ•ญโ”€ Hermes UI Direct โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ # โ”‚ UI: http://127.0.0.1:3333 โ”‚ # โ”‚ Agent: โœ“ loaded โ”‚ # โ”‚ Model: anthropic/claude-sonnet โ”‚ # โ”‚ Provider: minimax โ”‚ # โ”‚ Mode: Direct (no gateway) โ”‚ # โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ``` -------------------------------- ### Create a New Conversation Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Initializes a new chat conversation by creating a session, setting up the conversation object, and resetting the input and messages state. This is used when starting a fresh chat. ```javascript const newConversation = () => { const sessionId = api.createSession(); const conv = { id: sessionId, title: 'New Chat', created: new Date().toLocaleString(), messages: [] }; setConversations(prev => [conv, ...prev]); setActiveConvId(sessionId); setMessages([]); setInput(''); }; ``` -------------------------------- ### Skills API - Get Modification Timestamps Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Fetches the modification timestamps for installed skills, useful for sorting skills by recency. ```bash # Get skill modification timestamps (for sorting by newest) curl http://localhost:3333/skills/dates ``` ```json { "dates": { "web-search": 1705312345.123, "code-review": 1705298765.456 } } ``` -------------------------------- ### Enable HTTPS with Tailscale Serve Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Run this command to enable HTTPS for Hermes UI, providing a secure connection and a clean URL. Traffic is served in the background. ```bash tailscale serve --bg 3333 ``` -------------------------------- ### Troubleshoot Context Compression Source: https://github.com/pyrate-llama/hermes-ui/blob/main/README.md Fixes for agent hangs caused by incorrect summary base URL configurations. ```yaml compression: summary_base_url: null # โ† this causes a 404 and hangs the agent ``` ```yaml compression: summary_base_url: https://api.minimax.io/anthropic ``` ```bash hermes restart ``` -------------------------------- ### Initialize Refs Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Ref declarations for DOM elements and persistent data storage. ```javascript const textareaRef = useRef(null); const messagesEndRef = useRef(null); const fileInputRef = useRef(null); // abortControllerRef removed โ€” now in streamingMap per-session const messagesRef = useRef([]); const activeConvRef = useRef(null); const conversationsRef = useRef([]); const activityLogRef = useRef(null); ``` -------------------------------- ### Render Model Status UI Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Displays model information, uptime, and installed skills using inline styles. ```jsx style={{display:'flex',alignItems:'center',gap:'12px'}}>
{'๐Ÿง '}
{modelName}
Active model
Uptime
{uptime}
Skills
{skills.length}
Installed Skills ({skills.length})
{skills.map(function(skill, i) { return {skill.name}; })} {skills.length === 0 && No skills installed}
); } ``` -------------------------------- ### Browse File System Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Browse the ~/.hermes directory tree to view configuration files, logs, and memories. You can list the root directory or a specific subdirectory by providing the 'path' parameter. ```bash # List root of ~/.hermes curl "http://localhost:3333/browse" # Response: { "items": [ {"name": "config.yaml", "type": "file", "path": "config.yaml", "size": 1234}, {"name": "logs", "type": "dir", "path": "logs"}, {"name": "memories", "type": "dir", "path": "memories"}, {"name": "skills", "type": "dir", "path": "skills"} ], "path": "" } # Browse subdirectory curl "http://localhost:3333/browse?path=logs" # Response: { "items": [ {"name": "gateway.log", "type": "file", "path": "logs/gateway.log", "size": 45678}, {"name": "errors.log", "type": "file", "path": "logs/errors.log", "size": 1234} ], "path": "logs" } ``` -------------------------------- ### Get Tailscale IP Address Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Run this command on your server to find its Tailscale IP address, which you will use to access Hermes UI. ```bash tailscale ip ``` -------------------------------- ### Remote Access with Tailscale Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Instructions for setting up secure remote access to the Hermes UI using Tailscale. ```APIDOC ## Remote Access with Tailscale ### Description Access Hermes UI securely from any device using Tailscale VPN. This involves installing Tailscale, starting the service, and then accessing the UI via the Tailscale IP address. ### Installation ```bash # macOS brew install tailscale # Linux curl -fsSL https://tailscale.com/install.sh | sh ``` ### Starting Tailscale ```bash tailscale up ``` ### Getting Your Tailscale IP ```bash tailscale ip ``` ### Accessing the UI Access the UI using the command `http://:3333/hermes-ui.html`. ### Enabling HTTPS (Optional) To enable HTTPS, use Tailscale Serve: ```bash tailscale serve --bg 3333 ``` This will provide an HTTPS URL like `https://your-machine.tail1234.ts.net`. ``` -------------------------------- ### Initialize UI State Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html State hooks for managing UI components, themes, and session data. ```javascript const [personality, setPersonality] = useState('default'); const [connected, setConnected] = useState(null); const [showSettings, setShowSettings] = useState(false); const [pendingImages, setPendingImages] = useState([]); const [lightboxSrc, setLightboxSrc] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => window.innerWidth <= 768); const [activityLog, setActivityLog] = useState([]); const [activityPanelOpen, setActivityPanelOpen] = useState(() => { try { const saved = localStorage.getItem('hermes-activity-panel'); return saved ? JSON.parse(saved) : false; } catch(e) { return false; } }); const [sessionTokens, setSessionTokens] = useState({ input: 0, output: 0 }); const [editingMessageId, setEditingMessageId] = useState(null); const [editingContent, setEditingContent] = useState(''); const [showMCPBrowser, setShowMCPBrowser] = useState(false); const [showTerminal, setShowTerminal] = useState(false); const [artifacts, setArtifacts] = useState([]); const [artifactPanelWide, setArtifactPanelWide] = useState(false); const [localLogs, setLocalLogs] = useState([]); var addLocalLog = function(text) { var ts = new Date().toLocaleTimeString(); setLocalLogs(function(prev) { var next = prev.concat([{ id: Date.now() + Math.random(), line: '[' + ts + '] ' + text }]); if (next.length > 500) next = next.slice(next.length - 500); return next; }); }; const [mcpTools, setMcpTools] = useState([]); const [searchMode, setSearchMode] = useState(false); const [currentTheme, setCurrentTheme] = useState(getStoredTheme); const [showShortcuts, setShowShortcuts] = useState(false); const [showWelcome, setShowWelcome] = useState(function() { try { return !localStorage.getItem('hermes-ui-welcomed'); } catch(e) { return false; } }); ``` -------------------------------- ### Cancel Chat Session Source: https://context7.com/pyrate-llama/hermes-ui/llms.txt Cancel an ongoing streaming chat session using either a GET or POST request with the stream ID. ```bash # Cancel via GET curl "http://localhost:3333/api/chat/cancel?stream_id=abc123def456" # Or via POST curl -X POST http://localhost:3333/api/chat/cancel \ -H "Content-Type: application/json" \ -d '{"stream_id": "abc123def456"}' # Response: { "ok": true, "cancelled": true, "stream_id": "abc123def456" } ``` -------------------------------- ### System Architecture Diagram Source: https://github.com/pyrate-llama/hermes-ui/blob/main/README.md Visual representation of the browser, proxy, and agent communication flow. ```text โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Browser โ”‚โ”€โ”€โ”€โ–ถโ”‚ serve.py โ”‚โ”€โ”€โ”€โ–ถโ”‚ Hermes Agent โ”‚ โ”‚ (React 18) โ”‚ โ”‚ port 3333 โ”‚ โ”‚ port 8642 โ”‚ โ”‚ โ”‚โ—€โ”€โ”€โ”€โ”‚ proxy + โ”‚โ—€โ”€โ”€โ”€โ”‚ (WebAPI) โ”‚ โ”‚ Single HTML โ”‚ โ”‚ log stream โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -------------------------------- ### StatusBar Component Implementation Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Displays system metrics including model, memory usage, session count, and uptime. Fetches data from the API every 30 seconds. ```javascript function StatusBar({ connected, conversations }) { var [stats, setStats] = useState({ model: '...', memory: 0, sessions: 0, uptime: '' }); useEffect(function() { var fetchStats = async function() { try { var [memR, healthR] = await Promise.all([ fetch(API_BASE + '/api/memory').catch(function() { return null; }), fetch(API_BASE + '/health').catch(function() { return null; }) ]); var memCount = 0, model = '...', uptime = ''; if (memR && memR.ok) { var md = await memR.json(); if (md.targets) { for (var t = 0; t < md.targets.length; t++) { memCount += (md.targets[t].entries || []).length; } } else { memCount = (md.memories || md.items || []).length; } } if (healthR && healthR.ok) { var hd = await healthR.json(); model = hd.model || hd.default_model || model; if (hd.uptime) { var hrs = Math.floor(hd.uptime / 3600); var mins = Math.floor((hd.uptime % 3600) / 60); uptime = hrs + 'h ' + mins + 'm'; } } var sessCount = conversations ? conversations.length : 0; setStats({ model: model, memory: memCount, sessions: sessCount, uptime: uptime }); } catch(e) {} }; fetchStats(); var iv = setInterval(fetchStats, 30000); return function() { clearInterval(iv); }; }, []); return (
{connected === false ? 'Offline' : 'Connected'}
{'๐Ÿง '} {stats.model}
{'๐Ÿ’พ'} {stats.memory} memories
{'๐Ÿ’ฌ'} {stats.sessions} sessions
{stats.uptime && (
{'โฑ'} {stats.uptime}
)}
Hermes v1.0
); } ``` -------------------------------- ### Detect Tool Progress with Regex in JavaScript Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Identifies tool progress markers within text deltas using a regular expression. This function is used to detect when a tool starts or is performing an action. ```javascript var toolMarkerRegex = /^`[\p{Emoji}\uFE0F\u200D]*\s*(.+?)`$/u; var pendingToolDelta = ''; function detectToolProgress(delta) { var trimmed = delta.trim(); if (!trimmed) return false; var match = trimmed.match(toolMarkerRegex); if (match) { var toolLabel = match[1].trim(); if (onToolEvent) { onToolEvent({ type: 'started', toolName: toolLabel, label: toolLabel }); } if (onThinking) { onThinking('[tool] ' + toolLabel); } return true; } return false; } ``` -------------------------------- ### Initialize EventSource for Chat Stream in JavaScript Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Sets up an EventSource connection to the chat API stream. It handles various events like 'token', 'reasoning', 'tool', 'done', and 'error' to process the chat response in real-time. ```javascript return new Promise(function(resolve, reject) { var source = new EventSource(API_BASE + '/api/chat/stream?stream_id=' + encodeURIComponent(streamId)); if (signal) { signal.addEventListener('abort', function() { source.close(); fetch(API_BASE + '/api/chat/cancel?stream_id=' + encodeURIComponent(streamId)).catch(function(){}); wrappedOnDone(fullContent); resolve({ content: fullContent, usage: streamUsage }); }); } source.addEventListener('token', function(e) { var d = JSON.parse(e.data); if (d.text) { fullContent = processReasoningDelta(d.text); onChunk(fullContent); } }); source.addEventListener('reasoning', function(e) { var d = JSON.parse(e.data); if (d.text && onThinking) { onThinking('[reasoning] ' + d.text.replace(/\n/g, ' ').trim().slice(0, 300)); } }); source.addEventListener('tool', function(e) { var d = JSON.parse(e.data); if (onToolEvent) onToolEvent({ type: 'started', toolName: d.name, label: d.preview || d.name, args: d.args }); if (onThinking) onThinking('[tool] ' + (d.name || 'tool')); }); source.addEventListener('tool_complete', function(e) { var d = JSON.parse(e.data); if (onToolEvent) onToolEvent({ type: 'completed', toolName: d.name, label: d.preview || d.name, duration: d.duration }); }); source.addEventListener('approval', function(e) { var d = JSON.parse(e.data); console.log('[stream] Approval requested:', d); if (onThinking) onThinking('[approval] ' + (d.description || d.command || 'Tool approval needed')); fetch(API_BASE + '/api/approval/respond', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, approved: true }), }).catch(function(err) { console.warn('[stream] Auto-approve failed:', err); }); }); source.addEventListener('done', function(e) { source.close(); var d = JSON.parse(e.data); if (d.usage) streamUsage = d.usage; if (d.session && d.session.messages && d.session.messages.length) { var lastAssistant = null; for (var i = d.session.messages.length - 1; i >= 0; i--) { if (d.session.messages[i].role === 'assistant') { var c = d.session.messages[i].content || ''; if (typeof c === 'string') lastAssistant = c; else if (Array.isArray(c)) lastAssistant = c.filter(function(p){return p&&p.type==='text';}).map(function(p){return p.text||'';}).join('\n'); break; } } if (lastAssistant && lastAssistant.trim()) fullContent = lastAssistant; } wrappedOnDone(fullContent); resolve({ content: fullContent, usage: streamUsage, session: d.session }); }); source.addEventListener('apperror', function(e) { source.close(); var d = JSON.parse(e.data); var errMsg = d.message || 'Agent error'; console.error('[stream] apperror:', errMsg); fullContent = '**Error:** ' + errMsg; onChunk(fullContent); wrappedOnDone(fullContent); resolve({ content: fullContent, usage: streamUsage }); }); source.addEventListener('cancel', function(e) { source.close(); wrappedOnDone(fullContent); resolve({ content: fullContent, usage: streamUsage }); }); source.addEventListener('error' ``` -------------------------------- ### Render MCP Tool List Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Displays a list of configured MCP tools with their status or a fallback message if none are available. ```jsx
{tool.name || 'Unknown Tool'}
{tool.description || 'No description'}
{'โœ“ Connected'}
))} ) : (

No MCP tools configured

)}
); } ``` -------------------------------- ### CSS for Responsive Design and UI Elements Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Contains CSS rules for various UI elements, adapting styles for different screen sizes and touch targets. Includes styles for messages, navigation, headers, inputs, and file views. ```css font-size: 24px; } .message-header { gap: 6px; } .message-avatar { width: 28px; height: 28px; font-size: 12px; } / * Larger touch targets */ .nav-item { padding: 10px 14px; min-height: 44px; } .header-btn { min-height: 36px; min-width: 36px; } .send-btn, .attach-btn { min-height: 40px; min-width: 40px; } / * Compact header */ .header { padding: 10px 12px; } .sidebar-header h1 { font-size: 18px; } .sidebar-close-btn { min-width: 40px !important; min-height: 40px !important; font-size: 18px !important; } / * Hide desktop-only controls on mobile */ .hide-mobile { display: none !important; } / * Add left padding to header so title clears the floating hamburger */ .header { padding-left: 52px; } / * Slim down header controls */ .header-controls { gap: 2px; flex-wrap: nowrap; } .token-counter { display: none; } .theme-switcher { display: none; } / * Hide input hint on mobile */ .input-hint { display: none; } / * Input area */ .input-area { padding: 10px 12px; } textarea { font-size: 16px; } / * prevents iOS zoom on focus */ / * Skills view mobile */ .skills-header .skills-title { font-size: 18px; } .view-skills > div:nth-child(2) { flex-wrap: wrap; } / * Header title truncation */ .header-title { font-size: 14px; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } / * Always show conv action buttons on mobile (no hover) */ .conv-item-delete, .conv-item-rename { opacity: 0.7 !important; } / * Files view: stack panels vertically */ .files-layout-mobile { flex-direction: column !important; } .files-tree-panel-mobile { width: 100% !important; border-right: none !important; border-bottom: 1px solid var(--border); max-height: 55vh; } .files-content-panel-mobile { flex: 1; min-height: 200px; } } @media (max-width: 390px) { .stat-card { padding: 12px; } .message-content { padding: 10px 12px; font-size: 13px; } .input-wrapper { border-radius: 10px; } .mobile-nav-item { min-width: 44px; padding: 6px 6px; font-size: 9px; } } ``` -------------------------------- ### File Tree and Search Panel Source: https://github.com/pyrate-llama/hermes-ui/blob/main/hermes-ui.html Renders the main file tree panel, including a search input and a refresh button. It displays the file structure, handles loading states, and shows 'No files found' or 'Loading...' messages as appropriate. ```javascript var displayItems = rootItems.filter(function(f) { if (!searchTerm) return true; return f.name.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1; }); return (
~/.hermes/
{rootItems.length === 0 && !loadingDirs[''] ? (
No files found
) : loadingDirs[''] ? (
Loading...
) : displayItems.map(function(item) { return renderItem(item, 0); })}
{selectedFilePath ? (
{fileType === 'image' ? '๐Ÿ–ผ' : '๐Ÿ“„'}
{fileMetadata ? fileMetadata.name : ''}
{'~/.hermes/' + (fileMetadata ? fileMetadata.path : '')}{fileMetadata && fileMetadata.size ? ' ยท ' + formatSize(fileMetadata.size) : ''}
{fileType === 'text' && !isEditing && ( )} {isEditing && (
{saveStatus === 'saved' && Saved!} {saveStatus === 'error' && Error saving}
)}
{fileType === 'text' ? (