UNPKG

lanonasis-memory

Version:

Memory as a Service integration - AI-powered memory management with semantic search (Compatible with CLI v3.0.6+)

348 lines (313 loc) 14.3 kB
import * as vscode from 'vscode'; import { EnhancedMemoryService } from '../services/EnhancedMemoryService'; import type { IMemoryService } from '../services/IMemoryService'; import * as os from 'os'; export class MemorySidebarProvider implements vscode.WebviewViewProvider { public static readonly viewType = 'lanonasis.sidebar'; private _view?: vscode.WebviewView; private _cachedMemories: any[] = []; private _cacheTimestamp: number = 0; private readonly CACHE_DURATION = 30000; // 30 seconds private brandIconUri?: string; private readonly userLabel: string; constructor( private readonly _extensionUri: vscode.Uri, private readonly memoryService: IMemoryService ) { this.userLabel = this.getUserLabel(); } public resolveWebviewView( webviewView: vscode.WebviewView, _context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { console.log('[Lanonasis] MemorySidebarProvider.resolveWebviewView called'); try { const activationChannel = vscode.window.createOutputChannel('Lanonasis Activation'); activationChannel.appendLine('[Lanonasis] MemorySidebarProvider.resolveWebviewView called'); } catch { // ignore in tests } try { this._view = webviewView; // Restrict resource access to only necessary directories webviewView.webview.options = { enableScripts: true, localResourceRoots: [ vscode.Uri.joinPath(this._extensionUri, 'media'), vscode.Uri.joinPath(this._extensionUri, 'out'), vscode.Uri.joinPath(this._extensionUri, 'images') ] }; webviewView.webview.html = this._getHtmlForWebview(webviewView.webview); // Handle messages from the webview webviewView.webview.onDidReceiveMessage(async (data) => { try { switch (data.type) { case 'authenticate': await vscode.commands.executeCommand('lanonasis.authenticate', data.mode); break; case 'searchMemories': await this.handleSearch(data.query); break; case 'createMemory': await vscode.commands.executeCommand('lanonasis.createMemory'); break; case 'openMemory': await vscode.commands.executeCommand('lanonasis.openMemory', data.memory); break; case 'refresh': await this.refresh(true); // Force refresh when user clicks refresh button break; case 'showSettings': await vscode.commands.executeCommand('workbench.action.openSettings', 'lanonasis'); break; case 'getApiKey': await vscode.env.openExternal(vscode.Uri.parse('https://api.lanonasis.com')); break; case 'openCommandPalette': await vscode.commands.executeCommand('workbench.action.quickOpen', '>Lanonasis: Authenticate'); break; } } catch (error) { console.error('[Lanonasis] Error handling webview message:', error); this._view?.webview.postMessage({ type: 'error', message: `Action failed: ${error instanceof Error ? error.message : String(error)}` }); } }); // Initial load with error handling and delay for auth settlement setTimeout(async () => { try { // Give auth time to settle await new Promise(resolve => setTimeout(resolve, 1000)); await this.refresh(); } catch (error) { console.error('[Lanonasis] Failed to load sidebar:', error); this._view?.webview.postMessage({ type: 'error', message: 'Failed to load Lanonasis Memory. Please try refreshing or check authentication.' }); } }, 500); } catch (error) { console.error('[Lanonasis] Fatal error in resolveWebviewView:', error); vscode.window.showErrorMessage(`Lanonasis extension failed to load: ${error instanceof Error ? error.message : String(error)}`); } } public async refresh(forceRefresh: boolean = false) { if (this._view) { try { const authenticated = this.memoryService.isAuthenticated(); // Show loading only if not using cache const now = Date.now(); const useCache = !forceRefresh && this._cachedMemories.length > 0 && (now - this._cacheTimestamp) < this.CACHE_DURATION; if (useCache) { // Use cached data immediately const enhancedInfo = this.memoryService instanceof EnhancedMemoryService ? this.memoryService.getCapabilities() : null; this._view.webview.postMessage({ type: 'updateState', state: { authenticated: authenticated, memories: this._cachedMemories, loading: false, enhancedMode: enhancedInfo?.cliAvailable || false, cliVersion: enhancedInfo?.version || null, cached: true, brandIcon: this.brandIconUri, userName: this.userLabel } }); return; } this._view.webview.postMessage({ type: 'updateState', state: { loading: true } }); if (!authenticated) { this._cachedMemories = []; this._cacheTimestamp = 0; this._view.webview.postMessage({ type: 'updateState', state: { authenticated: false, memories: [], loading: false, enhancedMode: false, cliVersion: null, brandIcon: this.brandIconUri, userName: this.userLabel } }); return; } const memories = await this.memoryService.listMemories(50); const enhancedInfo = this.memoryService instanceof EnhancedMemoryService ? this.memoryService.getCapabilities() : null; // Update cache this._cachedMemories = memories; this._cacheTimestamp = Date.now(); this._view.webview.postMessage({ type: 'updateState', state: { authenticated: authenticated, memories, loading: false, enhancedMode: enhancedInfo?.cliAvailable || false, cliVersion: enhancedInfo?.version || null, cached: false, brandIcon: this.brandIconUri, userName: this.userLabel } }); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); // Check for specific error types if (errorMsg.includes('Not authenticated') || errorMsg.includes('401')) { this._cachedMemories = []; this._cacheTimestamp = 0; this._view.webview.postMessage({ type: 'updateState', state: { authenticated: false, memories: [], loading: false } }); return; } // If we have cached data, show it with an error message if (this._cachedMemories.length > 0) { this._view.webview.postMessage({ type: 'error', message: `Failed to refresh: ${errorMsg}. Showing cached data.` }); const enhancedInfo = this.memoryService instanceof EnhancedMemoryService ? this.memoryService.getCapabilities() : null; this._view.webview.postMessage({ type: 'updateState', state: { authenticated: true, memories: this._cachedMemories, loading: false, enhancedMode: enhancedInfo?.cliAvailable || false, cliVersion: enhancedInfo?.version || null, cached: true, brandIcon: this.brandIconUri, userName: this.userLabel } }); } else { // Network/timeout errors if (errorMsg.includes('fetch') || errorMsg.includes('timeout') || errorMsg.includes('Network')) { this._view.webview.postMessage({ type: 'error', message: `Connection failed: ${errorMsg}. Check your network and API endpoint configuration.` }); } else { this._view.webview.postMessage({ type: 'error', message: `Failed to load memories: ${errorMsg}` }); } this._view.webview.postMessage({ type: 'updateState', state: { loading: false } }); } } } } public clearCache(): void { this._cachedMemories = []; this._cacheTimestamp = 0; } private async handleSearch(query: string) { if (!this._view) return; if (!this.memoryService.isAuthenticated()) { this._view.webview.postMessage({ type: 'updateState', state: { authenticated: false, memories: [], loading: false } }); return; } try { this._view.webview.postMessage({ type: 'updateState', state: { loading: true } }); const results = await this.memoryService.searchMemories(query); this._view.webview.postMessage({ type: 'searchResults', results, query }); } catch (error) { this._view.webview.postMessage({ type: 'error', message: error instanceof Error ? error.message : 'Search failed' }); } finally { this._view.webview.postMessage({ type: 'updateState', state: { loading: false } }); } } private _getHtmlForWebview(webview: vscode.Webview) { const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'sidebar.css')); const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'media', 'sidebar.js')); this.brandIconUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'images', 'brand-icon.svg')).toString(); // Get CSP const nonce = getNonce(); return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src 'nonce-${nonce}'; img-src ${webview.cspSource} https:; font-src ${webview.cspSource};"> <link href="${styleUri}" rel="stylesheet"> <title>Lanonasis Memory</title> </head> <body> <div id="root" data-brand-icon="${this.brandIconUri}" data-user-name="${this.userLabel}"> <div class="loading-state"> <div class="spinner"></div> <p>Loading Lanonasis Memory...</p> </div> </div> <script nonce="${nonce}" src="${scriptUri}"></script> </body> </html>`; } private getUserLabel(): string { try { const userInfo = os.userInfo(); if (userInfo?.username) { return userInfo.username; } } catch { // ignore lookup errors } return vscode.env.appName || 'creator'; } } function getNonce() { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; }