@vheins/local-memory-mcp 0.2.1 → 0.3.14

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 (50) hide show
  1. package/dist/capabilities.d.ts.map +1 -1
  2. package/dist/capabilities.js +16 -1
  3. package/dist/capabilities.js.map +1 -1
  4. package/dist/dashboard/public/app.js +475 -32
  5. package/dist/dashboard/public/index.html +137 -7
  6. package/dist/dashboard/server.js +137 -16
  7. package/dist/dashboard/server.js.map +1 -1
  8. package/dist/prompts/registry.d.ts +112 -0
  9. package/dist/prompts/registry.d.ts.map +1 -1
  10. package/dist/prompts/registry.js +172 -0
  11. package/dist/prompts/registry.js.map +1 -1
  12. package/dist/resources/index.test.js +2 -0
  13. package/dist/resources/index.test.js.map +1 -1
  14. package/dist/router.d.ts.map +1 -1
  15. package/dist/router.js +3 -0
  16. package/dist/router.js.map +1 -1
  17. package/dist/storage/sqlite.d.ts +7 -1
  18. package/dist/storage/sqlite.d.ts.map +1 -1
  19. package/dist/storage/sqlite.js +105 -13
  20. package/dist/storage/sqlite.js.map +1 -1
  21. package/dist/storage/sqlite.test.js +2 -0
  22. package/dist/storage/sqlite.test.js.map +1 -1
  23. package/dist/tasks.bulk.test.d.ts +2 -0
  24. package/dist/tasks.bulk.test.d.ts.map +1 -0
  25. package/dist/tasks.bulk.test.js +49 -0
  26. package/dist/tasks.bulk.test.js.map +1 -0
  27. package/dist/tools/memory.store.d.ts.map +1 -1
  28. package/dist/tools/memory.store.js +2 -0
  29. package/dist/tools/memory.store.js.map +1 -1
  30. package/dist/tools/memory.update.d.ts.map +1 -1
  31. package/dist/tools/memory.update.js +6 -0
  32. package/dist/tools/memory.update.js.map +1 -1
  33. package/dist/tools/schemas.d.ts +183 -1
  34. package/dist/tools/schemas.d.ts.map +1 -1
  35. package/dist/tools/schemas.js +82 -2
  36. package/dist/tools/schemas.js.map +1 -1
  37. package/dist/tools/task.bulk-manage.d.ts +9 -0
  38. package/dist/tools/task.bulk-manage.d.ts.map +1 -0
  39. package/dist/tools/task.bulk-manage.js +49 -0
  40. package/dist/tools/task.bulk-manage.js.map +1 -0
  41. package/dist/tools/task.manage.d.ts.map +1 -1
  42. package/dist/tools/task.manage.js +15 -2
  43. package/dist/tools/task.manage.js.map +1 -1
  44. package/dist/types.d.ts +5 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/utils/logger.test.js +19 -27
  47. package/dist/utils/logger.test.js.map +1 -1
  48. package/dist/v2-features.test.js +2 -0
  49. package/dist/v2-features.test.js.map +1 -1
  50. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;CAoBxB,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;CAoBxB,CAAC"}
@@ -1,8 +1,23 @@
1
+ import { fileURLToPath } from "url";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const pkgPath = path.join(__dirname, "../package.json");
6
+ let pkgVersion = "0.1.0";
7
+ try {
8
+ if (fs.existsSync(pkgPath)) {
9
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
10
+ pkgVersion = pkg.version;
11
+ }
12
+ }
13
+ catch (e) {
14
+ // Fallback to default version if reading fails
15
+ }
1
16
  // MCP Server Capabilities
2
17
  export const CAPABILITIES = {
3
18
  serverInfo: {
4
19
  name: "mcp-memory-local",
5
- version: "0.1.0"
20
+ version: pkgVersion
6
21
  },
7
22
  capabilities: {
8
23
  resources: {
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,EAAE;QACV,IAAI,EAAE,kBAAkB;QACxB,OAAO,EAAE,OAAO;KACjB;IACD,YAAY,EAAE;QACZ,SAAS,EAAE;YACT,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,SAAS,EAAE,IAAI;SAChB;QACD,KAAK,EAAE;YACL,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;SACX;QACD,OAAO,EAAE;YACP,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,IAAI;SACV;KACF;CACF,CAAC"}
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AACxD,IAAI,UAAU,GAAG,OAAO,CAAC;AAEzB,IAAI,CAAC;IACH,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;QACzD,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC;IAC3B,CAAC;AACH,CAAC;AAAC,OAAO,CAAC,EAAE,CAAC;IACX,+CAA+C;AACjD,CAAC;AAED,0BAA0B;AAC1B,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,UAAU,EAAE;QACV,IAAI,EAAE,kBAAkB;QACxB,OAAO,EAAE,UAAU;KACpB;IACD,YAAY,EAAE;QACZ,SAAS,EAAE;YACT,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;YACV,SAAS,EAAE,IAAI;SAChB;QACD,KAAK,EAAE;YACL,IAAI,EAAE,IAAI;YACV,IAAI,EAAE,IAAI;SACX;QACD,OAAO,EAAE;YACP,IAAI,EAAE,IAAI;YACV,GAAG,EAAE,IAAI;SACV;KACF;CACF,CAAC"}
@@ -6,6 +6,11 @@ let totalPages = 1;
6
6
  let totalItems = 0;
7
7
  let selectedIds = new Set();
8
8
  let currentPaginatedData = [];
9
+ let taskPagination = {
10
+ todo: { page: 1, pageSize: 20, hasMore: true, loading: false },
11
+ in_progress: { page: 1, pageSize: 20, hasMore: true, loading: false },
12
+ completed: { page: 1, pageSize: 20, hasMore: true, loading: false }
13
+ };
9
14
  let charts = {};
10
15
  let lastSyncTime = Date.now();
11
16
  let countdownSeconds = 30;
@@ -428,6 +433,11 @@ async function setCurrentRepo(repo) {
428
433
  currentPage = 1;
429
434
  recentActionsPage = 1;
430
435
  selectedIds.clear();
436
+
437
+ // Clear search input on repo change
438
+ const taskSearch = document.getElementById('taskSearchInput');
439
+ if (taskSearch) taskSearch.value = '';
440
+
431
441
  localStorage.setItem('selectedRepo', currentRepo);
432
442
  renderRepoSidebar();
433
443
  closeRepoSidebarDrawer();
@@ -435,6 +445,7 @@ async function setCurrentRepo(repo) {
435
445
  loadStats(),
436
446
  loadMemories(),
437
447
  loadRecentActions(),
448
+ loadTasks()
438
449
  ]);
439
450
  syncStickyOffsets();
440
451
  }
@@ -600,6 +611,21 @@ async function loadStats() {
600
611
  document.getElementById('mistakeCount').textContent = data.byType?.mistake || 0;
601
612
  document.getElementById('patternCount').textContent = data.byType?.pattern || 0;
602
613
 
614
+ // Fill Task stats
615
+ if (data.taskStats) {
616
+ document.getElementById('totalTasks').textContent = data.taskStats.total || 0;
617
+ document.getElementById('todoTasksCount').textContent = data.taskStats.todo || 0;
618
+ document.getElementById('inProgressTasksCount').textContent = data.taskStats.inProgress || 0;
619
+ document.getElementById('completedTasksCount').textContent = data.taskStats.completed || 0;
620
+
621
+ document.getElementById('todoStatCount').textContent = data.taskStats.todo || 0;
622
+ document.getElementById('inProgressStatCount').textContent = data.taskStats.inProgress || 0;
623
+ document.getElementById('completedStatCount').textContent = data.taskStats.completed || 0;
624
+ document.getElementById('blockedStatCount').textContent = data.taskStats.blocked || 0;
625
+
626
+ updateTaskStatusChart(data.taskStats);
627
+ }
628
+
603
629
  updateTypeChart(data.byType);
604
630
  updateTimeSeriesChart(data.timeSeries || {});
605
631
  updateScatterChart(data.scatterData || []);
@@ -655,6 +681,51 @@ function updateTypeChart(byType) {
655
681
  });
656
682
  }
657
683
 
684
+ function updateTaskStatusChart(taskStats) {
685
+ const ctx = document.getElementById('taskStatusChart');
686
+ if (!window.Chart || !ctx) return;
687
+ if (charts.taskStatusChart) charts.taskStatusChart.destroy();
688
+
689
+ const isDark = document.documentElement.classList.contains('dark');
690
+ const counts = [
691
+ taskStats?.todo || 0,
692
+ taskStats?.inProgress || 0,
693
+ taskStats?.completed || 0,
694
+ taskStats?.blocked || 0
695
+ ];
696
+
697
+ charts.taskStatusChart = new Chart(ctx, {
698
+ type: 'doughnut',
699
+ data: {
700
+ labels: ['To Do', 'In Progress', 'Completed', 'Blocked'],
701
+ datasets: [{
702
+ data: counts,
703
+ backgroundColor: ['#94a3b8', '#38bdf8', '#10b981', '#fb7185'],
704
+ borderWidth: 2,
705
+ borderColor: isDark ? '#1e293b' : '#ffffff',
706
+ hoverOffset: 12
707
+ }]
708
+ },
709
+ options: {
710
+ responsive: true,
711
+ maintainAspectRatio: false,
712
+ cutout: '68%',
713
+ plugins: {
714
+ legend: { display: false },
715
+ tooltip: {
716
+ backgroundColor: isDark ? '#1e293b' : '#ffffff',
717
+ titleColor: isDark ? '#f8fafc' : '#1e293b',
718
+ bodyColor: isDark ? '#94a3b8' : '#64748b',
719
+ borderColor: isDark ? '#334155' : '#e2e8f0',
720
+ borderWidth: 1,
721
+ padding: 10,
722
+ cornerRadius: 10
723
+ }
724
+ }
725
+ }
726
+ });
727
+ }
728
+
658
729
  function updateTopMemoriesChart(memories = []) {
659
730
  const ctx = document.getElementById('topMemoriesChart');
660
731
  if (!window.Chart) {
@@ -1089,6 +1160,7 @@ function renderTable(memories) {
1089
1160
  <td class="p-3">
1090
1161
  <div class="flex flex-col">
1091
1162
  <span class="text-[10px] font-bold text-sky-600 dark:text-sky-400 truncate max-w-[100px]" title="${m.agent || 'unknown'}">${m.agent || 'unknown'}</span>
1163
+ <span class="text-[9px] font-medium text-slate-500 dark:text-slate-400 truncate max-w-[100px]" title="${m.role || 'unknown'}">${m.role || 'unknown'}</span>
1092
1164
  <span class="text-[9px] text-gray-400 dark:text-gray-500 truncate max-w-[100px]" title="${m.model || 'unknown'}">${m.model || 'unknown'}</span>
1093
1165
  </div>
1094
1166
  </td>
@@ -1589,47 +1661,103 @@ let currentTasks = [];
1589
1661
 
1590
1662
  async function loadTasks() {
1591
1663
  if (!currentRepo) return;
1664
+
1665
+ // Reset pagination
1666
+ taskPagination.todo = { page: 1, pageSize: 20, hasMore: true, loading: false };
1667
+ taskPagination.in_progress = { page: 1, pageSize: 20, hasMore: true, loading: false };
1668
+ taskPagination.completed = { page: 1, pageSize: 20, hasMore: true, loading: false };
1669
+
1670
+ // Clear containers
1671
+ document.getElementById('todoTasks').innerHTML = '';
1672
+ document.getElementById('inProgressTasks').innerHTML = '';
1673
+ document.getElementById('completedTasks').innerHTML = '';
1674
+
1675
+ await Promise.all([
1676
+ loadTaskCategory('pending,blocked,canceled'),
1677
+ loadTaskCategory('in_progress'),
1678
+ loadTaskCategory('completed')
1679
+ ]);
1680
+
1681
+ setupTaskScrollListeners();
1682
+ }
1683
+
1684
+ async function loadTaskCategory(status) {
1685
+ const category = (status.includes('pending') || status.includes('blocked') || status.includes('canceled')) ? 'todo' : (status === 'in_progress' ? 'in_progress' : 'completed');
1686
+ const pag = taskPagination[category];
1687
+
1688
+ if (!pag.hasMore || pag.loading) return;
1689
+
1690
+ pag.loading = true;
1691
+ const containerId = { todo: 'todoTasks', in_progress: 'inProgressTasks', completed: 'completedTasks' }[category];
1692
+ const container = document.getElementById(containerId);
1693
+
1694
+ // Show loading indicator
1695
+ const loadingId = `loading-${category}`;
1696
+ if (!document.getElementById(loadingId)) {
1697
+ const loader = document.createElement('div');
1698
+ loader.id = loadingId;
1699
+ loader.className = 'py-4 text-center text-gray-400 text-[10px] animate-pulse w-full';
1700
+ loader.textContent = 'Loading more...';
1701
+ container.appendChild(loader);
1702
+ }
1703
+
1592
1704
  try {
1593
- const response = await fetch(`/api/tasks?repo=${encodeURIComponent(currentRepo)}`);
1705
+ const searchInput = document.getElementById('taskSearchInput');
1706
+ const searchTerm = searchInput ? encodeURIComponent(searchInput.value.trim()) : '';
1707
+ const response = await fetch(`/api/tasks?repo=${encodeURIComponent(currentRepo)}&status=${status}&page=${pag.page}&pageSize=${pag.pageSize}&search=${searchTerm}`);
1594
1708
  const data = await response.json();
1595
- currentTasks = data.tasks || [];
1596
- renderTasks();
1709
+
1710
+ const tasks = data.tasks || [];
1711
+
1712
+ // Remove loader
1713
+ const loader = document.getElementById(loadingId);
1714
+ if (loader) loader.remove();
1715
+
1716
+ if (tasks.length < pag.pageSize) {
1717
+ pag.hasMore = false;
1718
+ }
1719
+
1720
+ renderTaskCards(containerId, tasks, pag.page === 1);
1721
+ pag.page++;
1597
1722
  } catch (err) {
1598
- console.error('Failed to load tasks:', err);
1723
+ console.error(`Failed to load ${category} tasks:`, err);
1724
+ } finally {
1725
+ pag.loading = false;
1599
1726
  }
1600
1727
  }
1601
1728
 
1602
- function renderTasks() {
1603
- if (!document.getElementById('todoTasks')) return;
1604
-
1605
- const todoTasks = currentTasks.filter(t => t.status === 'pending' || t.status === 'blocked');
1606
- const inProgressTasks = currentTasks.filter(t => t.status === 'in_progress');
1607
- const completedTasks = currentTasks.filter(t => t.status === 'completed');
1608
-
1609
- document.getElementById('todoCount').textContent = todoTasks.length;
1610
- document.getElementById('inProgressCount').textContent = inProgressTasks.length;
1611
- document.getElementById('completedCount').textContent = completedTasks.length;
1612
-
1613
- renderTaskColumn('todoTasks', todoTasks);
1614
- renderTaskColumn('inProgressTasks', inProgressTasks);
1615
- renderTaskColumn('completedTasks', completedTasks);
1729
+ function setupTaskScrollListeners() {
1730
+ ['todoTasks', 'inProgressTasks', 'completedTasks'].forEach(id => {
1731
+ const el = document.getElementById(id);
1732
+ if (!el) return;
1733
+
1734
+ el.onscroll = () => {
1735
+ if (el.scrollTop + el.clientHeight >= el.scrollHeight - 50) {
1736
+ const category = id === 'todoTasks' ? 'todo' : (id === 'inProgressTasks' ? 'in_progress' : 'completed');
1737
+ const status = category === 'todo' ? 'pending,blocked,canceled' : (category === 'in_progress' ? 'in_progress' : 'completed');
1738
+ loadTaskCategory(status);
1739
+ }
1740
+ };
1741
+ });
1616
1742
  }
1617
1743
 
1618
- function renderTaskColumn(id, tasks) {
1619
- const container = document.getElementById(id);
1744
+ function renderTaskCards(containerId, tasks, clear = false) {
1745
+ const container = document.getElementById(containerId);
1620
1746
  if (!container) return;
1621
-
1622
- if (!tasks || tasks.length === 0) {
1747
+
1748
+ if (clear && (!tasks || tasks.length === 0)) {
1623
1749
  container.innerHTML = '<div class="text-center py-8 text-gray-400 text-xs italic bg-gray-50/50 dark:bg-gray-900/20 rounded-xl border border-dashed border-gray-200 dark:border-gray-700">No tasks</div>';
1624
1750
  return;
1625
1751
  }
1626
1752
 
1627
- container.innerHTML = tasks.map(t => `
1628
- <div class="bg-white dark:bg-gray-700 p-4 rounded-xl shadow-sm border border-gray-100 dark:border-gray-600 hover:shadow-md transition-all group">
1753
+ const html = tasks.map(t => `
1754
+ <div onclick="showTaskDetail('${t.id}')" class="bg-white dark:bg-gray-700 p-4 rounded-xl shadow-sm border border-gray-100 dark:border-gray-600 hover:shadow-md transition-all group cursor-pointer">
1629
1755
  <div class="flex items-center justify-between mb-2">
1630
1756
  <div class="flex items-center gap-2">
1631
1757
  <span class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-[10px] font-bold text-gray-600 dark:text-gray-400 font-mono border border-gray-200 dark:border-gray-700">${t.task_code}</span>
1632
1758
  <span class="text-[10px] font-bold uppercase tracking-wider text-gray-400">${t.phase}</span>
1759
+ ${t.status === 'blocked' ? '<span class="px-1 py-0.5 rounded bg-red-500 text-white text-[8px] font-bold uppercase">Blocked</span>' : ''}
1760
+ ${t.status === 'canceled' ? '<span class="px-1 py-0.5 rounded bg-slate-500 text-white text-[8px] font-bold uppercase">Canceled</span>' : ''}
1633
1761
  </div>
1634
1762
  <div class="flex items-center gap-1">
1635
1763
  ${t.priority >= 4 ? '<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse"></span>' : ''}
@@ -1637,7 +1765,25 @@ function renderTaskColumn(id, tasks) {
1637
1765
  </div>
1638
1766
  </div>
1639
1767
  <h4 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-1">${escapeHtml(t.title)}</h4>
1640
- <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mb-3">${escapeHtml(t.description || '')}</p>
1768
+ <p class="text-xs text-gray-500 dark:text-gray-400 line-clamp-2 mb-2">${escapeHtml(t.description || '')}</p>
1769
+
1770
+ ${t.doc_path ? `
1771
+ <div class="mb-3" onclick="event.stopPropagation()">
1772
+ <a href="${t.doc_path.startsWith('http') ? t.doc_path : '#'}" target="_blank" class="inline-flex items-center gap-1.5 px-2 py-1 rounded bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-[10px] text-slate-500 dark:text-gray-400 hover:text-sky-600 transition-colors">
1773
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
1774
+ <span class="truncate max-w-[150px]">${escapeHtml(t.doc_path)}</span>
1775
+ </a>
1776
+ </div>
1777
+ ` : ''}
1778
+
1779
+ <div class="flex items-center gap-2 mb-3">
1780
+ <div class="px-1.5 py-0.5 rounded bg-sky-50 dark:bg-sky-900/30 border border-sky-100 dark:border-sky-800 flex items-center gap-1">
1781
+ <span class="text-[9px] font-bold text-sky-600 dark:text-sky-400">${escapeHtml(t.agent || 'unknown')}</span>
1782
+ <span class="text-[8px] text-sky-400 dark:text-sky-600">|</span>
1783
+ <span class="text-[9px] font-medium text-sky-500 dark:text-sky-500">${escapeHtml(t.role || 'unknown')}</span>
1784
+ </div>
1785
+ </div>
1786
+
1641
1787
  ${t.depends_on ? `
1642
1788
  <div class="mt-2 pt-2 border-t border-gray-50 dark:border-gray-600 flex items-center gap-1.5">
1643
1789
  <svg class="w-3 h-3 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"></path></svg>
@@ -1646,10 +1792,117 @@ function renderTaskColumn(id, tasks) {
1646
1792
  ` : ''}
1647
1793
  <div class="mt-3 flex items-center justify-between">
1648
1794
  <span class="text-[10px] font-mono text-gray-400">${t.id.substring(0, 8)}</span>
1649
- <span class="text-[10px] text-gray-400">${formatDate(t.created_at)}</span>
1795
+ <span class="text-[10px] text-gray-400">${t.status === 'completed' && t.finished_at ? 'Done ' + formatDate(t.finished_at) : formatDate(t.created_at)}</span>
1650
1796
  </div>
1651
1797
  </div>
1652
1798
  `).join('');
1799
+
1800
+ if (clear) {
1801
+ container.innerHTML = html;
1802
+ } else {
1803
+ container.insertAdjacentHTML('beforeend', html);
1804
+ }
1805
+ }
1806
+
1807
+ async function showTaskDetail(id) {
1808
+ const drawer = document.getElementById('memoryDrawer');
1809
+ const title = document.getElementById('drawerTitle');
1810
+ const body = document.getElementById('drawerBody');
1811
+
1812
+ title.textContent = 'Loading Task...';
1813
+ body.innerHTML = `
1814
+ <div class="space-y-4">
1815
+ <div class="skeleton h-20 w-full"></div>
1816
+ <div class="grid gap-4 md:grid-cols-2">
1817
+ <div class="skeleton h-36 w-full"></div>
1818
+ <div class="skeleton h-36 w-full"></div>
1819
+ </div>
1820
+ <div class="skeleton h-64 w-full"></div>
1821
+ </div>
1822
+ `;
1823
+
1824
+ drawer.classList.remove('hidden');
1825
+ document.body.classList.add('drawer-open');
1826
+
1827
+ // Trigger slide-in animation
1828
+ setTimeout(() => {
1829
+ const aside = document.getElementById('drawerAside');
1830
+ if (aside) {
1831
+ aside.classList.remove('translate-x-full');
1832
+ aside.classList.add('translate-x-0');
1833
+ }
1834
+ }, 10);
1835
+
1836
+ try {
1837
+ const response = await fetch(`/api/tasks/${id}?repo=${encodeURIComponent(currentRepo)}`);
1838
+ const task = await response.json();
1839
+ if (!response.ok) throw new Error(task.error || 'Failed to load task');
1840
+
1841
+ title.textContent = `Task: ${task.task_code}`;
1842
+
1843
+ body.innerHTML = `
1844
+ <div class="space-y-6">
1845
+ <div class="p-5 bg-slate-50 dark:bg-slate-900/50 rounded-2xl border border-slate-100 dark:border-slate-800 shadow-sm">
1846
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Title</div>
1847
+ <h3 class="text-xl font-bold text-slate-900 dark:text-white leading-tight">${escapeHtml(task.title)}</h3>
1848
+ </div>
1849
+
1850
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
1851
+ <div class="p-4 bg-white dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-800">
1852
+ <div class="text-[10px] font-bold text-slate-400 uppercase mb-2">Status & Priority</div>
1853
+ <div class="flex flex-wrap gap-2">
1854
+ <span class="px-2 py-1 rounded-lg bg-sky-500/10 text-sky-600 dark:text-sky-400 text-xs font-bold border border-sky-500/20 capitalize">${task.status}</span>
1855
+ <span class="px-2 py-1 rounded-lg bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 text-xs font-bold border border-indigo-500/20">P${task.priority}</span>
1856
+ <span class="px-2 py-1 rounded-lg bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 text-xs font-bold border border-slate-200 dark:border-slate-700 uppercase">${task.phase}</span>
1857
+ </div>
1858
+ </div>
1859
+ <div class="p-4 bg-white dark:bg-slate-800/40 rounded-xl border border-slate-100 dark:border-slate-800">
1860
+ <div class="text-[10px] font-bold text-slate-400 uppercase mb-2">Owner</div>
1861
+ <div class="flex items-center gap-2">
1862
+ <div class="w-8 h-8 rounded-full bg-gradient-to-tr from-sky-400 to-indigo-500 flex items-center justify-center text-white text-[10px] font-bold">
1863
+ ${getRepoInitials(task.agent || 'UK')}
1864
+ </div>
1865
+ <div>
1866
+ <div class="text-xs font-bold">${escapeHtml(task.agent || 'unknown')}</div>
1867
+ <div class="text-[10px] text-slate-500 dark:text-slate-400">${escapeHtml(task.role || 'unknown')}</div>
1868
+ </div>
1869
+ </div>
1870
+ </div>
1871
+ </div>
1872
+
1873
+ <div class="p-5 bg-white dark:bg-slate-800/40 rounded-2xl border border-slate-100 dark:border-slate-800">
1874
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Description</div>
1875
+ <div class="text-sm text-slate-600 dark:text-slate-300 leading-relaxed markdown-body">
1876
+ ${task.description ? renderMarkdown(task.description) : '<span class="italic opacity-50">No description provided</span>'}
1877
+ </div>
1878
+ </div>
1879
+
1880
+ ${task.doc_path ? `
1881
+ <div class="p-4 bg-sky-500/5 dark:bg-sky-500/10 rounded-xl border border-sky-500/20">
1882
+ <div class="text-[10px] font-bold text-sky-600/60 dark:text-sky-400/60 uppercase mb-2">Documentation</div>
1883
+ <a href="${task.doc_path.startsWith('http') ? task.doc_path : '#'}" target="_blank" class="flex items-center gap-2 text-sky-600 dark:text-sky-400 hover:underline" onclick="event.stopPropagation()">
1884
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
1885
+ <span class="text-sm font-medium truncate">${escapeHtml(task.doc_path)}</span>
1886
+ </a>
1887
+ </div>
1888
+ ` : ''}
1889
+
1890
+ <div class="pt-4 border-t border-slate-100 dark:border-slate-800 grid grid-cols-2 gap-4">
1891
+ <div>
1892
+ <div class="text-[9px] font-bold text-slate-400 uppercase">Created</div>
1893
+ <div class="text-[11px] text-slate-500">${new Date(task.created_at).toLocaleString()}</div>
1894
+ </div>
1895
+ <div>
1896
+ <div class="text-[9px] font-bold text-slate-400 uppercase">Updated</div>
1897
+ <div class="text-[11px] text-slate-500">${new Date(task.updated_at).toLocaleString()}</div>
1898
+ </div>
1899
+ </div>
1900
+ </div>
1901
+ `;
1902
+ } catch (err) {
1903
+ showToast('Failed to load task: ' + err.message, 'error');
1904
+ closeDrawer();
1905
+ }
1653
1906
  }
1654
1907
 
1655
1908
  function getPriorityColor(p) {
@@ -1665,25 +1918,27 @@ function switchTab(tab) {
1665
1918
  const dashTab = document.getElementById('dashboardTabBtn');
1666
1919
  const memTab = document.getElementById('memoriesTabBtn');
1667
1920
  const taskTab = document.getElementById('tasksTabBtn');
1921
+ const refTab = document.getElementById('referenceTabBtn');
1668
1922
  const indicator = document.getElementById('tabIndicator');
1669
-
1923
+
1670
1924
  const dashContent = document.getElementById('dashboardContent');
1671
1925
  const memContent = document.getElementById('memoriesContent');
1672
1926
  const taskContent = document.getElementById('tasksContent');
1927
+ const refContent = document.getElementById('referenceContent');
1673
1928
 
1674
1929
  currentTab = tab;
1675
1930
  localStorage.setItem('activeTab', tab);
1676
1931
 
1677
- const tabs = [dashTab, memTab, taskTab];
1678
- const contents = [dashContent, memContent, taskContent];
1679
- const targetId = tab + 'TabBtn';
1932
+ const tabs = [dashTab, memTab, taskTab, refTab];
1933
+ const contents = [dashContent, memContent, taskContent, refContent]; const targetId = tab + 'TabBtn';
1680
1934
  const targetContentId = tab + 'Content';
1681
1935
 
1682
1936
  // Update indicator
1683
1937
  if (indicator) {
1684
1938
  if (tab === 'dashboard') indicator.style.transform = 'translateX(0)';
1685
- else if (tab === 'memories') indicator.style.transform = 'translateX(100%)';
1686
- else if (tab === 'tasks') indicator.style.transform = 'translateX(200%)';
1939
+ if (tab === 'memories') indicator.style.transform = 'translateX(100%)';
1940
+ if (tab === 'tasks') indicator.style.transform = 'translateX(200%)';
1941
+ if (tab === 'reference') indicator.style.transform = 'translateX(300%)';
1687
1942
  }
1688
1943
 
1689
1944
  // Update button states
@@ -1714,6 +1969,8 @@ function switchTab(tab) {
1714
1969
  if (tab === 'tasks') loadTasks();
1715
1970
  if (tab === 'dashboard') loadStats();
1716
1971
  if (tab === 'memories') loadMemories();
1972
+ if (tab === 'reference') loadCapabilities();
1973
+ syncStickyOffsets();
1717
1974
  }
1718
1975
 
1719
1976
  window.loadTasks = loadTasks;
@@ -1765,8 +2022,194 @@ safeAddEventListener('repoCollapsedSummaryButton', 'click', () => {
1765
2022
  }
1766
2023
  });
1767
2024
 
2025
+ safeAddEventListener('taskSearchInput', 'input', () => {
2026
+ if (window.taskSearchDebounce) clearTimeout(window.taskSearchDebounce);
2027
+ window.taskSearchDebounce = setTimeout(() => {
2028
+ loadTasks();
2029
+ }, 300);
2030
+ });
2031
+
1768
2032
  window.addEventListener('resize', syncStickyOffsets);
1769
2033
 
2034
+ let currentCapabilities = { tools: [], resources: [], prompts: [] };
2035
+
2036
+ async function loadCapabilities() {
2037
+ const toolsList = document.getElementById('toolsList');
2038
+ const resourcesList = document.getElementById('resourcesList');
2039
+ const promptsList = document.getElementById('promptsList');
2040
+
2041
+ if (!toolsList) return;
2042
+
2043
+ toolsList.innerHTML = '<div class="text-center py-4 text-gray-400 text-xs">Loading capabilities...</div>';
2044
+
2045
+ try {
2046
+ const response = await fetch('/api/capabilities');
2047
+ const data = await response.json();
2048
+
2049
+ currentCapabilities.tools = Array.isArray(data.tools) ? data.tools : [];
2050
+ currentCapabilities.resources = Array.isArray(data.resources) ? data.resources : [];
2051
+ currentCapabilities.prompts = Array.isArray(data.prompts) ? data.prompts : [];
2052
+
2053
+ toolsList.innerHTML = currentCapabilities.tools.map(t => `
2054
+ <div onclick="showCapabilityDetail('tools', '${t.name}')" class="p-3 bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 rounded-lg cursor-pointer hover:border-sky-500/50 transition-all group">
2055
+ <div class="font-bold text-xs text-sky-600 dark:text-sky-400 mb-1 group-hover:text-sky-500">${escapeHtml(t.name)}</div>
2056
+ <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(t.description)}</p>
2057
+ </div>
2058
+ `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No tools available</div>';
2059
+
2060
+ resourcesList.innerHTML = currentCapabilities.resources.map(r => `
2061
+ <div onclick="showCapabilityDetail('resources', '${r.name}')" class="p-3 bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 rounded-lg cursor-pointer hover:border-indigo-500/50 transition-all group">
2062
+ <div class="font-bold text-xs text-indigo-600 dark:text-indigo-400 mb-1 group-hover:text-indigo-500">${escapeHtml(r.name)}</div>
2063
+ <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(r.description || 'No description')}</p>
2064
+ <div class="text-[8px] font-mono text-gray-400 mt-1 truncate">${escapeHtml(r.uri)}</div>
2065
+ </div>
2066
+ `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No resources available</div>';
2067
+
2068
+ promptsList.innerHTML = currentCapabilities.prompts.map(p => `
2069
+ <div onclick="showCapabilityDetail('prompts', '${p.name}')" class="p-3 bg-slate-50 dark:bg-slate-900/50 border border-slate-100 dark:border-slate-800 rounded-lg cursor-pointer hover:border-emerald-500/50 transition-all group">
2070
+ <div class="font-bold text-xs text-emerald-600 dark:text-emerald-400 mb-1 group-hover:text-emerald-500">${escapeHtml(p.name)}</div>
2071
+ <p class="text-[10px] text-gray-500 dark:text-gray-400 line-clamp-2">${escapeHtml(p.description || 'No description')}</p>
2072
+ </div>
2073
+ `).join('') || '<div class="text-center py-4 text-gray-400 text-xs">No prompts available</div>';
2074
+
2075
+ } catch (err) {
2076
+ console.error('Failed to load capabilities:', err);
2077
+ toolsList.innerHTML = `<div class="text-center py-4 text-red-400 text-xs">Error: ${err.message}</div>`;
2078
+ }
2079
+ }
2080
+
2081
+ async function handleCsvImport(event) {
2082
+ const file = event.target.files[0];
2083
+ if (!file || !currentRepo) return;
2084
+
2085
+ const reader = new FileReader();
2086
+ reader.onload = async (e) => {
2087
+ const csvData = e.target.result;
2088
+ try {
2089
+ const response = await fetch('/api/tasks/import-csv', {
2090
+ method: 'POST',
2091
+ headers: { 'Content-Type': 'application/json' },
2092
+ body: JSON.stringify({ repo: currentRepo, csvData })
2093
+ });
2094
+ const result = await response.json();
2095
+ if (result.success) {
2096
+ showToast(`Successfully imported ${result.count} tasks`, 'success');
2097
+ loadTasks();
2098
+ loadStats();
2099
+ } else {
2100
+ showToast(result.error || 'Failed to import CSV', 'error');
2101
+ }
2102
+ } catch (err) {
2103
+ showToast('Import failed: ' + err.message, 'error');
2104
+ }
2105
+ };
2106
+ reader.readAsText(file);
2107
+ event.target.value = '';
2108
+ }
2109
+
2110
+ function downloadCsvTemplate() {
2111
+ const headers = "task_code,phase,title,description,priority,status,agent,role,doc_path";
2112
+ const example = "TASK-001,research,Integrate CSV,Add import feature to dashboard,4,pending,Gemini CLI,expert,docs/tasks.md";
2113
+ const csv = `${headers}\n${example}`;
2114
+
2115
+ const blob = new Blob([csv], { type: 'text/csv' });
2116
+ const url = window.URL.createObjectURL(blob);
2117
+ const a = document.createElement('a');
2118
+ a.setAttribute('hidden', '');
2119
+ a.setAttribute('href', url);
2120
+ a.setAttribute('download', 'task_template.csv');
2121
+ document.body.appendChild(a);
2122
+ a.click();
2123
+ document.body.removeChild(a);
2124
+ }
2125
+
2126
+ function showCapabilityDetail(type, name) {
2127
+ const item = currentCapabilities[type].find(i => i.name === name);
2128
+ if (!item) return;
2129
+
2130
+ const drawer = document.getElementById('memoryDrawer');
2131
+ const title = document.getElementById('drawerTitle');
2132
+ const body = document.getElementById('drawerBody');
2133
+
2134
+ title.textContent = `${type.charAt(0).toUpperCase() + type.slice(1)}: ${name}`;
2135
+
2136
+ let contentHtml = `
2137
+ <div class="space-y-6">
2138
+ <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2139
+ <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Description</h4>
2140
+ <p class="text-sm text-slate-600 dark:text-slate-300">${escapeHtml(item.description || 'No description')}</p>
2141
+ </div>
2142
+ `;
2143
+
2144
+ if (type === 'tools' && item.inputSchema) {
2145
+ contentHtml += `
2146
+ <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2147
+ <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Input Schema</h4>
2148
+ <pre class="text-[10px] font-mono text-sky-600 dark:text-sky-400 overflow-x-auto">${JSON.stringify(item.inputSchema, null, 2)}</pre>
2149
+ </div>
2150
+ `;
2151
+ }
2152
+
2153
+ if (type === 'resources' && item.uri) {
2154
+ contentHtml += `
2155
+ <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2156
+ <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">URI</h4>
2157
+ <code class="text-xs font-mono text-indigo-600 dark:text-indigo-400">${escapeHtml(item.uri)}</code>
2158
+ </div>
2159
+ `;
2160
+ }
2161
+
2162
+ if (type === 'prompts' && item.arguments) {
2163
+ contentHtml += `
2164
+ <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2165
+ <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Arguments</h4>
2166
+ <div class="space-y-3">
2167
+ ${item.arguments.map(arg => `
2168
+ <div class="border-l-2 border-emerald-500 pl-3 py-1">
2169
+ <div class="text-xs font-bold text-emerald-600 dark:text-emerald-400">${escapeHtml(arg.name)} ${arg.required ? '<span class="text-[8px] bg-emerald-500 text-white px-1 rounded">REQUIRED</span>' : ''}</div>
2170
+ <div class="text-[10px] text-gray-500 dark:text-gray-400">${escapeHtml(arg.description || '')}</div>
2171
+ </div>
2172
+ `).join('')}
2173
+ </div>
2174
+ </div>
2175
+ `;
2176
+ }
2177
+
2178
+ if (type === 'prompts' && item.messages) {
2179
+ contentHtml += `
2180
+ <div class="p-4 bg-slate-50 dark:bg-slate-900/50 rounded-xl border border-slate-100 dark:border-slate-800">
2181
+ <h4 class="text-xs font-bold text-slate-400 uppercase mb-2">Instructions</h4>
2182
+ <div class="space-y-4">
2183
+ ${item.messages.map(msg => {
2184
+ const rawContent = typeof msg.content === 'string' ? msg.content : (msg.content?.text || '');
2185
+ const renderedMarkdown = window.marked ? window.marked.parse(rawContent) : escapeHtml(rawContent);
2186
+ return `
2187
+ <div class="space-y-1">
2188
+ <div class="text-[10px] font-bold uppercase tracking-wider text-slate-400">${escapeHtml(msg.role)}</div>
2189
+ <div class="p-4 bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 rounded-lg text-sm markdown-body">
2190
+ ${renderedMarkdown}
2191
+ </div>
2192
+ </div>
2193
+ `;
2194
+ }).join('')}
2195
+ </div>
2196
+ </div>
2197
+ `;
2198
+ }
2199
+
2200
+ contentHtml += `</div>`;
2201
+ body.innerHTML = contentHtml;
2202
+
2203
+ drawer.classList.remove('hidden');
2204
+ document.body.classList.add('drawer-open');
2205
+ setTimeout(() => document.getElementById('drawerAside').classList.remove('translate-x-full'), 10);
2206
+ }
2207
+
2208
+ window.handleCsvImport = handleCsvImport;
2209
+ window.downloadCsvTemplate = downloadCsvTemplate;
2210
+ window.loadCapabilities = loadCapabilities;
2211
+ window.showCapabilityDetail = showCapabilityDetail;
2212
+
1770
2213
  initTheme();
1771
2214
  initRepoSidebarState();
1772
2215
  initPinnedRepos();