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
text/typescript
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;
}