runtime-memory 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,771 @@
1
+ /**
2
+ * Runtime Memory Web UI Application
3
+ */
4
+
5
+ class MemoryLayerApp {
6
+ constructor() {
7
+ this.currentView = 'dashboard';
8
+ this.memories = [];
9
+ this.currentPage = 1;
10
+ this.pageSize = 50;
11
+ this.selectedMemory = null;
12
+ this.stats = null;
13
+
14
+ this.init();
15
+ }
16
+
17
+ // =========================================================================
18
+ // Initialization
19
+ // =========================================================================
20
+
21
+ async init() {
22
+ this.setupEventListeners();
23
+ this.loadTheme();
24
+ await this.loadDashboard();
25
+ }
26
+
27
+ setupEventListeners() {
28
+ // Navigation
29
+ document.querySelectorAll('.nav-btn').forEach(btn => {
30
+ btn.addEventListener('click', (e) => {
31
+ this.showView(e.target.dataset.view);
32
+ });
33
+ });
34
+
35
+ // Theme toggle
36
+ document.getElementById('theme-toggle').addEventListener('click', () => {
37
+ this.toggleTheme();
38
+ });
39
+
40
+ // Add memory form
41
+ document.getElementById('add-memory-form').addEventListener('submit', (e) => {
42
+ e.preventDefault();
43
+ this.addMemory();
44
+ });
45
+
46
+ // Search on enter
47
+ document.getElementById('search-input').addEventListener('keypress', (e) => {
48
+ if (e.key === 'Enter') {
49
+ this.performSearch();
50
+ }
51
+ });
52
+
53
+ // Filters
54
+ document.getElementById('filter-category').addEventListener('change', () => {
55
+ this.loadMemories();
56
+ });
57
+ document.getElementById('filter-project').addEventListener('change', () => {
58
+ this.loadMemories();
59
+ });
60
+ document.getElementById('filter-search').addEventListener('input', () => {
61
+ this.filterMemoriesLocally();
62
+ });
63
+
64
+ // Close modal on background click
65
+ document.getElementById('memory-modal').addEventListener('click', (e) => {
66
+ if (e.target.id === 'memory-modal') {
67
+ this.closeModal();
68
+ }
69
+ });
70
+
71
+ // Escape key closes modal
72
+ document.addEventListener('keydown', (e) => {
73
+ if (e.key === 'Escape') {
74
+ this.closeModal();
75
+ }
76
+ });
77
+ }
78
+
79
+ // =========================================================================
80
+ // Theme Management
81
+ // =========================================================================
82
+
83
+ loadTheme() {
84
+ const savedTheme = localStorage.getItem('theme') || 'dark';
85
+ document.documentElement.setAttribute('data-theme', savedTheme);
86
+ this.updateThemeIcon(savedTheme);
87
+ }
88
+
89
+ toggleTheme() {
90
+ const current = document.documentElement.getAttribute('data-theme');
91
+ const next = current === 'dark' ? 'light' : 'dark';
92
+ document.documentElement.setAttribute('data-theme', next);
93
+ localStorage.setItem('theme', next);
94
+ this.updateThemeIcon(next);
95
+ }
96
+
97
+ updateThemeIcon(theme) {
98
+ const icon = document.querySelector('.theme-icon');
99
+ icon.textContent = theme === 'dark' ? '\u263E' : '\u2600';
100
+ }
101
+
102
+ // =========================================================================
103
+ // View Navigation
104
+ // =========================================================================
105
+
106
+ showView(viewName) {
107
+ this.currentView = viewName;
108
+
109
+ // Update nav buttons
110
+ document.querySelectorAll('.nav-btn').forEach(btn => {
111
+ btn.classList.toggle('active', btn.dataset.view === viewName);
112
+ });
113
+
114
+ // Show/hide views
115
+ document.querySelectorAll('.view').forEach(view => {
116
+ view.classList.toggle('active', view.id === `view-${viewName}`);
117
+ });
118
+
119
+ // Load data for view
120
+ switch (viewName) {
121
+ case 'dashboard':
122
+ this.loadDashboard();
123
+ break;
124
+ case 'memories':
125
+ this.loadMemories();
126
+ break;
127
+ case 'search':
128
+ document.getElementById('search-input').focus();
129
+ break;
130
+ case 'beads':
131
+ this.loadBeads();
132
+ break;
133
+ }
134
+ }
135
+
136
+ // =========================================================================
137
+ // Dashboard
138
+ // =========================================================================
139
+
140
+ async loadDashboard() {
141
+ try {
142
+ this.stats = await api.getStats();
143
+ this.renderStats();
144
+ await this.loadRecentMemories();
145
+ } catch (error) {
146
+ this.showToast('Failed to load dashboard', 'error');
147
+ }
148
+ }
149
+
150
+ renderStats() {
151
+ const stats = this.stats;
152
+ document.getElementById('stat-total').textContent = stats.total_memories || 0;
153
+ document.getElementById('stat-active').textContent = stats.active_memories || 0;
154
+ document.getElementById('stat-archived').textContent = stats.archived_memories || 0;
155
+ document.getElementById('stat-avg-score').textContent =
156
+ (stats.avg_outcome_score || 0).toFixed(2);
157
+
158
+ // Render category bars
159
+ this.renderCategoryBars(stats.by_category || {});
160
+ }
161
+
162
+ renderCategoryBars(byCategory) {
163
+ const container = document.getElementById('category-bars');
164
+ const maxCount = Math.max(...Object.values(byCategory), 1);
165
+
166
+ // All categories with their colors (matching CSS)
167
+ const categoryColors = {
168
+ 'architecture': '#3b82f6', // blue
169
+ 'convention': '#8b5cf6', // purple
170
+ 'decision': '#06b6d4', // cyan
171
+ 'pattern': '#10b981', // green (success)
172
+ 'gotcha': '#ef4444', // red (danger)
173
+ 'workaround': '#f97316', // orange
174
+ 'troubleshooting': '#f59e0b', // amber (warning)
175
+ 'command': '#64748b', // slate
176
+ 'preference': '#ec4899', // pink
177
+ 'general': '#6b7280' // gray
178
+ };
179
+
180
+ const categories = Object.keys(categoryColors);
181
+
182
+ container.innerHTML = categories
183
+ .map(cat => {
184
+ const count = byCategory[cat] || 0;
185
+ const width = maxCount > 0 ? (count / maxCount * 100).toFixed(1) : 0;
186
+ const color = categoryColors[cat];
187
+ return `
188
+ <div class="category-bar">
189
+ <span class="label">${cat}</span>
190
+ <div class="bar-container">
191
+ <div class="bar" style="width: ${width}%; background-color: ${color}"></div>
192
+ </div>
193
+ <span class="count">${count}</span>
194
+ </div>
195
+ `;
196
+ })
197
+ .join('');
198
+ }
199
+
200
+ async loadRecentMemories() {
201
+ try {
202
+ const memories = await api.getMemories({ limit: 5 });
203
+ this.renderRecentMemories(memories);
204
+ } catch (error) {
205
+ console.error('Failed to load recent memories', error);
206
+ }
207
+ }
208
+
209
+ renderRecentMemories(memories) {
210
+ const container = document.getElementById('recent-memories');
211
+
212
+ if (!memories || memories.length === 0) {
213
+ container.innerHTML = '<div class="empty-state"><p>No memories yet</p></div>';
214
+ return;
215
+ }
216
+
217
+ container.innerHTML = memories.map(m => `
218
+ <div class="recent-item" onclick="app.viewMemory('${m.id}')">
219
+ <div class="content">${this.escapeHtml(m.content)}</div>
220
+ <div class="meta">
221
+ <span class="category-badge ${m.category}">${m.category}</span>
222
+ <span>${this.formatDate(m.created_at)}</span>
223
+ </div>
224
+ </div>
225
+ `).join('');
226
+ }
227
+
228
+ // =========================================================================
229
+ // Memories List
230
+ // =========================================================================
231
+
232
+ async loadMemories() {
233
+ try {
234
+ const category = document.getElementById('filter-category').value;
235
+ const project = document.getElementById('filter-project').value;
236
+
237
+ console.log('Loading memories...');
238
+ this.memories = await api.getMemories({
239
+ category: category || undefined,
240
+ project: project || undefined,
241
+ limit: 100
242
+ });
243
+ console.log('Loaded memories:', this.memories);
244
+
245
+ this.renderMemoriesTable();
246
+ this.updateProjectFilter();
247
+ } catch (error) {
248
+ console.error('Failed to load memories:', error);
249
+ const msg = error.message || JSON.stringify(error);
250
+ this.showToast('Failed to load memories: ' + msg, 'error');
251
+ }
252
+ }
253
+
254
+ renderMemoriesTable() {
255
+ const tbody = document.getElementById('memories-tbody');
256
+ const start = (this.currentPage - 1) * this.pageSize;
257
+ const pageMemories = this.memories.slice(start, start + this.pageSize);
258
+
259
+ if (pageMemories.length === 0) {
260
+ tbody.innerHTML = `
261
+ <tr>
262
+ <td colspan="6" class="empty-state">
263
+ <p>No memories found</p>
264
+ </td>
265
+ </tr>
266
+ `;
267
+ return;
268
+ }
269
+
270
+ tbody.innerHTML = pageMemories.map(m => `
271
+ <tr onclick="app.viewMemory('${m.id}')">
272
+ <td><span class="category-badge ${m.category}">${m.category}</span></td>
273
+ <td class="content-cell">${this.escapeHtml(m.content)}</td>
274
+ <td class="score-cell ${m.outcome_score > 0 ? 'positive' : m.outcome_score < 0 ? 'negative' : ''}">
275
+ ${m.outcome_score.toFixed(2)}
276
+ </td>
277
+ <td>${m.project || 'global'}</td>
278
+ <td>${this.formatDate(m.created_at)}</td>
279
+ <td class="actions-cell">
280
+ <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); app.viewMemory('${m.id}')">
281
+ View
282
+ </button>
283
+ </td>
284
+ </tr>
285
+ `).join('');
286
+
287
+ this.renderPagination();
288
+ }
289
+
290
+ renderPagination() {
291
+ const totalPages = Math.ceil(this.memories.length / this.pageSize);
292
+ const container = document.getElementById('pagination');
293
+
294
+ if (totalPages <= 1) {
295
+ container.innerHTML = '';
296
+ return;
297
+ }
298
+
299
+ let html = '';
300
+ for (let i = 1; i <= totalPages; i++) {
301
+ html += `<button class="${i === this.currentPage ? 'active' : ''}"
302
+ onclick="app.goToPage(${i})">${i}</button>`;
303
+ }
304
+ container.innerHTML = html;
305
+ }
306
+
307
+ goToPage(page) {
308
+ this.currentPage = page;
309
+ this.renderMemoriesTable();
310
+ }
311
+
312
+ updateProjectFilter() {
313
+ const projects = [...new Set(this.memories.map(m => m.project).filter(Boolean))];
314
+ const select = document.getElementById('filter-project');
315
+ const current = select.value;
316
+
317
+ select.innerHTML = '<option value="">All Projects</option>' +
318
+ projects.map(p => `<option value="${p}">${p}</option>`).join('');
319
+
320
+ select.value = current;
321
+ }
322
+
323
+ filterMemoriesLocally() {
324
+ const searchTerm = document.getElementById('filter-search').value.toLowerCase();
325
+ const rows = document.querySelectorAll('#memories-tbody tr');
326
+
327
+ rows.forEach(row => {
328
+ const text = row.textContent.toLowerCase();
329
+ row.style.display = text.includes(searchTerm) ? '' : 'none';
330
+ });
331
+ }
332
+
333
+ // =========================================================================
334
+ // Search
335
+ // =========================================================================
336
+
337
+ async performSearch() {
338
+ const query = document.getElementById('search-input').value.trim();
339
+ if (!query) return;
340
+
341
+ const categories = Array.from(document.querySelectorAll('.search-cat:checked'))
342
+ .map(cb => cb.value);
343
+ const searchType = document.getElementById('search-type').value;
344
+
345
+ const container = document.getElementById('search-results');
346
+ container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
347
+
348
+ console.log('Performing search:', { query, categories, searchType });
349
+
350
+ try {
351
+ const results = await api.search(query, {
352
+ categories: categories.length > 0 ? categories : null,
353
+ limit: 20,
354
+ searchType: searchType
355
+ });
356
+
357
+ console.log('Search results:', results);
358
+ this.renderSearchResults(results, searchType);
359
+ } catch (error) {
360
+ console.error('Search failed:', error);
361
+ container.innerHTML = '<div class="empty-state"><p>Search failed: ' + (error.message || 'Unknown error') + '</p></div>';
362
+ this.showToast('Search failed: ' + (error.message || 'Unknown error'), 'error');
363
+ }
364
+ }
365
+
366
+ renderSearchResults(results, searchType = 'semantic') {
367
+ const container = document.getElementById('search-results');
368
+
369
+ if (!results || results.length === 0) {
370
+ container.innerHTML = '<div class="empty-state"><p>No results found</p></div>';
371
+ return;
372
+ }
373
+
374
+ container.innerHTML = results.map(r => `
375
+ <div class="search-result" onclick="app.viewMemory('${r.memory.id}')">
376
+ <div class="header">
377
+ <span class="category-badge ${r.memory.category}">${r.memory.category}</span>
378
+ ${searchType === 'semantic'
379
+ ? `<span class="relevance-score">${(r.score * 100).toFixed(0)}% match</span>`
380
+ : `<span class="relevance-score">Keyword match</span>`
381
+ }
382
+ </div>
383
+ <div class="content">${this.escapeHtml(r.memory.content)}</div>
384
+ <div class="meta">
385
+ <span>Score: ${r.memory.outcome_score.toFixed(2)}</span>
386
+ <span>${r.memory.project || 'global'}</span>
387
+ <span>${this.formatDate(r.memory.created_at)}</span>
388
+ </div>
389
+ </div>
390
+ `).join('');
391
+ }
392
+
393
+ // =========================================================================
394
+ // Add Memory
395
+ // =========================================================================
396
+
397
+ async addMemory() {
398
+ const content = document.getElementById('memory-content').value.trim();
399
+ const category = document.getElementById('memory-category').value;
400
+ const project = document.getElementById('memory-project').value.trim() || null;
401
+ const tagsInput = document.getElementById('memory-tags').value.trim();
402
+ const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(Boolean) : [];
403
+
404
+ if (!content) {
405
+ this.showToast('Content is required', 'warning');
406
+ return;
407
+ }
408
+
409
+ try {
410
+ await api.createMemory({
411
+ content,
412
+ category,
413
+ project,
414
+ tags
415
+ });
416
+
417
+ this.showToast('Memory saved successfully', 'success');
418
+ document.getElementById('add-memory-form').reset();
419
+ this.showView('memories');
420
+ } catch (error) {
421
+ this.showToast('Failed to save memory: ' + error.message, 'error');
422
+ }
423
+ }
424
+
425
+ // =========================================================================
426
+ // Memory Detail Modal
427
+ // =========================================================================
428
+
429
+ async viewMemory(id) {
430
+ try {
431
+ const memory = await api.getMemory(id);
432
+ this.selectedMemory = memory;
433
+ this.renderMemoryDetail(memory);
434
+ document.getElementById('memory-modal').classList.add('active');
435
+ } catch (error) {
436
+ this.showToast('Failed to load memory', 'error');
437
+ }
438
+ }
439
+
440
+ renderMemoryDetail(memory) {
441
+ const body = document.getElementById('modal-body');
442
+ body.innerHTML = `
443
+ <div class="detail-row">
444
+ <div class="detail-label">Category</div>
445
+ <div class="detail-value">
446
+ <span class="category-badge ${memory.category}">${memory.category}</span>
447
+ </div>
448
+ </div>
449
+ <div class="detail-row">
450
+ <div class="detail-label">Content</div>
451
+ <div class="detail-content">${this.escapeHtml(memory.content)}</div>
452
+ </div>
453
+ <div class="detail-row">
454
+ <div class="detail-label">Outcome Score</div>
455
+ <div class="detail-value">${memory.outcome_score.toFixed(2)}</div>
456
+ </div>
457
+ <div class="detail-row">
458
+ <div class="detail-label">Project</div>
459
+ <div class="detail-value">${memory.project || 'global'}</div>
460
+ </div>
461
+ <div class="detail-row">
462
+ <div class="detail-label">Tags</div>
463
+ <div class="detail-value">${memory.tags?.join(', ') || 'None'}</div>
464
+ </div>
465
+ <div class="detail-row">
466
+ <div class="detail-label">Created</div>
467
+ <div class="detail-value">${this.formatDate(memory.created_at)}</div>
468
+ </div>
469
+ <div class="detail-row">
470
+ <div class="detail-label">ID</div>
471
+ <div class="detail-value" style="font-family: monospace; font-size: 0.8rem;">${memory.id}</div>
472
+ </div>
473
+ `;
474
+ }
475
+
476
+ closeModal() {
477
+ document.getElementById('memory-modal').classList.remove('active');
478
+ this.selectedMemory = null;
479
+ }
480
+
481
+ async recordOutcome(outcome) {
482
+ if (!this.selectedMemory) return;
483
+
484
+ try {
485
+ await api.recordOutcome(this.selectedMemory.id, outcome);
486
+ this.showToast(`Recorded outcome: ${outcome}`, 'success');
487
+ this.closeModal();
488
+ await this.loadDashboard();
489
+ } catch (error) {
490
+ this.showToast('Failed to record outcome', 'error');
491
+ }
492
+ }
493
+
494
+ async deleteMemory() {
495
+ if (!this.selectedMemory) return;
496
+
497
+ if (!confirm('Are you sure you want to delete this memory?')) {
498
+ return;
499
+ }
500
+
501
+ try {
502
+ await api.deleteMemory(this.selectedMemory.id);
503
+ this.showToast('Memory deleted', 'success');
504
+ this.closeModal();
505
+ await this.loadMemories();
506
+ await this.loadDashboard();
507
+ } catch (error) {
508
+ this.showToast('Failed to delete memory', 'error');
509
+ }
510
+ }
511
+
512
+ // =========================================================================
513
+ // Export
514
+ // =========================================================================
515
+
516
+ async exportMemories() {
517
+ try {
518
+ const memories = await api.exportMemories();
519
+ const json = JSON.stringify(memories, null, 2);
520
+ const blob = new Blob([json], { type: 'application/json' });
521
+ const url = URL.createObjectURL(blob);
522
+
523
+ const a = document.createElement('a');
524
+ a.href = url;
525
+ a.download = `memory-layer-export-${new Date().toISOString().split('T')[0]}.json`;
526
+ a.click();
527
+
528
+ URL.revokeObjectURL(url);
529
+ this.showToast('Export complete', 'success');
530
+ } catch (error) {
531
+ this.showToast('Export failed', 'error');
532
+ }
533
+ }
534
+
535
+ // =========================================================================
536
+ // Unified Tasks Integration (Phase 7 - Beads + Claude Code)
537
+ // =========================================================================
538
+
539
+ async loadBeads() {
540
+ // Now uses unified tasks API
541
+ try {
542
+ const [statsResponse, tasksResponse] = await Promise.all([
543
+ api.getTasksStats(),
544
+ api.getTasks({ limit: 100 })
545
+ ]);
546
+
547
+ this.renderTasksStats(statsResponse);
548
+ this.renderTasks(tasksResponse.tasks || [], tasksResponse.sources || []);
549
+ } catch (error) {
550
+ console.error('Failed to load tasks:', error);
551
+ this.showToast('Failed to load tasks: ' + (error.message || 'Unknown error'), 'error');
552
+ }
553
+ }
554
+
555
+ renderTasksStats(stats) {
556
+ // Combine stats from all sources
557
+ let total = 0;
558
+ let inProgress = 0;
559
+ let pending = 0;
560
+ let done = 0;
561
+
562
+ // Add Beads stats
563
+ if (stats.beads && stats.beads.tasks) {
564
+ const beadsStats = stats.beads.tasks.by_status || {};
565
+ total += stats.beads.tasks.total_tasks || 0;
566
+ inProgress += beadsStats.in_progress || 0;
567
+ pending += beadsStats.pending || 0;
568
+ done += beadsStats.done || 0;
569
+ }
570
+
571
+ // Add Claude Code stats
572
+ if (stats.claude_code && stats.claude_code.tasks) {
573
+ const ccStats = stats.claude_code.tasks.by_status || {};
574
+ total += stats.claude_code.tasks.total_tasks || 0;
575
+ inProgress += ccStats.in_progress || 0;
576
+ pending += ccStats.pending || 0;
577
+ done += ccStats.completed || 0;
578
+ }
579
+
580
+ document.getElementById('beads-total').textContent = total;
581
+ document.getElementById('beads-in-progress').textContent = inProgress;
582
+ document.getElementById('beads-pending').textContent = pending;
583
+ document.getElementById('beads-done').textContent = done;
584
+
585
+ // Update sources info
586
+ const sourcesInfo = document.getElementById('tasks-sources');
587
+ if (sourcesInfo) {
588
+ const sources = stats.available_sources || [];
589
+ sourcesInfo.textContent = sources.length > 0 ? `Sources: ${sources.join(', ')}` : 'No task sources available';
590
+ }
591
+ }
592
+
593
+ renderTasks(tasks, sources) {
594
+ const container = document.getElementById('beads-task-list');
595
+
596
+ if (!tasks || tasks.length === 0) {
597
+ container.innerHTML = `
598
+ <div class="empty-state">
599
+ <p>No tasks found</p>
600
+ <p class="text-muted">${sources.length > 0 ? `Available sources: ${sources.join(', ')}` : 'No task sources available'}</p>
601
+ </div>
602
+ `;
603
+ return;
604
+ }
605
+
606
+ const statusColors = {
607
+ 'done': 'success',
608
+ 'completed': 'success',
609
+ 'in_progress': 'warning',
610
+ 'pending': 'secondary',
611
+ 'blocked': 'danger',
612
+ 'cancelled': 'danger'
613
+ };
614
+
615
+ const statusIcons = {
616
+ 'done': '✓',
617
+ 'completed': '✓',
618
+ 'in_progress': '►',
619
+ 'pending': '○',
620
+ 'blocked': '⊘',
621
+ 'cancelled': '✗'
622
+ };
623
+
624
+ const sourceLabels = {
625
+ 'beads': 'B',
626
+ 'claude_code': 'C'
627
+ };
628
+
629
+ const sourceColors = {
630
+ 'beads': 'source-beads',
631
+ 'claude_code': 'source-claude'
632
+ };
633
+
634
+ container.innerHTML = tasks.map(task => `
635
+ <div class="task-item ${task.status}" onclick="app.showTaskContext('${task.id}', '${task.source || ''}')">
636
+ <div class="task-status">
637
+ <span class="status-icon ${statusColors[task.status] || 'secondary'}">${statusIcons[task.status] || '?'}</span>
638
+ </div>
639
+ <div class="task-source">
640
+ <span class="source-badge ${sourceColors[task.source] || ''}" title="${task.source || 'unknown'}">${sourceLabels[task.source] || '?'}</span>
641
+ </div>
642
+ <div class="task-info">
643
+ <div class="task-title">${this.escapeHtml(task.title)}</div>
644
+ <div class="task-meta">
645
+ <span class="task-id">${task.id}</span>
646
+ <span class="task-status-text">${(task.status || '').replace('_', ' ')}</span>
647
+ </div>
648
+ </div>
649
+ <div class="task-actions">
650
+ <button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); app.showTaskContext('${task.id}', '${task.source || ''}')">
651
+ View Context
652
+ </button>
653
+ </div>
654
+ </div>
655
+ `).join('');
656
+ }
657
+
658
+ async showTaskContext(taskId, source = null) {
659
+ try {
660
+ // Use unified tasks API
661
+ const context = await api.getTaskContext(taskId, source);
662
+
663
+ document.getElementById('task-context').style.display = 'block';
664
+ document.getElementById('task-context-title').textContent = `Context: ${context.task_title}`;
665
+
666
+ const content = document.getElementById('task-context-content');
667
+
668
+ if (context.memories_count === 0) {
669
+ content.innerHTML = '<div class="empty-state"><p>No relevant memories found for this task</p></div>';
670
+ } else {
671
+ // Parse the formatted markdown-like content
672
+ content.innerHTML = `
673
+ <div class="context-header">
674
+ <span class="task-status-badge ${context.task_status}">${(context.task_status || '').replace('_', ' ')}</span>
675
+ <span class="task-source-badge">${context.source || 'unknown'}</span>
676
+ <span class="memories-count">${context.memories_count} relevant memories</span>
677
+ </div>
678
+ <div class="context-memories">
679
+ ${this.formatContextMemories(context.formatted)}
680
+ </div>
681
+ `;
682
+ }
683
+ } catch (error) {
684
+ console.error('Failed to load task context:', error);
685
+ this.showToast('Failed to load task context', 'error');
686
+ }
687
+ }
688
+
689
+ formatContextMemories(formatted) {
690
+ // Extract memory lines from formatted text
691
+ const lines = formatted.split('\n').filter(line => line.startsWith('- **['));
692
+
693
+ if (lines.length === 0) {
694
+ return '<p>No memories in context</p>';
695
+ }
696
+
697
+ return lines.map(line => {
698
+ // Parse: - **[category]** content...
699
+ const match = line.match(/- \*\*\[(\w+)\]\*\* (.+)/);
700
+ if (match) {
701
+ const [, category, content] = match;
702
+ return `
703
+ <div class="context-memory">
704
+ <span class="category-badge ${category}">${category}</span>
705
+ <span class="memory-content">${this.escapeHtml(content)}</span>
706
+ </div>
707
+ `;
708
+ }
709
+ return '';
710
+ }).join('');
711
+ }
712
+
713
+ closeTaskContext() {
714
+ document.getElementById('task-context').style.display = 'none';
715
+ }
716
+
717
+ async syncBeads() {
718
+ // Now uses unified tasks API
719
+ try {
720
+ this.showToast('Syncing tasks...', 'info');
721
+ const result = await api.syncTasks();
722
+
723
+ if (result.success) {
724
+ const synced = result.total_tasks_synced || result.tasks_synced || 0;
725
+ const outcomes = result.total_outcomes_recorded || result.outcomes_recorded || 0;
726
+ this.showToast(`Synced: ${synced} tasks, ${outcomes} outcomes`, 'success');
727
+ await this.loadBeads();
728
+ } else {
729
+ this.showToast('Sync failed', 'error');
730
+ }
731
+ } catch (error) {
732
+ console.error('Sync failed:', error);
733
+ this.showToast('Sync failed: ' + (error.message || 'Unknown error'), 'error');
734
+ }
735
+ }
736
+
737
+ // =========================================================================
738
+ // Utilities
739
+ // =========================================================================
740
+
741
+ escapeHtml(text) {
742
+ const div = document.createElement('div');
743
+ div.textContent = text;
744
+ return div.innerHTML;
745
+ }
746
+
747
+ formatDate(dateString) {
748
+ if (!dateString) return '';
749
+ const date = new Date(dateString);
750
+ return date.toLocaleDateString('en-US', {
751
+ year: 'numeric',
752
+ month: 'short',
753
+ day: 'numeric'
754
+ });
755
+ }
756
+
757
+ showToast(message, type = 'info') {
758
+ const container = document.getElementById('toast-container');
759
+ const toast = document.createElement('div');
760
+ toast.className = `toast ${type}`;
761
+ toast.textContent = message;
762
+ container.appendChild(toast);
763
+
764
+ setTimeout(() => {
765
+ toast.remove();
766
+ }, 3000);
767
+ }
768
+ }
769
+
770
+ // Initialize app
771
+ const app = new MemoryLayerApp();