UNPKG

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
/** * 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()">&times;</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">&times;</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">&times;</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