lanonasis-memory
Version:
Memory as a Service integration - AI-powered memory management with semantic search (Compatible with CLI v3.0.6+)
565 lines (487 loc) • 19.6 kB
JavaScript
// @ts-check
(function () {
const vscode = acquireVsCodeApi();
const rootElement = document.getElementById('root');
// State management
let state = {
authenticated: false,
memories: [],
loading: true,
enhancedMode: false,
cliVersion: null,
cached: false,
expandedGroups: new Set(['context', 'project', 'knowledge']),
promptInput: '',
refinedPrompt: '',
refinementNotes: [],
userName: rootElement?.dataset.userName || '',
brandIcon: rootElement?.dataset.brandIcon || ''
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
setupEventListeners();
render();
});
// Handle messages from extension
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'updateState':
state = { ...state, ...message.state };
render();
break;
case 'searchResults':
displaySearchResults(message.results, message.query);
break;
case 'error':
showError(message.message);
break;
}
});
function setupEventListeners() {
// Search
const searchInput = document.getElementById('search-input');
if (searchInput) {
searchInput.addEventListener('input', debounce((e) => {
const query = e.target.value.trim();
if (query.length > 2) {
vscode.postMessage({
type: 'searchMemories',
query: query
});
}
}, 300));
}
}
function render() {
const root = document.getElementById('root');
if (!root) return;
if (state.loading) {
root.innerHTML = getLoadingHTML();
return;
}
if (!state.authenticated) {
root.innerHTML = getAuthHTML();
attachAuthListeners();
return;
}
root.innerHTML = getMainHTML();
attachMainListeners();
}
function getLoadingHTML() {
return `
<div class="loading-state">
<div class="spinner"></div>
<p>Loading Lanonasis Memory...</p>
</div>
`;
}
function getAuthHTML() {
return `
<div class="auth-state">
<div class="auth-icon">🧭</div>
<h3>Welcome to Lanonasis Memory</h3>
<p>Choose how you'd like to connect. You can switch between OAuth and API key authentication at any time.</p>
<div class="auth-actions">
<button class="btn" id="oauth-btn">
<span class="btn-icon">🌐 Continue in Browser</span>
</button>
<button class="btn" id="api-key-btn">
<span class="btn-icon">🔑 Enter API Key</span>
</button>
</div>
<div class="auth-actions">
<button class="btn btn-secondary" id="command-palette-btn">
<span class="btn-icon">⌨️ Command Palette</span>
</button>
<button class="btn btn-secondary" id="get-key-btn">
<span class="btn-icon">📬 Get API Key</span>
</button>
<button class="btn btn-secondary" id="settings-btn">
<span class="btn-icon">⚙️ Settings</span>
</button>
</div>
<p class="auth-hint">If the buttons fail, run <code>Lanonasis: Authenticate</code> or <code>Lanonasis: Configure Authentication</code> from the command palette.</p>
</div>
`;
}
function getMainHTML() {
const hasMemories = state.memories && state.memories.length > 0;
return `
<div class="sidebar-header">
<div class="header-title">
${state.brandIcon ? `<img class="brand-icon" src="${state.brandIcon}" alt="Lanonasis logo">` : '<div class="brand-placeholder">LN</div>'}
<div class="header-text">
<h2>Lanonasis Memory</h2>
<div class="welcome-line">Welcome, ${escapeHtml(state.userName || 'creator')}</div>
</div>
<span class="status-badge">
<span class="status-dot"></span>
Connected
</span>
</div>
${state.enhancedMode ? getEnhancedBannerHTML() : ''}
<div class="search-container">
<span class="search-icon">🔍</span>
<input
type="text"
class="search-box"
id="search-input"
placeholder="Search memories..."
/>
</div>
<div class="action-buttons">
<button class="btn" id="create-memory-btn">
<span class="btn-icon">➕ Create</span>
</button>
<button class="btn btn-secondary" id="refresh-btn">
<span class="btn-icon">🔄 Refresh</span>
</button>
</div>
${state.cached ? '<div class="cache-indicator">📦 Cached data (click refresh for latest)</div>' : ''}
${getPromptRefinerHTML()}
</div>
${hasMemories ? getMemoriesHTML() : getEmptyStateHTML()}
`;
}
function getEnhancedBannerHTML() {
return `
<div class="enhanced-banner">
<div class="enhanced-banner-header">
🚀 Enhanced Mode Active
</div>
<div class="enhanced-banner-text">
CLI ${state.cliVersion || 'v3.0+'} detected - Performance optimized
</div>
</div>
`;
}
function getPromptRefinerHTML() {
const hasOutput = (state.refinedPrompt || '').trim().length > 0;
const notes = state.refinementNotes || [];
return `
<div class="card">
<div class="card-header">
<div>
<div class="label">Prompt Refiner</div>
<div class="subdued">Tightens prompts using REPL-style checklist</div>
</div>
<span class="pill">Beta</span>
</div>
<div class="field">
<label for="prompt-input">Raw prompt</label>
<textarea
id="prompt-input"
class="textarea"
rows="4"
placeholder="Paste your prompt here...">${escapeHtml(state.promptInput || '')}</textarea>
</div>
<div class="refiner-actions">
<button class="btn" id="refine-btn">
<span class="btn-icon">⚡ Refine</span>
</button>
<button class="btn btn-secondary" id="copy-refined-btn" ${hasOutput ? '' : 'disabled'}>
<span class="btn-icon">📋 Copy refined</span>
</button>
</div>
<div class="refined-output ${hasOutput ? '' : 'muted'}" id="refined-output">
${hasOutput ? escapeHtml(state.refinedPrompt) : 'Refined prompt will appear here'}
</div>
${notes.length ? `
<div class="refiner-notes">
${notes.map(note => `<div class="note-item">• ${escapeHtml(note)}</div>`).join('')}
</div>
` : ''}
</div>
`;
}
function getMemoriesHTML() {
const groupedMemories = groupMemoriesByType(state.memories);
const types = Object.keys(groupedMemories);
if (types.length === 0) {
return getEmptyStateHTML();
}
return `
<div class="memories-container">
<div class="section-header">
<span>Your Memories</span>
<span>${state.memories.length} total</span>
</div>
${types.map(type => getMemoryGroupHTML(type, groupedMemories[type])).join('')}
</div>
`;
}
function getMemoryGroupHTML(type, memories) {
const isExpanded = state.expandedGroups.has(type);
const icon = getTypeIcon(type);
return `
<div class="memory-group">
<div class="memory-type-header" data-type="${type}">
<span class="memory-type-icon">${icon}</span>
<span>${capitalizeFirst(type)}</span>
<span class="memory-count">${memories.length}</span>
</div>
${isExpanded ? `
<ul class="memory-list">
${memories.map(memory => getMemoryItemHTML(memory)).join('')}
</ul>
` : ''}
</div>
`;
}
function getMemoryItemHTML(memory) {
const date = new Date(memory.created_at).toLocaleDateString();
const preview = (memory.content || '').substring(0, 80);
return `
<li class="memory-item" data-id="${memory.id}">
<div class="memory-title">${escapeHtml(memory.title)}</div>
<div class="memory-meta">
<span>📅 ${date}</span>
${memory.tags && memory.tags.length > 0 ? `<span>🏷️ ${memory.tags.length}</span>` : ''}
</div>
</li>
`;
}
function getEmptyStateHTML() {
return `
<div class="empty-state">
<div class="empty-icon">📝</div>
<h3>No Memories Yet</h3>
<p>Get started by creating your first memory from selected text or a file.</p>
<button class="btn" id="create-first-memory-btn">
<span class="btn-icon">✨ Create Your First Memory</span>
</button>
<p class="mt-2" style="font-size: 12px; opacity: 0.7;">
Tip: Select text in any file and press Cmd+Shift+Alt+M
</p>
</div>
`;
}
function attachAuthListeners() {
const oauthBtn = document.getElementById('oauth-btn');
const apiKeyBtn = document.getElementById('api-key-btn');
const commandPaletteBtn = document.getElementById('command-palette-btn');
const getKeyBtn = document.getElementById('get-key-btn');
const settingsBtn = document.getElementById('settings-btn');
oauthBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'authenticate', mode: 'oauth' });
});
apiKeyBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'authenticate', mode: 'apikey' });
});
commandPaletteBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'openCommandPalette' });
});
getKeyBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'getApiKey' });
});
settingsBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'showSettings' });
});
}
function attachMainListeners() {
// Create memory button
const createBtn = document.getElementById('create-memory-btn');
const createFirstBtn = document.getElementById('create-first-memory-btn');
const refreshBtn = document.getElementById('refresh-btn');
const promptInput = document.getElementById('prompt-input');
const refineBtn = document.getElementById('refine-btn');
const copyRefinedBtn = document.getElementById('copy-refined-btn');
createBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'createMemory' });
});
createFirstBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'createMemory' });
});
refreshBtn?.addEventListener('click', () => {
vscode.postMessage({ type: 'refresh' });
});
promptInput?.addEventListener('input', (e) => {
state.promptInput = e.target.value;
});
refineBtn?.addEventListener('click', () => {
const rawPrompt = (state.promptInput || '').trim();
if (!rawPrompt) {
showInfo('Add a prompt to refine');
return;
}
const refined = refinePrompt(rawPrompt);
state.refinedPrompt = refined.prompt;
state.refinementNotes = refined.notes;
render();
});
copyRefinedBtn?.addEventListener('click', async () => {
const text = (state.refinedPrompt || '').trim();
if (!text) return;
try {
await navigator.clipboard.writeText(text);
showInfo('Refined prompt copied to clipboard');
} catch (error) {
showError('Unable to copy to clipboard');
}
});
// Memory type headers (toggle expand/collapse)
document.querySelectorAll('.memory-type-header').forEach(header => {
header.addEventListener('click', (e) => {
const type = e.currentTarget.getAttribute('data-type');
if (type) {
toggleGroupExpansion(type);
}
});
});
// Memory items
document.querySelectorAll('.memory-item').forEach(item => {
item.addEventListener('click', (e) => {
const memoryId = e.currentTarget.getAttribute('data-id');
const memory = state.memories.find(m => m.id === memoryId);
if (memory) {
vscode.postMessage({
type: 'openMemory',
memory: memory
});
}
});
});
}
function toggleGroupExpansion(type) {
if (state.expandedGroups.has(type)) {
state.expandedGroups.delete(type);
} else {
state.expandedGroups.add(type);
}
render();
}
function displaySearchResults(results, query) {
if (results.length === 0) {
showInfo(`No results found for "${query}"`);
return;
}
// Update state with search results
state.memories = results;
render();
}
function showError(message) {
const root = document.getElementById('root');
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
root?.prepend(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
function showInfo(message) {
const root = document.getElementById('root');
const infoDiv = document.createElement('div');
infoDiv.className = 'info-message';
infoDiv.textContent = message;
root?.prepend(infoDiv);
setTimeout(() => infoDiv.remove(), 3000);
}
// Utility functions
function groupMemoriesByType(memories) {
const grouped = {};
memories.forEach(memory => {
const type = memory.memory_type || 'context';
if (!grouped[type]) {
grouped[type] = [];
}
grouped[type].push(memory);
});
return grouped;
}
function getTypeIcon(type) {
const icons = {
'context': '💡',
'project': '📁',
'knowledge': '📚',
'reference': '🔗',
'personal': '👤',
'workflow': '⚙️',
'conversation': '💬'
};
return icons[type] || '📄';
}
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function refinePrompt(rawPrompt) {
const normalized = collapseWhitespace(rawPrompt);
const lines = normalized.split('\n').map(l => l.trim()).filter(Boolean);
const roleLine = lines.find(l => /act as|you are|role:/i.test(l));
const objectiveLine = lines.find(l => l !== roleLine) || normalized;
const context = [];
const constraints = [];
const outputs = [];
lines.forEach(line => {
if (line === roleLine || line === objectiveLine) return;
const lower = line.toLowerCase();
const cleaned = stripBullet(line);
if (/(output|format|respond|return|deliver)/.test(lower)) {
outputs.push(cleaned);
return;
}
if (/(avoid|do not|never|without|limit|must|should)/.test(lower)) {
constraints.push(cleaned);
return;
}
context.push(cleaned);
});
const result = [];
const notes = [];
if (roleLine) {
result.push(`Role: ${stripBullet(roleLine)}`);
notes.push('Kept explicit role/context from input');
}
result.push(`Goal: ${stripBullet(objectiveLine)}`);
notes.push('Elevated primary goal to the top');
if (context.length) {
result.push('Context:');
context.forEach(item => result.push(`- ${item}`));
}
if (constraints.length) {
result.push('Constraints:');
constraints.forEach(item => result.push(`- ${item}`));
notes.push('Preserved guardrails and boundaries');
}
if (outputs.length) {
result.push('Output:');
outputs.forEach(item => result.push(`- ${item}`));
} else {
result.push('Output:');
result.push('- Provide the final answer only. No extra commentary.');
}
result.push('Check-before-answer:');
result.push('- Ask for any missing detail before drafting the answer.');
result.push('- Keep responses concise and ordered.');
notes.push(`Normalized ${lines.length} input line(s) and removed filler whitespace`);
return {
prompt: result.join('\n').trim(),
notes
};
}
function stripBullet(text) {
return text.replace(/^[-*•\d.\s]+/, '').trim();
}
function collapseWhitespace(text) {
return text
.replace(/\r\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.replace(/[ \t]+/g, ' ')
.trim();
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
})();