claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
1,338 lines (1,168 loc) โข 176 kB
JavaScript
/**
* AgentsPage - Dedicated page for managing and viewing agent conversations
* Handles conversation display, filtering, and detailed analysis
*/
class AgentsPage {
constructor(container, services) {
this.container = container;
this.dataService = services.data;
this.stateService = services.state;
this.components = {};
this.filters = {
status: 'all',
timeRange: '7d',
search: ''
};
this.isInitialized = false;
// Initialize header component
this.headerComponent = null;
// Pagination state for conversations
this.pagination = {
currentPage: 0,
limit: 10,
hasMore: true,
isLoading: false
};
// Pagination state for messages
this.messagesPagination = {
currentPage: 0,
limit: 10,
hasMore: true,
isLoading: false,
conversationId: null
};
// Loaded conversations cache
this.loadedConversations = [];
this.loadedMessages = new Map(); // Cache messages by conversation ID (now stores paginated data)
// Agent data
this.agents = [];
this.selectedAgentId = null;
// State transition tracking for enhanced user experience
this.lastMessageTime = new Map(); // Track when last message was received per conversation
// Initialize tool display component
this.toolDisplay = new ToolDisplay();
// Subscribe to state changes
this.unsubscribe = this.stateService.subscribe(this.handleStateChange.bind(this));
// Subscribe to DataService events for real-time updates
this.dataService.addEventListener((type, data) => {
if (type === 'new_message') {
console.log('๐ WebSocket: New message received', { conversationId: data.conversationId });
this.handleNewMessage(data.conversationId, data.message, data.metadata);
} else if (type === 'console_interaction') {
console.log('๐ WebSocket: Console interaction request received', data);
this.showConsoleInteraction(data);
}
});
}
/**
* Initialize the agents page
*/
async initialize() {
if (this.isInitialized) return;
try {
this.stateService.setLoading(true);
await this.render();
await this.initializeComponents();
await this.loadAgentsData();
await this.loadConversationsData();
this.isInitialized = true;
} catch (error) {
console.error('Error initializing agents page:', error);
this.stateService.setError(error);
} finally {
this.stateService.setLoading(false);
}
}
/**
* Handle state changes from StateService (WebSocket updates)
* @param {Object} state - New state
* @param {string} action - Action that caused the change
*/
handleStateChange(state, action) {
switch (action) {
case 'update_conversations':
// Don't replace loaded conversations, just update states
break;
case 'update_conversation_states':
console.log('๐ WebSocket: Conversation states updated', { count: Object.keys(state.conversationStates?.activeStates || state.conversationStates || {}).length });
// Handle both direct states object and nested structure
const activeStates = state.conversationStates?.activeStates || state.conversationStates || {};
this.updateConversationStates(activeStates);
break;
case 'set_loading':
this.updateLoadingState(state.isLoading);
break;
case 'set_error':
this.updateErrorState(state.error);
break;
case 'conversation_state_change':
this.handleConversationStateChange(state);
break;
case 'data_refresh':
// On real-time data refresh, update conversation states but keep pagination
this.updateConversationStatesOnly();
break;
case 'new_message':
// Handle new message in real-time
this.handleNewMessage(state.conversationId, state.message, state.metadata);
break;
}
}
/**
* Handle new message received via WebSocket
* @param {string} conversationId - Conversation ID that received new message
* @param {Object} message - New message object
* @param {Object} metadata - Additional metadata
*/
handleNewMessage(conversationId, message, metadata) {
// Log essential message info for debugging
console.log('๐ WebSocket: Processing new message', {
conversationId,
role: message?.role,
hasTools: Array.isArray(message?.content) ? message.content.some(b => b.type === 'tool_use') : false,
hasToolResults: !!message?.toolResults
});
// Always update the message cache for this conversation
const existingMessages = this.loadedMessages.get(conversationId) || [];
// Track message timing for better state transitions
const now = Date.now();
this.lastMessageTime.set(conversationId, now);
// IMMEDIATE STATE TRANSITION based on message appearance
if (this.selectedConversationId === conversationId) {
if (message?.role === 'user') {
// User message just appeared - Claude immediately starts working
console.log('โก User message detected - Claude starting work immediately');
this.updateStateBanner(conversationId, 'Claude Code working...');
} else if (message?.role === 'assistant') {
// Assistant message appeared - analyze for specific state
const intelligentState = this.analyzeMessageForState(message, existingMessages);
console.log(`๐ค Assistant message detected - state: ${intelligentState}`);
this.updateStateBanner(conversationId, intelligentState);
// No additional timeout needed - state is determined by message content
}
}
// Check if we already have this message (avoid duplicates)
const messageExists = existingMessages.some(msg =>
msg.id === message.id ||
(msg.timestamp === message.timestamp && msg.role === message.role)
);
if (!messageExists) {
// Add new message to the end
const updatedMessages = [...existingMessages, message];
this.loadedMessages.set(conversationId, updatedMessages);
// Refresh only the conversation states to show updated status/timestamp
// Don't do full reload as it can interfere with message cache
this.updateConversationStatesOnly();
// If this conversation is currently selected, update the messages view
if (this.selectedConversationId === conversationId) {
// Re-render messages with new message
this.renderCachedMessages(updatedMessages, false);
// Auto-scroll to new message
this.scrollToBottom();
}
// Show notification
this.showNewMessageNotification(message, metadata);
}
}
/**
* Update only conversation states without affecting pagination
*/
async updateConversationStatesOnly() {
try {
const statesData = await this.dataService.getConversationStates();
const activeStates = statesData?.activeStates || {};
// Update StateService with fresh states
this.stateService.updateConversationStates(activeStates);
// Update states in already loaded conversations
this.updateConversationStateElements(activeStates);
// Update banner if we have a selected conversation
if (this.selectedConversationId && activeStates[this.selectedConversationId]) {
this.updateStateBanner(this.selectedConversationId, activeStates[this.selectedConversationId]);
}
} catch (error) {
console.error('Error updating conversation states:', error);
}
}
/**
* Analyze a message to determine intelligent conversation state
* @param {Object} message - The message to analyze
* @param {Array} existingMessages - Previous messages in conversation
* @returns {string} Intelligent state description
*/
analyzeMessageForState(message, existingMessages = []) {
const role = message?.role;
const content = message?.content;
const hasToolResults = !!message?.toolResults && message.toolResults.length > 0;
const messageTime = new Date(message?.timestamp || Date.now());
const now = new Date();
const messageAge = (now - messageTime) / 1000; // seconds
if (role === 'assistant') {
// Analyze assistant messages with enhanced logic
if (Array.isArray(content)) {
const hasToolUse = content.some(block => block.type === 'tool_use');
const hasText = content.some(block => block.type === 'text');
const textBlocks = content.filter(block => block.type === 'text');
const toolUseBlocks = content.filter(block => block.type === 'tool_use');
// Enhanced tool execution detection with immediate response
if (hasToolUse) {
const toolNames = toolUseBlocks.map(tool => tool.name).join(', ');
if (!hasToolResults) {
// Tool just sent - immediate execution state
console.log(`๐ง Tools detected: ${toolNames} - showing execution state`);
if (toolNames.includes('bash') || toolNames.includes('edit') || toolNames.includes('write') || toolNames.includes('multiedit')) {
return 'Executing tools...';
} else if (toolNames.includes('read') || toolNames.includes('grep') || toolNames.includes('glob') || toolNames.includes('task')) {
return 'Analyzing code...';
} else if (toolNames.includes('webfetch') || toolNames.includes('websearch')) {
return 'Fetching data...';
}
return 'Awaiting tool response...';
} else {
// Has tool results - Claude is processing them
console.log(`๐ Tools completed: ${toolNames} - analyzing results`);
return 'Analyzing results...';
}
}
// Enhanced text analysis
if (hasText) {
const textContent = textBlocks.map(block => block.text).join(' ').toLowerCase();
// Working indicators
if (textContent.includes('let me') ||
textContent.includes('i\'ll') ||
textContent.includes('i will') ||
textContent.includes('i\'m going to') ||
textContent.includes('let\'s') ||
textContent.includes('first, i\'ll') ||
textContent.includes('now i\'ll')) {
return 'Claude Code working...';
}
// Analysis indicators
if (textContent.includes('analyzing') ||
textContent.includes('examining') ||
textContent.includes('looking at') ||
textContent.includes('reviewing')) {
return 'Analyzing code...';
}
// Completion indicators
if (textContent.includes('completed') ||
textContent.includes('finished') ||
textContent.includes('done') ||
textContent.includes('successfully')) {
return 'Task completed';
}
// User input needed - enhanced detection
if (textContent.endsWith('?') ||
textContent.includes('what would you like') ||
textContent.includes('how can i help') ||
textContent.includes('would you like me to') ||
textContent.includes('should i') ||
textContent.includes('do you want') ||
textContent.includes('let me know') ||
textContent.includes('please let me know') ||
textContent.includes('what do you think') ||
textContent.includes('any questions')) {
return 'Waiting for your response';
}
// Error/problem indicators
if (textContent.includes('error') ||
textContent.includes('failed') ||
textContent.includes('problem') ||
textContent.includes('issue')) {
return 'Encountered issue';
}
}
}
// Recent assistant message suggests waiting for user
if (messageAge < 300) { // Extended to 5 minutes
return 'Waiting for your response';
}
// Default for older assistant messages
return 'Idle';
} else if (role === 'user') {
// User just sent a message - Claude should be processing
if (messageAge < 10) {
return 'Claude Code working...';
} else if (messageAge < 60) {
return 'Awaiting response...';
}
// Older user messages suggest Claude might be working on something complex
return 'Processing request...';
}
// Enhanced timing analysis
const lastMessage = existingMessages[existingMessages.length - 1];
if (lastMessage) {
const timeSinceLastMessage = Date.now() - new Date(lastMessage.timestamp).getTime();
if (timeSinceLastMessage < 30000) { // Less than 30 seconds
return lastMessage.role === 'user' ? 'Claude Code working...' : 'Recently active';
} else if (timeSinceLastMessage < 180000) { // Less than 3 minutes
return 'Idle';
} else if (timeSinceLastMessage < 1800000) { // Less than 30 minutes
return 'Waiting for your response';
}
}
return 'Inactive';
}
/**
* Show console interaction panel for Yes/No prompts
* @param {Object} interactionData - Interaction data from Claude Code
*/
showConsoleInteraction(interactionData) {
const panel = this.container.querySelector('#console-interaction-panel');
const description = this.container.querySelector('#interaction-description');
const prompt = this.container.querySelector('#interaction-prompt');
const choices = this.container.querySelector('#interaction-choices');
const textInput = this.container.querySelector('#interaction-text-input');
// Show the panel
panel.style.display = 'block';
// Set up the interaction content
if (interactionData.description) {
description.innerHTML = `
<div class="tool-action">
<strong>${interactionData.tool || 'Action'}:</strong>
<div class="tool-details">${interactionData.description}</div>
</div>
`;
}
if (interactionData.prompt) {
prompt.textContent = interactionData.prompt;
}
// Handle different interaction types
if (interactionData.type === 'choice' && interactionData.options) {
// Show multiple choice options
choices.style.display = 'block';
textInput.style.display = 'none';
const choicesHtml = interactionData.options.map((option, index) => `
<label class="interaction-choice">
<input type="radio" name="console-choice" value="${index}" ${index === 0 ? 'checked' : ''}>
<span class="choice-number">${index + 1}.</span>
<span class="choice-text">${option}</span>
</label>
`).join('');
choices.innerHTML = choicesHtml;
} else if (interactionData.type === 'text') {
// Show text input
choices.style.display = 'none';
textInput.style.display = 'block';
const textarea = this.container.querySelector('#console-text-input');
textarea.focus();
}
// Store interaction data for submission
this.currentInteraction = interactionData;
// Bind event listeners
this.bindInteractionEvents();
}
/**
* Hide console interaction panel
*/
hideConsoleInteraction() {
const panel = this.container.querySelector('#console-interaction-panel');
panel.style.display = 'none';
this.currentInteraction = null;
}
/**
* Bind event listeners for console interaction
*/
bindInteractionEvents() {
const submitBtn = this.container.querySelector('#interaction-submit');
const cancelBtn = this.container.querySelector('#interaction-cancel');
// Remove existing listeners
submitBtn.replaceWith(submitBtn.cloneNode(true));
cancelBtn.replaceWith(cancelBtn.cloneNode(true));
// Get fresh references
const newSubmitBtn = this.container.querySelector('#interaction-submit');
const newCancelBtn = this.container.querySelector('#interaction-cancel');
newSubmitBtn.addEventListener('click', () => this.handleInteractionSubmit());
newCancelBtn.addEventListener('click', () => this.handleInteractionCancel());
// Handle Enter key for text input
const textarea = this.container.querySelector('#console-text-input');
if (textarea) {
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && e.ctrlKey) {
this.handleInteractionSubmit();
}
});
}
}
/**
* Handle interaction submission
*/
async handleInteractionSubmit() {
if (!this.currentInteraction) return;
let response;
if (this.currentInteraction.type === 'choice') {
const selectedChoice = this.container.querySelector('input[name="console-choice"]:checked');
if (selectedChoice) {
response = {
type: 'choice',
value: parseInt(selectedChoice.value),
text: this.currentInteraction.options[selectedChoice.value]
};
}
} else if (this.currentInteraction.type === 'text') {
const textarea = this.container.querySelector('#console-text-input');
response = {
type: 'text',
value: textarea.value.trim()
};
}
if (response) {
// Send response via WebSocket
try {
await this.sendConsoleResponse(this.currentInteraction.id, response);
console.log('๐ WebSocket: Console interaction response sent', { id: this.currentInteraction.id, response });
this.hideConsoleInteraction();
} catch (error) {
console.error('Error sending console response:', error);
// Show error in UI
this.showInteractionError('Failed to send response. Please try again.');
}
}
}
/**
* Handle interaction cancellation
*/
async handleInteractionCancel() {
if (!this.currentInteraction) return;
try {
await this.sendConsoleResponse(this.currentInteraction.id, { type: 'cancel' });
console.log('๐ WebSocket: Console interaction cancelled', { id: this.currentInteraction.id });
this.hideConsoleInteraction();
} catch (error) {
console.error('Error cancelling console interaction:', error);
this.hideConsoleInteraction(); // Hide anyway on cancel
}
}
/**
* Send console response via WebSocket
* @param {string} interactionId - Interaction ID
* @param {Object} response - Response data
*/
async sendConsoleResponse(interactionId, response) {
// Send through DataService which will route to WebSocket
if (this.dataService && this.dataService.webSocketService) {
this.dataService.webSocketService.send({
type: 'console_response',
data: {
interactionId,
response
}
});
} else {
throw new Error('WebSocket service not available');
}
}
/**
* Show error in interaction panel
* @param {string} message - Error message
*/
showInteractionError(message) {
const panel = this.container.querySelector('#console-interaction-panel');
const existingError = panel.querySelector('.interaction-error');
if (existingError) {
existingError.remove();
}
const errorDiv = document.createElement('div');
errorDiv.className = 'interaction-error';
errorDiv.textContent = message;
const content = panel.querySelector('.interaction-content');
content.insertBefore(errorDiv, content.querySelector('.interaction-actions'));
// Remove error after 5 seconds
setTimeout(() => {
if (errorDiv.parentNode) {
errorDiv.remove();
}
}, 5000);
}
/**
* Update conversation state elements in the DOM
* @param {Object} activeStates - Active conversation states
*/
updateConversationStateElements(activeStates) {
const conversationItems = this.container.querySelectorAll('.sidebar-conversation-item');
conversationItems.forEach(item => {
const conversationId = item.dataset.id;
const state = activeStates[conversationId] || 'unknown';
const stateClass = this.getStateClass(state);
const stateLabel = this.getStateLabel(state);
// Update status dot
const statusDot = item.querySelector('.status-dot');
if (statusDot) {
statusDot.className = `status-dot ${stateClass}`;
}
// Update status badge
const statusBadge = item.querySelector('.sidebar-conversation-badge');
if (statusBadge) {
statusBadge.className = `sidebar-conversation-badge ${stateClass}`;
statusBadge.textContent = stateLabel;
}
});
}
/**
* Render the agents page structure
*/
async render() {
this.container.innerHTML = `
<div class="agents-page">
<!-- Page Header (will be replaced by HeaderComponent) -->
<div id="agents-header-container"></div>
<!-- Filters Section -->
<div class="conversations-filters">
<div class="filters-row">
<div class="filter-group">
<label class="filter-label">Status:</label>
<select class="filter-select" id="status-filter">
<option value="all">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="filter-group">
<label class="filter-label">Time Range:</label>
<select class="filter-select" id="time-filter">
<option value="1h">Last Hour</option>
<option value="24h">Last 24 Hours</option>
<option value="7d" selected>Last 7 Days</option>
<option value="30d">Last 30 Days</option>
</select>
</div>
<div class="filter-group search-group">
<label class="filter-label">Search:</label>
<div class="search-input-container">
<input type="text" class="filter-input search-input" id="search-filter" placeholder="Search conversations, projects, or messages...">
<button class="search-clear" id="clear-search" title="Clear search">ร</button>
</div>
</div>
</div>
</div>
<!-- Agents Section -->
<div class="agents-section">
<div class="agents-header">
<h4>Available Agents</h4>
<div class="agents-info">
<span class="agents-count" id="agents-count">0 agents</span>
<button class="refresh-agents-btn" id="refresh-agents" title="Refresh agents">
<span class="btn-icon">๐</span>
</button>
</div>
</div>
<div class="agents-list" id="agents-list">
<!-- Agent items will be rendered here -->
</div>
<!-- Loading state for agents -->
<div class="agents-loading" id="agents-loading" style="display: none;">
<div class="loading-spinner"></div>
<span class="loading-text">Loading agents...</span>
</div>
<!-- Empty state for agents -->
<div class="agents-empty" id="agents-empty" style="display: none;">
<div class="empty-icon">๐ค</div>
<p>No agents found</p>
<small>Create agents in your .claude/agents directory to see them here</small>
</div>
</div>
<!-- Loading State -->
<div class="loading-state" id="conversations-loading" style="display: none;">
<div class="loading-spinner"></div>
<span class="loading-text">Loading conversations...</span>
</div>
<!-- Error State -->
<div class="error-state" id="conversations-error" style="display: none;">
<div class="error-content">
<span class="error-icon">โ ๏ธ</span>
<span class="error-message"></span>
<button class="error-retry" id="retry-load">Retry</button>
</div>
</div>
<!-- Console Interaction Panel (Hidden by default) -->
<div id="console-interaction-panel" class="console-interaction-panel" style="display: none;">
<div class="interaction-header">
<div class="interaction-title">
<span class="interaction-icon">โก</span>
<span class="interaction-text">Claude Code needs your input</span>
</div>
<button class="interaction-close" onclick="this.hideConsoleInteraction()">×</button>
</div>
<div class="interaction-content">
<div id="interaction-description" class="interaction-description">
<!-- Tool description will be inserted here -->
</div>
<div id="interaction-prompt" class="interaction-prompt">
Do you want to proceed?
</div>
<!-- Multi-choice options -->
<div id="interaction-choices" class="interaction-choices" style="display: none;">
<!-- Radio button choices will be inserted here -->
</div>
<!-- Text input area -->
<div id="interaction-text-input" class="interaction-text-input" style="display: none;">
<label for="console-text-input">Your response:</label>
<textarea id="console-text-input" placeholder="Type your response here..." rows="4"></textarea>
</div>
<div class="interaction-actions">
<button id="interaction-submit" class="interaction-btn primary">Submit</button>
<button id="interaction-cancel" class="interaction-btn secondary">Cancel</button>
</div>
</div>
</div>
<!-- Two Column Layout -->
<div class="conversations-layout">
<!-- Left Sidebar: Conversations List -->
<div class="conversations-sidebar">
<div class="sidebar-header">
<h3>Chats</h3>
<span class="conversation-count" id="sidebar-count">0</span>
</div>
<div class="conversations-list" id="conversations-list">
<!-- Conversation items will be rendered here -->
</div>
<!-- Load More Indicator -->
<div class="load-more-indicator" id="load-more-indicator" style="display: none;">
<div class="loading-spinner"></div>
<span class="loading-text">Loading more conversations...</span>
</div>
</div>
<!-- Right Panel: Messages Detail -->
<div class="messages-panel">
<div class="messages-header" id="messages-header">
<div class="selected-conversation-info">
<h3 id="selected-conversation-title">Select a chat</h3>
<div class="selected-conversation-meta" id="selected-conversation-meta"></div>
</div>
<div class="messages-actions">
<button class="action-btn-small" id="export-conversation" title="Export conversation">
<span class="btn-icon-small">๐</span>
Export
</button>
</div>
</div>
<div class="messages-content" id="messages-content">
<div class="no-conversation-selected">
<div class="no-selection-icon">๐ฌ</div>
<h4>No conversation selected</h4>
<p>Choose a conversation from the sidebar to view its messages</p>
</div>
</div>
<!-- Conversation State Banner -->
<div class="conversation-state-banner" id="conversation-state-banner" style="display: none;">
<div class="state-indicator">
<span class="state-dot" id="state-dot"></span>
<span class="state-text" id="state-text">Ready</span>
</div>
<div class="state-timestamp" id="state-timestamp"></div>
</div>
</div>
</div>
<!-- Empty State -->
<div class="empty-state" id="empty-state" style="display: none;">
<div class="empty-content">
<span class="empty-icon">๐ฌ</span>
<h3>No conversations found</h3>
<p>No agent conversations match your current filters.</p>
<button class="empty-action" id="clear-filters">Clear Filters</button>
</div>
</div>
</div>
`;
this.bindEvents();
this.setupInfiniteScroll();
this.initializeHeaderComponent();
}
/**
* Initialize the header component
*/
initializeHeaderComponent() {
const headerContainer = this.container.querySelector('#agents-header-container');
if (headerContainer && typeof HeaderComponent !== 'undefined') {
this.headerComponent = new HeaderComponent(headerContainer, {
title: 'Claude Code Chats',
subtitle: 'Monitor and analyze Claude Code agent interactions in real-time',
version: 'v1.13.2', // Fallback version
showVersionBadge: true,
showLastUpdate: true,
showThemeSwitch: true,
showGitHubLink: true,
dataService: this.dataService // Pass DataService for dynamic version loading
});
this.headerComponent.render();
}
}
/**
* Initialize child components
*/
async initializeComponents() {
// Initialize ConversationTable for detailed view if available
const tableContainer = this.container.querySelector('#conversations-table');
if (tableContainer && typeof ConversationTable !== 'undefined') {
try {
this.components.conversationTable = new ConversationTable(
tableContainer,
this.dataService,
this.stateService
);
await this.components.conversationTable.initialize();
} catch (error) {
console.warn('ConversationTable initialization failed:', error);
// Show fallback content
tableContainer.innerHTML = `
<div class="conversation-table-placeholder">
<p>Detailed table view not available</p>
</div>
`;
}
}
}
/**
* Bind event listeners
*/
bindEvents() {
// Filter controls
const statusFilter = this.container.querySelector('#status-filter');
statusFilter.addEventListener('change', (e) => this.updateFilter('status', e.target.value));
const timeFilter = this.container.querySelector('#time-filter');
timeFilter.addEventListener('change', (e) => this.updateFilter('timeRange', e.target.value));
const searchInput = this.container.querySelector('#search-filter');
searchInput.addEventListener('input', (e) => this.updateFilter('search', e.target.value));
const clearSearch = this.container.querySelector('#clear-search');
clearSearch.addEventListener('click', () => this.clearSearch());
// Error retry
const retryBtn = this.container.querySelector('#retry-load');
if (retryBtn) {
retryBtn.addEventListener('click', () => this.loadConversationsData());
}
// Clear filters
const clearFiltersBtn = this.container.querySelector('#clear-filters');
if (clearFiltersBtn) {
clearFiltersBtn.addEventListener('click', () => this.clearAllFilters());
}
// Refresh agents
const refreshAgentsBtn = this.container.querySelector('#refresh-agents');
if (refreshAgentsBtn) {
refreshAgentsBtn.addEventListener('click', () => this.refreshAgents());
}
}
/**
* Setup infinite scroll for conversations list
*/
setupInfiniteScroll() {
const conversationsContainer = this.container.querySelector('#conversations-list');
if (!conversationsContainer) return;
conversationsContainer.addEventListener('scroll', () => {
const { scrollTop, scrollHeight, clientHeight } = conversationsContainer;
const threshold = 100; // Load more when 100px from bottom
if (scrollHeight - scrollTop - clientHeight < threshold) {
this.loadMoreConversations();
}
});
}
/**
* Update loading indicator
* @param {boolean} isLoading - Whether to show loading indicator
*/
updateLoadingIndicator(isLoading) {
const loadingIndicator = this.container.querySelector('#load-more-indicator');
if (loadingIndicator) {
loadingIndicator.style.display = isLoading ? 'flex' : 'none';
}
}
/**
* Load agents data from API
*/
async loadAgentsData() {
try {
this.showAgentsLoading(true);
const agentsData = await this.dataService.cachedFetch('/api/agents');
if (agentsData && agentsData.agents) {
this.agents = agentsData.agents;
this.renderAgents();
} else {
this.showAgentsEmpty();
}
} catch (error) {
console.error('Error loading agents data:', error);
this.showAgentsEmpty();
} finally {
this.showAgentsLoading(false);
}
}
/**
* Render global agents in the agents list (user-level only)
*/
renderAgents() {
const agentsList = this.container.querySelector('#agents-list');
const agentsCount = this.container.querySelector('#agents-count');
if (!agentsList || !agentsCount) return;
// Filter only global/user agents for main section
const globalAgents = this.agents.filter(agent => agent.level === 'user');
if (globalAgents.length === 0) {
this.showAgentsEmpty();
return;
}
// Update count for global agents only
agentsCount.textContent = `${globalAgents.length} global agent${globalAgents.length !== 1 ? 's' : ''}`;
// Render global agent items (compact rectangles)
const agentsHTML = globalAgents.map(agent => {
const levelBadge = agent.level === 'project' ? 'P' : 'U';
return `
<div class="agent-item" data-agent-id="${agent.name}">
<div class="agent-dot" style="background-color: ${agent.color}"></div>
<span class="agent-name">${agent.name}</span>
<span class="agent-level-badge ${agent.level}" title="${agent.level === 'project' ? 'Project Agent' : 'User Agent'}">${levelBadge}</span>
</div>
`;
}).join('');
agentsList.innerHTML = agentsHTML;
// Hide empty state and show list
this.hideAgentsEmpty();
agentsList.style.display = 'block';
// Bind agent events
this.bindAgentEvents();
}
/**
* Bind events for agent items
*/
bindAgentEvents() {
const agentItems = this.container.querySelectorAll('.agent-item');
agentItems.forEach(item => {
item.addEventListener('click', () => {
const agentId = item.dataset.agentId;
this.selectAgent(agentId);
});
});
}
/**
* Select an agent (opens modal with details)
* @param {string} agentId - Agent ID
*/
selectAgent(agentId) {
const agent = this.agents.find(a => a.name === agentId);
if (agent) {
this.openAgentModal(agent);
}
}
/**
* Open agent details modal
* @param {Object} agent - Agent object
*/
openAgentModal(agent) {
// If this is a specific tool, open the custom tool modal
if (agent.isToolDetails) {
this.openToolModal(agent);
return;
}
const modalHTML = `
<div class="agent-modal-overlay" id="agent-modal-overlay">
<div class="agent-modal">
<div class="agent-modal-header">
<div class="agent-modal-title">
<div class="agent-title-main">
<div class="agent-dot" style="background-color: ${agent.color}"></div>
<div class="agent-title-info">
<h3>${agent.name}</h3>
<div class="agent-subtitle">
<span class="agent-level-badge ${agent.level}">${agent.level === 'project' ? 'Project Agent' : 'User Agent'}</span>
${agent.projectName ? `<span class="agent-project-name">โข ${agent.projectName}</span>` : ''}
</div>
</div>
</div>
</div>
<button class="agent-modal-close" id="agent-modal-close">×</button>
</div>
<div class="agent-modal-content">
<div class="agent-info-section">
<h4>Description</h4>
<p>${agent.description}</p>
</div>
${agent.projectName ? `
<div class="agent-info-section">
<h4>Project</h4>
<p>${agent.projectName}</p>
</div>
` : ''}
<div class="agent-info-section">
<h4>Tools Access</h4>
<p>${agent.tools && agent.tools.length > 0
? `Has access to: ${agent.tools.join(', ')}`
: 'Has access to all available tools'}</p>
</div>
<div class="agent-info-section">
<h4>System Prompt</h4>
<div class="agent-system-prompt">${agent.systemPrompt ? agent.systemPrompt.replace(/\n/g, '<br>') : 'No system prompt available'}</div>
</div>
<div class="agent-usage-tips">
<h4>๐ก How to Use This Agent</h4>
<div class="usage-tips-content">
<p><strong>To invoke this agent explicitly:</strong></p>
<code class="usage-example">Use the ${agent.name} agent to [describe your request]</code>
<p><strong>Alternative ways to invoke:</strong></p>
<ul>
<li><code>Ask the ${agent.name} agent to [task]</code></li>
<li><code>Have the ${agent.name} agent [action]</code></li>
<li><code>Let the ${agent.name} agent handle [request]</code></li>
</ul>
<p><strong>Best practices:</strong></p>
<ul>
<li>Be specific about what you want the agent to do</li>
<li>Provide context when needed</li>
<li>The agent will automatically use appropriate tools</li>
</ul>
</div>
</div>
<div class="agent-metadata">
<small><strong>File:</strong> ${agent.filePath}</small><br>
<small><strong>Last modified:</strong> ${new Date(agent.lastModified).toLocaleString()}</small>
</div>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Bind close events
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
if (e.target.id === 'agent-modal-overlay') {
this.closeAgentModal();
}
});
// ESC key to close - store reference for cleanup
this.modalKeydownHandler = (e) => {
if (e.key === 'Escape') {
this.closeAgentModal();
}
};
document.addEventListener('keydown', this.modalKeydownHandler);
}
/**
* Open tool-specific modal for any tool
* @param {Object} toolData - Tool data object
*/
openToolModal(toolData) {
switch (toolData.name) {
case 'Read':
this.openReadToolModal(toolData);
break;
case 'Edit':
this.openEditToolModal(toolData);
break;
case 'Write':
this.openWriteToolModal(toolData);
break;
case 'Bash':
this.openBashToolModal(toolData);
break;
case 'Glob':
this.openGlobToolModal(toolData);
break;
case 'Grep':
this.openGrepToolModal(toolData);
break;
case 'TodoWrite':
this.openTodoWriteToolModal(toolData);
break;
default:
// Fallback to generic agent modal for unknown tools
this.openAgentModal({...toolData, isToolDetails: false});
break;
}
}
/**
* Open Read tool specific modal
* @param {Object} readToolData - Read tool data
*/
openReadToolModal(readToolData) {
const input = readToolData.input || {};
const filePath = input.file_path || 'Unknown file';
const fileName = filePath.split('/').pop() || 'Unknown';
const fileExtension = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : 'txt';
const offset = input.offset;
const limit = input.limit;
const toolId = readToolData.id || 'unknown';
// Analyze file context for project
const isConfigFile = ['json', 'yml', 'yaml', 'toml', 'ini', 'conf', 'config'].includes(fileExtension);
const isDocFile = ['md', 'txt', 'rst', 'adoc'].includes(fileExtension);
const isCodeFile = ['js', 'ts', 'py', 'go', 'rs', 'java', 'cpp', 'c', 'h', 'css', 'html', 'jsx', 'tsx'].includes(fileExtension);
const isProjectRoot = fileName.toLowerCase().includes('claude') || fileName.toLowerCase().includes('readme') || fileName.toLowerCase().includes('package');
const isTestFile = fileName.toLowerCase().includes('test') || fileName.toLowerCase().includes('spec');
let fileCategory = '';
let filePurpose = '';
let contextIcon = '๐';
if (isProjectRoot) {
fileCategory = 'Project Documentation';
filePurpose = 'Understanding project structure and setup';
contextIcon = '๐';
} else if (fileName.toLowerCase().includes('claude')) {
fileCategory = 'Claude Configuration';
filePurpose = 'Reading project instructions for AI assistant';
contextIcon = '๐ค';
} else if (isConfigFile) {
fileCategory = 'Configuration File';
filePurpose = 'Understanding project settings and dependencies';
contextIcon = 'โ๏ธ';
} else if (isDocFile) {
fileCategory = 'Documentation';
filePurpose = 'Reading project documentation or specifications';
contextIcon = '๐';
} else if (isTestFile) {
fileCategory = 'Test File';
filePurpose = 'Analyzing test cases and specifications';
contextIcon = '๐งช';
} else if (isCodeFile) {
fileCategory = 'Source Code';
filePurpose = 'Analyzing implementation and logic';
contextIcon = '๐ป';
} else {
fileCategory = 'Project File';
filePurpose = 'Reading project-related content';
contextIcon = '๐';
}
// Get directory context
const pathParts = filePath.split('/');
const projectContext = pathParts.includes('claude-code-templates') ? 'Claude Code Templates Project' : 'Current Project';
const relativeDir = pathParts.slice(-3, -1).join('/') || 'root';
const modalHTML = `
<div class="agent-modal-overlay" id="agent-modal-overlay">
<div class="agent-modal read-tool-modal">
<div class="agent-modal-header">
<div class="agent-modal-title">
<div class="agent-title-main">
<div class="tool-icon read-tool">
<span style="font-size: 20px;">${contextIcon}</span>
</div>
<div class="agent-title-info">
<h3>File Read: ${fileName}</h3>
<div class="agent-subtitle">
<span class="tool-type-badge">${fileCategory}</span>
<span class="tool-id-badge">ID: ${toolId.slice(-8)}</span>
</div>
</div>
</div>
</div>
<button class="agent-modal-close" id="agent-modal-close">×</button>
</div>
<div class="agent-modal-content">
<div class="raw-parameters-section primary-section">
<h4>๐ง Tool Parameters</h4>
<div class="raw-params-container">
<pre class="raw-params-json">${JSON.stringify(input, null, 2)}</pre>
</div>
<div class="params-summary">
<span class="param-chip">Tool ID: ${toolId.slice(-8)}</span>
<span class="param-chip">File: ${fileName}</span>
${offset ? `<span class="param-chip">From line: ${offset}</span>` : ''}
${limit ? `<span class="param-chip">Lines: ${limit}</span>` : '<span class="param-chip">Complete file</span>'}
</div>
</div>
<div class="read-operation-section">
<h4>๐ Read Operation Details</h4>
<div class="operation-details">
<div class="operation-item">
<span class="operation-label">Full Path:</span>
<code class="operation-value">${filePath}</code>
</div>
${offset ? `
<div class="operation-item">
<span class="operation-label">Starting Line:</span>
<code class="operation-value">${offset}</code>
</div>
` : `
<div class="operation-item">
<span class="operation-label">Read Scope:</span>
<code class="operation-value">From beginning</code>
</div>
`}
${limit ? `
<div class="operation-item">
<span class="operation-label">Lines Read:</span>
<code class="operation-value">${limit} lines</code>
</div>
` : `
<div class="operation-item">
<span class="operation-label">Read Scope:</span>
<code class="operation-value">Complete file</code>
</div>
`}
<div class="operation-item">
<span class="operation-label">Tool ID:</span>
<code class="operation-value">${toolId}</code>
</div>
</div>
</div>
<div class="file-insights-section">
<h4>๐ File Insights</h4>
<div class="insights-grid">
<div class="insight-card">
<div class="insight-header">
<span class="insight-icon">${contextIcon}</span>
<span class="insight-title">File Classification</span>
</div>
<div class="insight-content">${fileCategory}</div>
</div>
<div class="insight-card">
<div class="insight-header">
<span class="insight-icon">๐</span>
<span class="insight-title">Location Context</span>
</div>
<div class="insight-content">${relativeDir}</div>
</div>
<div class="insight-card">
<div class="insight-header">
<span class="insight-icon">๐ฏ</span>
<span class="insight-title">Read Strategy</span>
</div>
<div class="insight-content">${offset && limit ? `Partial read (${limit} lines from ${offset})` : limit ? `Limited read (${limit} lines)` : offset ? `From line ${offset}` : 'Complete file'}</div>
</div>
<div class="insight-card">
<div class="insight-header">
<span class="insight-icon">๐</span>
<span class="insight-title">Project Impact</span>
</div>
<div class="insight-content">${filePurpose}</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Bind close events
document.getElementById('agent-modal-close').addEventListener('click', () => this.closeAgentModal());
document.getElementById('agent-modal-overlay').addEventListener('click', (e) => {
if (e.target.id === 'agent-modal-overlay') {
this.closeAgentModal();
}
});
// ESC key to close - store reference for cleanup
this.modalKeydownHandler = (e) => {
if (e.key === 'Escape') {
this.closeAgentModal();
}
};
document.addEventListener('keydown', this.modalKeydownHandler);
}
/**
* Open Edit tool specific modal
* @param {Object} editToolData - Edit tool data
*/
openEditToolModal(editToolData) {
const input = editToolData.input || {};
const filePath = input.file_path || 'Unknown file';
const fileName = filePath.split('/').pop() || 'Unknown';
const oldString = input.old_string || '';
const newString = input.new_string || '';
const replaceAll = input.replace_all || false;
const toolId = editToolData.id || 'unknown';
const modalHTML = `
<div class="agent-modal-overlay" id="agent-modal-overlay">
<div class="agent-modal edit-tool-modal">
<div class="agent-modal-header">
<div class="agent-modal-title">
<div class="agent-title-main">
<div class="tool-icon edit-tool">
<span style="font-size: 20px;">โ๏ธ</span>
</div>
<div class="agent-title-info">
<h3>File Edit: ${fileName}</h3>
<div class="agent-subtitle">
<span class="tool-type-badge">File Modifica