### Install Agent Hub from Source
Source: https://github.com/potarix/agent-hub/blob/main/README.md
Instructions for cloning the repository, installing dependencies, and starting the application from source. Includes a detached launcher script.
```bash
git clone https://github.com/Potarix/agent-hub.git
cd agent-hub
npm install
npm start
```
```bash
./launch.sh
```
--------------------------------
### Install Agent Hub Prerequisites
Source: https://github.com/potarix/agent-hub/blob/main/README.md
Install the necessary agent CLIs for Agent Hub to function. After installation, log in or configure API keys.
```bash
npm install -g @anthropic-ai/claude-code # Claude Code (Anthropic)
```
```bash
npm install -g @openai/codex # Codex (OpenAI)
```
--------------------------------
### Start Agent Hub in Background
Source: https://github.com/potarix/agent-hub/blob/main/README.md
Use this command to start the Agent Hub application in the background.
```bash
npm run start-bg
```
--------------------------------
### Chat View Component Setup
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Initializes state variables and refs for the ChatView component, including input handling, streaming text, tool usage, and permissions.
```javascript
function ChatView({ agent, isActive, status, messages, setMessages, onUpdateAgent, completionStatus, setCompletionStatus, onLoadingChange, setLastActivityTime, findApiRef }) {
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const [streamText, setStreamText] = useState('');
const [streamThinking, setStreamThinking] = useState('');
const [toolUses, setToolUses] = useState([]);
const [permDenials, setPermDenials] = useState(null);
const [permRequests, setPermRequests] = useState([]);
// real-time approval requests
const [resolvedPerms, setResolvedPerms] = useState({}); // toolUseId -> 'approved'|'denied'
const [lastUserMessage, setLastUserMessage] = useState('');
const [sessionAllowedTools, setSessionAllowedTools] = useState([]);
const [alwaysAllowedTools, setAlwaysAllowedTools] = useState(new Set()); // tools auto-approved for this session
const [slashCommands, setSlashCommands] = useState([]);
const [showSlash, setShowSlash] = useState(false);
const [slashIdx, setSlashIdx] = useState(0);
const [showThinking, setShowThinking] = useState(true);
const [selectedImages, setSelectedImages] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [modelOpen, setModelOpen] = useState(false);
const [effortOpen, setEffortOpen] = useState(false);
const [codexModels, setCodexModels] = useState([]);
const [codexDefaultModel, setCodexDefaultModel] = useState('');
const [modelLoadState, setModelLoadState] = useState('idle');
const [modelLoadError, setModelLoadError] = useState('');
const bottomRef = useRef(null);
const inputRef = useRef(null);
const chatAreaRef = useRef(null);
const streamTextRef = useRef('');
const streamThinkingRef = useRef('');
const dragCounter = useRef(0);
const toolUsesRef = useRef([]);
const activeRequestIdRef = useRef(null);
const alwaysAllowedToolsRef = useRef(new Set());
const modelRef = useRef(null);
const effortRef = useRef(null);
const isClaudeProvider = isClaudeCodeProvider(agent.provider);
const isCodexAgent = isCodexProvider(agent.provider);
const supportsFileAttachments = isFileAttachmentProvider(agent.provider);
const hasModelPicker = isClaudeProvider || isCodexAgent;
const hasEffortPicker = isClaudeProvider || isCodexAgent;
// Close model/effort dropdowns on outside click
useEffect(() => {
const handler = (e) => {
if (modelRef.current && !modelRef.current.contains(e.target)) setModelOpen(false);
if (effortRef.current && !effortRef.current.contains(e.target)) setEffortOpen(false);
};
document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, []);
// Register the chat find engine while this view is active. The engine
// closes over chatAreaRef.current so it always sees the live container.
useEffect(() => {
if (!isActive || !findApiRef) return;
const engine = chatFindEngine(() => chatAreaRef.current);
findApiRef.current = engine;
return () => {
try {
engine.clear();
} catch {}
if (findApiRef.current === engine) findApiRef.current = null;
};
}, [isActive, agent.id]);
// Re-run the active query when messages change (streaming output, etc.)
// so the highlight count stays accurate.
useEffect(() => {
if (!isActive || !findApiRef) return;
const api = findApiRef.current;
if (!api || api.type !== 'chat') return;
const t = setTimeout(() => {
try {
api.refresh();
} catch {}
}, 80);
return () => clearTimeout(t);
}, [isActive, messages, streamText, streamThinking]);
useEffect(() => {
if (!isCodexAgent) {
setCodexModels([]);
setCodexDefaultModel('');
setModelLoadState('idle');
setModelLoadError('');
return;
}
let cancelled = false;
setModelLoadState('loading');
setModelLoadError('');
window.agentHub.listModels(agent)
.then(res => {
if (cancelled) return;
const models = Array.isArray(res?.models) ? res.models : [];
setCodexModels(models);
setCodexDefaultModel(res?.defaultModel || models[0]?.id || '');
setModelLoadError(res?.error || '');
setModelLoadState('ready');
})
.catch(err => {
if (cancelled) return;
setCodexModels([]);
setCodexDefaultModel('');
setModelLoadError(err.message || 'Could not load Codex models.');
setModelLoadState('error');
});
return () => {
cancelled = true;
};
}, [isCodexAgent, agent.id, agent.provider, agent.codexPath, agent.sshHost, agent.sshUser, agent.sshPort, agent.sshKey, agent.apiKey]);
const discoveredModelAliases
```
--------------------------------
### Run Development Server
Source: https://github.com/potarix/agent-hub/blob/main/CONTRIBUTING.md
Use these commands to run the application locally. `npm run dev` enables development tools, while `npm start` runs it normally.
```bash
npm run dev # Run with dev tools enabled
```
```bash
npm start # Run normally
```
--------------------------------
### Local DMG Build for macOS
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Installs dependencies and builds an unsigned DMG for local testing on macOS. Artifacts are placed in the 'dist/' directory.
```bash
npm install
CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist:mac
```
--------------------------------
### Initialize and Spawn Agent Terminal
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Initializes the xterm.js terminal, installs necessary addons, and spawns the agent's process when the component becomes active and the container is visible. It also registers the terminal with global handlers for copy/paste and active terminal tracking. Ensure 'window.agentHub.terminalSpawn' and 'fitAddonRef.current.fit' are available.
```javascript
if (!isActive || !xtermRef.current || !containerRef.current) return;
if (!openedRef.current) {
xtermRef.current.open(containerRef.current);
installMouseSelectionBridge(xtermRef.current);
fitAddonRef.current.fit();
openedRef.current = true;
// Register this xterm so the App-level Cmd+C/V dispatcher can
// find the term + agentId from a focused .xterm DOM element.
const el = xtermRef.current.element;
if (el) {
if (!window._agentHubXterms) window._agentHubXterms = new WeakMap();
if (!window._agentHubXtermsByAgent) window._agentHubXtermsByAgent = new Map();
const entry = { term: xtermRef.current, agentId: agent.id };
window._agentHubXterms.set(el, { term: xtermRef.current, agentId: agent.id });
window._agentHubXtermsByAgent.set(agent.id, entry);
window._agentHubActiveXterm = entry;
const markActive = () => {
window._agentHubActiveXterm = entry;
};
el.addEventListener('mousedown', markActive);
el.addEventListener('focusin', markActive);
}
}
if (!spawnedRef.current) {
spawnedRef.current = true;
setExited(false);
window.agentHub.terminalSpawn(agent).then(() => {
const dims = fitAddonRef.current.proposeDimensions();
if (dims) window.agentHub.terminalResize(agent.id, dims.cols, dims.rows);
});
}
// Fit and focus
setTimeout(() => {
fitAddonRef.current?.fit();
xtermRef.current?.focus();
}, 30);
```
--------------------------------
### macOS Build Variants
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Provides commands to build DMGs for specific macOS architectures or a universal build. Use 'dist:mac:dmg' for manual installation tests.
```bash
npm run dist:mac:arm64
```
```bash
npm run dist:mac:x64
```
```bash
npm run dist:mac:universal
```
```bash
npm run dist:mac:dmg
```
--------------------------------
### Discover and Filter Slash Commands
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Dynamically discovers slash commands from the CLI and filters them based on user input. Shows the slash command interface when input starts with '/' and commands are available.
```javascript
// Discover slash commands dynamically from the CLI tool
useEffect(() => {
// Start with cached/local commands immediately
window.agentHub.getSlashCommands(agent.provider).then(cmds => {
setSlashCommands(cmds || []);
});
// Then discover full commands from the CLI in the background
window.agentHub.discoverSlashCommands(agent).then(cmds => {
if (cmds && cmds.length > 0) setSlashCommands(cmds);
}).catch(() => {}); // silently fall back to cached
}, [agent.provider, agent.id]);
// Filter slash commands based on input
const filteredCommands = React.useMemo(() => {
if (!input.startsWith('/')) return [];
const query = input.toLowerCase();
return slashCommands.filter(c => c.name.toLowerCase().startsWith(query) || query === '/');
}, [input, slashCommands]);
useEffect(() => {
setShowSlash(input.startsWith('/') && filteredCommands.length > 0 && !loading);
setSlashIdx(0);
}, [input, filteredCommands.length, loading]);
```
--------------------------------
### Initialize and Configure xterm.js Terminal
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Sets up a new xterm.js terminal instance with specified options and loads necessary addons like FitAddon and SearchAddon. It also includes event listeners for theme changes and custom key event handling.
```javascript
const term = new Terminal({
cursorBlink: true,
cursorStyle: 'bar',
fontSize: 14,
fontFamily: "'JetBrains Mono', 'SF Mono', 'Menlo', 'Monaco', monospace",
fontWeight: 400,
lineHeight: 1.2,
letterSpacing: 0,
theme: isDark() ? darkTheme : lightTheme,
allowProposedApi: true,
scrollback: 10000,
macOptionIsMeta: true,
macOptionClickForcesSelection: true,
});
const fitAddon = new FitAddon.FitAddon();
term.loadAddon(fitAddon);
let searchAddon = null;
try {
if (typeof SearchAddon !== 'undefined') {
searchAddon = new SearchAddon.SearchAddon();
term.loadAddon(searchAddon);
}
} catch {}
xtermRef.current = term;
fitAddonRef.current = fitAddon;
searchAddonRef.current = searchAddon;
const onTheme = (dark) => {
term.options.theme = dark ? darkTheme : lightTheme;
};
window.agentHub.onThemeChange(onTheme);
```
--------------------------------
### Configure Apple API Key for Signing
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Sets environment variables for API key authentication with Apple Developer services, required for signing and notarization.
```bash
export APPLE_API_KEY=/path/to/AuthKey_XXXXXXXXXX.p8
export APPLE_API_KEY_ID=XXXXXXXXXX
export APPLE_API_ISSUER=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
```
--------------------------------
### Update Panel Component
Source: https://github.com/potarix/agent-hub/blob/main/index.html
React component for displaying and managing Agent Hub updates. It fetches app info and update status, and provides actions to check for and install updates.
```javascript
function UpdatePanel() {
const [appInfo, setAppInfo] = useState(null);
const [updateStatus, setUpdateStatus] = useState(null);
const isBusy = updateStatus?.status === 'checking' || updateStatus?.status === 'downloading';
const progress = Math.max(0, Math.min(100, updateStatus?.progress?.percent || 0));
useEffect(() => {
let cancelled = false;
window.agentHub.getAppInfo?.().then(info => {
if (!cancelled) setAppInfo(info);
});
window.agentHub.getUpdateStatus?.().then(status => {
if (!cancelled) setUpdateStatus(status);
});
const offUpdater = window.agentHub.onUpdaterStatus?.(status => setUpdateStatus(status));
return () => {
cancelled = true;
offUpdater?.();
};
}, []);
const check = async () => {
setUpdateStatus(s => ({ ...(s || {}), status: 'checking', error: null }));
const status = await window.agentHub.checkForUpdates?.();
if (status) setUpdateStatus(status);
};
const install = async () => {
await window.agentHub.installUpdate?.();
};
const statusClass = updateStatus?.status === 'ready' ? 'ready' : updateStatus?.status === 'error' ? 'error' : '';
return (
Agent Hub
Version {appInfo?.version || updateStatus?.currentVersion || 'unknown'}
{updateStatus?.canInstall && (
)}
{(updateStatus?.status || updateStatus?.error) && (
{formatUpdateText(updateStatus)}
)}
{updateStatus?.status === 'downloading' && progress > 0 && (
)}
);
}
```
--------------------------------
### Create Unsigned Local DMG/ZIP Build
Source: https://github.com/potarix/agent-hub/blob/main/README.md
This command creates an unsigned local build of the application for macOS. Release signing and notarization are documented separately.
```bash
CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist:mac
```
--------------------------------
### Local Release Build and Publish
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Builds and publishes macOS release artifacts directly from your laptop, bypassing the CI workflow. Ensure all required artifacts are included in the GitHub release.
```bash
npm run publish:mac
```
--------------------------------
### Build Agent Configuration
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Constructs the agent configuration by merging base agent settings with session-allowed tools. This is useful for dynamically updating agent capabilities.
```javascript
// Build agent config with session-allowed tools merged in
const buildAgentWithSessionTools = (baseAgent, extraTools) => {
const all = extraTools || sessionAllowedTools;
if (!all.length) return baseAgent;
const existing = (baseAgent.allowedTools || '').sp
```
--------------------------------
### Set Up Drag and Drop Event Listeners
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Sets up and tears down drag and drop event listeners on the document for the active chat window. Listeners are only active when `isActive` is true and are removed on component unmount or when `isActive` becomes false.
```javascript
useEffect(() => {
if (!isActive) {
setIsDragging(false);
dragCounter.current = 0;
return;
}
document.addEventListener('dragenter', handleDragEnter);
document.addEventListener('dragleave', handleDragLeave);
document.addEventListener('dragover', handleDragOver);
document.addEventListener('drop', handleDrop);
return () => {
document.removeEventListener('dragenter', handleDragEnter);
document.removeEventListener('dragleave', handleDragLeave);
document.removeEventListener('dragover', handleDragOver);
document.removeEventListener('drop', handleDrop);
};
}, [isActive, supportsFileAttachments]);
```
--------------------------------
### Initializing and Managing Xterm.js Terminal
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Sets up and manages an Xterm.js terminal instance within a React component, including theme configuration and event handling for mouse selection.
```javascript
function TerminalView({ agent, isActive, status, setLastActivityTime, findApiRef }) {
const containerRef = useRef(null);
const xtermRef = useRef(null);
const fitAddonRef = useRef(null);
const searchAddonRef = useRef(null);
const spawnedRef = useRef(false);
const openedRef = useRef(false);
const [exited, setExited] = useState(false);
const isDark = () => document.documentElement.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches;
const darkTheme = {
background: '#0a0a0f',
foreground: '#e0e0e0',
cursor: '#7c6aff',
cursorAccent: '#0a0a0f',
selectionBackground: 'rgba(124,106,255,0.3)',
selectionForeground: '#ffffff',
black: '#1a1a2e',
red: '#ff6b6b',
green: '#51cf66',
yellow: '#ffd43b',
blue: '#74b0ff',
magenta: '#cc5de8',
cyan: '#22b8cf',
white: '#e0e0e0',
brightBlack: '#555577',
brightRed: '#ff8787',
brightGreen: '#69db7c',
brightYellow: '#ffe066',
brightBlue: '#91c4ff',
brightMagenta: '#da77f2',
brightCyan: '#3bc9db',
brightWhite: '#ffffff',
};
const lightTheme = {
background: '#f5f5f7',
foreground: '#1a1a2e',
cursor: '#7c6aff',
cursorAccent: '#ffffff',
selectionBackground: 'rgba(124,106,255,0.2)',
selectionForeground: '#000000',
black: '#1a1a2e',
red: '#d63031',
green: '#00b894',
yellow: '#cc8800',
blue: '#0A84FF',
magenta: '#a855f7',
cyan: '#0891b2',
white: '#d0d0d0',
brightBlack: '#7f8c8d',
brightRed: '#e74c3c',
brightGreen: '#2ecc71',
brightYellow: '#f39c12',
brightBlue: '#3498db',
brightMagenta: '#9b59b6',
brightCyan: '#1abc9c',
brightWhite: '#2d3436',
};
const installMouseSelectionBridge = (term) => {
const selectionService = term?._core?._selectionService;
const coreMouseService = term?._core?.coreMouseService;
if (!selectionService || !coreMouseService || selectionService._agentHubMouseSelectionPatched) return;
const originalShouldForceSelection = selectionService.shouldForceSelection?.bind(selectionService);
const originalTriggerMouseEvent = coreMouseService.triggerMouseEvent?.bind(coreMouseService);
if (!originalShouldForceSelection) return;
if (!originalTriggerMouseEvent) return;
// tmux mouse mode keeps terminal-app scrolling working, but xterm
// disables normal selection while mouse reporting is active. Force
// primary-button drags down xterm's selection path; wheel events still
// flow through to tmux/Claude/Codex. Suppress the matching mouse-up
// report too, otherwi
```
--------------------------------
### Agent Hub Event Listeners
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Sets up listeners for tool use, permission denied, and permission request events. Ensure to call the returned cleanup function to unsubscribe.
```javascript
window.agentHub.onStreamToolUse((id, toolInfo) => { if (cancelled || id !== activeRequestIdRef.current) return; toolUsesRef.current = [...toolUsesRef.current, toolInfo]; setToolUses([...toolUsesRef.current]); });
const offPermDenied = window.agentHub.onPermissionDenied((id, denials) => { if (cancelled || id !== activeRequestIdRef.current) return; setPermDenials(denials); });
const offPermRequest = window.agentHub.onPermissionRequest((id, request) => { if (cancelled || id !== activeRequestIdRef.current) return; // Auto-approve if this tool is in the "always allow" set if (alwaysAllowedToolsRef.current.has(request.tool)) { window.agentHub.sendPermissionResponse(id, request.toolUseId, { behavior: 'allow', updatedInput: request.input, }); setPermRequests(prev => [...prev, { ...request, requestId: id }]); setResolvedPerms(prev => ({ ...prev, [request.toolUseId]: 'approved' })); return; } setPermRequests(prev => [...prev, { ...request, requestId: id }]); // Increment completion status if user is NOT viewing this agent // This shows the notification badge for pending permission requests if (!isActiveRef.current) { setCompletionStatus(prev => { const current = typeof prev === 'number' ? prev : 0; return current + 1; }); } // Always play notification sound for permission requests const audio = document.getElementById('notificationSound'); if (audio) { audio.volume = 1.0; // Ensure volume is at max audio.currentTime = 0; // Reset to beginning const playPromise = audio.play(); if (playPromise !== undefined) { playPromise.then(() => { console.log('Notification sound played successfully for completion'); }).catch(err => { console.error('Notification sound play failed:', err); // Try alternative approach for Electron try { audio.load(); audio.play(); } catch(e) { console.error('Alternative play method also failed:', e); } }); } } else { console.error('Notification audio element not found'); } });
return () => {
cancelled = true;
offChunk();
offThinking();
offDone();
offError();
offToolUse();
offPermDenied();
offPermRequest();
};
```
--------------------------------
### Load and Save Agents Configuration
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Handles loading agent configurations from local storage, with a default fallback, and saving them back. It also clears stale session IDs upon loading.
```javascript
const { useState, useEffect, useRef, useCallback } = React; // ── Default agents config ── const DEFAULT_AGENTS = [ { id: 'claude-code-1', name: 'Claude Code', provider: 'claude-code', workDir: '', model: '', claudePath: '', claudeArgs: '', emoji: 'CC', providerClass: 'provider-claude-code', maxTokens: 16384, }, ]; function loadAgents() { try { const saved = localStorage.getItem('agent-hub-agents'); if (!saved) return DEFAULT_AGENTS; const agents = JSON.parse(saved); // Clear stale sessionIds — they won't survive an app restart return agents.map(a => ({ ...a, sessionId: null })); } catch { return DEFAULT_AGENTS; } } function saveAgents(agents) { localStorage.setItem('agent-hub-agents', JSON.stringify(agents)); }
```
--------------------------------
### Configure App-Specific Password for Signing
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Sets environment variables using an Apple ID and app-specific password for authentication with Apple Developer services.
```bash
export APPLE_ID=you@example.com
export APPLE_APP_SPECIFIC_PASSWORD=xxxx-xxxx-xxxx-xxxx
export APPLE_TEAM_ID=TEAMID1234
```
--------------------------------
### Send Message with Attachments
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Constructs and sends a user message, including text input, selected images (as base64), and selected files. It handles empty messages and routes slash commands separately. Clears input and selections after sending.
```javascript
const sendMessage = () => {
const text = input.trim();
if (!text && selectedImages.length === 0 && selectedFiles.length === 0) return;
// If it's a slash command, route it (still sequential)
if (text.startsWith('/')) {
if (loadingRef.current) return;
executeSlashCommand(text);
return;
}
const fallbackContent = selectedFiles.length > 0 ? 'Please use the attached files.' : 'Please analyze these images';
const userMsg = {
role: 'user',
content: text || fallbackContent
};
// Add images to message if any are selected
if (selectedImages.length > 0) {
userMsg.images = selectedImages.map(img => ({
name: img.name,
path: img.path,
base64: img.base64,
mimeType: img.mimeType
}));
}
if (selectedFiles.length > 0) {
userMsg.files = selectedFiles.map(file => ({
name: file.name,
path: file.path,
mimeType: file.mimeType,
size: file.size,
isImage: file.isImage,
}));
}
setMessages(prev => [...prev, userMsg]);
setInput('');
setSelectedImages([]); // Clear selected images after sending
setSelectedFiles([]);
setLastUserMessage(userMsg.content);
if (inputRef.cur
```
--------------------------------
### Release Flow: Versioning and Pushing Tags
Source: https://github.com/potarix/agent-hub/blob/main/RELEASE.md
Increments the package version and pushes tags to trigger the CI release workflow. Ensure a 'v*' tag is pushed to initiate the build and publish process.
```bash
npm version patch
git push --follow-tags
```
--------------------------------
### Handling Application Pinning States
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Renders different UI states based on the application's pinning status (pinning, pinned, error). Provides actions for system settings, download, and retry.
```javascript
const handleOpenAxSettings = () => {
window.agentHub.desktopApp.openAxSettings();
};
const handleRetry = () => {
setPinState({ status: 'pinning', error: null, code: null });
window.agentHub.desktopApp.pin(appKey).then(res => {
if (res?.error) setPinState({ status: 'error', error: res.error, code: res.code });
else setPinState({ status: 'pinned', error: null, code: null });
});
};
const handleInstall = () => {
if (meta.downloadUrl) window.agentHub.openExternal(meta.downloadUrl);
};
return (
{pinState.status === 'pinning' && (
{meta.initials}
Pinning {meta.name} to this window…
)}
{pinState.status === 'pinned' && (
{meta.name} is pinned here. Drag or resize Agent Hub to reflow.
)}
{pinState.status === 'error' && (
Couldn't pin {meta.name}
{pinState.error}
{pinState.code === 'no-ax-permission' && (
)}
{pinState.code === 'not-installed' && meta.downloadUrl && (
)}
)}
);
```
--------------------------------
### Handle Always Allow Permission
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Permanently allows denied tools by updating the agent's configuration and retrying the action. Also updates session tools and resets stream state. Requires existing permission denials and the last user message.
```javascript
const handleAlwaysAllow = () => {
if (!permDenials || !lastUserMessage) return;
const deniedTools = [...new Set(permDenials.map(d => d.tool))];
// Persist to agent config permanently
const existing = (agent.allowedTools || '').split(/[,\s]+/).filter(Boolean);
const merged = [...new Set([...existing, ...deniedTools])].join(' ');
onUpdateAgent({ ...agent, allowedTools: merged });
// Also update session tools and retry
setSessionAllowedTools(prev => [...new Set([...prev, ...deniedTools])]);
setPermDenials(null);
setPermRequests([]);
setResolvedPerms({});
setLoading(true);
const apiMessages = [];
if (agent.systemPrompt) apiMessages.push({ role: 'system', content: agent.systemPrompt });
apiMessages.push(...(messages || []).slice(-20));
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
activeRequestIdRef.current = requestId;
const retryAgent = { ...agent, allowedTools: merged };
window.agentHub.chatStream(requestId, retryAgent, apiMessages);
};
```
--------------------------------
### Handle Terminal Window Resize
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Adds a window resize event listener to refit the terminal dimensions. This ensures the terminal correctly adjusts its size when the browser window is resized. Requires 'isActive' and 'fitAddonRef.current' to be available.
```javascript
const onResize = () => {
if (!isActive || !fitAddonRef.current) return;
fitAddonRef.current.fit();
const dims = fitAddonRef.current.proposeDimensions();
if (dims) window.agentHub.terminalResize(agent.id, dims.cols, dims.rows);
};
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
```
--------------------------------
### Agent Configuration Options
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Defines various agent configurations including local and SSH instances for Hermes, Codex, and Terminal.
```javascript
s: 'provider-hermes', desc: 'Local Hermes Agent instance' }, { name: 'Hermes (SSH)', emoji: 'H', provider: 'hermes', sshHost: '', sshUser: 'root', model: '', providerClass: 'provider-hermes', desc: 'Hermes on a remote VPS' }, { name: 'Codex (Local)', emoji: 'CX', provider: 'codex', model: '', skipGitRepoCheck: true, providerClass: 'provider-codex', desc: 'Local OpenAI Codex CLI agent' }, { name: 'Codex (SSH)', emoji: 'CX', provider: 'codex-ssh', sshHost: '', sshUser: 'root', model: '', skipGitRepoCheck: true, providerClass: 'provider-codex', desc: 'Codex on a remote server' }, { name: 'Terminal (Local)', emoji: '>', provider: 'terminal', workDir: '', providerClass: 'provider-terminal', desc: 'Local shell terminal' }, { name: 'Terminal (SSH)', emoji: '>', provider: 'terminal-ssh', sshHost: '', sshUser: 'root', providerClass: 'provider-terminal', desc: 'SSH remote terminal' }
```
--------------------------------
### Agent Presets Configuration
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Defines a list of agent presets, each with a name, emoji, provider, and description. These presets represent different configurations for embedding or connecting to various AI models and services.
```javascript
const PRESETS = [
{
name: 'Claude Desktop',
emoji: 'CD',
provider: 'claude-desktop',
providerClass: 'provider-claude-code',
desc: 'Embed the real /Applications/Claude.app with full MCP, skills, extensions'
},
{
name: 'Codex Desktop',
emoji: 'CX',
provider: 'codex-desktop',
providerClass: 'provider-codex',
desc: 'Embed the real /Applications/Codex.app inside Agent Hub'
},
{
name: 'Claude Code (Local)',
emoji: 'CC',
provider: 'claude-code',
model: '',
workDir: '',
providerClass: 'provider-claude-code',
desc: 'Local Claude Code CLI instance'
},
{
name: 'Claude Code (SSH)',
emoji: 'CC',
provider: 'claude-code-ssh',
sshHost: '',
sshUser: 'root',
model: '',
workDir: '',
providerClass: 'provider-claude-code',
desc: 'Claude Code on a remote server'
},
{
name: 'OpenClaw (Local)',
emoji: 'OC',
provider: 'openclaw-local',
model: '',
providerClass: 'provider-openclaws',
desc: 'Local OpenClaw gateway'
},
{
name: 'OpenClaw (Remote)',
emoji: 'OC',
provider: 'openclaw',
sshHost: '',
sshUser: 'root',
model: '',
providerClass: 'provider-openclaws',
desc: 'Remote OpenClaw gateway'
},
{
name: 'Hermes (Local)',
emoji: 'H',
provider: 'hermes-local',
model: '',
providerClas
```
--------------------------------
### App Layout Structure
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Defines the main layout for the application, ensuring the root element fills the viewport and uses flexbox for a column-based layout. Includes styling for the title bar.
```css
#root {
height: 100vh;
display: flex;
flex-direction: column;
}
.titlebar {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 80px;
background: var(--bg-1);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
gap: 10px;
-webkit-app-region: drag;
}
.titlebar-title {
font-size: 11px;
font-weight: 700;
color: var(--text-2);
letter-spacing: 1.5px;
text-transform: uppercase;
}
.titlebar-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px var(--accent-glow);
}
.app-body {
flex: 1;
display: flex;
overflow: hidden;
}
```
--------------------------------
### Desktop App Integration View
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Manages the display of a placeholder for foreign macOS applications like Claude.app or Codex.app. It shows a 'pinning...' status and error overlays when active.
```javascript
// ── Terminal View (xterm.js) ── // DesktopAppView: pins a foreign macOS app (Claude.app, Codex.app, …) over // Agent Hub's content area while this thread is active. Renders a // placeholder div because the real app draws its own UI; only the // "pinning…" status and error overlays are drawn here.
const DESKTOP_APP_LABELS = {
claude: { name: 'Claude Desktop', initials: 'CD', downloadUrl: 'https://claude.ai/download' },
codex: { name: 'Codex Desktop', initials: 'CX', downloadUrl: 'https://chatgpt.com/codex' },
};
function DesktopAppView({ agent, isActive, appKey }) {
const [pinState, setPinState] = useState({ status: 'idle', error: null, code: null });
const meta = DESKTOP_APP_LABELS[appKey] || { name: 'Desktop App', initials: '?', downloadUrl:
```
--------------------------------
### React App Rendering
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Renders the main React application component into the DOM. Ensure the 'root' element exists in your HTML.
```javascript
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render();
```
--------------------------------
### Send Message to Agent
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Builds API messages and initiates a chat stream with an agent. Resets stream state and updates loading status. Handles system prompts and recent messages, limiting to the last 20.
```javascript
const handleSend = () => {
if (!inputRef.current) return;
inputRef.current.style.height = 'auto'; // Reset stream state
streamTextRef.current = '';
streamThinkingRef.current = '';
toolUsesRef.current = [];
setStreamText('');
setStreamThinking('');
setToolUses([]);
setPermDenials(null);
setPermRequests([]);
setResolvedPerms({});
setLoading(true);
// Build API messages
const apiMessages = [];
if (agent.systemPrompt) apiMessages.push({ role: 'system', content: agent.systemPrompt });
const allMsgs = [...(messages || []), userMsg];
apiMessages.push(...allMsgs.slice(-20));
// Generate a unique request ID and start streaming
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
activeRequestIdRef.current = requestId;
// Update last activity time when sending a message
if (setLastActivityTime) {
setLastActivityTime(prev => ({ ...prev, [agent.id]: Date.now() }));
}
window.agentHub.chatStream(requestId, buildAgentWithSessionTools(agent), apiMessages);
};
```
--------------------------------
### Process Selected Files for Display and Attachments
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Handles the processing of selected files, separating image files for base64 encoding and preview generation, while adding all attachable files to the state. It conditionally uses file attachments based on `supportsFileAttachments`.
```javascript
const processSelectedFiles = (files) => {
const fileList = files.filter(Boolean);
const imageFiles = fileList.filter(file => file.type.startsWith('image/'));
if (supportsFileAttachments) {
addSelectedFiles(fileList);
}
imageFiles.forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target.result.split(',')[1]; // Remove data:image/...;base64, prefix
setSelectedImages(prev => [...prev, { name: file.name, path: file.path || '', mimeType: file.type, size: file.size || 0, base64: base64, preview: e.target.result }]);
};
reader.readAsDataURL(file);
});
};
```
--------------------------------
### Implement Text Search Functionality
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Provides a search engine for text content, allowing users to find, navigate, and highlight matches. It supports case-sensitive searches and smooth scrolling to the current match.
```javascript
function textFindEngine(getContainer, collectMatches, clearHighlights, writeHighlights, scrollCurrentIntoView) {
let ranges = [];
let currentIdx = -1;
let lastQuery = '';
let lastCaseSensitive = false;
const scrollIntoViewIfNeeded = (element) => {
try {
const rect = element.getBoundingClientRect();
const isInView = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
if (!isInView && rect.bottom - 40) {
const node = ranges[currentIdx].startContainer.parentElement;
if (node && node.scrollIntoView) {
node.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
}
} catch {}
};
return {
type: 'chat',
search(q, opts) {
lastQuery = q || '';
lastCaseSensitive = !!(opts && opts.caseSensitive);
if (!lastQuery) {
ranges = [];
currentIdx = -1;
clearHighlights();
return { current: 0, total: 0 };
}
const container = getContainer();
ranges = collectMatches(container, lastQuery, lastCaseSensitive);
currentIdx = ranges.length ? 0 : -1;
writeHighlights();
scrollCurrentIntoView();
return { current: currentIdx + 1, total: ranges.length };
},
next() {
if (!ranges.length) return { current: 0, total: 0 };
currentIdx = (currentIdx + 1) % ranges.length;
writeHighlights();
scrollCurrentIntoView();
return { current: currentIdx + 1, total: ranges.length };
},
prev() {
if (!ranges.length) return { current: 0, total: 0 };
currentIdx = (currentIdx - 1 + ranges.length) % ranges.length;
writeHighlights();
scrollCurrentIntoView();
return { current: currentIdx + 1, total: ranges.length };
},
clear() {
ranges = [];
currentIdx = -1;
lastQuery = '';
clearHighlights();
},
refresh() {
if (!lastQuery) return { current: 0, total: 0 };
const container = getContainer();
ranges = collectMatches(container, lastQuery, lastCaseSensitive);
if (currentIdx >= ranges.length) currentIdx = ranges.length ? 0 : -1;
if (currentIdx < 0 && ranges.length) currentIdx = 0;
writeHighlights();
return { current: currentIdx + 1, total: ranges.length };
},
};
}
```
--------------------------------
### Initialize Terminal Mouse Wheel Listener
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Attaches a wheel event listener to an element for terminal scrolling. Handles different delta modes and sends PageUp/PageDn sequences for significant scroll amounts. Ensure 'window.agentHub.terminalInput' is available.
```javascript
const onWheel = (e) => {
const px = e.deltaMode === 1 ? e.deltaY * PX_PER_LINE : e.deltaMode === 2 ? e.deltaY * PX_PER_PAGE : e.deltaY;
accum += px;
while (Math.abs(accum) >= STEP) {
const seq = accum < 0 ? '\x1b[5~' : '\x1b[6~'; // PgUp / PgDn
window.agentHub.terminalInput(agent.id, seq);
accum += accum < 0 ? STEP : -STEP;
}
};
el.addEventListener('wheel', onWheel, { capture: true, passive: false });
return () => el.removeEventListener('wheel', onWheel, { capture: true });
```
--------------------------------
### Configure Claude Code Agent with Permission Mode
Source: https://github.com/potarix/agent-hub/blob/main/PERMISSION_APPROVAL_GUIDE.md
Configure your agent to use the 'claude-code' provider and set the desired permission mode. Options include 'acceptEdits' for automatic approval of safe edits, 'ask' for interactive prompts, or 'bypassPermissions' to disable checks.
```javascript
const agent = {
name: 'Claude Code Agent',
provider: 'claude-code',
permissionMode: 'ask', // or 'acceptEdits', 'bypassPermissions'
model: 'claude-3-5-sonnet-20241022',
workDir: '/path/to/project'
};
```
--------------------------------
### Message Content Rendering
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Handles the rendering of message content, converting it to HTML using the 'marked' library. It includes configuration for safe rendering and uses a ref to manage the DOM element.
```javascript
function MessageContent({ content }) { const [html, setHtml] = useState(''); const contentRef = useRef(null); useEffect(() => { if (!content) { setHtml(''); return; } // Configure marked options for safe rendering if (window.marked) { window.marked.setOptions({
```
--------------------------------
### Build File Attachment Object
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Constructs a standardized file attachment object from a file input. Ensures essential properties like name, path, mime type, and size are present, and identifies image files.
```javascript
const buildFileAttachment = (file) => ({
name: file.name || 'Untitled',
path: file.path || '',
mimeType: file.type || '',
size: file.size || 0,
isImage: !!file.type && file.type.startsWith('image/'),
});
```
--------------------------------
### Handle Permission Retry
Source: https://github.com/potarix/agent-hub/blob/main/index.html
Allows retrying an action after permission denials by adding denied tools to session-allowed tools. Resets stream state and rebuilds messages for the retry. Requires existing permission denials and the last user message.
```javascript
const handleAllowRetry = () => {
if (!permDenials || !lastUserMessage) return;
const deniedTools = [...new Set(permDenials.map(d => d.tool))];
// Add to session-allowed tools (not persisted to agent config)
setSessionAllowedTools(prev => [...new Set([...prev, ...deniedTools])]);
setPermDenials(null);
setPermRequests([]);
setResolvedPerms({});
setLoading(true);
// Reset stream state for retry
streamTextRef.current = '';
streamThinkingRef.current = '';
toolUsesRef.current = [];
setStreamText('');
setStreamThinking('');
setToolUses([]);
setLoading(true);
// Re-build messages and retry
const apiMessages = [];
if (agent.systemPrompt) apiMessages.push({ role: 'system', content: agent.systemPrompt });
apiMessages.push(...(messages || []).slice(-20));
const requestId = Date.now().toString(36) + Math.random().toString(36).slice(2);
activeRequestIdRef.current = requestId;
const retryAgent = buildAgentWithSessionTools(agent, [...new Set([...sessionAllowedTools, ...deniedTools])]);
window.agentHub.chatStream(requestId, retryAgent, apiMessages);
};
```