Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
Intelligence.tsx29.3 kB
import React, { useState, useEffect, useCallback, useId } from 'react'; import './styles.css'; declare const vscode: any; interface FolderInfo { id: string; path: string; hostPath?: string; fileCount: number; chunkCount: number; embeddingCount: number; status: 'active' | 'inactive' | 'stopped' | 'error'; lastSync: string; patterns?: string[]; isIndexing?: boolean; error?: string | null; } /** * Type guard to validate FolderInfo objects at runtime * Ensures all required fields exist before attempting to use folder data */ function isFolderInfo(obj: any): obj is FolderInfo { return ( obj && typeof obj === 'object' && typeof obj.id === 'string' && obj.id.length > 0 && typeof obj.path === 'string' && typeof obj.fileCount === 'number' && typeof obj.chunkCount === 'number' && typeof obj.embeddingCount === 'number' && typeof obj.status === 'string' && ['active', 'inactive', 'stopped', 'error'].includes(obj.status) && typeof obj.lastSync === 'string' ); } interface IndexingProgress { path: string; totalFiles: number; indexed: number; skipped: number; errored: number; currentFile?: string; status: 'queued' | 'indexing' | 'completed' | 'cancelled' | 'error'; startTime?: number; endTime?: number; } interface IndexStats { totalFolders: number; totalFiles: number; totalChunks: number; totalEmbeddings: number; byType: Record<string, number>; byExtension: Record<string, number>; } interface SearchResult { id: string; type: string; title: string; path: string; absolute_path?: string; similarity: number; parent_file?: { path: string; absolute_path: string; name: string; language: string; }; } export function Intelligence() { const [folders, setFolders] = useState<FolderInfo[]>([]); const [stats, setStats] = useState<IndexStats | null>(null); const [loading, setLoading] = useState(false); const [apiUrl, setApiUrl] = useState('http://localhost:9042'); const [authHeaders, setAuthHeaders] = useState<Record<string, string>>({}); const [error, setError] = useState<string | null>(null); const [progressMap, setProgressMap] = useState<Map<string, IndexingProgress>>(new Map()); const [configReceived, setConfigReceived] = useState(false); // Memoize auth headers to prevent infinite loops const authHeadersRef = React.useRef<Record<string, string>>({}); React.useEffect(() => { authHeadersRef.current = authHeaders; }, [authHeaders]); // Vector search state const [searchQuery, setSearchQuery] = useState<string>(''); const [searchResults, setSearchResults] = useState<SearchResult[]>([]); const [isSearching, setIsSearching] = useState(false); const [hasSearched, setHasSearched] = useState(false); const [showSearchSettings, setShowSearchSettings] = useState(false); const [searchSettings, setSearchSettings] = useState({ minSimilarity: 0.75, limit: 20 }); // Generate stable IDs for form inputs const minSimilarityId = useId(); const searchLimitId = useId(); const loadFolders = useCallback(async () => { try { const [foldersResponse, statusResponse] = await Promise.all([ fetch(`${apiUrl}/api/indexed-folders`, { headers: authHeadersRef.current }), fetch(`${apiUrl}/api/indexing-status`, { headers: authHeadersRef.current }).catch(() => null) ]); if (!foldersResponse.ok) throw new Error(`HTTP ${foldersResponse.status}`); const foldersData = await foldersResponse.json() as any; let folders = foldersData.folders || []; // Validate folder data before processing folders = folders.filter((folder: any) => { if (!isFolderInfo(folder)) { console.error('[Intelligence] Invalid folder data received, missing required fields:', folder); return false; } return true; }); // Merge indexing status if available if (statusResponse && statusResponse.ok) { const statusData = await statusResponse.json() as any; const statusMap = new Map( statusData.statuses?.map((s: any) => [s.path, s.isIndexing]) || [] ); folders = folders.map((folder: FolderInfo) => ({ ...folder, isIndexing: statusMap.get(folder.path) || false })); } setFolders(folders); } catch (err: any) { console.error('Failed to load folders:', err); // Don't clear folders on error - keep existing data // setFolders([]); } }, [apiUrl]); const loadStats = useCallback(async () => { try { const response = await fetch(`${apiUrl}/api/index-stats`, { headers: authHeadersRef.current }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json() as any; setStats(data); } catch (err: any) { console.error('Failed to load stats:', err); // Don't clear stats on error - keep existing data // setStats(null); } }, [apiUrl]); const loadData = useCallback(async () => { setLoading(true); setError(null); try { await Promise.all([loadFolders(), loadStats()]); } catch (err: any) { console.error('[Intelligence] Failed to load data:', err); setError(`Network error: Unable to connect to server. Please check if the server is running.`); // Don't clear existing data on network errors } finally { setLoading(false); } }, [loadFolders, loadStats]); const performRemoveFolder = useCallback(async (id: string, path: string) => { console.log('[Intelligence] Performing deletion for ID:', id, 'path:', path); try { console.log('[Intelligence] Sending DELETE request to:', `${apiUrl}/api/indexed-folders`); const response = await fetch(`${apiUrl}/api/indexed-folders`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', ...authHeadersRef.current }, body: JSON.stringify({ id }) }); console.log('[Intelligence] DELETE response status:', response.status); if (!response.ok) { const error = await response.text(); console.error('[Intelligence] DELETE request failed:', error); throw new Error(error); } console.log('[Intelligence] Folder deleted successfully'); vscode.postMessage({ command: 'showMessage', type: 'info', message: `✅ Removed folder from indexing: ${path}` }); loadData(); } catch (err: any) { console.error('[Intelligence] Error during deletion:', err); vscode.postMessage({ command: 'showMessage', type: 'error', message: `❌ Failed to remove folder: ${err.message}` }); } }, [apiUrl, loadData]); useEffect(() => { // Tell extension we're ready to receive config vscode.postMessage({ command: 'ready' }); // Listen for messages from extension const handleMessage = (event: MessageEvent) => { const message = event.data; switch (message.command) { case 'config': console.log('[Intelligence] Received config:', message.apiUrl, 'Auth headers:', Object.keys(message.authHeaders || {}).length > 0 ? 'Present' : 'Empty'); if (message.authHeaders && Object.keys(message.authHeaders).length > 0) { console.log('[Intelligence] Auth header keys:', Object.keys(message.authHeaders)); } setApiUrl(message.apiUrl || 'http://localhost:9042'); setAuthHeaders(message.authHeaders || {}); setConfigReceived(true); break; case 'refresh': loadData(); break; case 'removeFolderConfirmed': // Extension confirmed deletion, proceed with API call performRemoveFolder(message.id, message.path); break; } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [loadData, performRemoveFolder]); // Load data once config is received (with or without auth headers) useEffect(() => { if (configReceived) { const hasAuth = Object.keys(authHeaders).length > 0; console.log(`[Intelligence] Config received (auth: ${hasAuth ? 'yes' : 'no'}), loading data`); loadData(); } }, [configReceived, authHeaders, loadData]); // SSE connection for real-time indexing progress useEffect(() => { // Don't connect until we have the actual API URL from config if (!configReceived) { return; } // Build SSE URL with auth token as query parameter (EventSource can't send custom headers) let sseUrl = `${apiUrl}/api/indexing-progress`; // Extract Bearer token from Authorization header if present if (authHeadersRef.current?.Authorization) { const authHeader = authHeadersRef.current.Authorization; const token = authHeader.startsWith('Bearer ') ? authHeader.substring(7) : authHeader; sseUrl += `?access_token=${encodeURIComponent(token)}`; console.log('[Intelligence] SSE connecting with auth token'); } else { console.log('[Intelligence] SSE connecting without auth (security may be disabled)'); } const eventSource = new EventSource(sseUrl); eventSource.onmessage = (event) => { if (event.data && event.data !== ': heartbeat') { try { const progress: IndexingProgress = JSON.parse(event.data); console.log('[Intelligence] Received progress for path:', progress.path, `(${progress.indexed}/${progress.totalFiles})`, `status: ${progress.status}`, progress.currentFile ? `file: ${progress.currentFile}` : ''); setProgressMap((prev) => { const newMap = new Map(prev); newMap.set(progress.path, progress); console.log('[Intelligence] Updated progressMap, size:', newMap.size, 'keys:', Array.from(newMap.keys())); return newMap; }); // Reload folder data when indexing completes if (progress.status === 'completed') { setTimeout(() => { loadFolders(); }, 1000); } } catch (err) { console.error('[Intelligence] Failed to parse SSE data:', err); } } }; eventSource.onerror = (err) => { console.error('[Intelligence] SSE connection error:', err); eventSource.close(); }; return () => { eventSource.close(); }; }, [apiUrl, loadFolders, configReceived, authHeaders]); const handleAddFolder = () => { // Ask extension to open folder picker vscode.postMessage({ command: 'selectFolder' }); }; const handleRemoveFolder = (id: string, path: string) => { console.log('[Intelligence] Delete button clicked for ID:', id, 'path:', path); if (!id || typeof id !== 'string' || id.trim().length === 0) { console.error('[Intelligence] ERROR: Invalid or missing folder ID!'); vscode.postMessage({ command: 'showMessage', type: 'error', message: `❌ Cannot remove folder: invalid ID` }); return; } // Request confirmation from extension host (webviews can't use confirm()) vscode.postMessage({ command: 'confirmRemoveFolder', id: id, path: path }); }; const handleReactivateFolder = async (id: string, path: string) => { console.log('[Intelligence] Reactivate button clicked for ID:', id, 'path:', path); console.log('[Intelligence] ID type:', typeof id, 'ID value:', id); if (!id || typeof id !== 'string' || id.trim().length === 0) { console.error('[Intelligence] ERROR: Invalid or missing folder ID!'); vscode.postMessage({ command: 'showMessage', type: 'error', message: `❌ Cannot reactivate: folder ID is missing` }); return; } try { const response = await fetch(`${apiUrl}/api/indexed-folders/reactivate`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', ...authHeadersRef.current }, body: JSON.stringify({ id }) }); if (!response.ok) { const error = await response.text(); throw new Error(error); } vscode.postMessage({ command: 'showMessage', type: 'info', message: `✅ Reactivated watch: ${path}` }); loadData(); } catch (err: any) { console.error('[Intelligence] Error reactivating watch:', err); vscode.postMessage({ command: 'showMessage', type: 'error', message: `❌ Failed to reactivate watch: ${err.message}` }); } }; const handleRefresh = () => { loadData(); }; const handleSearch = async () => { if (!searchQuery.trim()) { return; } setIsSearching(true); setHasSearched(true); setError(null); try { const params = new URLSearchParams({ query: searchQuery, limit: searchSettings.limit.toString(), min_similarity: searchSettings.minSimilarity.toString(), types: 'file' // Will be expanded to file,file_chunk by the server }); const response = await fetch(`${apiUrl}/api/nodes/vector-search?${params}`, { headers: authHeadersRef.current }); const data = await response.json(); if (response.ok) { setSearchResults(data.results || []); } else { setError(data.error || 'Search failed'); setSearchResults([]); } } catch (err: any) { setError(`Search failed: ${err.message}`); setSearchResults([]); } finally { setIsSearching(false); } }; const clearSearch = () => { setSearchQuery(''); setSearchResults([]); setIsSearching(false); setHasSearched(false); }; const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const value = e.target.value; setSearchQuery(value); // Clear results when input is cleared if (!value.trim()) { setSearchResults([]); setHasSearched(false); } }; const formatNumber = (num: number) => { return num.toLocaleString(); }; const formatDate = (dateStr: string) => { try { const date = new Date(dateStr); return date.toLocaleString(); } catch { return dateStr; } }; const getStatusIcon = (status: string) => { switch (status) { case 'active': return '✅'; case 'inactive': return '⏸️'; case 'stopped': return '🛑'; case 'error': return '❌'; default: return '❓'; } }; const getStatusLabel = (status: string) => { switch (status) { case 'active': return 'Active'; case 'inactive': return 'Inactive'; case 'stopped': return 'Stopped'; case 'error': return 'Error'; default: return 'Unknown'; } }; const getTopExtensions = (byExtension: Record<string, number>) => { return Object.entries(byExtension) .sort((a, b) => b[1] - a[1]) .slice(0, 10); }; return ( <div className="intelligence-container"> {/* Header */} <div className="intelligence-header"> <div className="header-title"> <span className="header-icon">🧠</span> <h1>Mimir Code Intelligence</h1> </div> <div className="header-subtitle"> File indexing, chunking, and embedding management </div> </div> {/* Error Banner */} {error && ( <div className="error-banner"> <span>⚠️ {error}</span> <button type="button" onClick={() => setError(null)}>✕</button> </div> )} {/* Vector Search */} <div className="search-container"> <div className="search-bar"> <input type="text" className="search-input" placeholder="🔍 Search indexed files by content..." value={searchQuery} onChange={handleInputChange} onKeyDown={(e) => { if (e.key === 'Enter') { handleSearch(); } }} /> {searchQuery && ( <button type="button" className="clear-search-btn" onClick={clearSearch} title="Clear search" > ✕ </button> )} <button type="button" className="search-btn" onClick={handleSearch} disabled={isSearching} > {isSearching ? '⏳' : '🔍'} </button> <button type="button" className="settings-btn" onClick={() => setShowSearchSettings(!showSearchSettings)} title="Search settings" > ⚙️ </button> </div> {showSearchSettings && ( <div className="search-settings"> <div className="setting-item"> <label htmlFor={minSimilarityId}>Min Similarity:</label> <input id={minSimilarityId} type="range" min="0.5" max="1.0" step="0.05" value={searchSettings.minSimilarity} onChange={(e) => setSearchSettings(prev => ({ ...prev, minSimilarity: parseFloat(e.target.value) }))} /> <span className="setting-value">{searchSettings.minSimilarity.toFixed(2)}</span> </div> <div className="setting-item"> <label htmlFor={searchLimitId}>Max Results:</label> <input id={searchLimitId} type="number" min="5" max="100" value={searchSettings.limit} onChange={(e) => setSearchSettings(prev => ({ ...prev, limit: parseInt(e.target.value) || 20 }))} /> <span className="setting-value">{searchSettings.limit}</span> </div> </div> )} {hasSearched && ( <div className="search-status"> {isSearching ? ( <span>🔍 Searching indexed files...</span> ) : searchResults.length > 0 ? ( <span>✅ Found {searchResults.length} matching file{searchResults.length !== 1 ? 's' : ''}</span> ) : ( <span>❌ No results found</span> )} </div> )} {searchResults.length > 0 && ( <div className="search-results"> <h3>Search Results</h3> {searchResults.map((result) => ( <div key={result.id} className="search-result-item"> <div className="result-header"> <span className="result-icon">📄</span> <span className="result-title">{result.title || result.path}</span> <span className="result-similarity" title="Similarity score"> {(result.similarity * 100).toFixed(1)}% </span> </div> <div className="result-path"> {result.parent_file?.absolute_path || result.absolute_path || result.path} </div> {result.parent_file && ( <div className="result-meta"> <span className="result-language">{result.parent_file.language}</span> </div> )} </div> ))} </div> )} </div> {/* Loading State */} {loading && ( <div className="loading-overlay"> <div className="loading-spinner"> <div className="spinner"></div> <p>Loading index data...</p> </div> </div> )} {/* Statistics Dashboard */} {stats && ( <div className="stats-section"> <h2>📊 Index Statistics</h2> <div className="stats-grid"> <div className="stat-card"> <div className="stat-icon">📁</div> <div className="stat-value">{formatNumber(stats.totalFolders)}</div> <div className="stat-label">Folders Watched</div> </div> <div className="stat-card"> <div className="stat-icon">📄</div> <div className="stat-value">{formatNumber(stats.totalFiles)}</div> <div className="stat-label">Files Indexed</div> </div> <div className="stat-card"> <div className="stat-icon">🧩</div> <div className="stat-value">{formatNumber(stats.totalChunks)}</div> <div className="stat-label">Chunks Created</div> </div> <div className="stat-card"> <div className="stat-icon">🎯</div> <div className="stat-value">{formatNumber(stats.totalEmbeddings)}</div> <div className="stat-label">Embeddings Generated</div> </div> </div> </div> )} {/* Indexed Folders */} <div className="folders-section"> <div className="section-header"> <h2>📂 Indexed Folders</h2> <div className="section-actions"> <button type="button" className="button-refresh" onClick={handleRefresh} title="Refresh"> 🔄 Refresh </button> <button type="button" className="button-primary" onClick={handleAddFolder}> + Add Folder </button> </div> </div> {folders.length === 0 ? ( <div className="empty-state"> <div className="empty-icon">📁</div> <p>No folders are currently being indexed</p> <p className="empty-hint">Click "Add Folder" to start indexing a workspace folder</p> </div> ) : ( <div className="folders-list"> {folders.map((folder) => { const progress = progressMap.get(folder.path); console.log('[Intelligence] Checking progress for folder.path:', folder.path, 'found:', progress ? 'YES' : 'NO', progress?.status); const isIndexing = progress?.status === 'indexing'; const isQueued = progress?.status === 'queued'; const isCompleted = progress?.status === 'completed'; let statusClass = folder.status; if (isIndexing) statusClass += ' folder-indexing'; if (isQueued) statusClass += ' folder-queued'; if (isCompleted) statusClass += ' folder-completed'; return ( <div key={folder.path} className={`folder-item ${statusClass}`}> <div className="folder-info"> <span className="folder-status" title={folder.status}> {getStatusIcon(folder.status)} </span> <div className="folder-details"> <div className="folder-path"> {folder.hostPath || folder.path} <span style={{ marginLeft: '8px', fontSize: '11px', color: folder.status === 'inactive' ? 'var(--vscode-descriptionForeground)' : 'var(--vscode-foreground)', opacity: folder.status === 'inactive' ? 0.7 : 1 }}> ({getStatusLabel(folder.status)}) </span> {isIndexing && progress && ( <span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--vscode-charts-blue)', fontWeight: 'bold' }}> 🔄 {progress.indexed}/{progress.totalFiles} ({progress.currentFile || '...'}) </span> )} {isQueued && ( <span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--vscode-charts-orange)', fontWeight: 'bold' }}> ⏳ Queued... </span> )} {isCompleted && ( <span style={{ marginLeft: '8px', fontSize: '12px', color: 'var(--vscode-charts-green)', fontWeight: 'bold' }}> ✅ Complete ({progress.indexed} indexed, {progress.skipped} skipped, {progress.errored} errors) </span> )} {folder.status === 'inactive' && folder.error && ( <span style={{ marginLeft: '8px', fontSize: '11px', color: 'var(--vscode-errorForeground)', fontStyle: 'italic' }}> ({folder.error}) </span> )} </div> <div className="folder-stats"> <span title="Files">📄 {formatNumber(folder.fileCount)}</span> <span title="Chunks">🧩 {formatNumber(folder.chunkCount)}</span> <span title="Embeddings">🎯 {formatNumber(folder.embeddingCount)}</span> {folder.lastSync && ( <span title="Last synced" className="folder-sync"> ⏰ {formatDate(folder.lastSync)} </span> )} </div> </div> </div> <div className="folder-actions"> {folder.status === 'inactive' ? ( <> <button type="button" className="button-primary" onClick={() => handleReactivateFolder(folder.id, folder.path)} title="Reactivate this watch" style={{ marginRight: '8px' }} > ▶️ Reactivate </button> <button type="button" className="button-danger" onClick={() => handleRemoveFolder(folder.id, folder.path)} title="Permanently remove from indexing" > 🗑️ Delete </button> </> ) : ( <button type="button" className="button-danger" onClick={() => handleRemoveFolder(folder.id, folder.path)} title={folder.isIndexing ? 'Cancel indexing and remove folder' : 'Remove from indexing'} > {folder.isIndexing ? '🛑 Cancel & Remove' : '🗑️ Remove'} </button> )} </div> </div> ); })} </div> )} </div> {/* File Type Breakdown */} {stats && stats.byExtension && Object.keys(stats.byExtension).length > 0 && ( <div className="breakdown-section"> <h2>📋 File Type Breakdown</h2> <div className="breakdown-list"> {getTopExtensions(stats.byExtension).map(([ext, count]) => { const percentage = ((count / stats.totalFiles) * 100).toFixed(1); return ( <div key={ext} className="breakdown-row"> <div className="breakdown-info"> <span className="breakdown-ext">{ext || '(no extension)'}</span> <span className="breakdown-count"> {formatNumber(count)} files ({percentage}%) </span> </div> <div className="breakdown-bar-container"> <div className="breakdown-bar" style={{ width: `${percentage}%` }} /> </div> </div> ); })} </div> </div> )} {/* Footer */} <div className="intelligence-footer"> <p>💡 Tip: Only folders within your mounted workspace can be indexed</p> </div> </div> ); }

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/orneryd/Mimir'

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