Skip to main content
Glama
App.jsx60.3 kB
import React, { useState, useEffect, useMemo, useRef } from 'react'; import TaskTable from './components/TaskTable'; import ReleaseNotes from './components/ReleaseNotes'; import Help from './components/Help'; import TemplateManagement from './components/TemplateManagement'; import TemplateEditor from './components/TemplateEditor'; import TemplatePreview from './components/TemplatePreview'; import ActivationDialog from './components/ActivationDialog'; import DuplicateTemplateView from './components/DuplicateTemplateView'; import HistoryView from './components/HistoryView'; import HistoryTasksView from './components/HistoryTasksView'; import GlobalSettingsView from './components/GlobalSettingsView'; import SubAgentsView from './components/SubAgentsView'; import ProjectAgentsView from './components/ProjectAgentsView'; import ToastContainer from './components/ToastContainer'; import LanguageSelector from './components/LanguageSelector'; import ChatAgent from './components/ChatAgent'; import { useTranslation } from 'react-i18next'; import { parseUrlState, updateUrl, pushUrlState, getInitialUrlState, cleanUrlStateForTab } from './utils/urlStateSync'; import NestedTabs from './components/NestedTabs'; function AppContent() { const { t, i18n } = useTranslation(); const currentLanguage = i18n.language; // Initialize URL state const [urlStateInitialized, setUrlStateInitialized] = useState(false); const initialUrlState = useMemo(() => getInitialUrlState(), []); const [profiles, setProfiles] = useState([]); const [selectedProfile, setSelectedProfile] = useState(initialUrlState.profile || ''); const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [autoRefresh, setAutoRefresh] = useState(() => { const saved = localStorage.getItem('autoRefresh'); return saved !== null ? saved === 'true' : true; }); const [refreshInterval, setRefreshInterval] = useState(() => { const saved = localStorage.getItem('refreshInterval'); return saved !== null ? Number(saved) : 30; }); const [globalFilter, setGlobalFilter] = useState(''); const [showAddProfile, setShowAddProfile] = useState(false); const [draggedTabIndex, setDraggedTabIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const [projectRoot, setProjectRoot] = useState(null); const [showEditProfile, setShowEditProfile] = useState(false); const [editingProfile, setEditingProfile] = useState(null); const [isInDetailView, setIsInDetailView] = useState(false); const [isInEditMode, setIsInEditMode] = useState(false); const [forceResetDetailView, setForceResetDetailView] = useState(0); // Outer tab state const [selectedOuterTab, setSelectedOuterTab] = useState(initialUrlState.tab || 'projects'); // 'projects', 'release-notes', 'readme', 'templates' // Template management states const [templates, setTemplates] = useState([]); const [templatesLoading, setTemplatesLoading] = useState(false); const [templatesError, setTemplatesError] = useState(''); const [templateView, setTemplateView] = useState(initialUrlState.templateView || 'list'); // 'list', 'edit', 'preview', or 'duplicate' const [editingTemplate, setEditingTemplate] = useState(null); const [previewingTemplate, setPreviewingTemplate] = useState(null); const [templateEditorLoading, setTemplateEditorLoading] = useState(false); const [activatingTemplate, setActivatingTemplate] = useState(false); const [showActivationDialog, setShowActivationDialog] = useState(false); const [templateToActivate, setTemplateToActivate] = useState(null); const [duplicatingTemplate, setDuplicatingTemplate] = useState(null); // Global settings state const [claudeFolderPath, setClaudeFolderPath] = useState(''); const [globalSettingsLoaded, setGlobalSettingsLoaded] = useState(false); // Inner project tab state (tasks, history, settings) const [projectInnerTab, setProjectInnerTab] = useState(initialUrlState.projectTab || 'tasks'); // 'tasks', 'history', 'settings' const [agentsTabRefresh, setAgentsTabRefresh] = useState(0); // Trigger for refreshing agents view const [tasksTabRefresh, setTasksTabRefresh] = useState(0); // Trigger for refreshing tasks view // History management states const [historyView, setHistoryView] = useState(initialUrlState.history || ''); // 'list' or 'details' or '' for normal view const [historyData, setHistoryData] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [historyError, setHistoryError] = useState(''); const [selectedHistoryEntry, setSelectedHistoryEntry] = useState(null); const [selectedHistoryTasks, setSelectedHistoryTasks] = useState([]); // Toast notifications state const [toasts, setToasts] = useState([]); // Current task state for chat context const [currentTask, setCurrentTask] = useState(null); // Toast helper functions const showToast = (message, type = 'success', duration = 3000) => { const id = Date.now(); setToasts(prev => [...prev, { id, message, type, duration }]); }; const removeToast = (id) => { setToasts(prev => prev.filter(toast => toast.id !== id)); }; // Auto-refresh interval useEffect(() => { let interval; if (autoRefresh && selectedProfile && !isInEditMode && !isInDetailView) { interval = setInterval(() => { console.log(`Auto-refreshing tasks every ${refreshInterval}s...`); loadTasks(selectedProfile, true); // Force refresh for auto-refresh }, refreshInterval * 1000); } return () => { if (interval) clearInterval(interval); }; }, [autoRefresh, selectedProfile, refreshInterval, isInEditMode, isInDetailView]); // Load profiles on mount useEffect(() => { loadProfiles(); }, []); // Note: Environment variable checking is now handled in GlobalSettingsView component // Load global settings on mount useEffect(() => { const loadGlobalSettings = async () => { try { const response = await fetch('/api/global-settings'); if (response.ok) { const settings = await response.json(); setClaudeFolderPath(settings.claudeFolderPath || ''); } } catch (err) { console.error('Error loading global settings:', err); } finally { setGlobalSettingsLoaded(true); } }; loadGlobalSettings(); }, []); // Handle browser back/forward navigation useEffect(() => { const handlePopState = (event) => { const urlState = parseUrlState(); // Update tab if (urlState.tab && urlState.tab !== selectedOuterTab) { setSelectedOuterTab(urlState.tab); if (urlState.tab === 'projects' && urlState.profile && profiles.some(p => p.id === urlState.profile)) { setSelectedProfile(urlState.profile); loadTasks(urlState.profile); } else if (urlState.tab === 'templates') { loadTemplates(); } } // Update profile if on projects tab if (urlState.tab === 'projects' && urlState.profile && urlState.profile !== selectedProfile) { if (profiles.some(p => p.id === urlState.profile)) { setSelectedProfile(urlState.profile); loadTasks(urlState.profile); } } // Update project inner tab if (urlState.projectTab && urlState.projectTab !== projectInnerTab) { setProjectInnerTab(urlState.projectTab); if (urlState.projectTab === 'history' && !historyData.length && urlState.profile) { loadHistory(urlState.profile); } } // Update history view if (urlState.history !== historyView) { setHistoryView(urlState.history || ''); if (urlState.history === 'list' && urlState.profile) { loadHistory(urlState.profile); } else if (urlState.history === 'details' && urlState.historyDate) { loadHistoryTasks(urlState.historyDate, { date: urlState.historyDate }); } } // Update template view if (urlState.templateView && urlState.templateView !== templateView) { setTemplateView(urlState.templateView); } }; window.addEventListener('popstate', handlePopState); return () => window.removeEventListener('popstate', handlePopState); }, [selectedOuterTab, selectedProfile, profiles, historyView, templateView, projectInnerTab]); // Save selected profile and outer tab to localStorage when they change useEffect(() => { if (selectedProfile) { localStorage.setItem('selectedProfile', selectedProfile); } localStorage.setItem('selectedOuterTab', selectedOuterTab); }, [selectedProfile, selectedOuterTab]); // Save auto-refresh settings to localStorage when they change useEffect(() => { localStorage.setItem('autoRefresh', autoRefresh.toString()); }, [autoRefresh]); useEffect(() => { localStorage.setItem('refreshInterval', refreshInterval.toString()); }, [refreshInterval]); // Update URL when history view changes useEffect(() => { if (urlStateInitialized) { const urlState = { tab: selectedOuterTab, profile: selectedProfile, lang: currentLanguage, history: historyView || undefined, historyDate: selectedHistoryEntry?.date || undefined }; updateUrl(urlState); } }, [historyView, selectedHistoryEntry, urlStateInitialized]); // Update URL when template view changes useEffect(() => { if (urlStateInitialized && selectedOuterTab === 'templates') { const urlState = { tab: 'templates', lang: currentLanguage, templateView: templateView !== 'list' ? templateView : undefined, templateId: (editingTemplate?.name || previewingTemplate?.name || duplicatingTemplate?.name) || undefined }; updateUrl(urlState); } }, [templateView, editingTemplate, previewingTemplate, duplicatingTemplate, urlStateInitialized]); const loadProfiles = async () => { try { const response = await fetch('/api/agents'); if (!response.ok) throw new Error('Failed to load profiles'); const data = await response.json(); setProfiles(data); // On initial load, restore state from URL if (!urlStateInitialized && data.length > 0) { setUrlStateInitialized(true); // If on projects tab and profile is specified in URL if (initialUrlState.tab === 'projects' && initialUrlState.profile) { if (data.some(p => p.id === initialUrlState.profile)) { handleProfileChange(initialUrlState.profile); } else { // Profile not found, use first one handleProfileChange(data[0].id); } } else if (initialUrlState.tab === 'projects') { // No profile in URL, use saved or first profile const savedProfile = localStorage.getItem('selectedProfile'); if (savedProfile && data.some(p => p.id === savedProfile)) { handleProfileChange(savedProfile); } else { handleProfileChange(data[0].id); } } else if (initialUrlState.tab === 'templates') { loadTemplates(); } // If history view is specified in URL if (initialUrlState.history) { setHistoryView(initialUrlState.history); if (initialUrlState.history === 'list' && initialUrlState.profile) { loadHistory(initialUrlState.profile); } else if (initialUrlState.history === 'details' && initialUrlState.historyDate) { loadHistoryTasks(initialUrlState.historyDate, { date: initialUrlState.historyDate }); } } } } catch (err) { setError('Failed to load profiles: ' + err.message); } }; // Cache for loaded tasks to avoid redundant API calls const tasksCache = useMemo(() => new Map(), []); const loadingRef = useRef(false); const loadTasks = async (profileId, forceRefresh = false) => { if (!profileId) { setTasks([]); return; } // Prevent multiple simultaneous loads if (loadingRef.current) { console.log('Load already in progress, skipping...'); return; } // Check cache first unless force refresh if (!forceRefresh && tasksCache.has(profileId)) { const cachedData = tasksCache.get(profileId); setTasks(cachedData.tasks); setProjectRoot(cachedData.projectRoot); return; } loadingRef.current = true; setLoading(true); setError(''); try { const response = await fetch(`/api/tasks/${profileId}?t=${Date.now()}`); if (!response.ok) { throw new Error(`Failed to load tasks: ${response.status}`); } const data = await response.json(); console.log('Received tasks data:', data.tasks?.length, 'tasks'); // Check if there's a message about missing tasks.json if (data.message && data.tasks?.length === 0) { setError(''); // Clear any previous errors // Show informative message instead of error showToast(data.message, 'info', 7000); } // Cache the data tasksCache.set(profileId, { tasks: data.tasks || [], projectRoot: data.projectRoot || null }); setTasks(data.tasks || []); setProjectRoot(data.projectRoot || null); } catch (err) { setError('❌ Error loading tasks: ' + err.message); setTasks([]); } finally { setLoading(false); loadingRef.current = false; } }; const handleProfileChange = (profileId) => { console.log('handleProfileChange:', profileId, 'selectedProfile:', selectedProfile, 'isInDetailView:', isInDetailView); // If clicking the same tab and we're in detail view, just refresh to go back to list if (profileId === selectedProfile && isInDetailView) { console.log('Resetting detail view...'); // Force reset the detail view setForceResetDetailView(prev => { console.log('Setting forceResetDetailView from', prev, 'to', prev + 1); return prev + 1; }); setIsInDetailView(false); return; } // Reset history view when switching profiles if (historyView) { setHistoryView(''); setHistoryData([]); setSelectedHistoryEntry(null); setSelectedHistoryTasks([]); } // Clear any stuck loading state and force reload loadingRef.current = false; setSelectedProfile(profileId); setGlobalFilter(''); // Clear search when switching profiles loadTasks(profileId, true); // Force refresh to bypass cache // Update URL when profile changes const urlState = { tab: selectedOuterTab, profile: profileId, projectTab: projectInnerTab, lang: currentLanguage }; pushUrlState(urlState); }; const handleOuterTabChange = (tab) => { setSelectedOuterTab(tab); // Reset states when switching outer tabs if (tab === 'templates') { loadTemplates(); setTemplateView('list'); setEditingTemplate(null); } else if (tab === 'projects' && profiles.length > 0) { // Reset history state when switching back to projects setHistoryView(''); setHistoryData([]); setSelectedHistoryEntry(null); setSelectedHistoryTasks([]); // When switching back to projects, ensure a profile is selected if (!selectedProfile || selectedProfile === 'release-notes' || selectedProfile === 'help' || selectedProfile === 'templates') { const savedProfile = localStorage.getItem('selectedProfile'); if (savedProfile && profiles.some(p => p.id === savedProfile)) { handleProfileChange(savedProfile); } else { handleProfileChange(profiles[0].id); } } } // Update URL when tab changes const cleanState = cleanUrlStateForTab(tab, { tab, profile: selectedProfile, lang: currentLanguage, history: historyView, historyDate: selectedHistoryEntry?.date, templateView, templateId: editingTemplate?.name || previewingTemplate?.name }); pushUrlState(cleanState); }; const handleAddProfile = async (name, file, projectRoot, filePath) => { try { let body; if (filePath) { // Direct file path method body = JSON.stringify({ name, filePath, projectRoot: projectRoot || null }); console.log('Sending profile with file path:', { name, filePath, projectRoot }); } else { // File upload method const taskFileContent = await file.text(); body = JSON.stringify({ name, taskFile: taskFileContent, projectRoot: projectRoot || null }); console.log('Sending profile with file content'); } console.log('Request body:', body); const response = await fetch('/api/add-project', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || 'Failed to add profile'); } const responseText = await response.text(); let newProfile; try { newProfile = JSON.parse(responseText); } catch (parseErr) { console.error('Failed to parse response:', responseText); throw new Error('Invalid response format from server'); } console.log('New profile created:', newProfile); // Show success toast showToast(t('profileAddedSuccess', { name }), 'success'); setShowAddProfile(false); // Load profiles first, then select the new one await loadProfiles(); // Use setTimeout to ensure state updates have propagated setTimeout(() => { if (newProfile && newProfile.id) { console.log('Auto-selecting profile:', newProfile.id); setSelectedProfile(newProfile.id); setGlobalFilter(''); // Clear search when switching profiles loadTasks(newProfile.id); } }, 100); } catch (err) { const errorMessage = 'Failed to add profile: ' + err.message; setError(errorMessage); showToast(errorMessage, 'error', 5000); } }; const handleRemoveProfile = async (profileId) => { if (!confirm(t('confirmRemoveProfile'))) { return; } try { const response = await fetch(`/api/remove-project/${profileId}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Failed to remove profile'); } // Find profile name for toast const profile = profiles.find(p => p.id === profileId); const profileName = profile ? profile.name : profileId; // Show success toast showToast(t('profileRemovedSuccess', { name: profileName }), 'success'); // If we're removing the currently selected profile, clear selection if (selectedProfile === profileId) { setSelectedProfile(''); setTasks([]); } await loadProfiles(); } catch (err) { const errorMessage = 'Failed to remove profile: ' + err.message; setError(errorMessage); showToast(errorMessage, 'error', 5000); } }; const handleUpdateProfile = async (profileId, updates) => { try { const response = await fetch(`/api/update-project/${profileId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!response.ok) { throw new Error(`Failed to update profile: ${response.status}`); } const updatedProfile = await response.json(); // Update profiles in state setProfiles(prev => prev.map(p => p.id === profileId ? { ...p, ...updatedProfile } : p )); // Update projectRoot if it was changed if (updates.projectRoot !== undefined) { setProjectRoot(updates.projectRoot); } setShowEditProfile(false); setEditingProfile(null); // Reload tasks to reflect any changes (especially if task path was changed) loadTasks(profileId); } catch (err) { setError('Failed to update profile: ' + err.message); } }; // Template management functions const loadTemplates = async () => { setTemplatesLoading(true); setTemplatesError(''); try { const response = await fetch('/api/templates'); if (!response.ok) { throw new Error(`Failed to load templates: ${response.status}`); } const data = await response.json(); // Transform data to match TemplateManagement component expectations const templatesData = data.map(template => ({ functionName: template.name, description: `${template.status} template from ${template.source}`, status: template.status, source: template.source, contentLength: template.contentLength, category: 'Task Management' })); setTemplates(templatesData); } catch (err) { setTemplatesError('❌ Error loading templates: ' + err.message); setTemplates([]); } finally { setTemplatesLoading(false); } }; const handleEditTemplate = async (template) => { if (!template) { setTemplatesError('No template provided for editing'); return; } const functionName = template.functionName || template.name; if (!functionName) { setTemplatesError('Template is missing functionName'); return; } try { setTemplateEditorLoading(true); const response = await fetch(`/api/templates/${functionName}`); if (!response.ok) { throw new Error(`Failed to load template: ${response.status}`); } const templateData = await response.json(); setEditingTemplate({ ...templateData, functionName }); setTemplateView('edit'); } catch (err) { setTemplatesError('Failed to load template for editing: ' + err.message); showToast('Failed to load template for editing', 'error'); } finally { setTemplateEditorLoading(false); } }; const handleSaveTemplate = async (templateData) => { try { setTemplateEditorLoading(true); setTemplatesError(''); // Clear any previous errors const response = await fetch(`/api/templates/${templateData.functionName}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: templateData.content, mode: templateData.mode }) }); if (!response.ok) { throw new Error(`Failed to save template: ${response.status}`); } // Show success toast showToast(t('templateSavedSuccess', { name: templateData.functionName }), 'success'); // Go back to list and reload templates setTemplateView('list'); setEditingTemplate(null); loadTemplates(); } catch (err) { const errorMessage = 'Failed to save template: ' + err.message; setTemplatesError(errorMessage); showToast(errorMessage, 'error', 5000); } finally { setTemplateEditorLoading(false); } }; const handleCancelTemplateEdit = () => { setTemplateView('list'); setEditingTemplate(null); }; const handleResetTemplate = async (template) => { const functionName = template.functionName; if (!confirm(t('confirmResetTemplate', { name: functionName }))) { return; } try { const response = await fetch(`/api/templates/${functionName}`, { method: 'DELETE' }); if (!response.ok) { throw new Error(`Failed to reset template: ${response.status}`); } // Show success toast showToast(t('templateResetSuccess', { name: functionName }), 'success'); // Reload templates to reflect changes loadTemplates(); } catch (err) { const errorMessage = 'Failed to reset template: ' + err.message; setTemplatesError(errorMessage); showToast(errorMessage, 'error', 5000); } }; const handleDuplicateTemplate = async (template) => { setDuplicatingTemplate(template); setTemplateView('duplicate'); }; const confirmDuplicateTemplate = async (newName) => { if (!duplicatingTemplate) return; const functionName = duplicatingTemplate.functionName; try { const response = await fetch(`/api/templates/${functionName}/duplicate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ newName }) }); if (!response.ok) { throw new Error(`Failed to duplicate template: ${response.status}`); } // Show success toast showToast(t('templateDuplicatedSuccess', { name: newName }), 'success'); // Return to template list setTemplateView('list'); setDuplicatingTemplate(null); // Reload templates to reflect changes loadTemplates(); } catch (err) { const errorMessage = 'Failed to duplicate template: ' + err.message; setTemplatesError(errorMessage); showToast(errorMessage, 'error', 5000); throw err; // Re-throw to handle in the view } }; const handlePreviewTemplate = async (template) => { const functionName = template.functionName; try { const response = await fetch(`/api/templates/${functionName}`); if (!response.ok) { throw new Error(`Failed to load template: ${response.status}`); } const templateData = await response.json(); // Ensure functionName is preserved in the previewing template setPreviewingTemplate({ ...templateData, functionName: functionName, status: template.status // Also preserve status }); setTemplateView('preview'); setTemplatesError(''); } catch (err) { setTemplatesError('Failed to load template for preview: ' + err.message); showToast('Failed to load template for preview', 'error'); } }; const handleActivateTemplate = async (template) => { // Ensure we have the full template data if (!template.content && template.functionName) { // Fetch the full template if we only have basic info try { const response = await fetch(`/api/templates/${template.functionName}`); if (response.ok) { const fullTemplate = await response.json(); setTemplateToActivate(fullTemplate); setShowActivationDialog(true); } else { showToast('Failed to load template for activation', 'error'); } } catch (err) { showToast('Error loading template: ' + err.message, 'error'); } } else { // Show the activation dialog with the template we have setTemplateToActivate(template); setShowActivationDialog(true); } }; // History management functions const loadHistory = async (profileId) => { if (!profileId) { setHistoryData([]); return; } setHistoryLoading(true); setHistoryError(''); try { const response = await fetch(`/api/history/${profileId}?t=${Date.now()}`); if (!response.ok) { throw new Error(`Failed to load history: ${response.status}`); } const data = await response.json(); setHistoryData(data.history || []); // Show message if no history found if (data.message && data.history?.length === 0) { showToast(data.message, 'info', 7000); } } catch (err) { setHistoryError('❌ Error loading history: ' + err.message); setHistoryData([]); } finally { setHistoryLoading(false); } }; const loadHistoryTasks = async (historyEntry) => { if (!selectedProfile || !historyEntry) return; setHistoryLoading(true); setHistoryError(''); try { const response = await fetch(`/api/history/${selectedProfile}/${historyEntry.filename}?t=${Date.now()}`); if (!response.ok) { throw new Error(`Failed to load history tasks: ${response.status}`); } const data = await response.json(); setSelectedHistoryTasks(data.tasks || []); setSelectedHistoryEntry(historyEntry); setHistoryView('details'); } catch (err) { setHistoryError('❌ Error loading history tasks: ' + err.message); setSelectedHistoryTasks([]); } finally { setHistoryLoading(false); } }; // Drag and drop handlers for tab reordering const handleDragStart = (e, index) => { setDraggedTabIndex(index); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/html', ''); // Required for Firefox compatibility }; const handleDragOver = (e, index) => { e.preventDefault(); // Required to allow drop setDragOverIndex(index); // Visual feedback for drop target }; const handleDragEnd = () => { // Clean up drag state regardless of drop success setDraggedTabIndex(null); setDragOverIndex(null); }; const handleDrop = (e, dropIndex) => { e.preventDefault(); // Ignore invalid drops (same position or invalid state) if (draggedTabIndex === null || draggedTabIndex === dropIndex) { return; } // Reorder profiles array using splice operations const newProfiles = [...profiles]; const draggedProfile = newProfiles[draggedTabIndex]; // Remove dragged item from original position newProfiles.splice(draggedTabIndex, 1); // Insert at new position newProfiles.splice(dropIndex, 0, draggedProfile); // Update state and clear drag indicators setProfiles(newProfiles); setDraggedTabIndex(null); setDragOverIndex(null); }; // Memoized task statistics to avoid recalculation on every render const stats = useMemo(() => { const total = tasks.length; const completed = tasks.filter(t => t.status === 'completed').length; const inProgress = tasks.filter(t => t.status === 'in_progress').length; const pending = tasks.filter(t => t.status === 'pending').length; return { total, completed, inProgress, pending }; }, [tasks]); return ( <div className="app"> <ToastContainer toasts={toasts} removeToast={removeToast} /> {showActivationDialog && ( <ActivationDialog template={templateToActivate} onClose={() => { setShowActivationDialog(false); setTemplateToActivate(null); showToast(t('rememberToRestartClaude'), 'info', 5000); }} /> )} <header className="header"> <h1>{t('appTitle')}</h1> <div className="header-content"> <div className="version-info"> <span>{t('version')} 3.0.0</span> • <a href="#" onClick={(e) => { e.preventDefault(); handleOuterTabChange('release-notes'); }}> {t('releaseNotes')} </a> • <a href="#" onClick={(e) => { e.preventDefault(); handleOuterTabChange('readme'); }}> {t('help')} </a> • <a href="#" onClick={(e) => { e.preventDefault(); handleOuterTabChange('templates'); }}> {t('templates')} </a> </div> <LanguageSelector /> </div> </header> <NestedTabs profiles={profiles} selectedProfile={selectedProfile} handleProfileChange={handleProfileChange} handleRemoveProfile={handleRemoveProfile} setShowAddProfile={setShowAddProfile} projectInnerTab={projectInnerTab} setProjectInnerTab={(tab) => { setProjectInnerTab(tab); if (tab === 'history' && !historyData.length && selectedProfile) { loadHistory(selectedProfile); } // Trigger refresh for agents tab to reset viewing state if (tab === 'agents') { setAgentsTabRefresh(prev => prev + 1); } // Trigger refresh for tasks tab to reset viewing state if (tab === 'tasks') { setTasksTabRefresh(prev => prev + 1); setForceResetDetailView(prev => prev + 1); // Reset task detail view } // Update URL pushUrlState({ tab: 'projects', profile: selectedProfile, projectTab: tab, lang: currentLanguage }); }} selectedOuterTab={selectedOuterTab} onOuterTabChange={handleOuterTabChange} draggedTabIndex={draggedTabIndex} dragOverIndex={dragOverIndex} handleDragStart={handleDragStart} handleDragOver={handleDragOver} handleDragEnd={handleDragEnd} handleDrop={handleDrop} loading={loading} error={error} claudeFolderPath={claudeFolderPath} children={{ tasks: !selectedProfile && profiles.length > 0 ? ( <div className="content-container" name="no-profile-container"> <div className="loading" name="no-profile-message" title="Choose a profile from the dropdown above">Select a profile to view tasks</div> </div> ) : profiles.length === 0 ? ( <div className="content-container" name="no-profile-container"> <div className="loading" name="no-profile-message" title={t('noProfilesAvailable')}>{t('noProfilesClickAddTab')}</div> </div> ) : loading && selectedProfile ? ( <div className="content-container" name="loading-container"> <div className="loading" name="loading-indicator" title="Loading tasks from file">Loading tasks... ⏳</div> </div> ) : error ? ( <div className="error" name="error-message-display" title="Error information">{error}</div> ) : ( <div className="content-container" name="main-content-area"> <div className="stats-and-search-container" name="stats-and-search-row"> <div className="search-container" name="task-search-controls"> <input type="text" name="task-search-input" className="search-input" placeholder={t('searchTasksPlaceholder')} value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)} title="Search and filter tasks by any text content" /> </div> <div className="stats-grid" name="task-statistics-display"> <div className="stat-card" name="total-tasks-counter" title="Total number of tasks in this profile"> <h3>{t('totalTasks')}</h3> <div className="value">{stats.total}</div> </div> <div className="stat-card" name="completed-tasks-counter" title="Number of completed tasks"> <h3>{t('completed')}</h3> <div className="value">{stats.completed}</div> </div> <div className="stat-card" name="in-progress-tasks-counter" title="Number of tasks currently in progress"> <h3>{t('inProgress')}</h3> <div className="value">{stats.inProgress}</div> </div> <div className="stat-card" name="pending-tasks-counter" title="Number of pending tasks"> <h3>{t('pending')}</h3> <div className="value">{stats.pending}</div> </div> </div> <div className="controls-right" name="right-side-controls"> <button name="refresh-profile-button" className="refresh-button profile-refresh" onClick={() => loadTasks(selectedProfile, true)} // Force refresh on manual click disabled={loading || !selectedProfile} title="Refresh current profile data - reload tasks from file" > {loading ? '⏳' : '🔄'} </button> <div className="auto-refresh-controls" name="auto-refresh-controls" title="Configure automatic task refresh"> <label className="auto-refresh" name="auto-refresh-toggle"> <input type="checkbox" name="auto-refresh-checkbox" checked={autoRefresh} onChange={(e) => setAutoRefresh(e.target.checked)} title={`Enable automatic refresh every ${refreshInterval} seconds`} /> {t('autoRefresh')} </label> <select className="refresh-interval-select" name="refresh-interval-selector" value={refreshInterval} onChange={(e) => setRefreshInterval(Number(e.target.value))} disabled={!autoRefresh} title="Set how often to automatically refresh task data" > <option value={5}>5s</option> <option value={10}>10s</option> <option value={15}>15s</option> <option value={30}>30s</option> <option value={60}>1m</option> <option value={120}>2m</option> <option value={300}>5m</option> </select> </div> </div> </div> <TaskTable data={tasks} globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter} projectRoot={projectRoot} onDetailViewChange={(inDetailView, inEditMode, taskId) => { setIsInDetailView(inDetailView); setIsInEditMode(inEditMode || false); // Set current task for chat context if (inDetailView && taskId) { const task = tasks.find(t => t.id === taskId); setCurrentTask(task || null); } else { setCurrentTask(null); } // Update URL when viewing/editing task if (inDetailView && taskId) { pushUrlState({ tab: 'projects', profile: selectedProfile, projectTab: 'tasks', taskView: inEditMode ? 'edit' : 'view', taskId: taskId, lang: currentLanguage }); } else { // Clear task state when going back to list pushUrlState({ tab: 'projects', profile: selectedProfile, projectTab: 'tasks', lang: currentLanguage }); } }} resetDetailView={forceResetDetailView} profileId={selectedProfile} onTaskSaved={async () => { // Force refresh tasks after save await loadTasks(selectedProfile, true); showToast(t('taskSavedSuccess'), 'success'); }} onDeleteTask={async (taskId) => { try { const response = await fetch(`/api/tasks/${selectedProfile}/${taskId}/delete`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Failed to delete task'); } // Refresh tasks after delete await loadTasks(selectedProfile, true); showToast(t('taskDeletedSuccess'), 'success'); } catch (err) { console.error('Error deleting task:', err); showToast('Failed to delete task: ' + err.message, 'error'); } }} /> </div> ), history: ( <div className="content-container" name="history-content-area"> {historyView === 'details' ? ( <HistoryTasksView tasks={selectedHistoryTasks} historyEntry={selectedHistoryEntry} loading={historyLoading} error={historyError} onBack={() => { setHistoryView('list'); setSelectedHistoryEntry(null); setSelectedHistoryTasks([]); }} /> ) : ( <HistoryView data={historyData} loading={historyLoading} error={historyError} onViewTasks={loadHistoryTasks} onBack={null} // No back button needed in tab view profileId={selectedProfile} /> )} </div> ), agents: ( <ProjectAgentsView profileId={selectedProfile} projectRoot={projectRoot} showToast={showToast} refreshTrigger={agentsTabRefresh} onAgentViewChange={(view, agentId) => { // Update URL when viewing/editing agent if (view && agentId) { pushUrlState({ tab: 'projects', profile: selectedProfile, projectTab: 'agents', agentView: view, agentId: agentId, lang: currentLanguage }); } else { // Clear agent state when going back to list pushUrlState({ tab: 'projects', profile: selectedProfile, projectTab: 'agents', lang: currentLanguage }); } }} /> ), settings: ( <div className="content-container" name="settings-content-area"> <div className="settings-panel"> <h2>{t('projectSettings')}</h2> {profiles.find(p => p.id === selectedProfile) && ( <form name="settings-form" onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.target); const name = formData.get('name'); const taskPath = formData.get('taskPath'); const projectRoot = formData.get('projectRoot'); // Validate task path format if (taskPath && !taskPath.trim().endsWith('.json')) { setError('Task path must point to a .json file'); return; } handleUpdateProfile(selectedProfile, { name: name.trim(), taskPath: taskPath?.trim() || null, projectRoot: projectRoot || null }); showToast(t('settingsSaved'), 'success'); }}> <div className="form-group" name="profile-name-group"> <label htmlFor="settingsProfileName">{t('profileName')}:</label> <input type="text" id="settingsProfileName" name="name" defaultValue={profiles.find(p => p.id === selectedProfile)?.name} placeholder="e.g., Team Alpha Tasks" title="Edit the profile name" required /> </div> <div className="form-group" name="task-path-group"> <label htmlFor="settingsTaskPath">{t('taskPath')}:</label> <input type="text" id="settingsTaskPath" name="taskPath" defaultValue={profiles.find(p => p.id === selectedProfile)?.taskPath || profiles.find(p => p.id === selectedProfile)?.filePath || profiles.find(p => p.id === selectedProfile)?.path || ''} placeholder={t('taskPathPlaceholder')} title={t('taskPathTitle')} required /> <span className="form-hint"> {t('taskPathHint')} </span> </div> <div className="form-group" name="project-root-group"> <label htmlFor="settingsProjectRoot">{t('projectRoot')} ({t('optional')}):</label> <input type="text" id="settingsProjectRoot" name="projectRoot" defaultValue={profiles.find(p => p.id === selectedProfile)?.projectRoot || ''} placeholder={t('projectRootEditPlaceholder')} title={t('projectRootEditTitle')} /> <span className="form-hint"> {t('projectRootEditHint')} </span> </div> <div className="form-actions" name="settings-form-buttons"> <button type="submit" name="submit-settings" className="primary-btn" title="Save project settings" > {t('saveChanges')} </button> </div> </form> )} </div> </div> ), releaseNotes: <ReleaseNotes />, readme: <Help />, templates: ( <div className="content-container" name="templates-content-area"> {templateView === 'list' ? ( <> <div className="stats-and-search-container" name="templates-stats-and-search-row"> <div className="search-container" name="template-search-controls"> <input type="text" name="template-search-input" className="search-input" placeholder={t('searchTemplatesPlaceholder')} value={globalFilter} onChange={(e) => setGlobalFilter(e.target.value)} title="Search and filter templates by function name or description" /> </div> <div className="stats-grid" name="template-statistics-display"> <div className="stat-card" name="total-templates-counter" title="Total number of templates"> <h3>Total Templates</h3> <div className="value">{templates.length}</div> </div> <div className="stat-card" name="default-templates-counter" title="Number of default templates"> <h3>Default</h3> <div className="value">{templates.filter(t => t.status === 'default').length}</div> </div> <div className="stat-card" name="custom-templates-counter" title="Number of custom templates"> <h3>Custom</h3> <div className="value">{templates.filter(t => t.status === 'custom').length}</div> </div> <div className="stat-card" name="env-override-templates-counter" title="Number of environment overridden templates"> <h3>Env Override</h3> <div className="value">{templates.filter(t => t.status.includes('env')).length}</div> </div> </div> <div className="controls-right" name="template-controls-right"> <button name="refresh-templates-button" className="refresh-button template-refresh" onClick={() => loadTemplates()} disabled={templatesLoading} title="Refresh template data" > {templatesLoading ? '⏳' : '🔄'} </button> </div> </div> <TemplateManagement data={templates} globalFilter={globalFilter} onGlobalFilterChange={setGlobalFilter} loading={templatesLoading} error={templatesError} onEditTemplate={handleEditTemplate} onResetTemplate={handleResetTemplate} onDuplicateTemplate={handleDuplicateTemplate} onPreviewTemplate={handlePreviewTemplate} onActivateTemplate={handleActivateTemplate} /> {templatesError && ( <div className="error" name="templates-error-message" title="Template error information">{templatesError}</div> )} </> ) : templateView === 'edit' ? ( // Template Editor View <TemplateEditor template={editingTemplate} onSave={handleSaveTemplate} onCancel={handleCancelTemplateEdit} loading={templateEditorLoading} error={templatesError} onActivate={handleActivateTemplate} /> ) : templateView === 'preview' ? ( // Template Preview View <TemplatePreview template={previewingTemplate} onBack={() => { setTemplateView('list'); setPreviewingTemplate(null); }} onActivate={handleActivateTemplate} onEdit={handleEditTemplate} activating={activatingTemplate} /> ) : templateView === 'duplicate' ? ( // Template Duplicate View <DuplicateTemplateView template={duplicatingTemplate} onBack={() => { setTemplateView('list'); setDuplicatingTemplate(null); }} onConfirm={confirmDuplicateTemplate} /> ) : null} </div> ), globalSettings: ( <GlobalSettingsView showToast={showToast} /> ), subAgents: claudeFolderPath ? ( <SubAgentsView showToast={showToast} onNavigateToSettings={() => handleOuterTabChange('global-settings')} /> ) : ( <div className="content-container"> <div className="loading"> Claude folder path is not configured. Please configure it in{' '} <span className="settings-link" onClick={() => handleOuterTabChange('global-settings')} style={{ color: '#3498db', cursor: 'pointer', textDecoration: 'underline', fontWeight: 'bold' }} title="Click to go to settings" > Global Settings </span>{' '} to access Sub-Agents. </div> </div> ) }} /> {showAddProfile && ( <div className="modal-overlay" name="add-profile-modal-overlay" onClick={() => setShowAddProfile(false)} title="Click outside to close"> <div className="modal-content" name="add-profile-modal" onClick={(e) => e.stopPropagation()} title="Add new profile form"> <h3>{t('addNewProfile')}</h3> <form name="add-profile-form" onSubmit={(e) => { e.preventDefault(); const formData = new FormData(e.target); const name = formData.get('name'); const folderPath = formData.get('folderPath'); const projectRoot = formData.get('projectRoot'); if (name && folderPath) { // Auto-append tasks.json to the folder path const filePath = folderPath.endsWith('/') ? folderPath + 'tasks.json' : folderPath + '/tasks.json'; handleAddProfile(name, null, projectRoot, filePath); } }}> <div className="form-group" name="profile-name-group"> <label htmlFor="profileName">{t('profileName')}:</label> <input type="text" id="profileName" name="name" placeholder="e.g., Team Alpha Tasks" title="Enter a descriptive name for this profile" required /> </div> <div className="form-group" name="task-folder-group"> <label htmlFor="folderPath">{t('taskFolderPath')}:</label> <input type="text" id="folderPath" name="folderPath" placeholder="/path/to/shrimp_data_folder" title="Enter the path to your shrimp data folder containing tasks.json" required /> <span className="form-hint"> <strong>Tip:</strong> Navigate to your shrimp data folder in terminal and <strong style={{ color: '#f59e0b' }}>type <code>pwd</code> to get the full path</strong><br /> Example: /home/user/project/shrimp_data_team </span> </div> <div className="form-group" name="project-root-group"> <label htmlFor="projectRoot">{t('projectRootPath')} ({t('optional')}):</label> <input type="text" id="projectRoot" name="projectRoot" placeholder="/path/to/your/project/root" title="Optional: Enter the project root path for VS Code file links" /> <small className="form-hint">This enables clickable file links that open in VS Code</small> </div> <div className="form-actions" name="modal-form-buttons"> <button type="submit" name="submit-add-profile" className="primary-btn" title="Create the new profile with the provided information" > Add Profile </button> <button type="button" name="cancel-add-profile" className="secondary-btn" onClick={() => setShowAddProfile(false)} title={t('cancelAndCloseDialog')} > {t('cancel')} </button> </div> </form> </div> </div> )} {/* Chat Agent - Available on all pages when a profile is selected */} {selectedProfile && ( <ChatAgent currentPage={ selectedOuterTab === 'projects' ? (isInDetailView ? 'task-detail' : (projectInnerTab === 'history' ? 'history' : projectInnerTab === 'settings' ? 'project-settings' : 'task-list')) : selectedOuterTab } currentTask={currentTask} tasks={tasks} profileId={selectedProfile} profileName={profiles.find(p => p.id === selectedProfile)?.name} projectRoot={projectRoot} showToast={showToast} projectInnerTab={projectInnerTab} isInDetailView={isInDetailView} onTaskUpdate={async (taskId, updates) => { // Handle task updates from chat console.log('onTaskUpdate called with:', taskId, updates); console.log('Current task:', currentTask?.id); try { const response = await fetch(`/api/tasks/${selectedProfile}/update`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ taskId, updates }) }); console.log('Update response:', response.ok, response.status); if (response.ok) { // Update local state immediately for better UX if (currentTask && currentTask.id === taskId) { console.log('Updating current task with:', updates); setCurrentTask(prev => { const updated = { ...prev, ...updates, updatedAt: new Date().toISOString() }; console.log('Updated current task:', updated); return updated; }); } // Update the tasks list as well console.log('Updating tasks list'); setTasks(prevTasks => prevTasks.map(task => task.id === taskId ? { ...task, ...updates, updatedAt: new Date().toISOString() } : task ) ); return true; } return false; } catch (err) { console.error('Error updating task from chat:', err); return false; } }} /> )} </div> ); } export default AppContent;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cjo4m06/mcp-shrimp-task-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server